在C#中,何时应该使用struct而不是class?

1626
什么时候应该在C#中使用struct而不是class?我的概念模型是,在项目仅仅是值类型的集合时使用struct。这是一种逻辑上将它们全部组合在一起的方式。
我遇到了这些规则(链接)
  • 结构体应该代表单个值。
  • 结构体的内存占用应小于16字节。
  • 结构体在创建后不应更改。
这些规则是否有效?从语义上讲,结构体意味着什么?

304
System.Drawing.Rectangle 违反了这三条规则。 - ChrisW
7
有相当多的商业游戏是用C#编写的,关键在于它们被用于优化代码。 - BlackTigerX
36
当您想要将小型值类型集合分组在一起时,结构体可以提供更好的性能。这种情况在游戏编程中经常发生,例如,三维模型中的一个顶点将具有位置、纹理坐标和法线,并且通常是不可变的。单个模型可能具有几千个顶点,也可能只有十几个,但在这种使用情况下,结构体总体上提供了更少的开销。我通过自己的引擎设计进行了验证。 - Chris D.
6
@ErikForbes:我认为这被普遍认为是最大的BCL“失误” - user1228
8
我明白了,但是这些值不是代表一个矩形吗,也就是说,一个“单一”的值?像Vector3D或Color,它们也包含几个值,但我认为它们代表的是单一的值? - Marson Mao
显示剩余15条评论
31个回答

684

OP提供的来源有一定可信度,但是微软的态度如何 - 对结构使用有何立场?我从Microsoft中寻找了一些额外的学习材料,以下是我发现的:

如果类型的实例很小且通常是短暂的或通常嵌入其他对象中,请考虑定义一个结构而不是类。

除非该类型具备以下所有特征,请勿定义结构:

  1. 它在逻辑上表示单个值,类似于原始类型(整数、双精度等)。
  2. 它的实例大小小于16个字节。
  3. 它是不可变的。
  4. 它不需要经常装箱。

微软一贯违反这些规则

好吧,至少第2和第3条。我们心爱的字典有2个内部结构:

[StructLayout(LayoutKind.Sequential)]  // default for structs
private struct Entry  //<Tkey, TValue>
{
    //  View code at *Reference Source
}

[Serializable, StructLayout(LayoutKind.Sequential)]
public struct Enumerator : 
    IEnumerator<KeyValuePair<TKey, TValue>>, IDisposable, 
    IDictionaryEnumerator, IEnumerator
{
    //  View code at *Reference Source
}

*参考源代码

'JonnyCantCode.com'的源代码3个结构体中有2个是值类型(struct),这是有道理的,因为它们只表示单个值并且速度快。如果你发现自己要对结构体进行装箱(boxing),重新思考你的架构。为什么微软会使用这些结构体呢?让我们来看看:

  1. 每个结构体(EntryEnumerator)都表示单个值。
  2. 速度快。
  3. Entry从不在Dictionary类外部作为参数传递。进一步调查显示,为了满足IEnumerable的实现,Dictionary使用枚举器(Enumerator)结构体,并在每次请求枚举器时复制该结构体……很合理。
  4. 在Dictionary类内部使用。 Enumerator是public的,因为Dictionary可枚举并且必须具有相等的IEnumerator接口实现的可访问性——例如IEnumerator getter。

更新-此外,请意识到当一个结构体实现一个接口(如Enumerator所做的那样),并被转换为该实现的类型时,结构体变成了引用类型并被移动到堆(heap)上。在Dictionary类内部,Enumerator仍然是值类型。然而,一旦一个方法调用GetEnumerator(),就会返回一个引用类型的IEnumerator。

这里我们看不到任何试图保持结构体不可变或保持实例大小为16字节或更小的要求或证明:

  1. 以上结构体中没有任何声明为readonly——也不是不可变的(li)
  2. 这些结构体的大小可能远远超过16个字节
  3. Entry的生命周期是未确定的(从Add(), Remove(), Clear()或垃圾回收开始);

