ChatGPT解决这个技术问题 Extra ChatGPT

什么时候应该在 C# 中使用结构而不是类?

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

我遇到了这些规则here

一个结构应该代表一个单一的值。

结构的内存占用应小于 16 字节。

创建后不应更改结构。

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

System.Drawing.Rectangle 违反了所有这三个规则。
用C#编写的商业游戏不少,关键是它们用于优化代码
当您想要组合在一起的值类型的小集合时,结构会提供更好的性能。这在游戏编程中经常发生,例如,3D 模型中的顶点将具有位置、纹理坐标和法线,它通常也是不可变的。单个模型可能有几千个顶点,也可能有十几个,但在这种使用场景中,结构体提供的总体开销较小。我已经通过我自己的引擎设计验证了这一点。
@ChrisW 我明白了,但是这些值不是代表一个矩形,即“单个”值吗?像 Vector3D 或 Color 一样,它们里面也是几个值,但我认为它们代表单个值?

I
IAbstract

OP 引用的来源具有一定的可信度……但是 Microsoft 呢?对 struct 使用的立场是什么?我寻找了一些额外的 learning from Microsoft,这就是我发现的:

如果类型的实例很小且通常短暂存在或通常嵌入在其他对象中,请考虑定义结构而不是类。除非类型具有以下所有特征,否则不要定义结构: 它在逻辑上表示单个值,类似于原始类型(整数、双精度等)。它的实例大小小于 16 字节。它是不可变的。它不必经常装箱。

微软一贯违反这些规则

好吧,无论如何#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
}

*Reference Source

'JonnyCantCode.com' 来源获得了 4 分中的 3 分——这是可以原谅的,因为 #4 可能不会成为问题。如果您发现自己在装箱结构,请重新考虑您的架构。

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

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

更新 - 此外,要意识到当一个结构实现一个接口时 - 就像 Enumerator 所做的那样 - 并被强制转换为该实现的类型,该结构成为一个引用类型并被移动到堆中。在 Dictionary 类的内部,Enumerator is 仍然是一个值类型。但是,只要方法调用 GetEnumerator(),就会返回引用类型 IEnumerator

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

