我什么时候应该在 C# 中使用结构而不是类? [英] When should I use a struct rather than a class in C#?

查看:50
本文介绍了我什么时候应该在 C# 中使用结构而不是类?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

什么时候应该在 C# 中使用结构而不是类?我的概念模型是在项目仅仅是值类型的集合时使用结构.一种将它们合乎逻辑地组合成一个有凝聚力的整体的方法.

When should you use struct and not class in C#? My conceptual model is that structs are used in times when the item is merely a collection of value types. A way to logically hold them all together into a cohesive whole.

我遇到了这些规则 此处:

  • 一个结构体应该代表一个价值.
  • 一个结构体应该有一个内存占用空间小于 16 字节.
  • 结构不应该在之后改变创作.

这些规则有效吗?结构体在语义上是什么意思?

Do these rules work? What does a struct mean semantically?

推荐答案

OP 引用的源代码具有一定的可信度……但是 Microsoft 呢——对 struct 使用的立场是什么?我寻求了一些额外的向微软学习,这是我的发现:

The source referenced by the OP has some credibility ...but what about Microsoft - what is the stance on struct usage? I sought some extra learning from Microsoft, and here is what I found:

考虑定义一个结构而不是一个类,如果类型较小且通常寿命较短或通常嵌入其他对象.

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

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

微软一贯违反这些规则

好吧,无论如何,#2 和 #3.我们心爱的字典有两个内部结构:

Microsoft consistently violates those rules

Okay, #2 and #3 anyway. Our beloved dictionary has 2 internal structs:

