什么时候应该在C#中使用结构而不是类?我的概念模型是,当项只是值类型的集合时,使用结构。一种将它们逻辑地结合在一起的方法。
我在这里遇到了这些规则:
结构应表示单个价值结构应具有内存占用空间小于16字节。结构不应在之后更改创造
这些规则有效吗?结构在语义上意味着什么?
什么时候应该在C#中使用结构而不是类?我的概念模型是,当项只是值类型的集合时,使用结构。一种将它们逻辑地结合在一起的方法。
我在这里遇到了这些规则:
结构应表示单个价值结构应具有内存占用空间小于16字节。结构不应在之后更改创造
这些规则有效吗?结构在语义上意味着什么?
当前回答
我不同意原帖子中给出的规则。以下是我的规则:
当存储在数组中时,可以使用结构来提高性能。(另请参见结构何时是答案?)在向C/C传递结构化数据的代码中需要它们++除非需要,否则不要使用结构:它们的行为与赋值下的“正常对象”(引用类型)不同当作为参数传递时,这可能导致意外行为;如果查看代码的人不知道他们正在处理一个结构。它们不能被继承。将结构作为参数传递比类更昂贵。
其他回答
OP引用的消息来源有一定的可信度。。。但微软呢?对结构使用的立场是什么?我向微软寻求了一些额外的学习,以下是我的发现:
如果类型很小,通常寿命很短,或者通常嵌入其他对象。除非类型具有以下所有特征,否则不要定义结构:它在逻辑上表示单个值,类似于基本类型(整数、双精度等)。它的实例大小小于16字节。它是不可变的。它不必经常装箱。
Microsoft一贯违反这些规则
好吧,无论如何,第二和第三。我们喜爱的字典有两个内部结构:
[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名可能不会成为问题。如果您发现自己正在装箱一个结构,请重新思考您的体系结构。
让我们来看看为什么微软会使用这些结构:
每个结构Entry和Enumerator表示单个值。速度条目永远不会作为Dictionary类之外的参数传递。进一步的调查表明,为了满足IEnumerable的实现,Dictionary使用了每次请求枚举器时都会复制的枚举器结构。。。有道理。Dictionary类的内部。枚举器是公共的,因为Dictionary是可枚举的,并且必须对IEnumerator接口实现(例如IEnumeratorgetter)具有同等的可访问性。
更新-此外,请注意,当一个结构实现了一个接口(如Enumerator)并被强制转换为该实现的类型时,该结构将成为一个引用类型并被移动到堆中。在Dictionary类内部,Enumerator仍然是值类型。然而,一旦方法调用GetEnumerator(),就会返回一个引用类型IEnumerator。
我们在这里没有看到任何保持结构不可变或保持实例大小仅为16字节或更少的尝试或证明:
上面的结构中没有任何内容声明为只读-不是不可变的这些结构的大小可能远远超过16字节条目具有未确定的生存期(从Add()到Remove()、Clear()或垃圾收集);
和4.两个结构都存储TKey和TValue,我们都知道它们非常适合作为引用类型(添加了额外的信息)
尽管有哈希键,但字典速度很快,部分原因是实例化结构比引用类型更快。这里,我有一个Dictionary<int,int>,它存储了300000个随机整数和顺序递增的键。
容量:312874内存大小:2660827字节完成调整大小:5ms填充总时间:889ms
容量:必须调整内部数组大小之前可用的元素数。
MemSize:通过将字典序列化为MemoryStream并获得字节长度(对于我们的目的来说足够精确)来确定。
完成调整大小:将内部数组从150862个元素调整为312874个元素所需的时间。如果您认为每个元素都是通过Array.CopyTo()顺序复制的,那就不太糟糕了。
填充总时间:由于日志记录和我添加到源中的OnResize事件,确实存在偏差;然而,在操作期间填充300k个整数并调整大小15次仍然令人印象深刻。只是出于好奇,如果我已经知道容量,那么总的填充时间是多少?13毫秒
那么,现在,如果Entry是一个类呢?这些时间或指标真的会有那么大的不同吗?
容量:312874内存大小:2660827字节完成调整大小:26ms填充总时间:964ms
显然,最大的区别在于调整大小。如果字典是用容量初始化的,有什么区别吗?不够关心。。。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结构在使用过程中必须保持完整,除非需要对其进行销毁以满足规则#1
…我们从中得到了什么:简而言之,对价值类型的使用负责。它们快速高效,但如果维护不当(即无意复制),则能够导致许多意外行为。
以下是在Microsoft网站上定义的规则:
✔️ 如果类型的实例很小且通常很短,或者通常嵌入在其他对象中,请考虑定义结构而不是类。
❌ 避免定义结构,除非该类型具有以下所有特征:
它在逻辑上表示单个值,类似于原始类型(int、double等)。
它的实例大小小于16字节。
它是不可变的。
它不必经常装箱。
供进一步阅读
我使用结构来打包或解包任何类型的二进制通信格式。这包括读取或写入磁盘、DirectX顶点列表、网络协议或处理加密/压缩数据。
在这方面,你列出的三条准则对我来说并不有用。当我需要以特殊顺序写出400字节的内容时,我将定义一个400字节的结构,并用它应该具有的任何不相关的值填充它,我将以当时最合理的方式设置它。(好吧,四百字节会很奇怪——但当我以写Excel文件为生的时候,我处理的是多达四十字节的结构,因为这就是一些BIFF记录的大小。)
结构在大多数方面类似于类/对象。结构可以包含函数、成员,并且可以继承。但C#中的结构仅用于数据保存。结构比类占用更少的RAM,并且垃圾收集器更容易收集。但当您在结构中使用函数时,编译器实际上采用的结构与类/对象非常相似,所以如果您想要使用函数,请使用类/对象。
在需要使用StructLayoutAttribute显式指定内存布局(通常用于PInvoke)的情况下,需要使用“结构”。
编辑:注释指出,可以将类或结构与StructLayoutAttribute一起使用,这当然是正确的。在实践中,您通常会使用一个结构——它是在堆栈上分配的,而不是在堆上分配的——如果您只是向非托管方法调用传递一个参数,这是有意义的。