何时使用struct?


1390

什么时候应该在C#中使用struct而不是class?我的概念模型是当项目仅仅是值类型的集合时使用结构。一种逻辑上将它们组合在一起的方法。

我在这里遇到了这些规则:

  • 结构应代表单个值。
  • 结构的内存占用量应少于16个字节。
  • 创建后不应更改结构。

这些规则有效吗?结构在语义上是什么意思?


247
System.Drawing.Rectangle违反了所有这三个规则。
ChrisW

4
有很多用C#编写的商业游戏,关键是它们用于优化代码
BlackTigerX

25
当您希望将小的值类型集合在一起时,结构可以提供更好的性能。这种情况在游戏编程中一直存在,例如3D模型中的顶点将具有位置,纹理坐标和法线,而且通常也将是不可变的。单个模型可能具有数千个顶点,或者可能具有十二个顶点,但是在此使用场景中,结构提供的总体开销较小。我已经通过自己的引擎设计对此进行了验证。
克里斯D.

6
@ErikForbes:我认为这通常是最大的BCL“哎呀”

4
@ChrisW我知道了,但是这些值不是代表一个矩形,即“单个”值吗?像Vector3D或Color一样,它们在里面也是多个值,但是我认为它们代表单个值?
Marson Mao

Answers:


604

OP引用的来源具有一定的信誉...但是,Microsoft呢?使用结构的立场是什么?我从Microsoft寻求一些额外的学习,这是我发现的:

如果类型的实例较小且通常为短寿命或通常嵌入在其他对象中,请考虑定义结构而不是类。

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

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

Microsoft一直违反这些规则

好吧,还是#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个获得4分-可以原谅,因为#4可能不是问题。如果您发现自己正在装箱一个结构,请重新考虑您的体系结构。

让我们看看Microsoft为什么要使用这些结构:

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

更新 -另外,请注意,当结构实现一个接口(如Enumerator一样)并将其强制转换为该实现的类型时,该结构将成为引用类型并移至堆中。内部的Dictionary类,枚举仍然值类型。但是,一旦方法调用GetEnumerator()IEnumerator就会返回一个引用类型。

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

  1. 上面的结构中没有声明readonly- 没有不变的
  2. 这些结构的大小可能超过16个字节
  3. Entry具有不确定的寿命(从Add(),到Remove()Clear()或垃圾收集);

... ... 4.两个结构都存储TKey和TValue,我们都知道它们完全可以作为引用类型(添加了奖励信息)

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

容量:312874
内存
大小:2660827字节已完成调整大小:5毫秒
总填充时间:889毫秒

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

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

完成调整大小:将内部数组的大小从150862个元素调整为312874个元素所需的时间。当您确定每个元素都是通过顺序复制的Array.CopyTo()时,就不会太破旧。

总填充时间:由于日志记录和OnResize我添加到源中的事件而造成的偏差;但是,在操作过程中将大小调整为15倍时,填充30万个整数仍然令人印象深刻。出于好奇,如果我已经知道处理能力,那么总的工作时间将是多少?13毫秒

那么,现在,如果Entry上课怎么办?这些时间或指标真的会有很大的不同吗?

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

显然,最大的区别在于调整大小。如果使用Capacity初始化Dictionary,有什么不同?不足以关注... 12ms

发生的是,由于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,否则结构在使用过程中必须保持完整

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


8
至于Microsoft的规则,关于不变性的规则似乎旨在以某种方式阻止使用值类型,以使它们的行为与引用类型的行为有所不同,尽管分段可变的值语义可能有用。如果具有分段可变的类型将使其更易于使用,并且如果该类型的存储位置应在逻辑上彼此分离,则该类型应为“可变”结构。
2012年


2
Microsoft的许多类型都违反这些规则的事实并不表示这些类型存在问题,而是表明这些规则不应应用于所有结构类型。如果结构表示单个实体(如DecimalDateTime),则如果它不遵守其他三个规则,则应将其替换为类。如果一个结构拥有固定的变量集合,每个变量可以拥有对其类型有效的任何值[例如Rectangle],则它应遵守不同的规则,其中一些规则与“单值”结构相反。
2013年

4
@IAbstract:有人会基于Dictionary条目类型只是内部类型来证明其合理性,认为性能比语义或其他借口更重要。我的观点是,像这样的类型Rectangle应将其内容显示为可单独编辑的字段,而不是“因为”性能优势超过产生的语义缺陷,而是因为该类型在语义上表示一组固定的独立值,因此可变结构既是表现更好,语义上更好
2013年

