哪个更适合数据存储,结构体还是类?

34
我们已经在SO上看到了很多讨论关于在C#中使用class和struct的问题,大多数都结束于结论认为它们是堆/栈内存分配,并推荐在小数据结构中使用struct。
现在我面临一个选择简单数据存储的情况。目前在我们的应用程序中有成千上万个类,只充当简单的数据存储(仅公开暴露字段)并在不同的模块和服务之间传递。
根据我的理解,我觉得出于性能原因,与其使用类而不是结构体更好。因为它们只是充当数据存储的简单数据结构。
在继续之前,我需要一些有经验的人的专业建议。
你的理解正确吗?
我已经看到大多数ORM都将类作为数据存储方式。所以我怀疑应该有理由使用类而不是结构体。那会是什么呢?

ORM对象不是“普通”的类似于结构体的数据,因为它们包装在另一个数据存储器周围,抽象了数据库组件,使其看起来像您只是访问一个对象。所有这些都是通过方法和反射完成的,这些都是类的特性。 - Andres Jaan Tack
9个回答

62

根据以下标准进行选择:

  • 引用类型 vs 值类型语义。如果两个对象仅在它们是相同的对象时才相等,则表明具有引用类型语义 => 类。如果其成员的值定义了相等性(例如,如果两个DateTimes表示相同的时间点,则它们即使是两个不同的对象也相等),则表明具有值类型语义 => 结构。
  • 对象占用的内存空间。如果对象很大且频繁分配,则将其作为结构体会更快地消耗堆栈,因此我更愿意将其作为类。相反,我宁愿避免小值类型的GC损失;因此将它们作为结构体。
  • 能否使该对象不可变? 我发现结构体非常适合“值对象”-来自DDD书籍。
  • 根据使用情况是否会面临装箱和拆箱的惩罚? 如果是,则选择类。

16

结构体相对于类的一个不太为人所知的优势是,在结构体中有GetHashCode和Equals的自动实现。这在需要字典键时非常有用。

结构体实现GetHashCode和Equals基于结构体实例的二进制内容+反射引用成员(如字符串成员和其他类的实例)

因此,以下代码适用于GethashCode / Equals:

public struct Person
{
    public DateTime Birthday { get; set; }
    public int Age{ get; set; }
    public String Firstname { get; set; }
}
class Program
{
    static void Main(string[] args)
    {
        Person p1 = new Person { Age = 44, Birthday = new DateTime(1971, 5, 24), Firstname = "Emmanuel" };
        Person p2 = new Person { Age = 44, Birthday = new DateTime(1971, 5, 24), Firstname = "Emmanuel" };
        Debug.Assert(p1.Equals(p2));
        Debug.Assert(p1.GetHashCode() == p2.GetHashCode());
    }
}

当Person是一个结构体时,两个断言都成功。 如果Person是类而不是结构体,则两个断言都失败。

参考: https://msdn.microsoft.com/en-Us/library/2dts52z7%28v=vs.110%29.aspx

祝好,愉快编程!


7

在定义类时不应该使用结构体,而应该定义为不可变的结构。如果你认为你的对象将是小而不可变的,那么可以将它们定义为结构体,否则请让它们成为类。


不管一个人是否喜欢让任何东西可变,如果某个东西是可变的,它应该成为一个类的想法是完全错误的。假设Rectangle是一个可变的类而不是一个结构体。如果尝试执行MyControl.Bounds.Width += 20;,那么应该发生什么是完全不清楚的。Bounds是一个结构体而不是一个类的事实使得以上操作无效(甚至连编译都无法通过),我们必须将Bounds复制到一个临时的Rectangle中进行修改,然后使用新的矩形设置bounds,或者找到其他方式... - supercat
更新控件的边界(实际上,有一个目的是更新Bounds中任意组合字段的方法,但必须搜索Control的定义;相比之下,将Bounds复制到临时Rectangle中,修改它,然后将其设置回来,正如我们只需要知道Rectangle是一个具有可变字段的结构体,而Bounds是类型为Rectangle的读写属性一样,这种方法完全符合预期。无需进一步的文档说明。) - supercat

