我啥时候应该使用结构而不是类?

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 中有方法时唯一的例外是回调

以上是关于我啥时候应该使用结构而不是类?的主要内容,如果未能解决你的问题,请参考以下文章

我啥时候应该选择 SAX 而不是 StAX?

我啥时候应该使用 CROSS APPLY 而不是 INNER JOIN?

我啥时候应该在子进程中使用`wait`而不是`communicate`?

我啥时候应该使用 StringComparison.InvariantCulture 而不是 StringComparison.CurrentCulture 来测试字符串是不是相等?

我啥时候应该继承 EnumMeta 而不是 Enum?

我啥时候应该使用导航控制器?