2
@supercat:我同意……我的回答的重点是,“准则”非常薄弱,应该在充分了解和理解行为的基础上使用结构。见我的可变结构答案在这里:stackoverflow.com/questions/8108920/...
IAbstract

154

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


1
那只是一个“ caveat”。(Guid)null除其他事项外,还应考虑“提升”值类型和情况(例如,可以将null转换为引用类型)。

1
比C / C ++更昂贵?在C ++中,推荐的方法是按值传递对象
Ion Todirel 2013年

@IonTodirel不是出于内存安全性原因,而不是出于性能原因吗?这始终是一个折衷,但是通过堆栈传递32 B总是比通过寄存器传递4 B引用要慢。但是,还请注意,在C#和C ++中,“值/引用”的使用有些不同-当您传递对对象的引用时,即使您传递的是引用,您仍按值传递(基本上传递参考的值,而不是参考的参考)。这不是值语义,而是技术上的“按值传递”。
罗安2015年

@Luaan复制只是成本的一​​方面。由于指针/引用而导致的额外间接也需要每次访问。在某些情况下,该结构甚至可以移动,因此甚至不需要复制。
Onur 2016年

@Onur很有意思。您如何“移动”而不进行复制?我以为asm的“ mov”指令实际上并没有“ move”。复制。
温格·森登

148

我不同意原始帖子中给出的规则。这是我的规则:

1)将结构存储在数组中时,可以使用其性能。(另请参见何时构造答案?

2)在将结构化数据往返于C / C ++的代码中需要它们

3)除非需要它们,否则不要使用它们:

  • 它们的行为不同于“正常对象”(引用类型在赋值和作为参数传递时的),这可能导致意外行为;如果查看代码的人不知道自己正在处理结构,则这特别危险。
  • 它们不能被继承。
  • 将结构作为参数传递比使用类昂贵。

4
+1是,我完全同意第一点(在处理图像等事物时这是一个巨大的优势),并指出它们与“正常对象” 不同,并且有一种已知的方式来了解这一点,但现有知识除外或检查类型本身。另外,您不能将空值强制转换为结构类型:-)实际上,这是我几乎希望非核心值类型有一些“匈牙利语”或在变量声明站点中使用强制性“ struct”关键字的一种情况。

@pst:的确是一个必须知道某事的人struct才能知道它的行为,但是如果某个事物是struct有暴露字段的,那便是所有人所必须知道的。如果对象公开了暴露字段结构类型的属性,并且如果代码将结构读取为变量并进行了修改,则可以安全地预测这样的操作将不会影响被读取其属性的对象,除非或直到写入结构背部。相比之下,如果属性是可变的类类型,则读取并修改它可能会按预期更新基础对象,但是……
supercat 2012年

...它也可能最终什么也没有改变,或者可能改变或破坏了一个人不打算改变的对象。拥有语义说“随心所欲地更改此变量;更改直到您将其明确存储到任何地方”的代码,似乎比拥有“说您正在获取对某个对象的引用,该对象可能与任何数字共享”的代码更清楚。其他参考,或者可能根本不会共享;您必须弄清楚还有谁可能对此对象进行参考,以了解如果更改它将会发生什么。”
超级猫

当场#1。与具有对象引用的列表(适用于适当大小的结构)相比,一个包含结构的列表可以将更多相关数据压缩到L1 / L2缓存中。
马特·史蒂芬森

2
继承很少是工作的正确工具,而对性能进行过多分析而不进行概要分析是一个坏主意。首先,可以通过引用传递结构。其次,通过引用或按值传递很少是一个重大的性能问题。最后,您无需考虑类需要进行的其他堆分配和垃圾回收。就个人而言,我更喜欢将结构视为纯旧数据,将类视为事情(对象),但你可以在结构定义方法为好。
weberc2

87

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

编辑

不知道为什么人们不赞成这一点,但这是有道理的,是在操作者澄清他的问题之前提出的,这是结构最根本的基本原因。

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


13
每个人都知道。似乎他在寻找的不仅仅是“结构是一种价值类型”的答案。
TheSmurf

21
这是最基本的情况,应该为任何阅读此帖子但不知道的人说。
2009年