上面的结构中没有任何内容被声明为只读 - 不是不可变的 这些结构的大小可能远远超过 16 个字节 条目具有未确定的生命周期(从 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 内存大小:2660827 字节完成调整大小:26 毫秒总填充时间:964 毫秒

显然,最大的区别在于调整大小。如果 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# 编译器)不允许结构具有默认构造函数。

它实际上很简单,我们将借用 Asimov's Three Laws of Robotics

结构必须可以安全使用 结构必须有效地执行其功能,除非这会违反规则 #1

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


至于 Microsoft 的规则,关于不变性的规则似乎旨在阻止以这样一种方式使用值类型,即它们的行为与引用类型的行为不同,尽管分段可变的值语义可能很有用。如果具有分段可变的类型会使其更易于使用,并且如果类型的存储位置应该在逻辑上彼此分离,则该类型应该是“可变”结构。
Microsoft 的许多类型违反这些规则这一事实并不代表这些类型存在问题,而是表明这些规则不应适用于所有结构类型。如果一个结构表示单个实体 [如 DecimalDateTime],则如果它不遵守其他三个规则,则应将其替换为一个类。如果一个结构包含一个固定的变量集合,每个变量都可以包含对其类型有效的任何值 [例如 Rectangle],那么它应该遵守 不同 规则,其中一些规则是与“单值”结构相反。
@IAbstract:有些人会证明 Dictionary 条目类型只是内部类型,性能被认为比语义或其他借口更重要。我的观点是,像 Rectangle 这样的类型应该将其内容公开为可单独编辑的字段,而不是“因为”性能优势超过了由此产生的语义缺陷,而是因为 该类型在语义上表示一组固定的独立值,因此可变结构在性能和语义上都优越
@IAbstract:如果我有能力从 .net 更改一份早期文档,那可能是关于结构的。如果我在编写文档,我会建议结构应尽可能适合两种模式之一——要么是所描述的模式,要么是独立公共字段的集合。在需要后者的情况下,我认为建议将结构伪装成不可变的类对象是很疯狂的,这只能笨拙地完成暴露字段结构可以做得好的事情。
@supercat:我同意……我回答的重点是“准则”非常薄弱,应该在充分了解和理解行为的情况下使用结构。在此处查看我对可变结构的回答:stackoverflow.com/questions/8108920/…
e
einpoklum

每当您:

不需要多态性,需要值语义,并希望避免堆分配和相关的垃圾收集开销。

然而,需要注意的是,结构(任意大)比类引用(通常是一个机器字)更昂贵,因此在实践中类最终可能会更快。


这只是一个“警告”。还应该考虑“提升”值类型和情况,例如 (Guid)null(可以将 null 转换为引用类型)等等。
比 C/C++ 贵?在 C++ 中,推荐的方法是按值传递对象
@IonTodirel 这不是出于内存安全原因,而不是性能?这总是一个权衡,但通过堆栈传递 32 B 总是比通过寄存器传递 4 B 引用要慢。但是,还要注意,“值/引用”在 C# 和 C++ 中的使用有点不同——当你传递一个对象的引用时,你仍然是按值传递,即使你正在传递一个引用(你'重新传递引用的值,而不是对引用的引用,基本上)。这不是价值语义,而是技术上的“按值传递”。
@Luaan 复制只是成本的一方面。由于指针/引用导致的额外间接访问也需要每次访问成本。在某些情况下,结构甚至可以移动,因此甚至不需要复制。
U
Uwe Keim

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

当存储在数组中时,您可以使用结构来提高性能。 (另请参阅结构何时是答案?)您在将结构化数据传递给/从 C/C++ 的代码中需要它们 除非您需要它们,否则不要使用结构:它们在赋值和传递时的行为与“普通对象”(引用类型)不同作为参数,可能导致意外行为;如果查看代码的人不知道他们正在处理一个结构,这尤其危险。它们不能被继承。将结构作为参数传递比类更昂贵。


+1是的,我完全同意#1(这是处理图像等事物时的一个巨大优势)并指出它们与“普通对象”不同,并且除了现有知识之外,还有知道这一点的已知方法或检查类型本身。此外,您不能将空值强制转换为结构类型 :-) 这实际上是一种情况,我几乎希望在变量声明站点有一些用于非核心值类型的“匈牙利语”或强制的“结构”关键字.
@pst:确实,必须知道某物是 struct 才能知道它的行为方式,但如果某物是具有暴露字段的 struct,那么这就是人们所需要知道的。如果一个对象公开了一个暴露字段结构类型的属性,并且如果代码将该结构读取到一个变量并进行了修改,那么可以安全地预测,除非或直到该结构被写入,否则这种操作不会影响读取其属性的对象背部。相比之下,如果属性是可变类类型,读取并修改它可能会按预期更新基础对象,但是......
...它也可能最终什么都不改变,或者它可能会改变或破坏一个人不打算改变的对象。拥有语义为“随心所欲地更改此变量;更改不会做任何事情,直到您将它们显式存储在某个地方”的代码似乎比“您正在获取对某个对象的引用,该对象可能与任何数字共享”的代码更清晰“
继承很少是适合这项工作的工具,并且在没有分析的情况下过多地推理性能是一个坏主意。首先,结构可以通过引用传递。其次,通过引用或值传递很少是一个重要的性能问题。最后,您没有考虑一个类需要进行的额外堆分配和垃圾收集。就个人而言,我更喜欢将结构视为普通的旧数据,将类视为做事的事物(对象),尽管您也可以在结构上定义方法。
@ILoveFortran 你不能简单地说对象的行为与“普通结构”不同,如果这个人不知道他们正在处理一个对象而不是一个结构,他们可能会假设该值在作为参数传递时被复制到一个方法。
r
ruffin

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

编辑

不知道为什么人们不赞成这一点,但这是一个有效的观点,并且使before操作澄清了他的问题,这是结构的最基本原因。

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


每个人都知道这一点。似乎他正在寻找的不仅仅是“结构是一种值类型”的答案。
这是最基本的情况,应该为任何阅读这篇文章但不知道的人说明。
并不是说这个答案不正确;显然是。这不是重点。
@Josh:对于任何不知道它的人来说,简单地说这是一个不充分的答案,因为他们很可能也不知道这意味着什么。
我刚刚对此投了反对票,因为我认为其他答案之一应该排在首位 - 任何说“与非托管代码互操作,否则避免”的答案。
M
Marc Gravell

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

处理这些数据很尴尬,因为数据对于结构来说可能过大,而且您不想一直复制胖值。但是,直接在数组中访问它不会复制结构 - 它是就地的(与复制的列表索引器形成对比)。这意味着大量的索引工作:

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

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

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

同样,这是就地的——我们没有复制该值。

在非常具体的场景中,这种策略可以非常成功;但是,这是一个相当高级的场景,只有在您知道自己在做什么以及为什么做的情况下才应该尝试。这里的默认值是一个类。


+1 有趣的答案。您愿意分享有关使用这种方法的任何真实世界轶事吗?
@Jordao 在移动设备上,但在谷歌搜索:+gravell +"assault by GC"
非常感谢。我找到了它here
@MarcGravell 你为什么提到:使用数组(不是列表)List 我相信,在幕后使用 Array。不 ?
@RoyiNamir 我也对此感到好奇,但我相信答案在于马克答案的第二段。 “但是,直接在数组中访问它不会复制结构 - 它是就地的(与复制的列表索引器形成对比)。”
L
Luke Baughan

C# Language specification

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 参数,不能创建对结构的引用,这排除了它们在许多情况下的使用。


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

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


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

这是一个基本规则。

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

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

示例

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
}

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

