什么时候应该在 C# 中使用结构而不是类?
Posted
技术标签:
【中文标题】什么时候应该在 C# 中使用结构而不是类?【英文标题】:When should I use a struct rather than a class in C#? 【发布时间】:2021-06-13 08:47:18 【问题描述】:什么时候应该在 C# 中使用 struct 而不是 class?我的概念模型是当项目只是值类型的集合时使用结构。一种在逻辑上将它们组合成一个有凝聚力的整体的方法。
我遇到了这些规则here:
一个结构应该代表一个单一的 价值。 结构应该有内存 占用空间小于 16 个字节。 结构不应更改后 创作。这些规则有效吗?结构在语义上意味着什么?
【问题讨论】:
System.Drawing.Rectangle
违反了所有这三个规则。
用C#写的商业游戏也不少,重点是用于优化代码
当您想要组合在一起的值类型的小集合时,结构会提供更好的性能。这在游戏编程中经常发生,例如,3D 模型中的顶点将具有位置、纹理坐标和法线,它通常也是不可变的。单个模型可能有几千个顶点,也可能有十几个,但在这种使用场景中,结构体提供的总体开销较小。我已经通过自己的引擎设计验证了这一点。
@ErikForbes:我认为this is commonly held as the biggest BCL "oops"
@ChrisW 我明白了,但这些值不是代表一个矩形,即一个“单一”值吗?和 Vector3D 或者 Color 一样,它们里面也是几个值,但我觉得它们代表的是单个值?
【参考方案1】:
当您需要值语义而不是引用语义时,请使用结构。
编辑
不知道为什么人们不赞成这一点,但这是一个有效的观点,并且before 操作员澄清了他的问题,这是结构的最基本原因。
如果您需要引用语义,则需要一个类而不是结构。
【讨论】:
每个人都知道这一点。似乎他在寻找的不仅仅是“结构是一种值类型”的答案。 这是最基本的情况,应该为任何阅读这篇文章但不知道的人说明。 并不是说这个答案不正确;显然是。这不是重点。 @Josh:对于任何不知道它的人来说,简单地说这是一个不充分的答案,因为他们很可能也不知道这意味着什么。 我刚刚对此投了反对票,因为我认为其他答案之一应该放在首位 - 任何说“与非托管代码互操作,否则避免”的答案。【参考方案2】:无论何时:
-
不需要多态性,
想要值语义,并且
希望避免堆分配和相关的垃圾回收开销。
但需要注意的是,结构(任意大)比类引用(通常是一个机器字)的传递成本更高,因此类最终在实践中可能会更快。
【讨论】:
这只是一个“警告”。还应该考虑“提升”值类型和案例,例如(Guid)null
(可以将 null 转换为引用类型)等等。
比 C/C++ 贵吗?在 C++ 中,推荐的方法是按值传递对象
@IonTodirel 这不是出于内存安全的原因,而不是性能?这总是一个权衡,但通过堆栈传递 32 B 总是比通过寄存器传递 4 B 引用要慢。 但是,还要注意“值/引用”的使用在 C# 和 C++ 中有点不同 - 当您传递对对象的引用时,您仍然按值传递,即使您重新传递引用(基本上,您传递的是引用的值,而不是对引用的引用)。这不是价值语义,而是技术上的“按值传递”。
@Luaan 复制只是成本的一方面。由于指针/引用导致的额外间接访问也需要每次访问成本。在某些情况下,结构甚至可以移动,因此甚至不需要复制。【参考方案3】:
我认为一个好的初步近似值是“从不”。
我认为一个好的第二个近似值是“从不”。
如果您迫切需要性能,请考虑它们,但要始终衡量。
【讨论】:
我不同意这个答案。结构在许多情况下都有合法用途。这是一个示例 - 以原子方式跨进程封送数据。 你应该编辑你的帖子并详细说明你的观点 - 你已经给出了你的意见,但你应该支持你为什么接受这个意见。 我认为他们需要一个等效的 Totin' 芯片卡 (en.wikipedia.org/wiki/Totin%27_Chip) 来使用结构。说真的。 87.5K 人如何发布这样的答案?他小时候做过吗? @Rohit - 那是六年前的事了;当时的现场标准非常不同。不过,这仍然是一个糟糕的答案,你是对的。【参考方案4】:结构适用于数据的原子表示,其中所述数据可以被代码复制多次。克隆对象通常比复制结构更昂贵,因为它涉及分配内存、运行构造函数以及在完成后解除分配/垃圾收集。
【讨论】:
是的,但是大型结构可能比类引用更昂贵(传递给方法时)。【参考方案5】:在您想使用StructLayoutAttribute 显式指定内存布局的情况下,您需要使用“结构” - 通常用于 PInvoke。
编辑:评论指出您可以将类或结构与 StructLayoutAttribute 一起使用,这当然是正确的。在实践中,您通常会使用结构 - 它是在堆栈上分配而不是在堆上分配,如果您只是将参数传递给非托管方法调用,这是有意义的。
【讨论】:
StructLayoutAttribute 可以应用于结构或类,因此这不是使用结构的理由。 如果您只是将参数传递给非托管方法调用,为什么有意义?【参考方案6】:第一:互操作场景或者需要指定内存布局的时候
第二:当数据的大小与引用指针几乎相同时。
【讨论】:
【参考方案7】:除了运行时直接使用的值类型和其他各种用于 PInvoke 目的的值类型外,您应该只在 2 个场景中使用值类型。
-
当您需要复制语义时。
当您需要自动初始化时,通常在这些类型的数组中。
【讨论】:
#2 似乎是 .Net 集合类中结构流行的原因的部分。.. 如果在创建类类型的存储位置时要做的第一件事是创建该类型的新实例,在该位置存储对它的引用,并且永远不要将引用复制到任何地方否则也不覆盖它,那么结构和类的行为将相同。结构有一种方便的标准方法可以将所有字段从一个实例复制到另一个实例,并且通常会在永远不会复制对类的引用的情况下提供更好的性能(除了用于调用其方法的临时this
参数);类允许重复引用。【参考方案8】:
我使用结构来打包或解包任何类型的二进制通信格式。这包括读取或写入磁盘、DirectX 顶点列表、网络协议或处理加密/压缩数据。
在这种情况下,您列出的三个准则对我没有用。当我需要按特定顺序写出 400 字节的内容时,我将定义一个 400 字节的结构,并用它应该具有的任何不相关的值填充它,然后我会去以当时最有意义的方式进行设置。 (好吧,四百字节会很奇怪——但是当我以编写 Excel 文件为生时,我正在处理多达大约四十字节的结构,因为这就是一些 BIFF 记录的大小。)
【讨论】:
难道你不能同样轻松地使用引用类型吗?【参考方案9】:不——我不完全同意这些规则。它们是考虑性能和标准化的好指南,但不是考虑到可能性。
正如您在回复中看到的,有很多创造性的方式来使用它们。因此,为了性能和效率,这些准则必须是这样。
在这种情况下,我使用类来以较大的形式表示现实世界的对象,我使用结构来表示具有更精确用途的较小对象。就像你说的那样,“一个更有凝聚力的整体”。关键字具有凝聚力。这些类将是更多面向对象的元素,而结构可以具有其中一些特征,尽管规模较小。国际海事组织。
我在 Treeview 和 Listview 标签中经常使用它们,可以非常快速地访问常见的静态属性。我一直在努力以另一种方式获取这些信息。例如,在我的数据库应用程序中,我使用 Treeview,其中包含表、SP、函数或任何其他对象。我创建并填充我的结构,将其放入标签中,将其拉出,获取选择的数据等等。我不会在课堂上这样做!
我确实尝试让它们保持小,在单一实例情况下使用它们,并防止它们改变。谨慎注意内存、分配和性能。测试是如此必要。
【讨论】:
结构可以合理地用于表示轻量级的不可变对象,或者它们可以合理地用于表示固定的相关但独立变量集(例如点的坐标)。该页面上的建议适用于旨在服务于前一个目的的结构,但对于旨在服务于后一个目的的结构是错误的。我目前的想法是,具有任何私有字段的结构通常应该符合指定的描述,但是许多结构应该通过公共字段公开它们的整个状态。 如果“3d 点”类型的规范表明其整个状态通过可读成员 x、y 和 z 公开,并且可以使用double
值的任意组合创建实例对于那些坐标,这样的规范将迫使它在语义上与暴露字段结构相同,除了多线程行为的一些细节(不可变类在某些情况下会更好,而暴露字段结构在某些情况下会更好)其他;所谓的“不可变”结构在每种情况下都会更糟)。【参考方案10】:
我很少使用结构体。但这只是我。这取决于我是否需要对象可以为空。
正如其他答案中所述,我将类用于真实世界的对象。我也有结构用于存储少量数据的心态。
【讨论】:
【参考方案11】:我不同意原帖中给出的规则。这是我的规则:
当存储在数组中时,您使用结构来提高性能。 (另见When are structs the answer?)
在向/从 C/C++ 传递结构化数据的代码中需要它们
除非需要,否则不要使用结构体:
它们的行为不同于分配和分配下的“普通对象”(引用类型) 作为参数传递时,可能导致意外行为; 如果查看代码的人确实这样做,这尤其危险 不知道他们正在处理一个结构。 它们不能被继承。 将结构作为参数传递比类更昂贵。【讨论】:
+1 是的,我完全同意 #1(在处理图像等问题时,这是一个 巨大 优势)并指出它们是与“普通对象”不同,除了通过现有知识或检查类型本身之外,还有知道这一点的已知方法。此外,您不能将空值强制转换为结构类型 :-) 这实际上是一种情况,我几乎希望非核心值类型有一些“匈牙利语”或强制 ' struct' 变量声明处的关键字。 @pst:确实,必须知道某事物是struct
才能知道它的行为方式,但如果某事物是具有暴露字段的 struct
,这就是所有人都必须知道的.如果一个对象公开了一个暴露字段结构类型的属性,并且如果代码将该结构读取到一个变量并进行了修改,则可以安全地预测,除非或直到写入该结构,否则此类操作不会影响读取其属性的对象背部。相比之下,如果属性是可变类类型,读取并修改它可能会按预期更新底层对象,但是...
...它也可能最终什么都不改变,或者它可能会改变或破坏一个人不打算改变的对象。让代码的语义说“随心所欲地改变这个变量;在你明确地将它们存储在某个地方之前,更改不会做任何事情”似乎比“你正在获得对某个对象的引用,该对象可能与任何数字共享”的代码更清晰其他引用,或者根本不共享;你必须弄清楚还有谁可能引用了这个对象,才能知道如果你改变它会发生什么。"
继承很少是适合这项工作的工具,在没有分析的情况下过多地推理性能是一个坏主意。首先,结构可以通过引用传递。其次,通过引用或值传递很少是一个重要的性能问题。最后,您没有考虑一个类需要进行的额外堆分配和垃圾收集。就个人而言,我更喜欢将结构视为普通的旧数据,将类视为做事情(对象)的东西,尽管您也可以在结构上定义方法。
@ILoveFortran 你不能简单地说对象的行为与“普通结构”不同,如果人们不知道他们处理的是对象而不是结构,他们可能会假设当作为参数传递给方法时,值会被复制。【参考方案12】:
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' 来源获得了 3 分(满分 4 分)——这是可以原谅的,因为 #4 可能不会成为问题。如果您发现自己在装箱结构,请重新考虑您的架构。
让我们看看微软为什么会使用这些结构:
-
每个结构
Entry
和 Enumerator
代表单个值。
速度
Entry
永远不会作为 Dictionary 类之外的参数传递。进一步的调查表明,为了满足 IEnumerable 的实现,Dictionary 使用 Enumerator
结构,每次请求枚举器时都会复制该结构......这是有道理的。
Dictionary 类的内部。 Enumerator
是公开的,因为 Dictionary 是可枚举的,并且必须对 IEnumerator 接口实现具有同等的可访问性 - 例如IEnumerator 吸气剂。
更新 - 此外,请注意,当结构实现接口时 - 就像 Enumerator 所做的那样 - 并被强制转换为该实现的类型,该结构成为引用类型并被移动到堆中。在 Dictionary 类的内部,Enumerator is 仍然是一个值类型。但是,只要方法调用GetEnumerator()
,就会返回引用类型IEnumerator
。
我们在这里没有看到任何保持结构不可变或保持实例大小仅为 16 字节或更少的尝试或要求证明:
-
上面的结构中没有任何东西被声明为
readonly
- 不是不可变的
这些结构的大小可能超过 16 个字节
Entry
的生命周期未确定(从 Add()
到 Remove()
、Clear()
或垃圾回收);
还有... 4. 两个结构体都存储了 TKey 和 TValue,我们都知道它们非常有能力成为引用类型(额外的奖励信息)
尽管有散列键,但字典速度很快,部分原因是实例化结构比引用类型更快。在这里,我有一个Dictionary<int, int>
,它存储了 300,000 个随机整数和顺序递增的键。
容量:312874 内存大小:2660827 字节 完成调整大小:5 毫秒 总填充时间:889ms
容量:必须调整内部数组大小之前可用的元素数。
MemSize:通过将字典序列化为 MemoryStream 并获取字节长度来确定(对于我们的目的来说足够准确)。
完成调整大小:将内部数组的大小从 150862 个元素调整为 312874 个元素所需的时间。当您发现每个元素都通过Array.CopyTo()
顺序复制时,这不是太破旧。
填充总时间:诚然,由于日志记录和我添加到源的OnResize
事件而出现偏差;但是,在操作期间调整 15 次大小时填充 300k 整数仍然令人印象深刻。只是出于好奇,如果我已经知道容量,那么填充的总时间是多少? 13ms
那么,现在,如果 Entry
是一个类呢?这些时间或指标真的会有那么大的不同吗?
容量:312874 内存大小:2660827 字节 完成调整大小:26 毫秒 总填充时间:964ms
显然,最大的区别在于调整大小。如果 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
结构在使用期间必须保持完整,除非需要销毁它以满足规则 #1
...我们从中学到了什么:简而言之,负责使用值类型。它们快速高效,但如果维护不当(即无意复制),可能会导致许多意外行为。
【讨论】:
至于微软的规则,关于不变性的规则似乎是为了阻止值类型的使用,使得它们的行为与引用类型的行为不同,尽管 分段可变值语义可能很有用。如果一个类型是分段可变的会更容易使用,并且如果该类型的存储位置应该在逻辑上彼此分离,那么该类型应该是一个“可变”结构。 请记住readonly != immutable. Microsoft 的许多类型违反这些规则这一事实并不代表这些类型存在问题,而是表明这些规则不应适用于所有结构类型。如果一个结构代表一个单一的实体[如Decimal
或DateTime
],那么如果它不遵守其他三个规则,它应该被一个类替换。如果一个结构包含一个固定的变量集合,每个变量都可以包含对其类型有效的任何值[例如Rectangle
],那么它应该遵守不同的规则,其中一些规则与“单值”结构的规则相反。
@IAbstract:有些人会证明Dictionary
条目类型只是一个内部类型,性能被认为比语义更重要,或者其他一些借口。我的观点是,像 Rectangle
这样的类型应该将其内容公开为可单独编辑的字段,而不是“因为”性能优势超过了由此产生的语义缺陷,而是因为 该类型在语义上表示一组固定的独立值 i>,因此可变结构在性能上和语义上都优越。
@supercat:我同意……我回答的重点是“准则”非常薄弱,应该在完全了解和理解行为的情况下使用结构。在此处查看我对可变结构的回答:***.com/questions/8108920/…【参考方案13】:
除了“它是一个值”的答案之外,使用结构的一个特定场景是当您知道您有一组导致垃圾的数据收集问题,并且您有很多对象。例如,大量的 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 在移动设备上,但在 Google 上搜索:+gravell +"assault by GC" 非常感谢。我找到了here。 @MarcGravell 为什么提到:使用数组(不是列表)?List
我相信,在幕后使用 Array
。没有?
@RoyiNamir 我也对此感到好奇,但我相信答案在 Marc 答案的第二段。 “但是,直接在数组中访问它不会复制结构 - 它是就地的(与复制的列表索引器形成对比)。”【参考方案14】:
C# 或其他 .net 语言中的结构类型通常用于保存应该表现得像固定大小的值组的东西。结构类型的一个有用方面是结构类型实例的字段可以通过修改它所在的存储位置来修改,而不是其他方式。可以以这样一种方式对结构进行编码,即改变任何字段的唯一方法是构造一个全新的实例,然后使用结构赋值来改变目标的所有字段,方法是用新实例中的值覆盖它们,但是除非结构体不提供创建其字段具有非默认值的实例的方法,否则如果结构体本身存储在可变位置,则其所有字段都将是可变的。
请注意,如果结构包含私有类类型字段并将其自己的成员重定向到包装的类对象的成员,则可以设计一个结构类型,使其本质上表现得像一个类类型。例如,PersonCollection
可能提供属性SortedByName
和SortedById
,它们都持有对PersonCollection
(在其构造函数中设置)的“不可变”引用,并通过调用creator.GetNameSortedEnumerator
或实现GetEnumerator
或creator.GetIdSortedEnumerator
。此类结构的行为很像对PersonCollection
的引用,只是它们的GetEnumerator
方法将绑定到PersonCollection
中的不同方法。也可以有一个结构包裹数组的一部分(例如,可以定义一个ArrayRange<T>
结构,该结构将包含一个称为Arr
的T[]
、一个int Offset
和一个int Length
,带有一个索引属性,对于 0 到 Length-1
范围内的索引 idx
,将访问 Arr[idx+Offset]
)。不幸的是,如果foo
是此类结构的只读实例,当前的编译器版本将不允许像foo[3]+=4;
这样的操作,因为它们无法确定此类操作是否会尝试写入foo
的字段。
也可以设计一个结构,使其表现得像一个值类型,它包含一个可变大小的集合(只要结构存在,它就会被复制),但实现该工作的唯一方法是确保没有对象结构持有引用的对象将永远暴露给任何可能改变它的东西。例如,可以有一个类似数组的结构,它包含一个私有数组,其索引“put”方法创建一个新数组,其内容与原始数组相同,只是一个更改的元素。不幸的是,要使这样的结构有效地执行可能有些困难。虽然有时结构语义可能很方便(例如,能够将类似数组的集合传递给例程,调用者和被调用者都知道外部代码不会修改集合,但可能比同时要求调用者和被调用者更好)被调用者防御性地复制他们给出的任何数据),类引用指向永远不会改变的对象的要求通常是一个非常严格的约束。
【讨论】:
【参考方案15】:.NET 支持value types
和reference types
(在Java 中,您只能定义引用类型)。 reference types
的实例在托管堆中分配,并且在没有对它们的未完成引用时进行垃圾回收。另一方面,value types
的实例在stack
中分配,因此一旦它们的作用域结束,分配的内存就会被回收。当然,value types
通过值传递,reference types
通过引用传递。除 System.String 外,所有 C# 原始数据类型都是值类型。
何时使用结构而非类,
在 C# 中,structs
是 value types
,类是 reference types
。您可以在 C# 中使用 enum
关键字和 struct
关键字创建值类型。使用 value type
而不是 reference type
将导致托管堆上的对象减少,从而减少垃圾收集器 (GC) 上的负载,减少 GC 周期,从而提高性能。但是,value types
也有其缺点。传递一个大的struct
肯定比传递一个引用更昂贵,这是一个明显的问题。另一个问题是与boxing/unboxing
相关的开销。如果您想知道boxing/unboxing
是什么意思,请点击这些链接以获得关于boxing
和unboxing
的良好解释。除了性能之外,有时您只需要类型具有值语义,如果您只有 reference types
,这将很难(或丑陋)实现。你应该只使用value types
,当你需要复制语义或需要自动初始化时,通常在这些类型的arrays
中。
【讨论】:
复制小型结构或按值传递与复制或传递类引用或通过ref
传递结构一样便宜。通过ref
传递任何大小的结构与通过值传递类引用的成本相同。复制任何大小的结构或按值传递比执行类对象的防御性副本并存储或传递对它的引用要便宜。大时代类比结构存储值更好是(1)当类是不可变的(以避免防御性复制),并且创建的每个实例将被传递很多,或者......
...(2) 当由于各种原因一个结构根本无法使用时[例如因为需要对树之类的东西使用嵌套引用,或者因为需要多态性]。请注意,在使用值类型时,通常应该在没有特定原因的情况下直接公开字段(而大多数类类型字段应该包装在属性中)。许多所谓的可变值类型的“邪恶”源于属性中不必要的字段包装(例如,虽然一些编译器允许在只读结构上调用属性设置器,因为它有时会......
...做正确的事,所有编译器都会正确拒绝在此类结构上直接设置字段的尝试;确保编译器拒绝 readOnlyStruct.someMember = 5;
的最佳方法不是将 someMember
设为只读属性,而是将其设为字段。【参考方案16】:
来自C# Language specification:
1.7 结构
与类一样,结构是可以包含数据成员和函数成员的数据结构,但与类不同的是,结构是 值类型并且不需要堆分配。结构的变量 type 直接存储 struct 的数据,而 a 的变量 类类型存储对动态分配对象的引用。 struct 类型不支持用户指定的继承,所有的 struct 类型隐式继承自类型对象。
结构对于具有以下特征的小型数据结构特别有用 价值语义。复数,坐标系中的点,或 字典中的键值对都是很好的结构示例。这 对小型数据结构使用结构而不是类可以使 应用程序分配的内存数量差异很大 施行。例如下面的程序创建并初始化 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 会创建一个值的副本,并且该副本不受后续赋值给 a.x 的影响。
前面的例子强调了结构的两个限制。首先,复制整个结构通常比复制对象引用效率低,因此结构的赋值和值参数传递可能比引用类型更昂贵。其次,除了 ref 和 out 参数,不能创建对结构的引用,这排除了它们在许多情况下的使用。
【讨论】:
虽然不能持久化对结构的引用这一事实有时是一个限制,但它也是一个非常有用的特性。 .net 的主要弱点之一是没有像样的方法来传递外部代码对可变对象的引用,而不会永远失去对该对象的控制。相比之下,可以安全地将外部方法ref
分配给可变结构,并且知道外部方法将对它执行的任何突变都将在它返回之前完成。太糟糕了.net没有任何临时参数和函数返回值的概念,因为......
...这将允许通过类对象实现 ref
传递的结构的有利语义。本质上,局部变量、参数和函数返回值可以是持久的(默认)、可返回的或短暂的。代码将被禁止将短暂的东西复制到任何超出当前范围的东西。可返回的东西就像短暂的东西,除了它们可以从函数中返回。函数的返回值将受到适用于其任何“可返回”参数的最严格限制。【参考方案17】:
Struct 可用于提高垃圾收集性能。虽然您通常不必担心 GC 性能,但在某些情况下它可能会成为杀手。就像低延迟应用程序中的大型缓存一样。示例见这篇文章:
http://00sharp.wordpress.com/2013/07/03/a-case-for-the-struct/
【讨论】:
【参考方案18】:我的规则是
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)
存在重载,我会假设尝试使用float
和double
通常不应该应用隐式转换,而应该是错误。
将double
值直接分配给float
,或将其传递给可以采用float
参数但不能采用double
的方法,几乎总是会执行程序员的操作故意的。相比之下,将float
表达式分配给double
而不进行显式类型转换通常是错误的。唯一允许隐式double->float
转换会导致问题的情况是它会导致选择不太理想的重载。我认为防止这种情况的正确方法不应该是禁止隐式 double->float,而是用属性标记重载以禁止转换。【参考方案19】:
这是一个基本规则。
如果所有成员字段都是值类型,则创建一个 struct。
如果任何一个成员字段是引用类型,则创建一个类。这是因为引用类型字段无论如何都需要堆分配。
示例
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>
经常与类类型一起用作泛型参数。【参考方案20】:
类是引用类型。当创建类的对象时,分配给该对象的变量只保存对该内存的引用。当对象引用分配给新变量时,新变量引用原始对象。通过一个变量所做的更改会反映在另一个变量中,因为它们都引用相同的数据。 结构是一种值类型。创建结构时,分配给结构的变量保存结构的实际数据。当结构被分配给一个新变量时,它被复制。因此,新变量和原始变量包含相同数据的两个单独副本。对一个副本所做的更改不会影响另一个副本。 通常,类用于建模更复杂的行为,或在创建类对象后要修改的数据。结构最适合主要包含在结构创建后不打算修改的数据的小型数据结构。
Classes and Structs (C# Programming Guide)
【讨论】:
在需要将一些相关但独立的变量与胶带(例如点的坐标)固定在一起的情况下,结构也非常好。如果一个人试图生成行为类似对象的结构,但在设计聚合时就不太合适了,那么 MSDN 指南是合理的;在后一种情况下,其中一些几乎完全错误。例如,类型封装的变量的独立程度越高,使用暴露字段结构而不是不可变类的优势就越大。【参考方案21】:结构在大多数方面类似于类/对象。结构可以包含函数、成员并且可以被继承。但结构在 C# 中仅用于数据保存。结构确实比类占用更少的 RAM,并且垃圾收集器更容易收集。但是当您在结构中使用函数时,编译器实际上将该结构与类/对象非常相似,因此如果您想要具有函数的东西,请使用类/对象。
【讨论】:
结构不能被继承,见msdn.microsoft.com/en-us/library/0taef578.aspx【参考方案22】:我只是在处理 Windows Communication Foundation [WCF] 命名管道,我确实注意到使用 Structs 来确保数据交换是 值类型 而不是 参考类型。
【讨论】:
这是最好的线索,恕我直言。【参考方案23】:简而言之,使用 struct if:
您的对象属性/字段不需要更改。我的意思是你只想给它们一个初始值,然后读取它们。
对象中的属性和字段是值类型,它们不是那么大。
如果是这种情况,您可以利用结构来获得更好的性能和优化的内存分配,因为它们只使用堆栈而不是同时使用堆栈和堆(在类中)
【讨论】:
【参考方案24】:struct 是一种值类型。如果将结构分配给新变量,则新变量将包含原始变量的副本。
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
【讨论】:
【参考方案25】:我用BenchmarkDotNet 做了一个小基准测试,以更好地理解“结构”在数字上的好处。我正在测试遍历结构(或类)的数组(或列表)。创建这些数组或列表超出了基准测试的范围 - 很明显,“类”更重会占用更多内存,并且会涉及 GC。
所以结论是:小心使用 LINQ 和隐藏结构的装箱/拆箱,以及使用结构进行微优化严格使用数组。
附:关于通过调用堆栈传递结构/类的另一个基准是https://***.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 的情况。【参考方案26】:C# 结构是类的轻量级替代品。它可以做的几乎与类相同,但使用结构而不是类更“昂贵”。这样做的原因有点技术性,但总而言之,一个类的新实例放在堆上,新实例化的结构放在堆栈上。此外,您不是像处理类那样处理对结构的引用,而是直接使用结构实例。这也意味着当您将结构传递给函数时,它是按值传递的,而不是作为引用。关于函数参数的章节有更多相关内容。
因此,当您希望表示更简单的数据结构时,您应该使用结构,尤其是当您知道您将实例化大量它们时。 .NET 框架中有很多示例,其中 Microsoft 使用结构而不是类,例如 Point、Rectangle 和 Color 结构。
【讨论】:
【参考方案27】:误区 1:结构是轻量级的类
这个神话有多种形式。有些人认为值类型不能或 不应该有方法或其他重要的行为——它们应该被简单地使用 数据传输类型,仅具有公共字段或简单属性。 DateTime 类型是 很好的反例:它是一个值类型是有意义的,就存在而言 像数字或字符这样的基本单位,它也是有意义的 能够根据其值进行计算。从对方的角度看事情 方向,数据传输类型通常应该是引用类型——决定 应该基于期望的值或引用类型语义,而不是简单的 方式。 其他人认为值类型在术语上比引用类型“更轻” 的表现。事实是,在某些情况下,值类型的性能更高—— 他们不需要垃圾收集,除非他们被装箱,没有类型 例如,识别开销,并且不需要取消引用。但在其他 方式,引用类型更高效——参数传递,赋值给 变量、返回值和类似操作只需要复制 4 或 8 个字节(取决于您运行的是 32 位还是 64 位 CLR),而不是 复制所有数据。想象一下,如果 ArrayList 是某种“纯”值类型,并且 将 ArrayList 表达式传递给涉及复制其所有数据的方法!在几乎 在所有情况下,无论如何,性能并不是真正由这种决定决定的。瓶颈几乎永远不会出现在您认为会出现的地方,在您根据性能做出设计决策之前,您应该衡量不同的选项。 值得注意的是,这两种信念的结合也不起作用。它 一个类型有多少方法(无论是类还是结构)都无关紧要—— 每个实例占用的内存不受影响。 (内存是有代价的 占用代码本身,但只发生一次,而不是每个实例。)
误区 #2:引用类型存在于堆中;价值类型存在于堆栈中
这通常是由于重复它的人的懒惰造成的。首先 部分是正确的——引用类型的实例总是在堆上创建。这是 第二部分导致问题。正如我已经指出的,一个变量的值存在于它被声明的任何地方,所以如果你有一个具有 int 类型的实例变量的类,那么任何给定对象的该变量的值将始终是该对象的其余数据 是——在堆上。只有局部变量(在方法中声明的变量)和方法 参数存在于堆栈中。在 C# 2 及更高版本中,即使是一些局部变量也不是真的 正如我们在第 5 章中讨论匿名方法时所看到的那样,它们存在于堆栈中。 这些概念现在有用吗?有争议的是,如果您正在编写托管代码,您应该让运行时担心如何最好地使用内存。 事实上,语言规范并不能保证生活中的内容 在哪里;未来的运行时可能能够在堆栈上创建一些对象,如果它 知道它可以摆脱它,或者 C# 编译器可以生成代码 几乎不使用堆栈。 下一个神话通常只是一个术语问题。
误区 #3:对象在 C# 中默认通过引用传递
这可能是传播最广的神话。再次,制作这个的人 声称经常(尽管不总是)知道 C# 的实际行为,但他们不知道 “通过引用传递”的真正含义。不幸的是,这让那些 知道这意味着什么。 pass by reference 的正式定义比较复杂,涉及l-values 和类似的计算机科学术语,但重要的是,如果你通过了 通过引用变量,您调用的方法可以通过更改其参数值来更改调用者变量的值。现在,请记住引用的值 类型变量是引用,而不是对象本身。您可以更改内容 参数引用的对象,而参数本身没有通过引用传递。例如,下面的方法改变了 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
【讨论】:
非常好,假设你是正确的。添加参考也很好。【参考方案28】:以下是 Microsoft 网站上定义的规则:
✔️ 如果类型的实例很小且通常寿命短或通常嵌入其他对象中,请考虑定义结构而不是类。
❌ 避免定义结构,除非该类型具有以下所有特征:
它在逻辑上表示单个值,类似于原始类型(int、double 等)。
它的实例大小小于 16 字节。
它是不可变的。
不必经常装箱。
进一步reading
【讨论】:
这个答案似乎只是this existing answer(部分)的重复。【参考方案29】:✔️ 考虑结构的用法
-
创建对象或者不需要创建对象(直接赋值即可,创建对象)
需要速度或性能改进
不需要构造函数和析构函数(提供静态承包商)
不需要类继承,但可以接受接口
小工作负载对象工作,如果它变高,就会引发内存问题
您不能为变量设置默认值。
Struct 还提供方法、事件、静态构造函数、变量等
GC 工作量更少
不需要引用类型,只需要值类型(每次创建新对象时)
没有不可变对象(字符串是不可变对象,因为任何操作都不会在不更改原始字符串的情况下每次返回任何新字符串)
【讨论】:
【参考方案30】:除了通常提到的性能差异之外,让我添加另一个方面,那就是揭示默认值使用的意图。
如果结构的字段的默认值不能代表建模概念的合理默认值,则不要使用结构。
例如。
颜色或点是有意义的,即使它们的所有字段都设置为默认值。 RGB 0,0,0 是一种非常好的颜色, (0,0) 作为 2D 中的点也是如此。 但是地址或人员名称没有合理的默认值。我的意思是你能理解 FirstName=null 和 LastName=null 的 PersonName 吗?如果你用一个类来实现一个概念,那么你可以强制执行某些不变量,例如。一个人必须有名字和姓氏。但是对于结构,总是可以创建一个实例,并将其所有字段设置为默认值。
因此,当对没有合理默认值的概念进行建模时,更喜欢使用类。您的类的用户会理解 null 意味着未指定 PersonName,但如果您将所有属性都设置为 null 的 PersonName 结构实例交给他们,他们会感到困惑。
(通常的免责声明:性能考虑可能会覆盖此建议。如果您有性能问题,请务必在决定解决方案之前进行测量。试试BenchmarkDotNet,这太棒了!)
【讨论】:
以上是关于什么时候应该在 C# 中使用结构而不是类?的主要内容,如果未能解决你的问题,请参考以下文章