3
并不是说这个答案不正确;显然是。这不是重点。
TheSmurf,2009年

55
@Josh:对于尚不知道的人,只是说这是一个不足够的答案,因为很可能他们也不知道这意味着什么。
TheSmurf

1
我之所以对此表示反对,是因为我认为其他答案之一应该放在顶部-任何回答“对于非托管代码互操作,否则请避免”。
Daniel Earwicker,09年

59

除了“这是一个价值”答案之外,使用结构的一种特定情况是,当您知道有一组导致垃圾收集问题的数据,并且有很多对象时。例如,一个很大的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]);

同样,这是就位的-我们尚未复制该值。

在非常特定的情况下,此策略可能会非常成功。但是,这是一个相当高级的场景,只有在您知道自己在做什么和为什么的情况下才应尝试。这里的默认值是一个类。


+1有趣的答案。您是否愿意分享有关使用这种方法的现实世界的轶事?
Jordão酒店

@Jordao在移动设备上,但在Google上搜索:+ gravell +“ GC袭击”
Marc Gravell

1
非常感谢。我在这里找到的。
Jordão酒店

2
@MarcGravell为什么提到:使用数组(而不是列表)List我相信,使用Array后台。不行吗
罗伊·纳米尔

4
@RoyiNamir我也对此感到很好奇,但我相信答案就在Marc答案的第二段。“但是,直接在数组中访问它不会复制该结构-它是就位的(与列表索引器相反,后者会复制)。”
user1323245 2014年

40

根据C#语言规范

1.7结构

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

结构对于具有值语义的小型数据结构特别有用。复数,坐标系中的点或字典中的键-值对都是结构的良好示例。对于小型数据结构,使用结构而不是类可以在应用程序执行的内存分配数量上产生很大差异。例如,以下程序创建并初始化一个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运算符调用结构构造函数,但这并不意味着正在分配内存。代替动态分配对象并返回对其的引用,结构构造函数仅返回结构值本身(通常在堆栈的临时位置),然后根据需要复制该值。

对于类,两个变量可以引用同一对象,因此对一个变量的操作可能会影响另一个变量引用的对象。使用结构时,变量每个都有其自己的数据副本,并且对一个变量的操作不可能影响另一个变量。例如,以下代码片段产生的输出取决于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的赋值创建了该值的副本,并且此副本不受后续对ax赋值的影响

前面的示例突出了结构的两个局限性。首先,复制整个结构通常比复制对象引用的效率低,因此,与引用类型相比,结构的赋值和值参数传递可能更昂贵。其次,除了ref和out参数外,无法创建对结构的引用,这会在许多情况下排除它们的使用。


4
虽然有时无法保留对结构的引用这一事实是一个限制,但它也是一个非常有用的特征。.net的主要缺点之一是,没有一种可靠的方法可以在不永久失去对该对象的控制的情况下将外部代码的引用传递给可变对象。相比之下,可以安全地将外部方法a ref赋予可变的结构,并且知道外部方法将对其执行的任何突变都将在返回之前完成。太糟糕了。.net没有任何临时参数和函数返回值的概念,因为...
supercat 2012年

4
...这将允许ref使用类对象实现传递的结构的有利语义。本质上,局部变量,参数和函数返回值可以是可持久的(默认),可返回的或短暂的。禁止将临时性内容复制到超出当前范围的任何内容。可返回的东西就像短暂的东西,除了它们可以从函数中返回。函数的返回值将受到适用于其任何“可返回”参数的最严格的限制。
超级猫2012年

34

结构对于数据的原子表示很有用,其中所述数据可以通过代码多次复制。克隆对象通常比复制结构要昂贵,因为克隆涉及到分配内存,运行构造函数以及完成后回收/垃圾回收。


4
是的,但是大型结构可能比类引用(在传递给方法时)更昂贵。
亚历克斯

27

这是一个基本规则。

  • 如果所有成员字段都是值类型,则创建一个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
}

3
诸如此类的不可变引用类型string在语义上等效于值,并且将对不可变对象的引用存储到字段中并不需要堆分配。具有公开公共字段的结构与具有公开公共字段的类对象之间的区别在于,给定代码序列var q=p; p.X=4; q.X=5;p.X如果a是结构类型,则值为4;如果是类类型,则值为5。如果希望能够方便地修改该类型的成员,则应根据是否要q影响更改来选择“类”或“结构” p
2014年