6

我似乎永远也记不清楚,结构体(struct)和类(class)具体有什么不同,但它们确实存在微妙的差别。事实上,有时候它们会悄悄地给你带来问题。

所以,除非你知道自己在干什么,否则就坚持使用类吧。

我知道这听起来有点新手,我知道我现在应该去查一下它们之间的区别并在这里展示它们——但这已经被其他人做过了。我想说的是,添加不同类型的对象会增加语义负担,增加一些额外的复杂性,你应该谨慎考虑。

如果我没记错的话,其中一个最大的问题是结构体的值语义:传递它们将导致创建不同的对象(因为它们是按值传递的)。如果你在某个地方更改了某个字段,请注意在所有其他地方该字段没有被更改!这就是为什么每个人都建议对结构体进行不可变性处理!

编辑:对于您描述的情况,结构体将不起作用


1
+1;结构体提供了好处,但也带来了限制。不理解这些限制可能会抵消这些好处。不幸的是,要得到确切的需要考虑的限制的简单答案并不容易——因此,结构体经常在不理解它们的情况下使用。通常,类将提供几乎同样好的性能和更多的功能。类是安全的选择。 - STW
@Darren,“传递它们会导致不同的对象”-这是一个很好的观点...我现在真正理解了这个问题.. :) - RameshVel
@Darren... 我有一个愚蠢的疑问..我们可以将结构体作为ref参数传递..这样能解决不可变性的问题吗? - RameshVel
可能会出现这种情况。但我不会费心去考虑这个问题,因为这只会引发难以找到的愚蠢错误。你打算如何确保没有人会忘记使用 ref 关键字呢? - Daren Thomas
一个结构体基本上是一组存储位置粘在一起。如果 PQ 是类型为 Point 的变量(它是一个带有字段 XY 的结构体),那么 P=Q; 就相当于 P.X=Q.X; P.Y=Q.Y;。请注意,复制结构体将复制所有字段,包括公共和私有字段。如果你从这个角度来看待事物,就会明显地发现结构体的行为与类不同。把结构体和类视为“几乎相同”是不明智的。.net 隐式地为每个结构体定义了一个同名的类,该类具有相同的字段、属性和方法,这可能会让结构体看起来像类... - supercat
...这可能会导致一些混淆。如果将Point强制转换为Object,它将被转换为类型为Point的类对象。如果您将对象转换回Point并将其存储在该类型的变量中,则该对象的字段XY将被复制到Point变量中。 - supercat

4
一个类对象的优势在于可以传递对它的引用,如果这个引用超出了代码范围,那么它的作用域和生命周期就是无限的。一个结构体的优势在于虽然可以传递短暂的引用,但是不能传递永久的随意引用,这有助于避免担心是否存在这样的引用。
有人建议可变数据持有者不应该是结构体,我完全不同意。为了保存数据而存在的实体,在许多情况下应该是结构体,特别是如果它们是可变的。Eric Lippert 多次发帖说他认为可变值类型是邪恶的(搜索标签“mutable”和“struct”)。当然,.net 允许使用可变结构体进行某些操作,这本不应该,同时也不方便地禁止了一些本应该允许的东西,但是 POD(“Plain Old Data”)结构体没有变异方法,而是通过公共字段公开其整个状态,其行为具有非常有用的一致性,这种一致性与任何其他数据类型都不共享。使用 POD 结构体可能会让不熟悉它们如何工作的人感到困惑,但是对于任何熟悉它们的人来说,程序将变得更加可读。
例如,考虑以下代码,假设 EmployeeInfoStruct 仅包含值类型和不可变类类型(如 String):
[employeeInfoStruct 是包含以下字段的结构体] public Decimal YearlyBonus;
[someEmployeeContainer 是一个包括以下方法的类的实例] EmployeeInfoStruct GetEmployeeInfo(String id); // 只是签名--代码无关
[some other method 使用以下代码] EmployeeInfoStruct anEmployee = someEmployeeContainer.GetEmployeeInfo("123-45-6789"); anEmployee.YearlyBonus += 100;
Eric Lippert 抱怨上面的代码会修改 anEmployee 中的值,但是这个改变不会对容器产生任何影响。我认为这是一件好事——任何了解结构体工作方式的人都可以查看上面的代码,并知道对结构体变量的写入将影响该变量,但除非程序稍后使用某个其他方法(例如 SetEmployeeInfo)将该变量存储在某个地方,否则不会影响任何其他内容。
现在用 EmployeeInfoClass 替换 EmployeeInfoStruct,它具有类型为 YearlyBonus 的读/写属性。仅使用上面的信息,可以说什么关于 someEmployeeContainer 和 anEmployee 之间的关系?根据 anEmployee 类的实现(除非 EmployeeInfoClass 被封闭,否则可能或可能不是 EmployeeInfoClass)和 someEmployeeContainer,对象之间的关系可能是任何东西。对其中一个进行写入可能会:
  1. 对另一个没有影响
  2. 以“自然”方式更新另一个
  3. 以任意方式破坏另一个

