何时使用结构体比类更好?

15
重复问题:C#中何时使用结构体? 在Microsoft .NET 2.0 / 3.5中,是否有实际理由使用结构体而不是某些类?
“结构体和类之间的区别是什么?” - 这可能是“ .NET开发人员”职位面试中最常见的问题。 面试官认为唯一正确的答案是“结构体分配在堆栈上,而类则分配在堆上”,关于这个问题就没有进一步的问题了。
一些谷歌搜索显示:

a) 结构体与类相比具有众多限制和没有额外的能力
b) 在非常特殊的情况下,如:

  • 数据块大小小于16字节
  • 没有大量的装箱/取消装箱
  • 结构体的成员几乎是不变的
  • 整个数据集不大(否则会导致堆栈溢出)

堆栈(因此是结构体)可能更快。

(如果这个列表是错误或不完全的,请纠正/添加) 据我所知,大多数典型的商业项目(ERM,会计,银行解决方案等)甚至没有定义一个结构体,所有自定义数据类型都被定义为类。 这种方法是否有错误或不完美之处?

注意:问题是关于常规商业应用程序,请不要列出“不寻常”的情况,如游戏开发,实时动画,向后兼容性(COM / Interop),未管理的代码等 - 这些答案已在类似的问题下回答:何时使用结构体?

9
"结构体分配在栈上,类分配在堆上"。奇怪的是,面试官认为这是正确答案,但实际上并不是正确答案。你应该阅读http://blogs.msdn.com/ericlippert/archive/2009/04/27/the-stack-is-an-implementation-detail.aspx。 - Joren
@Joren,谢谢你提供的链接...很有价值的阅读。 - Sandeep Datta
5
一些面试官认为某些特定答案是“正确答案”,如果你指出他们的答案是错误的,他们可能会非常敌对。更令人惊讶的是,面试官会问问题来引出特定的答案;当我面试时,我试图问一些旨在测试应聘者智能推理、解决模糊不清的问题和理解复杂代码的问题。在大多数开发工作中,背诵正确的小知识答案并不是核心要求。 - Eric Lippert
Joren:这被认为是正确答案,因为Richter的《CLR via C#》书中有(简要)提到,并且这本书对于一些(很多?)面试官来说是圣经。 Eric:我完全同意你关于琐事(不)重要性的看法,但我只找到了几家(在我的地区)不问琐事的公司。 - coder
8个回答

12
据我所知,大多数典型的商业项目(企业资源计划(ERM)、会计、银行解决方案等)甚至都没有定义一个结构,所有自定义数据类型都是定义为类。这种方法有什么问题或不足之处吗?
不!这样做完全正确。你的一般规则应该是默认使用对象。毕竟我们谈论的是面向对象编程,而不是结构体编程(结构体本身缺少一些面向对象原则,如继承和抽象)。
然而,如果:
- 您需要对内存使用量进行精确控制(根据大小,结构体使用的内存比对象少得多); - 您需要精确控制内存布局。这对于与Win32或其他本地API进行互操作尤其重要; - 您需要最快的速度。(在许多具有较大数据集的情况下,正确使用结构体可以获得相当大的加速); - 您需要节省更少的内存并在数组中拥有大量结构化数据。特别是与数组结合使用时,结构体可以大量节省内存; - 您正在广泛使用指针。那么结构体提供了很多有趣的特性。

为什么会被踩?所有结构体的优点似乎都是正确的,我也同意一般的经验法则。 - Niki
我反对这种说法:“毕竟我们谈论的是面向对象编程而不是结构化编程。”使用结构体并不意味着它不是面向对象的。 - Sandeep Datta
如果这个问题得到了纠正,我会取消踩的评价。 - Sandeep Datta
2
它们本质上是非面向对象的,因为结构体始终是按值复制的。通常的按引用复制对于结构体不存在/不可能。因此,如果您想要有例如两个房屋与一个街道相关联,如果两者都是结构体,则无法设计,因为房屋将具有街道结构体的2个不同(尽管内容相同)副本。 - Foxfire
啊...好的,我完全忘记了结构体的继承问题...所以你是对的,它们不是面向对象的,但它们仍然是基于对象的http://en.wikipedia.org/wiki/Object-based 通过封装(私有成员),仍然支持抽象化。 - Sandeep Datta
显示剩余10条评论

8

我认为最重要的应用场景是包含大量小复合实体的数组。想象一下包含10^6个复数的数组,或者一个包含1000x1000个24位RGB值的二维数组。在这些情况下,使用struct而不是class可以产生巨大的差异。