第一:互操作场景或需要指定内存布局时

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


M
Maurice Flanagan

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

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


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

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

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


但是,您不能轻松地为此使用引用类型吗?
l
leppie

除了运行时直接使用的值类型和其他各种用于 PInvoke 目的的值类型外,您应该只在 2 个场景中使用值类型。

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


#2 似乎是 .Net 集合类中结构流行的部分原因。
如果在创建类类型的存储位置时要做的第一件事是创建该类型的新实例,在该位置存储对它的引用,并且永远不要将引用复制到其他任何地方或覆盖它,那么结构和类的行为相同。结构有一种方便的标准方法可以将所有字段从一个实例复制到另一个实例,并且通常会在永远不会复制对类的引用的情况下提供更好的性能(用于调用其方法的临时 this 参数除外);类允许重复引用。
R
Roman Pokrovskij

我用 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;
        }
    }

你有没有弄清楚为什么结构体在列表等中使用时会慢得多?是不是因为你提到的隐藏装箱和拆箱?如果是这样,为什么会发生?
访问数组中的结构应该更快,因为不需要额外的引用。装箱/拆箱是 linq 的情况。
S
Sujit

.NET 支持 value typesreference types(在 Java 中,您只能定义引用类型)。 reference types 的实例在托管堆中分配,并在没有对它们的未完成引用时进行垃圾收集。另一方面,value types 的实例在 stack 中分配,因此一旦它们的作用域结束,分配的内存就会被回收。当然,value types 通过值传递,reference types 通过引用传递。除 System.String 外,所有 C# 原始数据类型都是值类型。

何时在类上使用结构,

在 C# 中,structsvalue types,类是 reference types。您可以在 C# 中使用 enum 关键字和 struct 关键字创建值类型。使用 value type 而不是 reference type 将导致托管堆上的对象减少,从而减少垃圾收集器 (GC) 上的负载,减少 GC 周期,从而提高性能。但是,value types 也有其缺点。传递一个大的 struct 肯定比传递一个引用更昂贵,这是一个明显的问题。另一个问题是与 boxing/unboxing 相关的开销。如果您想知道 boxing/unboxing 的含义,请点击这些链接以获得关于 boxingunboxing 的详细说明。除了性能之外,有时您只需要类型具有值语义,如果您只有 reference types,这将很难(或难看)实现。您应该只使用 value types,当您需要复制语义或需要自动初始化时,通常在这些类型的 arrays 中。