是的,我同意引用变量将在堆栈上,但是它引用的对象将在堆上。尽管将结构和类分配给不同的变量时,它们的行为有所不同,但我认为这不是一个强力的决定因素。
Usman Zafar 2014年

可变结构和可变类的行为完全不同。如果一个是对的,那么另一个很可能是错误的。我不确定行为是不是决定使用结构还是类的决定因素。
超级猫

我说这不是一个决定性的因素,因为通常在创建类或结构时,您不确定如何使用它。因此,您将从设计的角度专注于事情如何变得更有意义。无论如何,我从未在.NET库中的一个结构中包含引用变量的地方见过它。
Usman Zafar 2014年

1
结构类型ArraySegment<T>封装了T[],它始终是类类型。结构类型KeyValuePair<TKey,TValue>通常与类类型一起用作通用参数。
超级猫

19

第一:互操作方案或需要指定内存布局的情况

第二:无论如何,数据的大小几乎都与参考指针相同。


17

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

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


5
StructLayoutAttribute可以应用于结构或类,因此这不是使用结构的原因。
斯蒂芬·马丁

如果您只是将参数传递给非托管方法调用,为什么这样做有意义?
David Klempfner '17

16

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

在这种情况下,您列出的三个指南对我没有用。当我需要按特定顺序写出400字节的内容时,我要定义一个400字节的结构,并用它应该具有的任何不相关值填充它,而我要以最有意义的方式进行设置。(好吧,四百个字节会很奇怪-但是当我以写作Excel文件为生时,我正在处理多达四十个字节的结构,因为这就是一些BIFF记录的大小。)


难道您不那么容易使用引用类型吗?
David Klempfner

15

除了运行时直接使用的值类型以及用于PInvoke的其他值类型外,您仅应在2种情况下使用值类型。

  1. 需要复制语义时。
  2. 当您需要自动初始化时,通常在这些类型的数组中。

#2似乎是.Net集合类中结构盛行的原因的一部分
。– IAbstract

如果在创建类类型的存储位置时要做的第一件事是创建该类型的新实例,在该位置存储对它的引用,并且永远不要将引用复制到其他地方也不会覆盖它,那么将构造一个和类的行为相同。结构是一种方便的标准方法,可以将所有字段从一个实例复制到另一个实例,并且在人们永远不会复制对类的引用(this用于调用其方法的临时参数除外)的情况下,结构通常会提供更好的性能;类允许一个重复的引用。
2013年

13

.NET支持value typesreference types(在Java中,您只能定义引用类型)。reference typesget 实例在托管堆中分配,并且在没有未引用的情况下被垃圾回收。value types另一方面,的实例是在中分配的stack,因此一旦其作用域结束,便会回收分配的内存。当然,value types要通过价值传递,并且reference types参考。除System.String外,所有C#基本数据类型都是值类型。

何时在类上使用struct,

在C#中,structsare是value typesreference types。您可以使用enum关键字和struct关键字在C#中创建值类型。使用a value type代替a reference type将导致托管堆上的对象减少,这将导致垃圾回收器(GC)上的负载减少,GC周期减少,从而提高了性能。但是,value types也有缺点。绕过struct大关绝对比通过参考要昂贵,这是一个明显的问题。另一个问题是与相关的开销boxing/unboxing。以防万一你在想什么。除了性能之外,有时您只需要类型具有值语义,这很难(或很难)实现,boxing/unboxing是意思,请通过以下链接在boxing和上进行详细说明unboxingreference types就是你所有的。value types仅在需要复制语义或需要自动初始化时才应使用,通常使用arrays这些类型。


复制小型结构或按值传递与复制或传递类引用或通过传递结构一样便宜ref。按大小传递任何大小结构ref与按值传递类引用的成本相同。复制任何大小的结构或按值传递比对类对象执行防御性复制并存储或传递对其的引用要便宜。大类比存储值的结构更好(1),当类是不可变的(以避免防御性复制)时,创建的每个实例都会被传递很多,或者...
supercat