编辑: 为了澄清:假设你有一个结构体

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

如果你声明一个1000x1000的RGB值数组,由于值类型存储在内联中,这个数组将占用精确的3 MB内存。
如果你使用类代替结构体,那么数组将包含1000000个引用。仅这一点就会占用4或8 MB(64位机器上)内存。如果你使用单独的对象初始化所有项,以便可以单独修改值,则将有1000000个对象在托管堆上旋转以使GC忙碌。每个对象都有一个开销(如果我没记错的话),即对象将使用11/19 MB内存。总共比简单结构体版本多占用5倍的内存。

你能解释一下结构体会带来什么不同以及原因吗? - G-Wiz
尼基:感谢您提供精确的内存使用量。但是有多少商业应用程序使用这种类型的数据?(您在示例中描述的是图像,对吗?) - coder
@Mikhail,我猜结构体的需求有限就是为什么它们没有被广泛使用的原因。 - user159335
我喜欢这个例子——它避免了结构体和对象的编程差异。在特殊的情况下,例如当结构体存储在数组中时,CLR 能够实现更有效的内存使用(是否重要取决于应用程序和目标)。 - user166390

6

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

当然,这前提是我们在谈论安全、托管代码。


你能澄清一下吗?不是所有在线程内部分配的东西,而不是外部引用的东西都处于同样的情况吗?无论是值类型还是引用类型。如果我在一个线程中使用"new"关键字创建一个对象,那么其他线程就不知道它在哪里,也无法销毁它,所以这样做也是安全的,对吗? - user159335
是的,但在托管代码中,堆栈上的值没有任何逃逸的可能。对象可能通过各种方式被公开。如果它们没有被公开,那么一切都很好,但您必须自己验证这一点。对于值类型,保证不会发生这种情况。 - Brian Rasmussen
一般来说这是正确的,但并不是所有值类型都分配在堆栈上,一旦你在C#中使用指针,你的线程安全性就会丢失。 - Foxfire
@Foxfire:没错。如果您在代码中使用不安全的内容,那就是另一套规则了。 - Brian Rasmussen
当您给一个负评时,请留下评论。 - Brian Rasmussen

3

与类的另一个不同之处在于,当您将结构实例分配给变量时,您不仅仅是复制引用,而是复制整个结构。因此,如果您修改其中一个实例(无论如何,由于结构实例旨在是不可变的,所以您不应该这样做),则另一个实例不会被修改。


请看下面我的帖子。它是一个棘手的问题。关于值类型或引用类型本身并没有什么固有的不可变性。(尽管我更喜欢最小化状态改变的代码。) - user166390

1
到目前为止,所有的答案都很好...我只需要补充一点,按照定义,值类型是不可为空的,因此在你不想费心创建一个新类实例并将其分配给字段的情况下,值类型是一个很好的选择,例如...
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>,其中T是值类型。 - user166390
是的,我知道...这是一个特殊情况...如果你在使用它,你应该知道它是可空的!它是一个接受编译器特殊处理的结构体。 - Sandeep Datta

1
如果一个类型的目的是将一小组独立的值(例如点的坐标、枚举字典条目的键和相关值、六项二维转换矩阵等)用胶带粘合在一起,最好的表示方式从效率和语义的角度来看,可能是一个可变的暴露字段结构。请注意,这代表了一种非常不同的使用场景,与结构体表示单个统一概念的情况(例如Decimal或DateTime)相比,Microsoft提供的关于何时使用结构体的建议仅适用于后者。Microsoft所描述的“不可变”结构体风格只适合表示单个统一的概念;如果需要表示一小组独立的值,则适当的替代方案不是不可变类(性能较差),也不是可变类(在许多情况下会提供不正确的语义),而是一个暴露字段结构(如果使用正确,将提供更好的语义和性能)。例如,如果有一个名为MyTransform的结构体,它保存一个二维转换矩阵,则可以使用以下方法:
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的字段就足以知道第一个方法修改这些字段并且没有其他副作用。相比之下,要知道其他方法的作用,就必须检查构造函数的代码。


0

已经有一些优秀的答案涉及使用结构体与类的实用性以及相互之间的区别,但我认为你最初提到的结构体是不可变的这一点是为什么类在LOB应用程序的高级设计中更常用的一个很好的论据。

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

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


-3
有时候,您只需在组件之间传输数据,那么结构体比类更好。例如,只携带数据的数据传输对象(DTO)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接