【问题标题】:When structures are better than classes? [duplicate]什么时候结构比类更好? [复制]
【发布时间】:2010-12-08 06:30:35
【问题描述】:

重复:When to use struct in C#?

在 Microsoft .NET 2.0/3.5 中使用结构而不是某些类有实际理由吗?

“结构和类有什么区别?” - 这可能是“.NET 开发人员”职位空缺面试中最受欢迎的问题。面试官认为正确的唯一答案是“结构在堆栈上分配,类在堆上分配”,对此没有进一步的问题。

一些谷歌搜索显示:

a) 结构与类和
b) 堆栈(因此 结构)可以在非常特殊条件下更快,包括:

  • 小于 16 字节的数据块大小
  • 没有广泛的装箱/拆箱
  • 结构的成员几乎是不可变的
  • 整组数据不大(否则会出现栈溢出)

(如有错误或不完整,请更正/添加到此列表中)

据我所知,大多数典型的商业项目(ERM、会计、银行解决方案等)都没有定义一个单一的结构,而是将所有自定义数据类型定义为类。这种方法是否有问题或至少不完善?

注意:问题是关于普通的商业应用程序,请不要列出“不寻常”的案例,如游戏开发、实时动画、向后兼容性(COM/Interop)、非托管代码等 -这些答案已经在这个类似的问题下:

When to use struct?

【问题讨论】:

  • “结构在栈上分配,类在堆上分配”。奇怪的是,面试官认为这是正确的答案,因为这不是正确的答案。你应该阅读blogs.msdn.com/ericlippert/archive/2009/04/27/…
  • @Joren 感谢您的链接...它是一个很好的阅读。
  • 有些面试官认为任何给定问题的“正确答案”令人惊讶,如果你指出他们最喜欢的答案是错误的,他们可能会非常敌对。更神奇的是,面试官提问是为了引出具体的答案;当我面试时,我会尝试问一些问题,以测试对现实问题进行智能推理、解决歧义和理解复杂代码的能力。对琐事问题说出正确答案并不是大多数开发工作的核心要求。
  • Joren:这被认为是正确的答案,因为 Richter 的“CLR via C#”一书(简要地)说了这一点,而这本书是一些(很多?)采访者的圣经。 Eric:完全同意你关于琐事的(非)重要性的看法,但设法找到(在我所在地区)只有几家公司不询问琐事。

标签: c# .net class structure


【解决方案1】:

如果类型的目的是将一个小的固定独立值集合与胶带绑定在一起(例如,点的坐标、枚举字典条目的键和关联值、六项 2d 变换矩阵,等),从效率和语义的角度来看,最好的表示可能是一个可变的暴露字段结构。请注意,这代表了与结构表示单个统一概念(例如DecimalDateTime)的情况非常不同的使用场景,并且微软关于何时使用结构的建议给出了仅适用于后者的建议.微软描述的“不可变”结构的风格只真正适合代表一个单一的统一概念;如果需要表示一小部分固定的独立值集合,则正确的替代方案不是不可变类(性能较差),也不是可变类(在许多情况下会提供不正确的语义),而是暴露字段结构(如果使用得当,可以提供卓越的语义和性能)。例如,如果一个结构 MyTransform 包含一个 2d 变换矩阵,则方法如下:

static void Offset(ref it, double x, double y)
{
  it.dx += x;
  it.dy += y;
}

更快更清晰
static void Offset(ref it, double x, double y)
{
  it = new Transform2d(it.xx, int.xy, it.yx, it.yy, it.dx+x, it.dy+y);
}

Transform2d Offset(double dx, double dy)
{
  it = new Transform2d(xx, xy, yx, yy, dx+x, dy+y);
}

知道 dx 和 dy 是 Transform2d 的字段就足以知道第一种方法修改了这些字段并且没有其他副作用。相比之下,要知道其他方法的作用,就必须检查构造函数的代码。

