题 何时使用struct?


什么时候应该在C#中使用struct而不是class?我的概念模型是结构在项目的时候使用 只是一组价值类型。一种逻辑上将它们组合在一起形成一个有凝聚力的整体的方法。

我遇到了这些规则 这里

  • 结构应该代表单个结构 值。
  • 结构应该有一个内存 占用空间小于16个字节。
  • 之后不应该更改结构 创建。

这些规则有用吗?结构在语义上意味着什么?


1201
2018-02-06 17:37


起源


System.Drawing.Rectangle 违反了所有这三条规则。 - ChrisW
是的,无论如何都是他们的一部分。我知道它用于部分游戏,如NWN2 World Creator。 C仍然通常处于核心(引擎)。 XNA Game Studio,谷歌吧:) - Refracted Paladin
有很多用C#编写的商业游戏,重点是它们用于优化代码 - BlackTigerX
当您想要组合在一起的少量值类型集合时,结构可以提供更好的性能。这种情况一直发生在游戏编程中,例如,3D模型中的顶点将具有位置,纹理坐标和法线,它通常也将是不可变的。单个模型可能有几千个顶点,或者它可能有十几个,但结构在这种使用场景中提供的总体开销更少。我通过自己的引擎设计验证了这一点。 - Chris D.
@ErikForbes:我想 这通常被认为是最大的BCL“oops” - Will


答案:


OP引用的源代码具有一定的可信性......但是微软呢?结构使用的立场是什么?我寻求一些额外的 向微软学习,这是我发现的:

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

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

  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'来源获得了4分中的3分 - 相当可原谅,因为#4可能不会成为问题。如果您发现自己正在装箱结构,请重新考虑您的架构。

让我们看看为什么微软会使用这些结构:

  1. 每个结构, Entry 和 Enumerator,代表单一的价值观。
  2. 速度
  3. Entry 永远不会作为Dictionary类之外的参数传递。进一步调查表明,为了满足IEnumerable的实现,Dictionary使用了 Enumerator 每次请求枚举器时它复制的struct ......都有意义。
  4. Dictionary类的内部。 Enumerator 是公共的,因为Dictionary是可枚举的,并且必须具有与IEnumerator接口实现相同的可访问性 - 例如IEnumerator getter。

更新  - 另外,要意识到当一个struct实现一个接口时 - 就像Enumerator那样 - 并且被强制转换为该实现的类型,struct将成为一个引用类型并被移动到堆中。 Dictionary类的内部,Enumerator  仍然是一种价值类型。但是,只要方法调用 GetEnumerator(),引用类型 IEnumerator 被退回。

我们在这里没有看到的任何尝试或证明要求保持结构不可变或维持实例大小只有16个字节或更少:

  1. 上面的结构中没有任何内容被声明 readonly  -  一成不变
  2. 这些结构的大小可能超过16个字节
  3. Entry 有一个不确定的寿命(来自 Add(), 至 Remove()Clear()或垃圾收集);

而......  4.两个结构都存储了TKey和TValue,我们都知道它们很有能力作为参考类型(添加奖励信息)

尽管有散列键,但字典很快部分是因为实例化结构比引用类型更快。在这里,我有一个 Dictionary<int, int> 使用顺序递增的密钥存储300,000个随机整数。

容量:312874
  MemSize:2660827字节
  完成调整大小:5ms
  总时间:889ms

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

MEMSIZE:通过将字典序列化为MemoryStream并获得字节长度(对于我们的目的来说足够准确)来确定。

完成调整大小:将内部数组从150862元素调整为312874元素所需的时间。当你想通过顺序复制每个元素时 Array.CopyTo(),这不是太寒酸。

总时间填补:由于伐木而且不可否认 OnResize 事件我添加到源;然而,在操作期间调整15次时,仍然令人印象深刻地填充300k整数。出于好奇,如果我已经知道容量,那么总的填充时间是多少? 13毫秒 

那么,现在,如果 Entry 是一堂课?这些时间或指标真的会有那么大差异吗?

容量:312874
  MemSize:2660827字节
  完成调整大小:26ms
  总时间:964ms

显然,最大的区别在于调整大小。如果使用容量初始化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:结构设计。简而言之:

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

如果一个结构定义了一个默认的构造函数,那么当数组的时候   结构被自动创建,公共语言运行时   在每个数组元素上执行默认构造函数。

