CSharp - 何时使用结构?

  显示原文与译文双语对照的内容

在 C# 中应使用结构而非类? 我的概念模型是结构用于项目时仅仅是值的集合类型。 一种逻辑上将它们全部放在一起的一种方法。

我在这里遇到了这些规则 ( 缓存的 ):

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

这些规则是否工作结构在语义上意味着什么?

时间:

源引用的op有可信度。但是微软——对结构使用的立场是什么? 我从微软寻求一些额外的学习,这是我的发现:

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

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

  1. 它逻辑上表示一个值,类似于基元类型( 整数,double等) 。
  2. 它的实例大小小于16字节。
  3. 它是不可变的。
  4. 它将不必频繁被装箱。

微软一贯违反那些规则

好,#2 和 #3 无论如何。 我们喜爱的字典有 2个内部结构:


[StructLayout(LayoutKind.Sequential)]//default for structs
private struct Entry//<Tkey, TValue>
{
//use Reflector to see the code
}

[Serializable, StructLayout(LayoutKind.Sequential)]
public struct Enumerator : 
 IEnumerator<KeyValuePair<TKey, TValue>>, IDisposable, 
 IDictionaryEnumerator, IEnumerator
{
//use Reflector to see the code
}

'JonnyCantCode.com'源得到 3 4——相当forgivable自 #4 可能不会是一个问题。 如果你发现自己正在打拳,请重新考虑你的架构。

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

  1. 每个结构 EntryEnumerator 代表单个值。
  2. 速度
  3. Entry 从不作为字典类之外的参数传递。 进一步的调查表明,为了满足IEnumerable的实现,字典使用了 Enumerator 结构,每次枚举器被请求时它都会使用它。
  4. 字典类内部。 Enumerator 是公共的,因为字典是可以枚举的,并且必须对IEnumerator接口实现具有同等的可以访问性- 比如 IEnumerator getter 。

更新——此外,意识到结构实现一个接口时,枚举器一样,是把实现类型,结构成为一个引用类型,并移动到堆中。 在字典类内部,枚举器仍然是一个值类型。 但是,只要一个方法调用 GetEnumerator(),就会返回一个 reference-type IEnumerator

我们这里没有看到的是任何保持结构不变或者保持一个实例大小仅 16字节或者更少的实例的尝试或者证明:

  1. 上结构中的任何内容都未声明为 readonly - 不可变
  2. 这些结构的大小可以超过 16字节
  3. Entry 具有未定的生存期( 从 Add(),到 Remove()Clear() 或者垃圾收集) ;

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

尽管散列键,字典是快,部分是因为比引用类型实例化一个struct更快。 这里,我有一个 Dictionary<int, int>,它存储 300,000个随机整数,顺序递增键。

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

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

MemSize: 通过将字典序列化到MemoryStream并获取字节长度( 满足我们的目的) 来确定。

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

填补总时间: 诚然倾斜由于伐木和 OnResize 事件我添加到源;然而,仍然令人印象深刻的填补 300k整数而调整 15次操作。 好奇的是,如果我已经知道了容量,那么填充的总时间是多少? 13ms

那么,如果 Entry 是一个类? 这些时间或者指标真的有那么多不同?

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

很明显,大差别在于调整大小。 使用容量初始化字典的任何差异? 没有足够的关注。。 12ms

发生了什么,因为 Entry 是一个结构,它不需要像引用类型那样初始化。 这既是价值类型的美丽又是 bane 。 为了使用 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中找到: 结构设计 。简而言之:

不提供结构的默认构造函数。

如果结构定义了默认构造函数,当结构的数组被创建时,公共语言运行库在每个数组元素上自动执行默认的构造函数。

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

它实际上是很简单,我们将借用 Asimov的机器人三定律 :

  1. 结构必须是安全
  2. 结构必须有效地执行它的函数,除非这违反了规则 #1
  3. 结构在使用期间必须保持不变,除非需要它的破坏才能满足规则 #1

。从这个带走什么呢: 简而言之,要负责使用值类型。 它们是快速和高效的,但如果没有正确维护( 例如 ),就会产生许多意想不到的行为。 无意的副本) 。

当不需要多态时,需要值语义,并希望避免堆分配和相关的垃圾收集开销。 然而,警告是结构( 任意大) 比类引用( 通常一个机器词) 要昂贵得多,所以类在实践中可能会更快。

我不同意原始文章中给出的规则。 以下是我的规则:

1 ) 在数组中存储时使用结构进行性能。 ( 另请参阅什么时候为结构)? )

2 ) 在将结构化数据传递到/c+ +的代码中需要它们

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

  • 它们的行为不同于"普通对象"( 引用类型 ) 和作为参数传递的参数,这可能导致意外行为;如果查看代码的人不知道他正在处理一个结构,这尤其危险。
  • 无法继承它们。
  • 将结构作为参数传递比类昂贵。