对于只包含值类型或不可变类字段的结构体,其语义始终是#1。无需查看结构体本身的代码或容器的代码即可知道。相比之下,如果anEmployee.Salary或someEmployeeContainer.GetEmployee是虚拟的,则真正知道语义将是不可能的。

需要注意的是,如果结构体很大,则按值传递或从函数返回它们可能很昂贵。通常最好在可能的情况下将大型结构体作为ref参数传递。虽然内置集合并没有很好地促进这种使用方式,但它可以使使用数百字节的结构体比使用类更便宜。


3
结构体不可变的评论是正确的。这就是可能会让你措手不及的地方。你可以定义具有字段设置器的结构体,但当你改变一个字段的值时,会创建一个新实例。因此,如果你持有对旧对象的引用,它仍将引用旧值。出于这个原因,我不喜欢使用可变的结构体,因为这可能会产生微妙和复杂的错误(特别是如果你使用复杂的复合语句)。
另一方面,也有很多使用不可变状态类的好理由(比如字符串)。

3
“当您更改字段值时,将创建一个新实例”- 我认为这句话有点误导。对于可变结构体(即具有字段设置器的结构体),您仅会更改该字段。指向结构体的任何代码都将更改它。对于不可变结构体,您需要明确提供一种使用不同字段值构造新实例的方法。 - vgru

2

我记得在MSDN上有一条建议,即结构体的大小不应超过16或21个字节。我正在寻找链接,但尚未找到。

主要含义是,一旦在您的数据类型中有一个字符串,请将其视为类而不是结构体。否则,结构体不应该包含太多内容。


字符串是一个类,因此它的内容在堆中,而不是栈中。如果你将字符串作为结构体的字段,那么它只占用这个结构体的 4 字节空间(一个指针)。 - VladV
1
处理值类型的代码在它们不超过16字节时采用了特殊优化。因此,使用一个17字节的结构体可能比使用一个16字节的结构体要慢得多。另一方面,有些结构体操作比类操作要便宜得多,甚至一个100字节的结构体可能比一个100字节的类更高效(某些结构体操作比类操作更快,而其他一些则更慢;结构体或类哪个执行速度更快将取决于所执行操作的组合)。 - supercat

2

我认为你想法正确。结构体的作用是模拟数据类型。它们是基于值而非基于引用的。如果你查看大多数基本数据类(如int、double、decimal等)的MSDN文档,它们都是基于结构体的。然而,正因为如此,结构体不应过度使用。一旦实例化结构体,它就会分配所有内容的存储空间,而类只会分配指向内部所有内容的引用的存储空间。如果数据的大小足够小,这不是问题,那么结构体是最好的选择。如果这是一个问题,请使用类。如果你不确定,最好还是坚持你熟悉的。


0
如果您有低延迟要求并且有很多对象,慢垃圾回收可能会成为一个问题。在这种情况下,结构体可以非常有帮助,因为垃圾收集器不需要扫描值类型的层次结构,如果该值类型不包含任何引用类型。
您可以在此处找到基准测试:http://00sharp.wordpress.com/2013/07/03/a-case-for-the-struct/

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