在C#中,什么情况下应该使用struct而不是class?

27 浏览
0 Comments

在C#中,什么情况下应该使用struct而不是class?

在C#中,什么时候应该使用结构体而不是类?我的概念模型是:当项目仅仅是值类型的集合时,使用结构体。这是一个将所有元素逻辑地统一为一个整体的方式。

我找到了这些规则(此处为外部链接)

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

这些规则是否奏效?从语义上来说,结构体代表着什么?

admin 更改状态以发布 2023年5月23日
0
0 Comments

无论何时:

  1. 不需要多态性,
  2. 需要值语义,以及
  3. 想要避免堆分配及相关垃圾回收开销。

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

0
0 Comments

OP引用的来源有一些可信度...但是微软公司的立场是什么?我从微软公司的学习资料中寻找到一些额外的信息,以下是我找到的:

如果实例类型很小且通常生命周期短暂,或通常嵌入其他对象中,则应考虑定义结构,而不是类。

除非类型具有以下所有特性,请勿定义结构:

  1. 它在逻辑上代表单个值,类似于原始类型(整数、双精度浮点数等)。
  2. 它的实例大小小于16个字节。
  3. 它是不可变的。
  4. 它不需要经常装箱。

微软一直违反这些规则

至少违反了#2和#3规则。我们所喜爱的字典有2个内部结构:

[StructLayout(LayoutKind.Sequential)]  // default for structs
private struct Entry  //
{
    //  View code at *Reference Source
}
[Serializable, StructLayout(LayoutKind.Sequential)]
public struct Enumerator : 
    IEnumerator>, IDisposable, 
    IDictionaryEnumerator, IEnumerator
{
    //  View code at *Reference Source
}

*参考来源

“JonnyCantCode.com”来源得到了4个中的3个 - 很难原谅,因为#4可能不是一个问题。如果您发现正在装箱一个结构,请重新思考您的架构。

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

  1. 每个结构,EntryEnumerator,都表示单个值。
  2. 速度
  3. Entry不会作为参数传递到Dictionary类外部。进一步调查表明,为了满足IEnumerable的实现,Dictionary使用了Enumerator结构,每当请求枚举器时都会复制它...有意义。
  4. 仅适用于Dictionary类内部。 Enumerator是公共的,因为Dictionary是可枚举的,并且必须具有等效的IEnumerator接口实现可访问性,例如IEnumerator getter。

更新 - 另外,请注意,当一个结构体实现一个接口 - 如Enumerator时 - 并转换为该实现类型时,结构体将变为引用类型并移动到堆上。在Dictionary类内部,Enumerator仍然是值类型。但是,一旦一个方法调用了GetEnumerator(),就会返回引用类型的IEnumerator

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

  1. 上述结构中没有声明readonly - 不是不可变的
  2. 这些结构体的大小可能远远超过16个字节
  3. Entry的生存期未确定(从Add(),到Remove()Clear()或垃圾回收);

而且...
4.两个结构体都存储TKey和TValue,我们都知道它们可以是引用类型(添加的额外信息)

尽管存在哈希键,但字典的快速部分是因为实例化结构比引用类型更快。这里,我有一个Dictionary,它存储了300,000个具有顺序递增键的随机整数。

容量:312874
MemSize:2660827字节
完成调整大小:5ms
填充的总时间:889ms

容量:在内部数组必须调整大小之前可用的元素数量。

MemSize:通过将字典序列化为MemoryStream并获取字节长度来确定(对于我们的目的而言足够准确)。

完成的调整大小:将内部数组从150862个元素调整为312874个元素所需的时间。当考虑到每个元素都通过Array.CopyTo()顺序复制时,这并不算太糟糕。

填充的总时间:由于日志记录和我添加到源代码中的OnResize事件而明显有偏移;感到好奇,如果我已经知道容量,填充的总时间是多少?13毫秒

所以,现在,如果Entry是一个类,这些时间或指标真的会有很大的区别吗?

容量:312874
MemSize: 2660827 bytes
已完成的调整大小:26ms
填充的总时间:964ms

显然,区别在于调整大小。如果使用Capacity初始化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。
结构必须在使用过程中保持完整,除非其销毁已被要求满足规则#1。

...我们从中得到什么结论:简而言之,要负责任地使用值类型。它们快速高效,但如果不正确维护(即意外副本),可能会导致许多意外的行为。

0