我啥时候应该使用结构而不是类?
Posted
技术标签:
【中文标题】我啥时候应该使用结构而不是类?【英文标题】:When should I use a struct instead of a class?我什么时候应该使用结构而不是类? 【发布时间】:2010-09-10 06:38:48 【问题描述】:MSDN 说,当您需要轻量级对象时,您应该使用结构。结构比类更可取时,还有其他情况吗?
有些人可能忘记了:
-
结构可以有方法。
结构不能被继承。
我了解结构体和类之间的技术差异,只是我对何时使用结构体没有很好的感觉。
【问题讨论】:
提醒一下 - 大多数人在这种情况下往往会忘记的是,在 C# 中,结构也可以有方法。 【参考方案1】:MSDN 有答案: Choosing Between Classes and Structures.
基本上,该页面为您提供了一个包含 4 项的清单,并说除非您的类型满足所有标准,否则使用一个类。
不要定义结构,除非 类型具有以下所有内容 特点:
它在逻辑上表示单个值,类似于原始类型 (整数、双精度等)。 它的实例大小小于 16 字节。 它是不可变的。 不必经常装箱。
【讨论】:
也许我遗漏了一些明显的东西,但我不太明白“不可变”部分背后的原因。为什么这是必要的?有人能解释一下吗? 他们可能推荐了这个,因为如果结构是不可变的,那么它具有值语义而不是引用语义就无关紧要了。仅当您在复制后对对象/结构进行变异时,区别才重要。 @DrJokepu:在某些情况下,系统会制作一个结构的临时副本,然后允许该副本通过引用更改它的代码传递;由于临时副本将被丢弃,因此更改将丢失。如果结构具有对其进行变异的方法,则此问题尤其严重。我坚决不同意可变性是使某物成为类的理由的观点,因为尽管 c# 和 vb.net 存在一些缺陷,但可变结构提供了任何其他方式都无法实现的有用语义;没有语义上的理由更喜欢不可变结构而不是类。 @Chuu:在设计 JIT 编译器时,微软决定优化用于复制 16 字节或更小的结构的代码;这意味着复制 17 字节结构可能比复制 16 字节结构慢得多。我认为没有特别的理由期望 Microsoft 将此类优化扩展到更大的结构,但重要的是要注意,虽然 17 字节结构的复制速度可能比 16 字节结构慢,但在许多情况下,大型结构可能比大型类对象,并且结构的相对优势随着结构的大小增长。 @Chuu:对大型结构应用与对类相同的使用模式很容易导致代码效率低下,但正确的解决方案通常不是用类替换结构,而是使用结构更有效率;最值得注意的是,应该避免按值传递或返回结构。只要合理,就将它们作为ref
参数传递。将具有 4,000 个字段的结构作为 ref 参数传递给更改一个的方法比将具有 4 个字段的结构按值传递给返回修改版本的方法要便宜。【参考方案2】:
我很惊讶我没有读过之前的任何答案,我认为这是最关键的方面:
当我想要一个没有标识的类型时,我会使用结构。例如一个 3D 点:
public struct ThreeDimensionalPoint
public readonly int X, Y, Z;
public ThreeDimensionalPoint(int x, int y, int z)
this.X = x;
this.Y = y;
this.Z = z;
public override string ToString()
return "(X=" + this.X + ", Y=" + this.Y + ", Z=" + this.Z + ")";
public override int GetHashCode()
return (this.X + 2) ^ (this.Y + 2) ^ (this.Z + 2);
public override bool Equals(object obj)
if (!(obj is ThreeDimensionalPoint))
return false;
ThreeDimensionalPoint other = (ThreeDimensionalPoint)obj;
return this == other;
public static bool operator ==(ThreeDimensionalPoint p1, ThreeDimensionalPoint p2)
return p1.X == p2.X && p1.Y == p2.Y && p1.Z == p2.Z;
public static bool operator !=(ThreeDimensionalPoint p1, ThreeDimensionalPoint p2)
return !(p1 == p2);
如果你有这个结构的两个实例,你不在乎它们是内存中的单个数据还是两个。你只关心他们持有的价值。
【讨论】:
使用结构的一个有趣的理由。我已经使用 GetHashCode 和 Equals 定义了与您在此处显示的类似的类,但是如果我将它们用作字典键,我总是必须小心不要改变这些实例。如果我将它们定义为结构,可能会更安全。 (因为那时键将是字段在结构成为字典键时的副本,因此如果我稍后更改原始键,键将保持不变。) 在您的示例中没关系,因为您只有 12 个字节,但请记住,如果该结构中有很多超过 16 个字节的字段,您必须考虑使用一个类并覆盖 GetHashCode 和 Equals方法。 DDD 中的值类型并不意味着您必须在 C# 中使用值类型【参考方案3】:Bill Wagner 在他的“有效 c#”(http://www.amazon.com/Effective-Specific-Ways-Improve-Your/dp/0321245660)一书中有一个关于此的章节。他总结了以下原则:
是类型数据存储的主要职责吗? 它的公共接口是否完全由访问或修改其数据成员的属性定义? 您确定您的类型永远不会有子类吗? 您确定您的类型永远不会被多态处理吗?
如果您对所有 4 个问题都回答“是”:使用结构。否则,使用 类。
【讨论】:
所以...数据传输对象 (DTO) 应该是结构体? 如果它符合上述 4 个标准,我会说是的。为什么需要以特定方式处理数据传输对象? @cruizer 取决于您的情况。在一个项目中,我们在 DTO 中有共同的审计字段,因此编写了一个其他人继承的基础 DTO。 除 (2) 之外的所有内容似乎都是出色的原则。需要看他的推理才能知道他所说的 (2) 到底是什么意思,以及为什么。 @ToolmakerSteve:您必须为此阅读本书。不要认为复制/粘贴一本书的大部分内容是公平的。【参考方案4】:当您想要值类型语义而不是引用类型时,请使用结构。结构是按值复制的,所以要小心!
另见之前的问题,例如
What's the difference between struct and class in .NET?
【讨论】:
【参考方案5】:在以下情况下使用类:
它的身份很重要。当通过值传递给方法时,结构会被隐式复制。 它将占用大量内存。 它的字段需要初始化器。 您需要从基类继承。 您需要多态行为;在以下情况下使用结构:
它的行为类似于原始类型(int、long、byte 等)。 它必须占用很小的内存。 您正在调用 P/Invoke 方法,该方法需要通过以下方式传入结构 价值。 您需要减少垃圾收集对应用程序性能的影响。 其字段只需初始化为其默认值。该值对于数值类型为零,对于布尔类型为 false,对于引用类型为 null。 请注意,在 C# 6.0 中,结构可以具有可用于初始化的默认构造函数 将结构的字段设置为非默认值。 您不需要从基类(ValueType 除外)继承 所有结构都继承)。 您不需要多态行为。【讨论】:
【参考方案6】:我会在以下情况下使用结构:
一个对象应该是只读的(每次你传递/分配一个结构时,它都会被复制)。只读对象在多线程处理方面非常有用,因为它们在大多数情况下不需要锁定。
一个物体很小而且寿命很短。在这种情况下,很有可能将对象分配到堆栈上,这比将其放在托管堆上要高效得多。更重要的是,对象分配的内存一旦超出其范围就会被释放。换句话说,垃圾收集器的工作量更少,内存使用效率更高。
【讨论】:
【参考方案7】:这是一个老话题,但想提供一个简单的基准测试。
我创建了两个 .cs 文件:
public class TestClass
public long ID get; set;
public string FirstName get; set;
public string LastName get; set;
和
public struct TestStruct
public long ID get; set;
public string FirstName get; set;
public string LastName get; set;
运行基准测试:
创建 1 个测试类 创建 1 个 TestStruct 创建 100 个测试类 创建 100 个 TestStruct 创建 10000 个测试类 创建10000个TestStruct结果:
BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362
Intel Core i5-8250U CPU 1.60GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.101
[Host] : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT [AttachedDebugger]
DefaultJob : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT
| Method | Mean | Error | StdDev | Ratio | RatiosD | Rank | Gen 0 | Gen 1 | Gen 2 | Allocated |
|--------------- |---------------:|--------------:|--------------:|----------:|--------:|-----:|---------:|------:|------:|----------:|
| UseStruct | 0.0000 ns | 0.0000 ns | 0.0000 ns | 0.000 | 0.00 | 1 | - | - | - | - |
| UseClass | 8.1425 ns | 0.1873 ns | 0.1839 ns | 1.000 | 0.00 | 2 | 0.0127 | - | - | 40 B |
| Use100Struct | 36.9359 ns | 0.4026 ns | 0.3569 ns | 4.548 | 0.12 | 3 | - | - | - | - |
| Use100Class | 759.3495 ns | 14.8029 ns | 17.0471 ns | 93.144 | 3.24 | 4 | 1.2751 | - | - | 4000 B |
| Use10000Struct | 3,002.1976 ns | 25.4853 ns | 22.5920 ns | 369.664 | 8.91 | 5 | - | - | - | - |
| Use10000Class | 76,529.2751 ns | 1,570.9425 ns | 2,667.5795 ns | 9,440.182 | 346.76 | 6 | 127.4414 | - | - | 400000 B |
【讨论】:
【参考方案8】:当我想将一些值组合在一起以从方法调用中传回内容时,我总是使用结构体,但在阅读了这些值之后,我不需要将它用于任何事情。就像保持清洁的一种方式。我倾向于将结构中的东西视为“一次性”,而将类中的东西视为更有用和“功能性”
【讨论】:
在设计原则中保持“干净”意味着您不会随意从函数返回多个值。预测调用者想要什么是一种反模式。【参考方案9】:如果一个实体是不可变的,那么是使用结构还是类的问题通常是性能问题而不是语义问题。在 32/64 位系统上,类引用需要 4/8 个字节来存储,而不管类中的信息量如何;复制一个类引用需要复制 4/8 个字节。另一方面,每个 distinct 类实例除了它持有的信息和引用它的内存成本外,还会有 8/16 字节的开销。假设需要一个包含 500 个实体的数组,每个实体包含四个 32 位整数。如果实体是结构类型,则该数组将需要 8,000 个字节,无论所有 500 个实体是否全都相同、全不同或介于两者之间。如果实体是类类型,则包含 500 个引用的数组将占用 4,000 个字节。如果这些引用都指向不同的对象,则每个对象将需要额外的 24 个字节(所有 500 个对象需要 12,000 个字节),总共 16,000 个字节——是结构类型存储成本的两倍。另一方面,代码创建了一个对象实例,然后复制了对所有 500 个数组槽的引用,该实例的总成本为 24 字节,数组为 4,000 字节——总共 4,024 字节。一大笔节省。很少有情况会像最后一种情况一样有效,但在某些情况下,可以将一些引用复制到足够多的数组槽以使这种共享变得有价值。
如果实体应该是可变的,那么使用类还是结构的问题在某些方面更容易。假设“Thing”是一个结构或类,它有一个名为 x 的整数字段,并且执行以下代码:
事物 t1,t2; ... t2 = t1; t2.x = 5;是否希望后一个语句影响 t1.x?
如果 Thing 是类类型,则 t1 和 t2 将是等价的,这意味着 t1.x 和 t2.x 也将是等价的。因此,第二条语句将影响 t1.x。如果 Thing 是结构体类型,则 t1 和 t2 将是不同的实例,这意味着 t1.x 和 t2.x 将引用不同的整数。因此,第二条语句不会影响 t1.x。
可变结构和可变类具有根本不同的行为,尽管 .net 在处理结构突变时有一些怪癖。如果想要值类型的行为(意味着“t2=t1”会将数据从 t1 复制到 t2,同时将 t1 和 t2 作为不同的实例),并且如果可以忍受 .net 处理值类型的怪癖,请使用一个结构。如果一个人想要值类型语义,但 .net 的怪癖会导致应用程序中的值类型语义被破坏,请使用一个类并喃喃自语。
【讨论】:
【参考方案10】:除了上面的优秀答案:
结构是值类型。
它们永远不能设置为Nothing。
设置结构 = Nothing ,会将其所有值类型设置为其默认值。
【讨论】:
【参考方案11】:当您并不真正需要行为,但您需要比简单数组或字典更多的结构时。
跟进 这就是我对一般结构的看法。我知道他们可以有方法,但我喜欢保持这种整体精神上的区别。
【讨论】:
你为什么这么说?结构可以有方法。【参考方案12】:正如@Simon 所说,结构提供“值类型”语义,因此如果您需要与内置数据类型类似的行为,请使用结构。由于结构是通过副本传递的,因此您需要确保它们的大小很小,大约 16 个字节。
【讨论】:
【参考方案13】:嗯……
我不会使用垃圾收集作为支持/反对使用结构与类的论据。托管堆的工作方式很像堆栈——创建一个对象只是将它放在堆的顶部,这几乎与在堆栈上分配一样快。此外,如果一个对象是短暂的并且无法在 GC 循环中存活,则释放是免费的,因为 GC 仅适用于仍可访问的内存。 (搜索MSDN,.NET内存管理有一系列文章,懒得去挖了)。
在我使用结构体的大多数时间里,我都会为此自责,因为后来我发现拥有引用语义会让事情变得更简单。
无论如何,上面发布的 MSDN 文章中的这四点似乎是一个很好的指导方针。
【讨论】:
如果你有时需要一个结构体的引用语义,只需声明class MutableHolder<T> public T Value; MutableHolder(T value) Value = value;
,然后MutableHolder<T>
将是一个具有可变类语义的对象(如果T
是一个struct 或不可变的类类型)。【参考方案14】:
结构在堆栈而不是堆上,因此它们是线程安全的,应该在实现传输对象模式时使用,你永远不想在堆上使用对象它们是易失性的,在这种情况下你希望使用调用堆栈,这是使用结构的基本案例,我对这里的所有答案感到惊讶,
【讨论】:
【参考方案15】:✔️ 如果类型的实例很小且通常寿命短或通常嵌入其他对象中,请考虑定义结构而不是类。
【讨论】:
【参考方案16】:我认为最好的答案就是当你需要的是属性集合时使用 struct,当它是属性和行为的集合时使用 class。
【讨论】:
结构也可以有方法 当然可以,但是如果你需要方法,99% 的可能性是你不正确地使用了结构而不是类。我发现在 struct 中有方法时唯一的例外是回调以上是关于我啥时候应该使用结构而不是类?的主要内容,如果未能解决你的问题,请参考以下文章
我啥时候应该使用 CROSS APPLY 而不是 INNER JOIN?
我啥时候应该在子进程中使用`wait`而不是`communicate`?
我啥时候应该使用 StringComparison.InvariantCulture 而不是 StringComparison.CurrentCulture 来测试字符串是不是相等?