某些编译器(如C#编译器)不允许使用结构   有默认的构造函数。

它实际上非常简单,我们将借鉴 阿西莫夫 机器人学的三个定律

  1. 结构必须安全使用
  2. 结构必须有效地执行其功能,除非这违反了规则#1
  3. 结构在使用过程中必须保持完整,除非要求销毁以满足规则#1

...我们从中得到了什么?:简而言之,负责使用值类型。它们快速有效,但如果维护不当(即无意复制),则有能力引发许多意外行为。


541
2017-08-07 13:44



至于微软的规则,关于不变性的规则似乎旨在阻止使用价值类型,使其行为与参考类型的行为不同,尽管事实是 分段可变值语义可能很有用。如果类型是分段可变的将使其更容易使用,并且如果该类型的存储位置应该在逻辑上彼此分离,则该类型应该是“可变”结构。 - supercat
请记住 readonly!=不可变的。 - Justin Morgan
许多Microsoft类型违反这些规则的事实并不代表这些类型的问题,而是表明规则不适用于所有结构类型。如果一个结构代表一个单独的实体[如同 Decimal 要么 DateTime],如果它不遵守其他三个规则,它应该被一个类替换。如果一个结构包含一个固定的变量集合,每个变量都可以包含任何对其类型有效的值[例如 Rectangle],那么它应该遵守 不同 规则,其中一些与“单一价值”结构相反。 - supercat
@IAbstract:有些人会证明这一点 Dictionary 入口类型只是基于它的内部类型,性能被认为比语义或其他一些借口更重要。我的观点是类似的类型 Rectangle 应该将其内容暴露为可单独编辑的字段而不是“因为”性能优势超过了由此产生的语义缺陷,而是因为 该类型在语义上表示一组固定的独立值,因此可变结构在性能和语义上都更高 优越。 - supercat
@supercat:我同意......而我的回答的全部意义在于,“指南”非常弱,结构应该充分了解和理解行为。在这里查看我对可变结构的回答: stackoverflow.com/questions/8108920/... - IAbstract


每当您不需要多态时,需要值语义,并希望避免堆分配和相关的垃圾收集开销。然而,需要注意的是,结构(任意大)传递比类引用(通常是一个机器字)更昂贵,因此类在实践中最终会更快。


142
2018-02-06 17:40



这只是一个“警告”。还应考虑“提升”价值类型和案例,如 (Guid)null (可以将null转换为引用类型),等等。
比C / C ++更贵?在C ++中,推荐的方法是按值传递对象 - Ion Todirel
@IonTodirel不是出于内存安全的原因,而不是性能?它始终是一种权衡,但是通过堆栈传递32 B总是(TM)比通过寄存器传递4 B参考慢。 然而,还要注意在C#和C ++中使用“值/引用”有点不同 - 当你传递对象的引用时,你仍然按值传递,即使你传递了一个引用(你是传递引用的值,而不是引用的引用,基本上)。这不是价值 语义,但它在技术上是“按价值传递”。 - Luaan
@Luaan复制只是成本的一​​个方面。由指针/引用引起的额外间接也是每次访问的成本。在某些情况下,结构甚至可以移动,因此甚至不需要复制。 - Onur
@Onur这很有意思。如何在没有复制的情况下“移动”?我认为asm“mov”指令实际上并没有“移动”。它复制。 - Winger Sendon


我不同意原帖中的规定。这是我的规则:

1)存储在数组中时使用结构体来提高性能。 (也可以看看 什么时候结构的答案?

2)您需要将代码传递给C / C ++的结构化数据

3)除非你需要,否则不要使用结构:

  • 它们的行为与“普通物体”不同(参考类型)在任务和 当作为参数传递时,可能导致意外行为; 如果看到代码的人这样做,这尤其危险 不知道他们正在处理一个结构。
  • 他们不能继承。
  • 将结构作为参数传递比类更昂贵。

133
2018-02-28 16:33



