什么时候应该使用结构而不是类?


303

MSDN说,当您需要轻量级对象时,应该使用结构。在某个结构比一类更可取的情况下,还有其他情况吗?

有些人可能忘记了:

  1. 结构可以有方法。
  2. 结构不能被继承。

我了解结构和类之间的技术差异,只是对何时使用结构感觉不太好。


提醒一下-在这种情况下,大多数人倾向于忘记的是,在C#结构中也可以具有方法。
彼得·k。

Answers:


296

MSDN的答案是: 在类和结构之间进行选择

基本上,该页面为您提供了4个项目的清单,并说要使用一个类,除非您的类型满足所有条件。

除非类型具有以下所有特征,否则请不要定义结构:

  • 它在逻辑上表示一个值,类似于基本类型(整数,双精度型,等等)。
  • 它的实例大小小于16个字节。
  • 这是一成不变的。
  • 不必经常装箱。

1
也许我缺少一些显而易见的东西,但是我并没有完全理解“不变”部分背后的原因。为什么这是必要的?有人可以解释吗?
Tamas Czinege,2009年

3
他们可能建议这样做,因为如果该结构是不可变的,那么它具有值语义而不是引用语义就无关紧要。仅当您在复制后变异对象/结构时,区别才有意义。
Stephen C. Steel

@DrJokepu:在某些情况下,系统会制作一个结构的临时副本,然后允许将该副本通过引用传递给更改它的代码。由于临时副本将被抛弃,因此更改将丢失。如果结构具有使它变异的方法,则此问题尤其严重。我强烈不同意可变性是使某类成为类的原因的观点,因为尽管c#和vb.net存在一些缺陷,但可变结构提供了有用的语义,这些语义无法通过其他方式实现。没有语义上的原因比类更喜欢不变的结构。
supercat

4
@Chuu:在设计JIT编译器时,Microsoft决定优化代码以复制16字节或更小的结构;这意味着复制17字节结构可能比复制16字节结构慢得多。我没有特别的理由期望微软将这种优化扩展到更大的结构,但是要注意的是,虽然17字节结构的复制速度可能比16字节结构的复制慢,但在许多情况下,大型结构可能比16字节结构更有效。大型类对象,并且结构的相对优势随结构的大小而增长
2013年

3
@Chuu:对大型结构使用与对类相同的用法模式容易导致代码效率低下,但是正确的解决方案通常不是用类替换结构,而是更有效地使用结构;最值得注意的是,应避免按值传递或返回结构。尽可能ref合理地将它们作为参数传递。将具有4,000个字段的结构作为ref参数传递给一个方法,将其更改会比将具有4个字段的按值传递给返回修改版本的方法便宜。
2013年

54

令我惊讶的是,我没有阅读任何先前的答案,我认为这是最关键的方面:

我想要没有身份的类型时使用结构。例如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);
    }
}

如果您有此结构的两个实例,则不必关心它们是内存中的单个数据还是两个。您只关心它们所拥有的价值。


4
谈不上话题,但是为什么当obj不是ThreeDimensionalPoint时为什么抛出ArgumentException?在这种情况下,您不应该只返回false吗?
Svish 2014年

4
没错,我太渴望了。return false是应该在那里的,现在进行纠正。
AndreiRînea2014年

使用结构的一个有趣原因。我用与定义的GetHashCode和Equals相似的类制作了类似于您在此处显示的内容,但是如果我将它们用作字典键,则始终必须小心不要使这些实例发生变异。如果我将它们定义为结构,可能会更安全。(因为密钥将是结构成为字典密钥的那一刻的字段的副本,所以如果以后我更改原始密钥,密钥将保持不变。)
ToolmakerSteve18年

在您的示例中可以,因为您只有12个字节,但是请记住,如果该结构中有很多字段超过16个字节,则必须考虑使用一个类并重写GetHashCode和Equals方法。
Daniel Botero Correa

DDD中的值类型并不意味着您必须在C#中使用值类型
Darragh

27

比尔·瓦格纳(Bill Wagner)在他的“有效的c#”一书中有一章对此进行了介绍(http://www.amazon.com/Effective-Specific-Ways-Improve-Your/dp/0321245660)。他总结使用以下原则:

  1. 类型数据存储的主要责任是什么?
  2. 它的公共接口是否完全由访问或修改其数据成员的属性定义?
  3. 您确定您的类型永远不会有子类吗?
  4. 您确定您的类型永远不会被多态处理吗?

如果您对所有4个问题回答“是”:请使用结构。否则,使用一个类。


1
那么...数据传输对象(DTO)应该是结构吗?
cruizer

1
如果满足上述4个条件,我会说是的。为什么需要以特定方式处理数据传输对象?
巴特·吉森斯

2
@cruizer取决于您的情况。在一个项目中,我们的DTO中具有通用的审计字段,因此编写了基本DTO,其他人也从中继承了DTO。
Andrew Grothe

1
除(2)以外的所有方法似乎都是极好的原则。将需要查看他的推理来知道他确切地表示(2)的含义,以及原因。
ToolmakerSteve

1
@ToolmakerSteve:您将必须阅读该书。认为复制/粘贴一本书的大部分不公平。
Bart Gijssens,


11