...(2)当由于各种原因而使一个结构根本不可用时(例如,因为一个树需要使用嵌套引用,例如树,或者因为一个人需要多态性)。请注意,在使用值类型时,通常应直接公开字段,而没有特定原因(而对于大多数类类型,字段应包装在属性中)。许多所谓的可变值类型的“恶作剧”源于属性中字段的不必要包装(例如,虽然某些编译器会允许人在只读结构上调用属性设置器,因为有时它会...
supercat

...做正确的事,所有编译器都会正确拒绝直接在此类结构上设置字段的尝试;确保编译器拒绝的最好方法readOnlyStruct.someMember = 5;是不使之someMember成为只读属性,而是使其成为一个字段。
超级猫2012年

12

结构是一个值类型。如果将结构分配给新变量,则新变量将包含原始副本。

public struct IntStruct {
    public int Value {get; set;}
}

执行以下操作会在内存中存储结构的5个实例

var struct1 = new IntStruct() { Value = 0 }; // original
var struct2 = struct1;  // A copy is made
var struct3 = struct2;  // A copy is made
var struct4 = struct3;  // A copy is made
var struct5 = struct4;  // A copy is made

// NOTE: A "copy" will occur when you pass a struct into a method parameter.
// To avoid the "copy", use the ref keyword.

// Although structs are designed to use less system resources
// than classes.  If used incorrectly, they could use significantly more.

是引用类型。将类分配给新变量时,该变量包含对原始类对象的引用。

public class IntClass {
    public int Value {get; set;}
}

执行以下操作只会在内存中产生类对象的一个实例

var class1 = new IntClass() { Value = 0 };
var class2 = class1;  // A reference is made to class1
var class3 = class2;  // A reference is made to class1
var class4 = class3;  // A reference is made to class1
var class5 = class4;  // A reference is made to class1  

结构可能会增加代码错误的可能性。如果将值对象视为可变的引用对象,则当所做的更改意外丢失时,开发人员可能会感到惊讶。

var struct1 = new IntStruct() { Value = 0 };
var struct2 = struct1;
struct2.Value = 1;
// At this point, a developer may be surprised when 
// struct1.Value is 0 and not 1

12

我用BenchmarkDotNet做了一个小基准,以更好地理解数字的“结构”收益。我正在测试遍历结构(或类)的数组(或列表)。创建这些数组或列表超出了基准测试的范围-显然,“类”更重将占用更多内存,并涉及GC。

因此得出的结论是:小心使用LINQ和隐藏结构进行装箱/拆箱,并使用进行微优化的结构严格保留在数组中。

PS关于通过调用栈传递结构/类的另一个基准是https://stackoverflow.com/a/47864451/506147

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 Grdinic

仅仅由于不需要其他引用,所以可以更快地访问数组中的struct。装箱/拆箱是linq的情况。
罗曼·波克罗夫斯基

10

通常使用C#或其他.net语言的结构类型来保存应该表现为固定大小的值组的事物。结构类型的一个有用方面是,可以通过修改保存结构类型实例的存储位置来修改结构类型实例的字段,而没有其他方式。可以通过以下方式对结构进行编码:突变任何字段的唯一方法是构造一个完整的新实例,然后使用结构赋值通过用新实例中的值覆盖目标的所有字段来对其进行突变,但是除非结构没有提供创建其字段具有非默认值的实例的方法,否则如果结构本身存储在可变位置中,则其所有字段都是可变的。

请注意,可以设计结构类型,以便在结构包含私有class-type字段并将其自身的成员重定向到包装的类对象的成员时,其本质上类似于类类型。例如,a PersonCollection可能提供属性SortedByNameSortedById,它们都持有对PersonCollection(在其构造函数中设置的)“不可变”的引用,并GetEnumerator通过调用creator.GetNameSortedEnumerator或来实现creator.GetIdSortedEnumerator。这样的结构的行为就像对a的引用PersonCollection,除了它们的GetEnumerator方法将被绑定到PersonCollection。一个也可以有一个结构来包装数组的一部分(例如,可以定义一个ArrayRange<T>结构来容纳T[]被调用Arr的int)。Offset和一个intLength,具有索引属性,对于idx0到范围内的索引,该属性Length-1将访问Arr[idx+Offset])。不幸的是,如果foo是此类结构的只读实例,则当前的编译器版本将不允许类似的操作,foo[3]+=4;因为它们无法确定此类操作是否会尝试写入的字段foo