并且... 4.这两个结构体都存储TKey和TValue,我们都知道它们可以是引用类型(附带信息)

尽管有哈希键,但字典之所以快速,部分原因在于实例化结构体比引用类型快。这里我有一个Dictionary<int, int>,存储300,000个随机整数和递增的键。

容量: 312874
MemSize: 2660827 bytes
完成调整: 5ms
填充总时间: 889ms

Capacity:内部数组必须调整大小之前可用的元素数。

MemSize:通过将字典序列化到MemoryStream并获取字节长度来确定(对于我们的目的而言足够准确)。

完成的调整大小: 调整内部数组从150862个元素到312874个元素所需的时间。考虑到每个元素都是通过Array.CopyTo()进行顺序复制的,这还不错。

填充总时间: 因为记录日志和添加到源代码的OnResize事件而导致偏差;但在操作期间调整大小15次并填充300k整数仍然很令人印象深刻。出于好奇,如果我已经知道容量,填充的总时间会是多少?13毫秒

那么,如果Entry是一个类会发生什么情况呢?这些时间或度量值真的会有很大的差异吗?

容量: 312874
MemSize: 2660827字节
完成的调整大小:26毫秒
填充总时间:964毫秒

显然,最大的区别在于调整大小。如果使用Capacity初始化Dictionary,会有什么不同吗?不足以引起担忧……12毫秒

由于Entry是一个结构体,因此不需要像引用类型一样进行初始化。这既是值类型的优点,也是缺点。为了将Entry用作引用类型,我必须插入以下代码:

/*
 *  Added to satisfy initialization of entry elements --
 *  this is where the extra time is spent resizing the Entry array
 * **/
for (int i = 0 ; i < prime ; i++)
{
    destinationArray[i] = new Entry( );
}
/*  *********************************************** */  
我之所以需要对Entry的每个数组元素进行初始化是因为其应该是一个引用类型,详情请参考MSDN: Structure Design。简而言之:

不要为结构提供默认构造函数。

如果结构定义了默认构造函数,则在创建结构的数组时,公共语言运行库将自动在每个数组元素上执行默认构造函数。