在以下情况下,我将使用结构:

  1. 一个对象应该是只读的(每次您传递/分配一个结构时,它都会被复制)。对于多线程处理,只读对象非常有用,因为在大多数情况下,它们不要求退出锁定。

  2. 一个物体很小,寿命短。在这种情况下,很有可能将对象分配到堆栈上,这比将其放在托管堆上的效率要高得多。而且,对象超出其范围后,将立即释放该对象分配的内存。换句话说,对于垃圾收集器,它的工作量更少,并且内存使用效率更高。


10

在以下情况下使用课程:

  • 它的身份很重要。当结构通过值传递给方法时,会隐式复制。
  • 它将占用大量内存。
  • 它的字段需要初始化器。
  • 您需要从基类继承。
  • 您需要多态行为;

在以下情况下使用结构:

  • 它将像原始类型一样工作(int,long,byte等)。
  • 它必须具有较小的内存空间。
  • 您正在调用P / Invoke方法,该方法需要按值传递结构。
  • 您需要减少垃圾回收对应用程序性能的影响。
  • 仅需将其字段初始化为其默认值。对于数字类型,此值将为零;对于布尔类型,此值将为false;对于引用类型,此值为null。
    • 请注意,在C#6.0中,结构可以具有默认构造函数,该构造函数可用于将结构的字段初始化为非默认值。
  • 您不需要从基类继承(除了ValueType之外,所有结构都从基类继承)。
  • 您不需要多态行为。

5

当我想将一些值组合在一起以将方法调用中的内容传递回去时,我一直使用结构,但是在读取这些值之后,就不需要将其用于任何东西。只是保持物品清洁的一种方式。我倾向于将结构中的事物视为“可丢弃”,而将类中的事物视为更为有用和“实用”


4

如果一个实体将是不可变的,那么使用结构还是类的问题通常是性能而非语义之一。在32/64位系统上,无论类中的信息量如何,类引用都需要4/8字节来存储。复制一个类引用将需要复制4/8字节。另一方面,每个不同类实例除了拥有的信息和对其引用的内存开销外,还将有8/16字节的开销。假设有人想要一个由500个实体组成的数组,每个实体包含四个32位整数。如果实体是结构类型,则该数组将需要8,000个字节,而不管所有500个实体都是相同,全部不同还是介于两者之间。如果实体是类类型,则500个引用的数组将占用4,000个字节。如果这些引用都指向不同的对象,则每个对象将需要额外的24个字节(对于所有500个对象,则需要12,000个字节),总共需要16,000个字节,这是结构类型的存储成本的两倍。另一方面,在代码中创建了一个对象实例,然后将引用复制到所有500个阵列插槽中,该实例的总成本为24个字节,而总成本为4,数组的000-共4,024字节。大量节省。很少有一种情况比最后一种情况更好,但是在某些情况下,可能可以将一些引用复制到足够的阵列插槽中,以使这种共享值得。

如果该实体应该是可变的,则在某些方面更容易使用类或结构的问题。假设“事物”是具有称为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的怪癖会导致一个人的应用程序中的值类型语义被破坏,请使用一类,然后喃喃自语。



2

当您实际上不需要行为,但是需要比简单数组或字典更多的结构。

后续操作 这就是我通常对结构的看法。我知道他们可以有方法,但是我喜欢保持整体的精神差异。


为什么这么说 结构可以有方法。
Esteban Araya

2

正如@Simon所说,结构提供“值类型”语义,因此,如果您需要与内置数据类型相似的行为,请使用结构。由于结构是通过副本传递的,因此您要确保它们的大小很小,大约16个字节。


2

这是一个古老的话题,但希望提供一个简单的基准测试。

我创建了两个.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个TestClass
  • 创建1个TestStruct
  • 创建100个TestClass
  • 创建100个TestStruct
  • 创建10000个TestClass
  • 创建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 |

1

嗯...

我不会使用垃圾回收作为/反对使用struct vs类的参数。托管堆的工作原理类似于堆栈-创建对象只是将其放在堆的顶部,这几乎与在堆栈上分配的速度一样快。此外,如果对象是短暂的并且无法在GC周期中幸存,则释放是免费的,因为GC仅与仍可访问的内存一起使用。(搜索MSDN,其中有一系列有关.NET内存管理的文章,我太懒了以至于无法对其进行深入研究)。

在大多数情况下,我使用struct时都会竭尽全力,因为后来我发现拥有引用语义会使事情变得更简单。

无论如何,上面发布的MSDN文章中的那四点似乎是一个很好的指南。


1
如果您有时需要带有struct的引用语义,只需声明class MutableHolder<T> { public T Value; MutableHolder(T value) {Value = value;} },然后a MutableHolder<T>将是具有可变类语义的对象(如果T是struct或不可变的类类型,则效果也很好)。
2013年

1

结构在堆栈上而不是堆上,因此它们是线程安全的,并且在实现传输对象模式时应使用它们,您永远不想在堆上使用易失性的对象,在这种情况下,您想使用调用堆栈,这是使用struct的基本情况,我对这里的所有答案都感到惊讶,


-3

我认为最好的答案是,当您需要的是属性集合时使用struct,而当它是属性和行为的集合时使用class。


结构可以有方法太
尼西德兰

当然,但是如果您需要方法,则有99%的机会是您不正确地使用struct而不是class。当可以在struct中使用方法时,我发现的唯一例外是回调
Lucian Gabriel Popescu
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.