协慌网

登录 贡献 社区

何时使用 struct?

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

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

  • 结构应该表示单个值。
  • 结构应具有小于 16 个字节的内存占用。
  • 创建后不应更改结构。

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

答案

OP 引用的来源具有一定的可信度...... 但是微软怎么样 - 结构使用的立场是什么?我从微软那里寻求了一些额外的学习 ,这就是我发现的:

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

除非类型具有以下所有特征,否则不要定义结构:

  1. 它逻辑上表示单个值,类似于基本类型(整数,双精度等)。
  2. 它的实例大小小于 16 个字节。
  3. 这是不可改变的。
  4. 它不必经常装箱。

微软一直违反这些规则

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

* 参考资料来源

'JonnyCantCode.com' 来源获得了 4 分中的 3 分 - 相当可原谅,因为#4 可能不会成为问题。如果你发现自己装了一个结构,重新考虑你的架构。

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

  1. 每个 struct, EntryEnumerator代表单个值。
  2. 速度
  3. Entry永远不会作为 Dictionary 类之外的参数传递。进一步的研究表明,为了满足 IEnumerable 的实现,Dictionary 使用Enumerator结构,它每次请求枚举器时都会复制... 这是有意义的。
  4. Dictionary 类的内部。 Enumerator是公共的,因为 Dictionary 是可枚举的,并且必须具有与 IEnumerator 接口实现相同的可访问性 - 例如 IEnumerator getter。

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

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

  1. 上面的结构中没有任何内容被声明为readonly - 不是一成不变的
  2. 这些结构的大小可能超过 16 个字节
  3. Entry具有未确定的生命周期(从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
MemSize:2660827 字节
完成调整大小:26ms
总时间: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#编译器)不允许结构具有默认构造函数。

它实际上非常简单,我们将借用阿西莫夫的三机器人法则

  1. 结构必须安全使用
  2. 结构必须有效地执行其功能,除非这违反规则#1
  3. 结构在使用过程中必须保持完整,除非要求销毁以满足规则#1

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

每当您不需要多态时,需要值语义,并希望避免堆分配和相关的垃圾收集开销。然而,需要注意的是,结构(任意大)传递比类引用(通常是一个机器字)更昂贵,因此类在实践中最终会更快。

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

1)存储在数组中时使用结构体来提高性能。 (另见结构何时?

2)您需要将代码传递给 C / C ++ 的结构化数据

3)除非您需要,否则不要使用结构:

  • 它们与赋值下的 “普通对象”( 引用类型 )和作为参数传递时的行为不同,这可能导致意外行为; 如果查看代码的人不知道他们正在处理结构,那么这尤其危险。
  • 他们不能继承。
  • 将结构作为参数传递比类更昂贵。