复制小型结构或按值传递与复制或传递类引用或通过 ref 传递结构一样便宜。通过 ref 传递任何大小结构的成本与通过值传递类引用的成本相同。复制任何大小的结构或按值传递比执行类对象的防御性副本并存储或传递对它的引用要便宜。大时代类比结构更好的存储值是(1)当类是不可变的(以避免防御性复制),并且创建的每个实例将被传递很多,或者......
...(2)当由于各种原因结构根本不可用时[例如,因为需要对树之类的东西使用嵌套引用,或者因为需要多态性]。请注意,在使用值类型时,通常应该在没有特定原因的情况下直接公开字段(而大多数类类型字段应该包装在属性中)。许多所谓的可变值类型的“邪恶”源于属性中不必要的字段包装(例如,虽然一些编译器允许在只读结构上调用属性设置器,因为它有时会......
...做正确的事,所有编译器都会正确拒绝在此类结构上直接设置字段的尝试;确保编译器拒绝 readOnlyStruct.someMember = 5; 的最佳方法不是将 someMember 设为只读属性,而是将其设为字段。
J
Jason Williams

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

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

h
habib

误区一:结构是轻量级的类

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

误区二:引用类型在堆上;价值类型存在于堆栈中

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

误区 #3:对象在 C# 中默认通过引用传递

这大概是流传最广的神话了。同样,经常(尽管并非总是)提出这种说法的人知道 C# 的实际行为方式,但他们不知道“通过引用传递”的真正含义。不幸的是,这让知道这意味着什么的人感到困惑。引用传递的正式定义比较复杂,涉及左值和类似的计算机科学术语,但重要的是,如果你通过引用传递一个变量,你调用的方法可以改变调用者变量的值通过更改其参数值。现在,请记住引用类型变量的值是引用,而不是对象本身。您可以更改参数引用的对象的内容,而无需通过引用传递参数本身。例如,以下方法更改了相关 StringBuilder 对象的内容,但调用者的表达式仍将引用与以前相同的对象:

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

调用此方法时,参数值(对 StringBuilder 的引用)按值传递。如果您要在方法中更改 builder 变量的值——例如,使用语句 builder = null;——调用者不会看到该更改,这与神话相反。有趣的是,不仅神话中的“通过引用”位不准确,“对象被传递”位也是如此。对象本身永远不会通过引用或值传递。当涉及引用类型时,要么变量通过引用传递,要么参数(引用)的值通过值传递。除此之外,这还回答了当 null 用作按值参数时会发生什么的问题——如果对象被传递,那会导致问题,因为没有对象可以传递!相反,空引用以与任何其他引用相同的方式按值传递。如果这个快速的解释让您感到困惑,您可能想看看我的文章“C# 中的参数传递”(http://mng.bz/otVt),它更详细。这些神话并不是唯一的。装箱和拆箱是因为它们存在相当大的误解,接下来我将尝试澄清。

参考:C# in Depth 3rd Edition by Jon Skeet


很好,假设你是对的。添加参考也很好。
s
supercat

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

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

也可以设计一个结构,使其表现得像一个值类型,它包含一个可变大小的集合(只要结构存在,它就会被复制),但完成这项工作的唯一方法是确保没有对象struct 持有一个引用将永远暴露给任何可能改变它的东西。例如,可以有一个类似数组的结构,该结构包含一个私有数组,其索引“put”方法创建一个新数组,其内容与原始数组相同,只是有一个更改的元素。不幸的是,要使这样的结构有效地执行可能有些困难。虽然有时结构语义很方便(例如,能够将类似数组的集合传递给例程,调用者和被调用者都知道外部代码不会修改集合,但可能比同时要求调用者和被调用者更好)被调用者防御性地复制他们给出的任何数据),类引用指向永远不会变异的对象的要求通常是一个非常严格的约束。


S
SnapJag

不——我不完全同意这些规则。它们是考虑性能和标准化的好指南,但不是考虑到可能性。

正如您在回复中看到的那样,有很多创造性的方式可以使用它们。因此,为了性能和效率,这些指导方针必须如此。

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

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

我确实尝试让它们保持小,在单一实例情况下使用它们,并防止它们改变。谨慎注意内存、分配和性能。测试是如此必要。