也可以设计一种结构,使其行为类似于一个值类型,该值类型包含一个可变大小的集合(只要有该结构,它就会被复制),但是唯一可行的方法是确保没有对象struct持有引用将永远暴露于任何可能使其变异的地方。例如,可能有一个类似数组的结构,该结构保存一个私有数组,并且其索引的“ put”方法创建了一个新数组,其内容与原始数组相似,但有一个更改的元素。不幸的是,要使这种结构有效地执行可能有些困难。虽然有时候结构语义可能很方便(例如,能够将类似数组的集合传递给例程,但调用者和被调用者都知道外部代码不会修改集合,


10

不,我不完全同意规则。它们是考虑性能和标准化的良好指导原则,但并非无所不能。

正如您在回复中看到的那样,有很多创造性的方式来使用它们。因此,始终出于性能和效率的考虑,这些准则仅需如此。

在这种情况下,我使用类以较大的形式表示现实世界的对象,使用结构表示具有更精确用途的较小的对象。您所说的是“更具凝聚力的整体”。关键字具有凝聚力。这些类将是更多的面向对象的元素,而结构可以具有某些特征,尽管规模较小。海事组织。

我在Treeview和Listview标记中经常使用它们,可以快速访问常见的静态属性。我一直在努力以其他方式获取此信息。例如,在我的数据库应用程序中,我使用树视图,其中有表,SP,函数或任何其他对象。我创建并填充我的结构,将其放入标签中,将其拉出,获取选择的数据,依此类推。我不会在课堂上这样做!

我会尽量减小它们的体积,在单实例情况下使用它们,并保持它们不变。谨慎考虑内存,分配和性能。测试非常必要。


结构可以明智地用于表示轻量级不变对象,或者它们可以明智地用于表示相关但独立变量的固定集合(例如点的坐标)。该页面上的建议对于旨在用于前一个目的的结构是好的,但是对于旨在用于后一个目的的结构是错误的。我目前的想法是,具有任何私有字段的结构通常应符合指示的描述,但是许多结构应通过公共字段公开其整个状态。
2013年

如果“ 3d点”类型的规范表明其整个状态通过可读成员x,y和z公开,并且可以double为这些坐标创建具有任意值组合的实例,则该规范将迫使它除了多线程行为的某些细节外,语义上的行为与暴露字段结构相同(在某些情况下,不可变类会更好,而在其他情况下,不可变类会更好;所谓的“不可变”结构会在每种情况下都会更糟)。
2013年

8

我的规则是

1,始终使用类;

2,如果有任何性能问题,我尝试根据@IAbstract提到的规则将某些类更改为struct,然后进行测试以查看这些更改是否可以提高性能。


Microsoft忽略的一个重要用法案例是,当一个人想要一个类型的变量Foo来封装一组固定的独立值(例如,一个点的坐标)时,人们有时会希望将它们作为一组传递,有时又希望独立地进行更改。我还没有找到一种使用类的模式,它结合了两个目的,几乎就像一个简单的暴露域结构(作为固定的独立变量集合,完全符合要求)一样。
2013年

1
@supercat:我认为怪罪微软是不完全公平的。真正的问题是C#作为一种面向对象的语言,根本不关注仅公开数据而没有太多行为的纯记录类型。C#与例如C ++一样,并不是一种多范式语言。话虽这么说,我相信很少有人会编写纯OOP,所以C#可能是一种过于理想化的语言。(我最近也开始公开public readonly类型中的字段,因为创建只读属性实在是太
费力了

1
@stakx:不需要它“专注”于此类类型;认识他们的本意就足够了。C#关于结构的最大缺点也是它在许多其他领域中的最大问题:该语言提供了不足的功能来指示何时进行某些转换是适当的还是不合适的,并且缺少此类功能会导致不幸的设计决策。例如,99%的“可变结构都是邪恶的”源于编译器将其转换MyListOfPoint[3].Offset(2,3);var temp=MyListOfPoint[3]; temp.Offset(2,3);,这种转换在应用时是虚假的……
supercat

...该Offset方法。防止此类伪代码的正确方法不应是使结构不必要地不变,而应允许像这样的方法Offset使用禁止上述转换的属性进行标记。如果可以对隐式数值转换进行标记,以使其仅在显而易见的情况下适用,则隐式数值转换也可能会更好。如果存在针对foo(float,float)和的重载foo(double,double),我会认为尝试使用floatdouble通常不应应用隐式转换,而应该是一个错误。
2013年

double值直接分配给float,或将其传递给可以带float参数但不能带参数的方法double,几乎总是可以满足程序员的预期。相比之下,floatdouble没有显式类型转换的情况下分配表达式通常是一个错误。唯一允许隐式double->float转换会导致问题的时间是将导致选择不理想的过载时。我认为防止这种情况的正确方法不应该禁止使用不明智的double-> float,而是使用属性标记过载以禁止转换。
2013年

8

类是引用类型。创建该类的对象时,为其分配对象的变量仅保留对该内存的引用。当对象引用分配给新变量时,新变量引用原始对象。通过一个变量进行的更改会反映在另一个变量中,因为它们都引用相同的数据。结构是一种值类型。创建结构时,为其分配变量的变量将保存该结构的实际数据。将结构分配给新变量后,将对其进行复制。因此,新变量和原始变量包含相同数据的两个单独副本。对一个副本所做的更改不会影响另一副本。通常,类用于建模更复杂的行为,或用于在创建类对象之后修改数据的模型。

类和结构(C#编程指南)


在必须将一些相关但无关的变量与管道胶带(例如点的坐标)一起固定的情况下,结构也非常好。如果有人试图产生行为类似于对象的结构,但在设计集合体时就不那么合适了,那么MSDN指南是合理的。在后一种情况下,其中一些几乎完全是错误的。例如,类型封装的变量的独立程度越高,使用暴露字段结构而不是不可变类的优势就越大。
超级猫

5

我认为一个很好的第一近似是“从不”。

我认为一个好的第二近似值是“从不”。

如果您迫切希望获得性能,请考虑使用它们,但是请务必进行测量。


24
我不同意这个答案。结构在许多情况下都有合法用途。这是一个示例-以原子方式封送数据跨进程。
弗朗西·佩诺夫09年

25
您应该编辑您的文章并详细说明您的观点-您已经给出了自己的观点,但是应该支持为什么采取此观点。
Erik Forbes,2009年

4
我认为他们需要使用等效的Totin'Chip卡(en.wikipedia.org/wiki/Totin%27_Chip)来使用结构。说真的
格雷格2010年

4
一个87.5万的人如何发布这样的答案?他小时候做过吗?
Rohit Vipin Mathews,2015年

3
@Rohit-六年前;当时的现场标准大不相同。但是,这仍然是一个错误的答案,您是对的。
Andrew Arnold

5

我只是在与Windows Communication Foundation [WCF]命名为Pipe打交道,但我确实注意到使用Structs确实有意义,以确保数据交换是值类型而不是引用类型


1
恕我直言,这是最好的线索。
伊万

5

误解1:结构是轻量级课程

这个神话有多种形式。有人认为,值类型不能或不应该具有方法或其他重要行为-它们应被用作简单的数据传输类型,仅具有公共字段或简单属性。DateTime类型是一个很好的反例:将其作为值类型是有意义的,因为它是数字或字符之类的基本单位,并且能够基于以下内容执行计算也是有意义的:它的价值。从另一个方向来看,无论如何,数据传输类型通常都应该是引用类型-决策应该基于所需的值或引用类型的语义,而不是类型的简单性。其他人认为,就绩效而言,价值类型比参考类型“更轻”。事实是,在某些情况下,值类型的性能更高-例如,除非将它们装箱,否则它们不需要垃圾回收,没有类型标识的开销,也不需要解引用。但是以其他方式,引用类型的性能更高-参数传递,为变量分配值,返回值以及类似的操作仅需要复制4或8个字节(取决于您运行的是32位还是64位CLR) ),而不是复制所有数据。想象一下ArrayList是否是某种“纯”值类型,然后将ArrayList表达式传递给涉及复制所有数据的方法!几乎在所有情况下,性能并不是由这种决定真正决定的。瓶颈几乎永远不会出现在您想像的地方,在您根据性能做出设计决定之前,您应该衡量不同的选择。值得注意的是,两种信念的结合也不起作用。类型具有多少个方法(无论是类还是结构)都没有关系-每个实例占用的内存不会受到影响。(就代码本身占用的内存而言,这是有代价的,但这只发生一次,而不是每个实例发生一次。)

误区2:HEAP上存在引用类型;堆栈上存在的值类型

这通常是由重复它的人的懒惰引起的。第一部分是正确的-始终在堆上创建引用类型的实例。这是引起问题的第二部分。正如我已经指出的那样,变量的值可以保存在声明的任何位置,因此,如果您的类具有实例类型为int的实例变量,则任何给定对象的变量值将始终位于该对象的其余数据所在的位置,在堆上。只有局部变量(在方法中声明的变量)和方法参数才存在于堆栈中。在C#2和更高版本中,甚至一些局部变量也并没有真正存在于堆栈中,正如您在第5章中讨论匿名方法时所看到的那样,这些概念现在是否相关?可以争论的是,如果要编写托管代码,则应让运行时担心如何最好地使用内存。确实,语言规范不保证生活在何处;如果将来的运行时知道它可以摆脱它,那么它也许可以在堆栈上创建一些对象,或者C#编译器可以生成几乎完全不使用堆栈的代码。下一个神话通常只是术语问题。

误区三:默认情况下,通过引用C#来传递对象

这可能是传播最广的神话。同样,经常提出此主张的人(尽管并非总是如此)知道C#的实际行为,但他们不知道“通过引用”的真正含义。不幸的是,这对于确实知道这意味着什么的人们感到困惑。通过引用传递的形式定义相对复杂,涉及l值和类似的计算机科学术语,但是重要的是,如果通过引用传递变量,则所调用的方法可以更改调用者变量的值通过更改其参数值。现在,请记住,引用类型变量的值是引用,而不是对象本身。您可以更改参数引用的对象的内容,而无需通过引用传递参数本身。例如,

void AppendHello(StringBuilder builder)
{
    builder.Append("hello");
}

调用此方法时,将按值传递参数值(对StringBuilder的引用)。如果要在方法中更改builder变量的值(例如,使用语句builder = null;),则与神话相反,调用者将看不到该更改。有趣的是,不仅神话的“引用”位不准确,而且“对象被传递”位也是如此。无论是通过引用还是通过值,都不会传递对象本身。当涉及到引用类型时,要么通过引用传递变量,要么通过值传递参数(引用)的值。除了别的什么,这回答了以下问题:将null用作值参数时会发生什么—如果传递对象,则将导致问题,因为将没有对象通过!代替,空引用以与其他任何引用相同的方式按值传递。如果这种快速的解释使您感到迷惑,则不妨看一下我的文章“ C#中的参数传递”,(http://mng.bz/otVt),其中有更多详细信息。这些神话并不是唯一的神话。装箱和拆箱是由于他们的误会而引起的,接下来我将尝试消除这些误会。

参考:乔恩·斯凯特(Jon Skeet)的《 Cep in Depth 3rd Edition》


1
很好,假设您是正确的。添加参考也很好。
NoChance

4

C#结构是类的轻型替代方案。它几乎可以与类相同,但是使用结构而不是类“花费较少”。这样做的理由有点技术性,但总而言之,将类的新实例放在堆上,而将新实例化的结构放在堆栈上。此外,您不像类那样处理对结构的引用,而是直接使用结构实例。这也意味着,当您将结构传递给函数时,它是按值而不是引用。关于功能参数的章节中有更多有关此的内容。

因此,当您希望表示更简单的数据结构时,应该使用结构,尤其是在您知道将实例化许多结构的情况下。.NET框架中有很多示例,其中Microsoft使用了结构而不是类,例如Point,Rectangle和Color结构。



3

可以在以下情况下使用结构或值类型-

  1. 如果要防止通过垃圾回收来收集对象。
  2. 如果是简单类型,并且没有成员函数修改其实例字段
  3. 如果不需要从其他类型派生或被派生到其他类型。

您可以在此链接上了解有关值类型和值类型的更多信息


3

简要地说,如果使用struct:

1-您的对象属性/字段无需更改。我的意思是您只想给他们一个初始值,然后阅读它们。

2-对象中的属性和字段是值类型,并且它们不是很大。

如果是这种情况,您可以利用结构以获得更好的性能和优化的内存分配,因为它们仅使用堆栈,而不使用堆栈和堆(在类中)


2

我很少使用结构来做事情。但这就是我。这取决于我是否需要该对象可为空。

如其他答案所述,我将类用于实际对象。我也有结构的心态,结构用于存储少量数据。


-11

结构在大多数方面都类似于类/对象。结构可以包含函数,成员,并且可以继承。但是结构在C#中仅用于数据保存。结构确实比类占用更少的RAM,并且垃圾收集器更易于收集。但是,当在结构中使用函数时,编译器实际上采用的结构与类/对象非常相似,因此,如果您希望将某些东西与函数一起使用,请使用class / object


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.