什么时候应该在C#中使用结构而不是类?我的概念模型是,当项只是值类型的集合时,使用结构。一种将它们逻辑地结合在一起的方法。
我在这里遇到了这些规则:
结构应表示单个价值结构应具有内存占用空间小于16字节。结构不应在之后更改创造
这些规则有效吗?结构在语义上意味着什么?
什么时候应该在C#中使用结构而不是类?我的概念模型是,当项只是值类型的集合时,使用结构。一种将它们逻辑地结合在一起的方法。
我在这里遇到了这些规则:
结构应表示单个价值结构应具有内存占用空间小于16字节。结构不应在之后更改创造
这些规则有效吗?结构在语义上意味着什么?
当前回答
我认为一个好的第一近似值是“从不”。
我认为一个好的二次近似是“从不”。
如果你渴望表现,那么考虑一下,但始终要衡量。
其他回答
.NET支持值类型和引用类型(在Java中,只能定义引用类型)。引用类型的实例在托管堆中分配,并且在没有未完成的引用时被垃圾收集。另一方面,值类型的实例是在堆栈中分配的,因此一旦其作用域结束,分配的内存就会被回收。当然,值类型通过值传递,引用类型通过引用传递。除了System.String之外,所有C#原语数据类型都是值类型。
何时在类上使用结构,
在C#中,结构是值类型,类是引用类型。您可以使用enum关键字和struct关键字在C#中创建值类型。使用值类型而不是引用类型将导致托管堆上的对象减少,从而减少垃圾收集器(GC)的负载,减少GC周期,从而提高性能。然而,价值类型也有其缺点。传递一个大结构肯定比传递一个引用成本更高,这是一个明显的问题。另一个问题是与装箱/拆箱相关的开销。如果您想知道装箱/拆箱是什么意思,请按照以下链接了解装箱和拆箱的详细说明。除了性能之外,有时您只需要类型具有值语义,如果引用类型是您的全部,那么这将很难实现(或者很难看)。当您需要复制语义或需要自动初始化时(通常在这些类型的数组中),您应该只使用值类型。
简单地说,如果:
您的对象财产/字段不需要更改。我的意思是你只想给它们一个初始值,然后读它们。对象中的财产和字段是值类型,它们并没有那么大。
如果是这种情况,您可以利用结构来获得更好的性能和优化的内存分配,因为它们只使用堆栈,而不是同时使用堆栈和堆(在类中)
无论何时:
不需要多态性,want值语义,以及希望避免堆分配和相关的垃圾收集开销。
然而,需要注意的是,结构(任意大)传递比类引用(通常是一个机器字)更昂贵,因此类在实践中可能会更快。
在需要使用StructLayoutAttribute显式指定内存布局(通常用于PInvoke)的情况下,需要使用“结构”。
编辑:注释指出,可以将类或结构与StructLayoutAttribute一起使用,这当然是正确的。在实践中,您通常会使用一个结构——它是在堆栈上分配的,而不是在堆上分配的——如果您只是向非托管方法调用传递一个参数,这是有意义的。
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
…我们从中得到了什么:简而言之,对价值类型的使用负责。它们快速高效,但如果维护不当(即无意复制),则能够导致许多意外行为。