结构可以合理地用于表示轻量级的不可变对象,或者它们可以合理地用于表示固定的相关但独立变量集(例如,点的坐标)。该页面上的建议适用于旨在服务于前一个目的的结构,但对于旨在服务于后一个目的的结构是错误的。我目前的想法是,具有任何私有字段的结构通常应该符合指定的描述,但是许多结构应该通过公共字段公开它们的整个状态。
如果“3d 点”类型的规范表明其整个状态通过可读成员 x、y 和 z 公开,并且可以使用这些坐标的 double 值的任意组合创建实例,那么这样的规范将强制它在语义上与暴露字段结构相同,除了多线程行为的一些细节(不可变类在某些情况下会更好,而暴露字段结构在其他情况下会更好;所谓的“不可变“ struct 在每种情况下都会更糟)。
r
rockXrock

我的规则是

1、始终使用类;

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


Microsoft 忽略的一个重要用例是,当一个人想要一个类型为 Foo 的变量来封装一个固定的独立值集合(例如,一个点的坐标)时,人们有时希望将其作为一个组传递,有时希望独立更改.我还没有找到任何使用类的模式,它结合了这两个目的几乎和一个简单的暴露字段结构一样好(它是一个固定的自变量集合,完全符合要求)。
@supercat:我认为将这归咎于微软并不完全公平。这里真正的问题是,作为面向对象的语言,C# 根本不关注仅公开数据而没有太多行为的普通记录类型。 C# 不像 C++ 那样是多范式语言。话虽如此,我相信很少有人编写纯 OOP,所以也许 C# 是一种过于理想化的语言。 (我最近也开始在我的类型中公开 public readonly 字段,因为创建只读属性的工作量太大,实际上没有任何好处。)
@stakx:没有必要“关注”这些类型;认清它们的本质就足够了。 C# 在结构方面的最大弱点也是它在许多其他领域的最大问题:该语言没有提供足够的工具来指示某些转换何时合适或不合适,并且缺乏这些工具会导致不幸的设计决策。例如,99% 的“可变结构是邪恶的”源于编译器将 MyListOfPoint[3].Offset(2,3); 转换为 var temp=MyListOfPoint[3]; temp.Offset(2,3);,这种转换在应用时是虚假的......
...到 Offset 方法。防止这种虚假代码的正确方法不应该是使结构不必要地不可变,而是允许像 Offset 这样的方法被标记为禁止上述转换的属性。如果可以对隐式数字转换进行标记,以便仅在其调用显而易见的情况下适用,那么隐式数值转换也可能会好得多。如果 foo(float,float)foo(double,double) 存在重载,我认为尝试使用 floatdouble 通常不应该应用隐式转换,而应该是错误。
double 值直接分配给 float,或将其传递给可以采用 float 参数但不能采用 double 的方法,几乎总是符合程序员的意图。相比之下,在没有显式类型转换的情况下将 float 表达式分配给 double 通常是错误的。唯一允许隐式 double->float 转换会导致问题的情况是它会导致选择不太理想的重载。我认为防止这种情况的正确方法不应该是禁止隐式 double->float,而是用属性标记重载以禁止转换。
J
J_hajian_nzd

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