一些编译器(如C#编译器)不允许结构具有默认构造函数。

事实上,它非常简单,我们可以借鉴阿西莫夫的三大法则

  1. 该结构必须安全使用
  2. 该结构必须高效完成其功能,除非这会违反规则#1
  3. 该结构在使用期间必须保持完整,除非为满足规则#1而需要销毁它

...我们从中得到什么启示:简而言之,要对值类型的使用负责。它们快速高效,但如果未经妥善维护(即意外复制),可能会导致许多意外行为。


11
关于微软的规则,对于不可变性的规则似乎旨在阻止以一种使值类型的行为与引用类型不同的方式使用它们,尽管局部可变的值语义可能是有用的。如果将类型部分地变为可变会使其更易于操作,并且如果该类型的存储位置应从逻辑上彼此分离,则该类型应为“可变”结构体。 - supercat
3
微软的许多类型违反这些规则并不代表这些类型有问题,而是表明这些规则不应适用于所有结构类型。如果一个结构体表示单个实体[如DecimalDateTime],那么如果它不遵守其他三个规则,就应该用类来替换它。如果一个结构体包含一组固定的变量,每个变量都可以保存其类型有效的任何值[例如Rectangle],那么它应该遵守不同的规则,其中一些与“单值”结构相反。 - supercat
7
有些人可能会辩称Dictionary条目类型仅为内部类型,性能比语义更重要,或者提出其他借口来为其辩护。我的观点是,像Rectangle这样的类型应该将其内容作为可单独编辑的字段公开,不是因为性能优势超过了结果上的语义缺陷,而是因为该类型在语义上表示一组固定且独立的值,因此可变结构既具有更高的性能,也具有更好的语义优越性 - supercat
3
@supercat:我同意……我的回答的主要点是,这些“指南”非常薄弱,应该在充分了解行为的情况下使用结构体。请参见我的有关可变结构体的回答:https://dev59.com/8Gsz5IYBdhLWcg3wHUIU#8109254 - IAbstract
2
感谢您提供的有用信息。我想补充一点,虽然通过枚举器调用GetEnumerator会将其移动到堆上,但除非您手动将其转换为接口,否则实际上不会发生这种情况。C#编译器不会使用接口,除非它找不到具有适当签名的适当命名方法,特别是为了避免装箱。因此,他们在这里并没有真正违反规则#4。他们修改了编译器本身以避免这种情况。 - Daniel
显示剩余13条评论

193

每当您:

  1. 不需要多态性,
  2. 需要值语义,并且
  3. 想要避免堆分配及相关的垃圾回收开销。

然而,需要注意的是,结构体(任意大小)在传递过程中比类引用(通常为一个机器字)更昂贵,因此在实践中可能会导致类更快。


1
那就只有一个“注意点”。还应该考虑值类型的“提升”以及像(Guid)null这样的情况(将null强制转换为引用类型是可以的),以及其他一些情况。 - user166390
1
比C/C++更昂贵吗?在C++中,推荐的方法是按值传递对象。 - user90843
@IonTodirel 这不是为了内存安全而不是性能吗?这总是一个权衡,但通过堆栈传递32 B始终会比通过寄存器传递4 B的引用慢。然而,还要注意在C#和C++中,“值/引用”的使用有些不同 - 当您传递对象的引用时,即使您传递引用(您传递引用的值,而不是引用的引用),您仍然是按值传递。这不是“值语义”,但从技术上讲是“按值传递”。 - Luaan
@Luaan 复制只是成本的一个方面。由于指针/引用所导致的额外间接性也会在每次访问时产生成本。在某些情况下,结构体甚至可以被移动,因此不需要复制。 - Onur

177

我不同意原帖中给出的规则。以下是我的规则:

  1. 在数组中存储时,您可以使用结构体来提高性能。(另请参见何时使用结构体?

  2. 需要在代码中传递结构化数据到/从C/C++

  3. 除非需要,否则不要使用结构体:

    • 它们在赋值和作为参数传递时与“普通对象”(引用类型)的行为不同,可能会导致意外的行为;如果查看代码的人不知道自己正在处理结构体,则尤其危险。
    • 它们无法继承。
    • 将结构体作为参数传递比类更昂贵。

4
+1 我完全同意第一点(在处理图像等内容时,这是一个巨大的优势),并指出它们与“普通对象”不同,并且除了现有知识或检查类型本身之外,没有其他方法可以了解这一点。此外,您无法将空值强制转换为结构体类型 :-) 实际上,这是一种情况,我几乎希望非核心值类型有一些'匈牙利式'或强制性的'struct'关键字在变量声明位置上。 - user166390
@pst:确实,人们必须知道某个东西是一个“结构体”才能知道它的行为方式,但如果某个东西是一个具有公开字段的“结构体”,那么这就是人们需要知道的全部。如果一个对象公开了一个公开字段结构体类型的属性,并且如果代码将该结构体读取到一个变量并进行修改,则可以安全地预测,除非或直到写回结构体,否则此类操作不会影响读取该属性的对象。相比之下,如果该属性是可变类类型,则读取并修改它可能会按预期更新底层对象,但... - supercat
#1非常准确。如果结构体列表大小合适,那么相比对象引用列表,结构体列表可以将更多相关数据压缩到L1/L2高速缓存中。 - Matt Stephenson
3
继承很少是正确的工具,没有经过性能分析就过度关注性能是不明智的。首先,结构体可以通过引用传递。其次,按引用或按值传递很少是重要的性能问题。最后,您没有考虑到需要为类进行附加堆分配和垃圾回收。个人而言,我更喜欢将结构体视为纯数据,将类视为可以执行操作的东西(对象),尽管您也可以在结构体上定义方法。 - weberc2
1
@ILoveFortran,你不认为可以说对象的行为与“普通结构体”有所不同吗?如果使用者不知道他们正在处理一个对象而不是一个结构体,他们可能会误以为当作为参数传递给方法时该值被复制了。 - David Klempfner
显示剩余3条评论

98

如果你想要值语义而不是引用语义,可以使用结构体。

如果需要引用语义,需要使用类而不是结构体。


25
大家都知道这个。似乎他想要的不仅仅是“结构体是值类型”的答案。 - TheSmurf
27
这是最基本的情况,并且应该对于阅读此帖但不了解此事的任何人都要说明。 - JoshBerke
5
这个回答并不是不真实的,显然它是真实的。但这并不是重点。 - TheSmurf
72
@Josh:对于那些不知道的人,仅仅说“它”是个不够充分的回答,因为很有可能他们也不知道它是什么意思。 - TheSmurf
1
我刚刚给这个回答点了踩,因为我认为其他的回答应该在最前面——任何一个回答都应该说“用于与非托管代码的互操作,否则避免使用”。 - Daniel Earwicker
显示剩余6条评论

67

除了“它是一个值”的答案之外,使用结构体的一个特定场景是当你知道你有一组数据会导致垃圾回收问题,并且你有大量对象。例如,一个大型的Person实例列表/数组。自然的比喻是一个类,但如果你有大量长期存在的Person实例,它们可能会堵塞GEN-2并导致GC停顿。如果情况需要,这里有一个潜在的方法是使用一个Person结构体的数组(而不是列表),即Person[]。现在,你只有一个块在LOH上,而不是在GEN-2中拥有数百万个对象(我假设这里没有字符串等 - 即一个纯值而没有任何引用)。这对GC影响很小。

与此数据一起工作很麻烦,因为这个数据可能对于一个结构体来说过大,你不想一直复制大块的值。然而,在数组中直接访问它不会复制结构体 - 它是原地的(与列表索引器相反,后者会复制)。这意味着需要使用索引进行大量的操作:

int index = ...
int id = peopleArray[index].Id;

需要注意的是保持值本身不可变将有所帮助。对于更复杂的逻辑,请使用一个具有按引用传递参数的方法:

void Foo(ref Person person) {...}
...
Foo(ref peopleArray[index]);

再次强调,这是原地操作——我们没有复制该值。

在非常特定的情况下,这种策略可能非常成功;然而,这是一个相当高级的场景,只有在您知道自己在做什么以及为什么时才应该尝试。在这里,默认使用类。


我想知道如果定义一个ICustomer,并有一个实现该接口的CustomerRef结构,它持有单个int索引,并根据数组项适当地执行操作,那么性能会如何?我认为,如果将接受Customer的方法通用地改为接受ICustomer,则应该可以获得与当前方法相当的性能,而无需广泛暴露底层数组。 - supercat
这样的东西可能是YAGNI(You Ain't Gonna Need It),但是如果例如客户数组将危及2GB的限制,可以将数组访问代码从“customers [index]”更改为“customers [index >> 16] [index&65535]”,而无需影响“CustomerRef”之外的任何内容。 - supercat
@supercat 或者在 4.5 中启用大数组支持。 - Marc Gravell
@MarcGravell:从未使用过那种方法。无论如何,我的观点是,通过 ICustomer 方法,如果需要,可以通过各种方式迁移离开使用单块数组(2GB的限制是其中一个原因,但不是唯一的原因)。顺便说一句,您的博客没有提到您的方法的“成本”之一是GC无法知道哪些数组插槽有对它们的引用。这减少了GC的成本,但意味着应用程序可能需要自己跟踪这些东西。 - supercat
2
@MarcGravell 为什么要提到:_使用数组(而不是列表)_?我相信 List 在幕后使用了一个 Array,不是吗? - Royi Namir
5
@RoyiNamir 我也很好奇,但我认为答案在Marc的回答的第二段中。 “然而,在数组中直接访问它不会复制结构 - 它是原地的(与列表索引器相反,后者确实会复制)。 - user1323245

46

来自C#语言规范:

1.7 结构体

结构体与类一样,都是可以包含数据成员和函数成员的数据结构, 但与类不同的是,结构体是值类型,不需要进行堆分配。结构体类型 直接存储结构体的数据,而类类型的变量则存储对动态分配对象的引用。 结构体类型不支持用户指定继承,并且所有结构体类型都隐式继承自object类型。

结构体特别适用于具有值语义的小型数据结构,例如复数、坐标系中的点或字典中的键值对。 对于小型数据结构而言,使用结构体而不是类可以大大减少应用程序执行的内存分配次数。 例如,以下程序创建并初始化了一个包含100个点的数组。如果Point实现为类,则会实例化101个不同的对象——一个用于数组,每个元素一个。

class Point
{
   public int x, y;

   public Point(int x, int y) {
      this.x = x;
      this.y = y;
   }
}

class Test
{
   static void Main() {
      Point[] points = new Point[100];
      for (int i = 0; i < 100; i++) points[i] = new Point(i, i);
   }
}

另一种方法是将 Point 设为一个结构体。

struct Point
{
   public int x, y;

   public Point(int x, int y) {
      this.x = x;
      this.y = y;
   }
}

现在只实例化了一个对象——数组的实例,并且Point实例在数组中被内联存储。

结构体构造函数使用new运算符调用,但这并不意味着正在分配内存。结构体构造函数不是动态分配对象并返回对其的引用,而是直接返回结构体值本身(通常在堆栈上的临时位置),然后根据需要复制该值。

使用类,两个变量可以引用同一个对象,因此操作一个变量可能会影响另一个变量所引用的对象。而对于结构体,每个变量都有自己的数据副本,一个变量上的操作不可能影响另一个变量。例如,以下代码片段产生的输出取决于Point是一个类还是结构体。

Point a = new Point(10, 10);
Point b = a;
a.x = 20;
Console.WriteLine(b.x);
如果Point是一个类,输出结果为20,因为变量a和b引用的是同一个对象。如果Point是一个结构体,则输出结果为10,因为将a赋值给b会创建一个值的副本,这个副本不会受到之后对a.x的赋值的影响。
上面的例子突出了结构体的两个限制。首先,复制整个结构体通常比复制对象引用要低效,因此使用结构体进行赋值和值参数传递可能比引用类型更加昂贵。其次,除了ref和out参数外,无法创建结构体的引用,这排除了在许多情况下使用它们的可能性。

6
虽然无法将结构体的引用持久化有时会限制其使用,但这也是一个非常有用的特性。.net 的主要弱点之一是没有很好的方法将可变对象的引用传递给外部代码,并且永远失去该对象的控制。相比之下,可以安全地将可变结构体的 ref 传递给外部方法,并知道外部方法对其执行的任何更改都将在其返回之前完成。遗憾的是,.net 没有任何关于短暂参数和函数返回值的概念,因为... - supercat
5
这段内容的意思是:希望能实现类对象传递时结构体通过“ref”传递所具有的优势语义。本质上,局部变量、参数和函数返回值可以是可持续(默认)、可返回或短暂的。代码将被禁止将短暂的东西复制到任何超出当前作用域的地方。可返回的东西就像短暂的东西,只是它们可以从函数中返回。函数的返回值将受到其“可返回”参数中适用的最严格限制的约束。 - supercat

40

这里是一个基本规则。

  • 如果所有成员字段都是值类型,请创建一个 struct

  • 如果任何一个成员字段是引用类型,请创建一个 class。这是因为引用类型字段将需要堆分配。

示例

public struct MyPoint 
{
    public int X; // Value Type
    public int Y; // Value Type
}

public class MyPointWithName 
{
    public int X; // Value Type
    public int Y; // Value Type
    public string Name; // Reference Type
}

8
string这样的不可变引用类型在语义上等同于值,将对不可变对象的引用存储到字段中并不需要堆分配。公开字段的结构体和公开字段的类对象之间的区别在于,给定代码序列var q = p; p.X = 4; q.X = 5;,如果a是结构体类型,则p.X将具有值4,而如果它是类类型,则为5。如果希望方便地修改类型的成员,则应根据是否希望更改q以影响p来选择“class”或“struct”。 - supercat
可变结构体和可变类的行为完全不同;如果一个是正确的,另一个很可能是错误的。我不确定行为如何不是决定使用结构体还是类的因素。 - supercat
我说这不是一个强有力的决定性因素,因为通常当你创建一个类或结构时,你不确定它将如何使用。所以你要专注于从设计角度让事情更有意义。无论如何,我从未在.NET库中看到过一个结构包含引用变量的地方。 - Usman Zafar
2
结构类型 ArraySegment<T> 封装了一个始终为类类型的 T[]。结构类型 KeyValuePair<TKey,TValue> 经常与类类型一起用作泛型参数。 - supercat
这是最佳答案。如果需要值语义,请使用结构体(通过参数传递会生成副本,而不允许编辑原始数据)。对于大型数据集合,使用 struct 实际上可以提高性能,但必须通过分析和运行单元测试来验证其可行性,并显示程序功能未受影响。如果您想在答案中包含此评论的一部分作为编辑,请随意这样做^^ - CoffeDeveloper
显示剩余2条评论

38

结构体适合原子数据的表示,其中所述数据可以被代码多次复制。通常情况下,克隆一个对象比复制一个结构体更昂贵,因为它涉及到分配内存、运行构造函数以及在使用完后进行撤销/垃圾回收。


4
是的,但是大型结构体在传递给方法时可能比类引用更昂贵。 - Alex from Jitbit

20

第一点:Interop场景或者需要指定内存布局时。

第二点:当数据的大小几乎等于一个引用指针时。


19

我使用BenchmarkDotNet制作了一个小型基准测试,以更好地了解“结构体”在数字方面的优势。我正在测试循环遍历结构体(或类)的数组(或列表)。创建这些数组或列表不在基准测试范围内 - 很明显,“类”更加沉重,会利用更多内存,并涉及GC。

所以结论是:要小心使用LINQ和隐藏的结构体装箱/拆箱,并严格使用数组来进行微观优化。

P.S. 关于通过调用堆栈传递结构体/类的另一个基准测试,请参见https://dev59.com/UHVD5IYBdhLWcg3wVKEb#47864451

BenchmarkDotNet=v0.10.8, OS=Windows 10 Redstone 2 (10.0.15063)
Processor=Intel Core i5-2500K CPU 3.30GHz (Sandy Bridge), ProcessorCount=4
Frequency=3233542 Hz, Resolution=309.2584 ns, Timer=TSC
  [Host] : Clr 4.0.30319.42000, 64bit RyuJIT-v4.7.2101.1
  Clr    : Clr 4.0.30319.42000, 64bit RyuJIT-v4.7.2101.1
  Core   : .NET Core 4.6.25211.01, 64bit RyuJIT


          Method |  Job | Runtime |      Mean |     Error |    StdDev |       Min |       Max |    Median | Rank |  Gen 0 | Allocated |
---------------- |----- |-------- |----------:|----------:|----------:|----------:|----------:|----------:|-----:|-------:|----------:|
   TestListClass |  Clr |     Clr |  5.599 us | 0.0408 us | 0.0382 us |  5.561 us |  5.689 us |  5.583 us |    3 |      - |       0 B |
  TestArrayClass |  Clr |     Clr |  2.024 us | 0.0102 us | 0.0096 us |  2.011 us |  2.043 us |  2.022 us |    2 |      - |       0 B |
  TestListStruct |  Clr |     Clr |  8.427 us | 0.1983 us | 0.2204 us |  8.101 us |  9.007 us |  8.374 us |    5 |      - |       0 B |
 TestArrayStruct |  Clr |     Clr |  1.539 us | 0.0295 us | 0.0276 us |  1.502 us |  1.577 us |  1.537 us |    1 |      - |       0 B |
   TestLinqClass |  Clr |     Clr | 13.117 us | 0.1007 us | 0.0892 us | 13.007 us | 13.301 us | 13.089 us |    7 | 0.0153 |      80 B |
  TestLinqStruct |  Clr |     Clr | 28.676 us | 0.1837 us | 0.1534 us | 28.441 us | 28.957 us | 28.660 us |    9 |      - |      96 B |
   TestListClass | Core |    Core |  5.747 us | 0.1147 us | 0.1275 us |  5.567 us |  5.945 us |  5.756 us |    4 |      - |       0 B |
  TestArrayClass | Core |    Core |  2.023 us | 0.0299 us | 0.0279 us |  1.990 us |  2.069 us |  2.013 us |    2 |      - |       0 B |
  TestListStruct | Core |    Core |  8.753 us | 0.1659 us | 0.1910 us |  8.498 us |  9.110 us |  8.670 us |    6 |      - |       0 B |
 TestArrayStruct | Core |    Core |  1.552 us | 0.0307 us | 0.0377 us |  1.496 us |  1.618 us |  1.552 us |    1 |      - |       0 B |
   TestLinqClass | Core |    Core | 14.286 us | 0.2430 us | 0.2273 us | 13.956 us | 14.678 us | 14.313 us |    8 | 0.0153 |      72 B |
  TestLinqStruct | Core |    Core | 30.121 us | 0.5941 us | 0.5835 us | 28.928 us | 30.909 us | 30.153 us |   10 |      - |      88 B |

代码:

[RankColumn, MinColumn, MaxColumn, StdDevColumn, MedianColumn]
    [ClrJob, CoreJob]
    [HtmlExporter, MarkdownExporter]
    [MemoryDiagnoser]
    public class BenchmarkRef
    {
        public class C1
        {
            public string Text1;
            public string Text2;
            public string Text3;
        }

        public struct S1
        {
            public string Text1;
            public string Text2;
            public string Text3;
        }

        List<C1> testListClass = new List<C1>();
        List<S1> testListStruct = new List<S1>();
        C1[] testArrayClass;
        S1[] testArrayStruct;
        public BenchmarkRef()
        {
            for(int i=0;i<1000;i++)
            {
                testListClass.Add(new C1  { Text1= i.ToString(), Text2=null, Text3= i.ToString() });
                testListStruct.Add(new S1 { Text1 = i.ToString(), Text2 = null, Text3 = i.ToString() });
            }
            testArrayClass = testListClass.ToArray();
            testArrayStruct = testListStruct.ToArray();
        }

        [Benchmark]
        public int TestListClass()
        {
            var x = 0;
            foreach(var i in testListClass)
            {
                x += i.Text1.Length + i.Text3.Length;
            }
            return x;
        }

        [Benchmark]
        public int TestArrayClass()
        {
            var x = 0;
            foreach (var i in testArrayClass)
            {
                x += i.Text1.Length + i.Text3.Length;
            }
            return x;
        }

        [Benchmark]
        public int TestListStruct()
        {
            var x = 0;
            foreach (var i in testListStruct)
            {
                x += i.Text1.Length + i.Text3.Length;
            }
            return x;
        }

        [Benchmark]
        public int TestArrayStruct()
        {
            var x = 0;
            foreach (var i in testArrayStruct)
            {
                x += i.Text1.Length + i.Text3.Length;
            }
            return x;
        }

        [Benchmark]
        public int TestLinqClass()
        {
            var x = testListClass.Select(i=> i.Text1.Length + i.Text3.Length).Sum();
            return x;
        }

        [Benchmark]
        public int TestLinqStruct()
        {
            var x = testListStruct.Select(i => i.Text1.Length + i.Text3.Length).Sum();
            return x;
        }
    }

你是否已经弄清楚了为什么在列表等数据结构中使用结构体会慢很多?这是因为你提到的隐藏装箱和拆箱吗?如果是,为什么会发生这种情况? - Marko Grdinić
访问数组中的结构应该更快,因为不需要额外的引用。装箱/拆箱是linq的情况。 - Roman Pokrovskij

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