当你想要值语义而不是引用语义时,使用结构。

编辑

不知道为什么人downvoting这但这是一个有效的点,和之前op澄清他的问题,这是最基本的一个结构体的基本原因。

如果你需要引用语义,你需要一个类而不是一个结构。

除了"这是一个值"回答,使用结构体的一个特定的场景是当你知道有一组数据导致垃圾收集的问题,和你有很多对象。 例如一个大型的List/数组的Person实例。 这里的自然隐喻是一个类,但是如果你有大量的long-lived Person实例,它们会阻塞GEN-2并导致GC停顿。 如果权证的场景,一个潜在的方法是使用一个 ( 不是 List ) 人结构体数组, 换句话说,Person[] 现在,在GEN-2中没有数百万个对象,你在 LOH ( 我假设这里没有字符串等- 换句话说,没有任何引用的纯数值) 上有一个单独的块。 这对GC的影响微乎其微。

处理这些数据很麻烦,因为数据可能是over-sized的结构,而且你不想一直复制fat值。 但是,直接在数组中访问它并不复制结构- 它是 in-place ( 与 List 索引器相对应,后者复制) 。 这意味着有大量的索引工作:


int index =.. .
int id = peopleArray[index].Id;

注意,保持值本身不变会在这里有所帮助。 对于更复杂的逻辑,请使用带有by-ref参数的方法:


void Foo(ref Person person) {...}
...
Foo(ref peopleArray[index]);

同样,这是 in-place - 我们没有复制值。

在非常特定的场景中,这种策略可能非常成功;但是,它是一个相当高级的scernario,应该只在你知道自己正在做什么和为什么要做的情况下尝试。 默认的是一个类。

结构对于数据的原子表示是很好的,因为这些数据可以被代码多次复制。 克隆一个对象通常比复制一个结构更昂贵,因为它涉及分配内存,运行构造函数和释放/垃圾回收当完成时。

C# 语言规范:

1.7 结构

类一样,结构是可以包含数据成员和函数成员的数据结构,但与类不同,结构是值类型,不需要堆分配。 结构类型的变量直接存储结构的数据,而类类型的变量存储对动态分配对象的引用。 结构类型不支持user-specified继承,并且所有结构类型都隐式继承自类型对象。

结构对于具有值语义的小数据结构尤其有用。 复数,坐标系中的点或者字典中的key-value对都是结构的好例子。 使用结构而不是类对小型数据结构的使用会在应用程序执行的内存分配数量上产生很大差异。 例如以下程序创建并初始化一个 100点数组。 通过实现作为类的点,101个独立的对象是数组的instantiated—one,其中一个是 100元素的一个。


class Point
{
 public int x, y;

 public Point(int x, int y) {
 this.x = x;
 this.y = y;
 }
}

class Test
{
 static void Main() {
 Point[] points = new Point[100];
 for (int i = 0; i <100; i++) points[i] = new Point(i, i);
 }
}

另一种方法是使点成为结构。


struct Point
{
 public int x, y;

 public Point(int x, int y) {
 this.x = x;
 this.y = y;
 }
}

现在,只有一个对象是instantiated—the一个用于 array—and 。点实例在数组中存储 in-line 。

结构构造函数用新运算符调用,但这并不意味着内存正在被分配。 结构构造函数不是动态地分配对象并返回对它的引用,而是返回struct值本身( 通常位于堆栈上的临时位置),然后根据需要复制这个值。

有了类,两个变量就可以引用同一个对象,因此在一个变量上的操作可以影响另一个变量引用的对象。 有了结构,每个变量都有自己的数据副本,而对一个操作的操作则不能影响另一个。 例如由以下代码Fragment生成的输出取决于点是类还是结构。


Point a = new Point(10, 10);
Point b = a;
a.x = 20;
Console.WriteLine(b.x);

如果点是类,输出是 20,因为a 和引用相同的对象。 如果点是一个结构,输出是 10,因为 a.x.的赋值创建了一个值的副本,而这个副本不影响到的后续赋值

上一个例子突出了结构的两个限制。 首先,复制整个结构通常比复制对象引用效率低,因此赋值和值参数传递的代价比引用类型更昂贵。 其次,除了引用和输出参数之外,不可能创建对结构的引用,这在许多情况下排除了它们的用法。

我认为最好的第一个近似是"从不"。

我认为一个好的第二个近似是"从不"。

如果你渴望性能,考虑它们,但是总是度量。

首先:互操作场景或者需要指定内存布局

第二:当数据与引用指针的大小几乎相同时。

除了直接使用的valuetypes段的运行时和其他各种pinvoke目的,你应该只使用 valuetypes 2场景。

  1. 当你需要复制语义时。
  2. 需要自动初始化时,通常在这些类型的数组中。
...