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

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

结构应表示单个价值结构应具有内存占用空间小于16字节。结构不应在之后更改创造

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


当前回答

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

其他回答

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

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

无论何时:

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

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

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

同样,这是正确的-我们没有复制值。

在非常具体的情况下,这种策略可能非常成功;然而,这是一个相当先进的scernario,只有当你知道自己在做什么和为什么时,才应该尝试。这里的默认值是类。

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

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

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