[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"来源获得了 3 分(满分 4 分) - 完全可以原谅,因为 #4 可能不是问题.如果您发现自己在装箱结构,请重新考虑您的架构.

The 'JonnyCantCode.com' source got 3 out of 4 - quite forgivable since #4 probably wouldn't be an issue. If you find yourself boxing a struct, rethink your architecture.

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

Let's look at why Microsoft would use these structs:

  1. 每个结构体,EntryEnumerator,代表单个值.
  2. 速度
  3. Entry 永远不会作为 Dictionary 类之外的参数传递.进一步调查表明,为了满足 IEnumerable 的实现,Dictionary 使用 Enumerator 结构,每次请求枚举器时它都会复制该结构......这是有道理的.
  4. Dictionary 类的内部.Enumerator 是公开的,因为 Dictionary 是可枚举的,并且必须对 IEnumerator 接口实现具有同等的可访问性 - 例如IEnumerator 获取器.
  1. Each struct, Entry and Enumerator, represent single values.
  2. Speed
  3. Entry is never passed as a parameter outside of the Dictionary class. Further investigation shows that in order to satisfy implementation of IEnumerable, Dictionary uses the Enumerator struct which it copies every time an enumerator is requested ...makes sense.
  4. Internal to the Dictionary class. Enumerator is public because Dictionary is enumerable and must have equal accessibility to the IEnumerator interface implementation - e.g. IEnumerator getter.

更新 - 此外,要意识到当一个结构体实现一个接口时——就像 Enumerator 那样——并被强制转换为该实现的类型,该结构体就变成了一个引用类型并被移到了堆中.在 Dictionary 类内部,Enumerator 仍然是值类型.但是,只要方法调用 GetEnumerator(),就会返回引用类型的 IEnumerator.

Update - In addition, realize that when a struct implements an interface - as Enumerator does - and is cast to that implemented type, the struct becomes a reference type and is moved to the heap. Internal to the Dictionary class, Enumerator is still a value type. However, as soon as a method calls GetEnumerator(), a reference-type IEnumerator is returned.

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

What we don't see here is any attempt or proof of requirement to keep structs immutable or maintaining an instance size of only 16 bytes or less:

  1. 上述结构中的任何内容均未声明 readonly - not 不可变
  2. 这些结构的大小可能超过 16 个字节
  3. Entry 有一个不确定的生命周期(从 Add(),到 Remove()Clear(),或垃圾收集);
  1. Nothing in the structs above is declared readonly - not immutable
  2. Size of these struct could be well over 16 bytes
  3. Entry has an undetermined lifetime (from Add(), to Remove(), Clear(), or garbage collection);

还有……4. 两个结构体都存储了 TKey 和 TValue,我们都知道它们非常适合作为引用类型(添加奖励信息)

And ... 4. Both structs store TKey and TValue, which we all know are quite capable of being reference types (added bonus info)

尽管是散列键,但字典速度很快,部分原因是实例化结构比引用类型更快.在这里,我有一个 Dictionary 存储 300,000 个随机整数和顺序递增的键.

Hashed keys notwithstanding, dictionaries are fast in part because instancing a struct is quicker than a reference type. Here, I have a Dictionary<int, int> that stores 300,000 random integers with sequentially incremented keys.

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

Capacity: 312874
MemSize: 2660827 bytes
Completed Resize: 5ms
Total time to fill: 889ms

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

Capacity: number of elements available before the internal array must be resized.

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

MemSize: determined by serializing the dictionary into a MemoryStream and getting a byte length (accurate enough for our purposes).

Completed Resize:将内部数组从150862个元素调整到312874个元素所花费的时间.当您认为每个元素都是通过 Array.CopyTo() 顺序复制时,这还不算太糟糕.

Completed Resize: the time it takes to resize the internal array from 150862 elements to 312874 elements. When you figure that each element is sequentially copied via Array.CopyTo(), that ain't too shabby.

总填充时间:由于日志记录和我添加到源中的 OnResize 事件,确实存在偏差;然而,在操作期间调整大小 15 次的同时填充 30 万个整数仍然令人印象深刻.只是出于好奇,如果我已经知道容量,那么填充的总时间是多少?13 毫秒

Total time to fill: admittedly skewed due to logging and an OnResize event I added to the source; however, still impressive to fill 300k integers while resizing 15 times during the operation. Just out of curiosity, what would the total time to fill be if I already knew the capacity? 13ms

那么,现在,如果 Entry 是一个类呢?这些时间或指标真的会有那么大的不同吗?

So, now, what if Entry were a class? Would these times or metrics really differ that much?

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

Capacity: 312874
MemSize: 2660827 bytes
Completed Resize: 26ms
Total time to fill: 964ms

显然,最大的区别在于调整大小.如果用容量初始化 Dictionary 有什么区别吗?不足以关心...12 毫秒.

Obviously, the big difference is in resizing. Any difference if Dictionary is initialized with the Capacity? Not enough to be concerned with ... 12ms.

发生的事情是,因为 Entry 是一个结构体,它不像引用类型那样需要初始化.这既是值类型的美妙之处,也是它的祸根.为了使用 Entry 作为引用类型,我必须插入以下代码:

What happens is, because Entry is a struct, it does not require initialization like a reference type. This is both the beauty and the bane of the value type. In order to use Entry as a reference type, I had to insert the following code:

/*
 *  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:结构设计.简而言之:

The reason I had to initialize each array element of Entry as a reference type can be found at MSDN: Structure Design. In short:

不要为结构提供默认构造函数.

如果一个结构体定义了一个默认构造函数,那么当结构体的数组结构被创建,公共语言运行时自动对每个数组元素执行默认构造函数.

If a structure defines a default constructor, when arrays of the structure are created, the common language runtime automatically executes the default constructor on each array element.

某些编译器,例如 C# 编译器,不允许结构体有默认构造函数.

Some compilers, such as the C# compiler, do not allow structures to have default constructors.

它实际上很简单,我们将借鉴阿西莫夫的机器人三定律:

It is actually quite simple and we will borrow from Asimov's Three Laws of Robotics:

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

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

...what do we take away from this: in short, be responsible with the use of value types. They are quick and efficient, but have the ability to cause many unexpected behaviors if not properly maintained (i.e. unintentional copies).

这篇关于我什么时候应该在 C# 中使用结构而不是类?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