+1是的,我完全同意#1(这是一个 巨大 处理图像等事情时的优势,并指出它们 是不同的 从“正常的对象”,有 知道了解这一点的方式 除了现有知识或检查类型本身。此外,您不能将null值转换为结构类型:-)这实际上是我的一个案例 几乎 希望在变量声明网站上有一些“非匈牙利”用于非核心价值类型或强制性“结构”关键字。
@pst:确实有人必须知道某事是一个 struct 知道它将如何表现,但如果有的话 struct 暴露的领域,这是所有人必须知道的。如果一个对象公开了一个exposed-field-struct类型的属性,并且如果代码将该结构读取到一个变量并进行修改,则可以安全地预测这样的操作不会影响其属性被读取的对象,除非或直到该结构被写入背部。相反,如果属性是一个可变类类型,读取它并修改它可能会按预期更新底层对象,但是...... - supercat
......它也可能最终没有改变任何东西,或者它可能会改变或破坏人们不打算改变的物体。拥有代码的语义说“改变你所喜欢的变量;更改将不会做任何事情,直到你明确地将它们存储在某个地方”似乎比代码说“你正在获得对某个对象的引用更清楚”,这可能与任何数字共享其他引用,或者可能根本不共享;你必须弄清楚还有谁可能引用这个对象,知道如果你改变它会发生什么。“ - supercat
发现#1。充满结构的列表可以将更多相关数据压缩到L1 / L2缓存中,而不是充满对象引用的列表(对于正确大小的结构)。 - Matt Stephenson
继承很少是工作的正确工具,如果不进行分析就过多地推理性能是一个坏主意。首先,结构可以通过引用传递。其次,通过引用或按值传递很少是一个重要的性能问题。最后,您没有考虑需要为类进行的额外堆分配和垃圾回收。就个人而言,我更喜欢将结构视为普通旧数据和类 做 事物(对象)虽然你也可以在结构上定义方法。 - weberc2


当您需要值语义而不是引用语义时,请使用结构。

编辑

不确定为什么人们正在贬低这一点,但这是一个有效的观点,并且在操作澄清他的问题之前做出,这是结构的最基本的基本原因。

如果需要引用语义,则需要一个类而不是结构。


82
2018-02-06 17:40



每个人都知道。看起来他正在寻找的不仅仅是“结构是价值类型”的答案。 - TheSmurf
不是每个人都知道。这显然是一个例子。 - BobbyShaftoe
这是最基本的案例,应该为阅读这篇文章并且不知道这一点的任何人说明。 - JoshBerke
不是说这个答案不是真的;它显然是。这不是重点。 - TheSmurf
@Josh:对于那些不了解它的人来说,简单地说这是一个不充分的答案,因为他们很可能也不知道这意味着什么。 - TheSmurf


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

使用这些数据很尴尬,因为数据可能超出了结构的大小,并且您不希望一直复制胖值。但是,直接在数组中访问它不会复制结构 - 它就位(与列表索引器相比,它会复制)。这意味着很多工作与索引:

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

请注意,保持值本身不可变将有助于此处。对于更复杂的逻辑,请使用带有by-ref参数的方法:

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

同样,这是就地 - 我们没有复制价值。

在非常具体的情况下,这种策略可以非常成功;但是,这是一个相当先进的scernario,只有当你知道自己在做什么以及为什么这样做时才应该尝试。这里的默认值是一个类。


54
2017-10-22 12:14



+1有趣的答案。您是否愿意分享关于使用这种方法的任何真实世界的轶事? - Jordão
@Jordao在手机上,但搜索谷歌:+ gravell +“攻击GC” - Marc Gravell♦
非常感谢。我找到了 这里。 - Jordão
@MarcGravell你为什么提到: 使用数组(不是列表) ? List 我相信,用一个 Array 幕后。不? - Royi Namir
@RoyiNamir我对此也很好奇,但我相信答案在于Marc的第二段回答。 “但是,直接在数组中访问它不会复制结构 - 它就位(与列表索引器相比,它会复制)。” - user1323245


来自 C#语言规范

1.7结构 

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

结构对于具有的小型数据结构特别有用   价值语义。复数,坐标系中的点,或   字典中的键值对都是结构的好例子。该   对于小型数据结构,可以使用结构而不是类   应用程序的内存分配数量差异很大   施行。例如,以下程序创建并初始化   一个100分的阵列。将Point实现为一个类,101   单独的对象被实例化 - 一个用于数组,一个用于数组   100个元素。

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运算符调用Struct构造函数,但这并不意味着正在分配内存。结构构造函数只是返回结构值本身(通常在堆栈上的临时位置),而不是动态分配对象并返回对它的引用,然后根据需要复制该值。