Classes and Structs (C# Programming Guide)


在需要将一些相关但独立的变量与胶带(例如点的坐标)固定在一起的情况下,结构也非常好。如果试图生成行为类似于对象但在设计聚合时不太合适的结构,则 MSDN 指南是合理的;在后一种情况下,其中一些几乎完全是错误的。例如,类型封装的变量的独立程度越高,使用暴露字段结构而不是不可变类的优势就越大。
N
N_E

我只是在处理 Windows Communication Foundation [WCF] Named Pipe,我确实注意到使用 Structs 来确保数据交换是值类型而不是引用类型是有意义的。


这是最好的线索,恕我直言。
S
Saeed Dini

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

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


P
Pang

简而言之,在以下情况下使用 struct:

您的对象属性/字段不需要更改。我的意思是你只想给它们一个初始值,然后读取它们。对象中的属性和字段是值类型,它们不是那么大。

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


B
Brian

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

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

如果您迫切需要性能,请考虑它们,但要始终衡量。


我不同意这个答案。结构在许多情况下都有合法用途。这是一个示例 - 以原子方式跨进程封送数据。
你应该编辑你的帖子并详细说明你的观点——你已经给出了你的观点,但你应该支持你为什么接受这个观点。
我认为他们需要一个等效的 Totin' Chip 卡 (en.wikipedia.org/wiki/Totin%27_Chip) 来使用结构。严重地。
一个 87.5K 的人如何发布这样的答案?他小时候做过吗?
@Rohit - 那是六年前的事了;当时的现场标准非常不同。不过,这仍然是一个糟糕的答案,你是对的。
R
Rabbit

结构可用于提高垃圾收集性能。虽然您通常不必担心 GC 性能,但在某些情况下它可能会成为杀手。就像低延迟应用程序中的大型缓存一样。有关示例,请参见此帖子:

http://00sharp.wordpress.com/2013/07/03/a-case-for-the-struct/


R
Rupesh

以下是 Microsoft 网站上定义的规则:

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

❌ 避免定义结构,除非该类型具有以下所有特征:

它在逻辑上表示单个值,类似于原始类型(int、double 等)。

它的实例大小小于 16 个字节。

它是不可变的。

它不必经常装箱。

进一步reading


这个答案似乎只是(部分)this existing answer的重复。
V
Vizu

除了通常提到的性能差异之外,让我添加另一个方面,那就是揭示默认值使用的意图。

如果结构的字段的默认值不代表建模概念的合理默认值,则不要使用结构。

例如。

即使所有字段都设置为默认值,颜色或点也是有意义的。 RGB 0,0,0 是一种非常好的颜色, (0,0) 作为 2D 中的点也是如此。

但是 Address 或 PersonName 没有合理的默认值。我的意思是你能理解 FirstName=null 和 LastName=null 的 PersonName 吗?

如果你用一个类实现一个概念,那么你可以强制执行某些不变量,例如。一个人必须有名字和姓氏。但是对于结构,总是可以创建一个实例,并将其所有字段设置为默认值。

因此,在对没有合理默认值的概念进行建模时,更喜欢使用类。您的类的用户将理解 null 意味着未指定 PersonName,但如果您将其所有属性都设置为 null 的 PersonName 结构实例交给他们,他们会感到困惑。

(通常的免责声明:性能考虑可能会覆盖此建议。如果您有性能问题,请始终在决定解决方案之前进行测量。试试BenchmarkDotNet,这太棒了!)


F
Flexo

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

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


l
logeshpalani31

✔️ 考虑结构用法

创建对象或不需要创建对象(直接可以赋值,它创建对象) 需要速度或性能改进 不需要构造函数和析构函数(静态承包商可用) 不需要类继承,但可以接受接口 工作量小对象工作,如果它变高,内存问题将引发您不能为变量设置默认值。 Struct 还有可用的方法、事件、静态构造函数、变量等 GC 的工作量更少 不需要引用类型,只有值类型(每次创建新对象时) 没有不可变对象(字符串是不可变对象,因为任何操作都不返回任何每次新字符串不改变原来的)


C
Cryo Erger

结构在大多数方面类似于类/对象。结构可以包含函数、成员并且可以被继承。但是结构在 C# 中仅用于数据保存。结构确实比类占用更少的 RAM,并且更容易被垃圾收集器收集。但是当您在结构中使用函数时,编译器实际上将该结构与类/对象非常相似,因此如果您想要带有函数的东西,请使用类/对象。


结构不能被继承,参见msdn.microsoft.com/en-us/library/0taef578.aspx
某物是否是结构与垃圾收集的难易程度无关。话虽如此,在正常情况下,结构根本不会被垃圾收集 - 因为它不是在堆上分配的。但是,仅仅因为它是一个结构并不意味着它永远不会在堆上分配。如果它被装箱,或者它是引用类型的成员,那么它将在堆上分配。但是一旦它被分配到堆上,垃圾收集器就会像任何其他引用类型一样处理它 - 垃圾收集并不比任何其他类型更容易。