【讨论】:

    【解决方案2】:

    已经有一些很好的答案涉及使用结构与类的实用性,反之亦然,但我认为你关于结构是不可变的原始评论是一个很好的论据,说明为什么类在高层中更频繁地使用LOB 应用程序的级设计。

    在领域驱动设计http://www.infoq.com/minibooks/domain-driven-design-quickly 中,实体/类和值对象/结构之间有些相似之处。 DDD 中的实体是业务域中的项目,我们需要使用标识符来跟踪其身份,例如CustomerId、ProductId 等。 值对象是我们可能对其值感兴趣的项目,但我们不使用标识符(例如价格或订单日期)跟踪其身份。实体在 DDD 中是可变的,除了它们的身份字段,而值对象没有身份。

    因此,在对典型的业务实体进行建模时,通常会设计一个类以及一个身份属性,该属性会跟踪从持久性存储来回的业务对象的身份。尽管在运行时我们可能会更改业务对象实例上的所有属性值,但只要标识符是不可变的,实体的身份就会被保留。对于与金钱或时间相对应的业务概念,结构体自然而然地适合,因为即使每次执行计算时都会创建一个新实例,这没关系,因为我们不跟踪身份,只存储一个值。

    【讨论】:

      【解决方案3】:

      堆栈分配值类型的一个优点是它们是线程本地的。这意味着它们本质上是线程安全的。对于堆上的对象不能这么说。

      这当然假设我们谈论的是安全的托管代码。

      【讨论】:

      • 你能澄清一下吗?但是,从线程内部分配并且未在外部引用的任何东西难道不是在同一条船上吗?无论是值类型还是引用类型。如果我从一个线程中“新建”一个对象,那么其他线程不知道它在哪里丢弃它,所以它同样安全不是吗?
      • 是的,但堆栈上的值无法在托管代码中“转义”。可以通过各种方式暴露对象。如果不是,一切都很好,但您必须自己验证这一点。使用值类型保证不会发生。
      • 一般来说这是对的,但并不是所有的值类型都分配在堆栈上,一旦你在 C# 中使用指针,你的线程安全就消失了。
      • @Foxfire:没错。如果您在代码中使用 unsafe,这是一组不同的规则。
      • 投反对票时请留言。
      【解决方案4】:

      据我所知,大多数典型的商业项目(ERM、会计、银行解决方案等)都没有定义一个单一的结构,而是将所有自定义数据类型定义为类。这种方法是否有问题或至少不完善?

      不!一切都很好。您的一般规则应该是默认始终使用对象。毕竟我们谈论面向对象的编程是有原因的,而不是面向结构的编程(结构本身缺少一些面向对象的原则,比如继承和抽象)。

      但是结构有时会更好如果:

      • 您需要精确控制所使用的内存量(结构使用(取决于大小)比对象少一点内存。
      • 您需要精确控制内存布局。这对于与 Win32 或其他原生 API 的互操作尤其重要
      • 您需要尽可能快的速度。 (在大量数据集较大的场景中,如果正确使用结构,您可以获得不错的加速)。
      • 您需要浪费更少的内存并在数组中拥有大量结构化数据。尤其是与数组结合使用时,您可以通过结构节省大量内存。
      • 您正在广泛使用指针。然后结构提供了许多有趣的特征。

      【讨论】:

      • 想解释一下是什么让结构本身不面向对象?
      • 为什么会被否决?结构的所有优点似乎都是正确的,我也同意一般的经验法则。
      • 我反对“毕竟我们谈论面向对象编程而不是面向结构编程”的说法。使用结构不会使其成为非 OO。
      • 如果问题得到纠正,我将删除反对票
      • 它们本质上是非 oo 的,因为结构总是按值复制。通常的按引用复制对于结构不存在/不可能。所以如果你想拥有例如与一条街道相关的两栋房屋,如果两者都是结构,则无法设计,因为房屋将具有街道结构的 2 个不同(尽管内容相同)副本。
      【解决方案5】:

      IMO 最重要的用例是小型复合实体的大型数组。想象一个包含 10^6 个复数的数组。或包含 1000x1000 24 位 RGB 值的二维数组。在这种情况下,使用 struct 而不是 classes 可以产生巨大的影响。

      编辑: 澄清一下:假设你有一个结构

      struct RGB 
      {
         public byte R,G,B;
      }
      

      如果您声明一个 1000x1000 RGB 值的数组,该数组将占用 3 MB 内存,因为值类型是内联存储的。

      如果您使用类而不是结构,则数组将包含 1000000 个引用。仅此一项就需要 4 或 8 MB(在 64 位机器上)内存。如果您使用单独的对象初始化所有项目,因此您可以单独修改值,您将有 1000000 个对象在托管堆上旋转以保持 GC 忙碌。每个对象都有 2 个引用的开销 (IIRC),即对象将使用 11/19 MB 的内存。总共是简单结构版本的 5 倍内存。

      【讨论】:

      • 你能解释一下结构会有什么不同吗?为什么?
      • nikie:感谢您提供准确的内存使用量。但是有多少业务应用程序使用这种数据呢? (您在示例中描述的是图像,不是吗?)
      • @Mikhail,我猜对结构的有限需求解释了为什么它们没有被大量使用。
      • 我喜欢这个例子——否则它避免了结构和对象差异之间的编程差异。在特殊情况下,如结构体存储在数组中,CLR 能够实现更有效的内存使用(这是否重要取决于应用程序和目标)。
      【解决方案6】:

      到目前为止所有好的答案...我只需要补充一点,根据定义,值类型不可为空,因此是在您不想为创建类的新实例而烦恼的场景中使用的良好候选者并将其分配给字段,例如...

      struct Aggregate1
      {
          int A;
      }
      
      struct Aggregate2
      {
          Aggregate1 A;
          Aggregate1 B;
      }
      

      请注意,如果 Aggregate1 是一个类,那么您将不得不手动初始化 Aggregate2 中的字段...

      Aggregate2 ag2 = new Aggregate2();
      ag2.A = new Aggregate1();
      ag2.B = new Aggregate1();
      

      这显然不是必需的,只要 Aggregate1 是一个结构...当您使用XmlSerializer 为明确的序列化/反序列化目的创建类/结构层次结构时,这可能会很有用许多看似神秘的异常在这种情况下,仅使用结构就会消失。

      【讨论】:

      • Nullable,其中 T 是值类型。
      • 是的,我知道..它是一种特殊情况...如果您使用它,您应该知道它可以为空!它是一个接受编译器特殊处理的结构。
      【解决方案7】:

      有时候,你只是想在组件之间传输数据,那么 struct 比 class 更好。例如只携带数据的数据传输对象(DTO)。

      【讨论】:

        【解决方案8】:

        与类的另一个区别是,当您将结构实例分配给变量时,您不仅复制了引用,而且实际上复制了整个结构。因此,如果您修改其中一个实例(无论如何都不应该修改,因为结构实例旨在是不可变的),则不会修改另一个实例。

        【讨论】:

        • 见下面我的帖子。这是一罐蠕虫。值类型或引用类型是不可变的,没有什么固有的。 (尽管我确实更喜欢将状态突变降至最低的代码。)
        猜你喜欢
        • 2013-04-21
        • 2011-09-27
        • 1970-01-01
        • 2020-01-03
        • 2016-12-10
        • 2011-08-17
        • 1970-01-01
        • 2012-07-24
        • 1970-01-01
        相关资源
        最近更新 更多