对于类,两个变量可以引用同一个对象,因此对一个变量的操作可能会影响另一个变量引用的对象。对于结构体,变量每个都有自己的数据副本,并且对一个变量的操作不可能影响另一个。例如,由以下代码片段生成的输出取决于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参数之外,不可能创建对结构的引用,结构排除了它们在许多情况下的使用。


36
2017-09-17 15:42



虽然对结构的引用不能持久的事实有时是一种限制,但它也是一个非常有用的特征。 .net的一个主要缺点是,没有可靠的方法将外部代码传递给可变对象的引用,而不会永远失去对该对象的控制。相比之下,人们可以安全地给出一种外部方法a ref 到一个可变的结构,并知道外部方法将对其执行的任何突变将在它返回之前完成。这太糟糕了.net没有任何短暂参数和函数返回值的概念,因为...... - supercat
...这将允许通过的结构的有利语义 ref 用类对象实现。本质上,局部变量,参数和函数返回值可以是可持久的(默认的),可返回的或短暂的。代码将被禁止将短暂的东西复制到任何比现在范围更长的东西。可回收的东西就像短暂的东西,除了它们可以从一个函数返回。函数的返回值将受适用于其任何“可返回”参数的最严格限制的约束。 - supercat


结构适用于数据的原子表示,其中所述数据可以由代码多次复制。克隆一个对象通常比复制一个结构更昂贵,因为它涉及分配内存,运行构造函数和完成它时解除分配/垃圾回收。


31
2018-02-06 17:58



是的,但是大型结构可能比类引用更昂贵(当传递给方法时)。 - Alex


这是一条基本规则。

  • 如果所有成员字段都是值类型,则创建一个 结构

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

Exmaples

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
}

24
2018-01-22 10:17



不可变的引用类型 string 在语义上等价于值,并且将对不可变对象的引用存储到字段中不需要堆分配。具有公开的公共字段的结构和具有公开的公共字段的类对象之间的区别在于给定代码序列 var q=p; p.X=4; q.X=5;, p.X 将具有值4如果 a 是结构类型,如果是类类型,则为5。如果希望能够方便地修改该类型的成员,则应根据是否需要更改来选择“类”或“结构” q 影响 p。 - supercat
是的我同意引用变量将在堆栈上,但它引用的对象将存在于堆上。虽然结构和类在分配给不同的变量时表现不同,但我认为这不是一个强大的决定因素。 - Usman Zafar
可变结构和可变类的行为完全不同;如果一个是对的,那么另一个很可能是错的。我不确定行为如何不是决定是使用结构还是类的决定性因素。 - supercat
我说它不是一个强大的决定因素,因为通常当你创建一个类或结构时,你不确定它将如何使用。因此,您可以从设计角度集中精力了解事物的含义。无论如何,我从未在.NET库中的一个地方看到过struct包含引用变量的地方。 - Usman Zafar
结构类型 ArraySegment<T> 封装一个 T[],这总是一种类型。结构类型 KeyValuePair<TKey,TValue> 通常与类类型一起用作通用参数。 - supercat


第一种:互操作方案或需要指定内存布局时

第二:当数据与参考指针的大小几乎相同时。


17
2018-02-06 18:12





在需要使用显式指定内存布局的情况下,需要使用“struct” StructLayoutAttribute  - 通常用于PInvoke。

编辑:注释指出您可以使用StructLayoutAttribute的类或结构,这当然是正确的。在实践中,您通常会使用一个结构 - 它在堆栈和堆上分配,如果您只是将参数传递给非托管方法调用,这是有意义的。


16
2018-02-06 18:09



StructLayoutAttribute可以应用于结构或类,因此这不是使用结构的原因。 - Stephen Martin
如果您只是将参数传递给非托管方法调用,为什么它有意义? - Backwards_Dave


我使用结构包装或解压缩任何种类的二进制通信格式。这包括读取或写入磁盘,DirectX顶点列表,网络协议或处理加密/压缩数据。

在此上下文中,您列出的三条准则对我没有用。当我需要在特定顺序中写出四百个字节的东西时,我将定义一个四百字节的结构,并且我将填充它应该具有的任何不相关的值,并且我将要去设置当时最有意义的方式。 (好吧,四百个字节会很奇怪 - 但是当我以Excel文件为生,我正在处理全部最多大约四十个字节的结构,因为那是BIFF记录的大小。)


15
2018-02-06 18:25



难道你不能轻易地使用引用类型吗? - Backwards_Dave