什么时候应该在C#中使用结构而不是类?

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

我遇到了这些规则这里

  • 一个结构体应该代表一个值。
  • 结构应该有内存占用空间小于16个字节。
  • 一个结构体不应该被改变创建。

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

336495 次浏览

当你想要价值语义学而不是参考语义学时,使用结构。

如果你需要参考语义学,你需要一个类而不是结构。

每当您:

  1. 不需要多态性,
  2. 重视语义学,并
  3. 希望避免堆分配和相关的垃圾回收机制开销。

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

我认为一个好的第一近似是“从不”。

我认为第二个好的近似是“从不”。

如果你渴望成功,考虑他们,但总是测量。

结构体适合用原子表示数据,代码可以多次复制数据。一般来说,克隆对象比复制结构体成本高,因为它涉及分配内存、运行构造函数以及释放内存/垃圾回收机制。

在想要使用结构布局属性显式指定内存布局的情况下,您需要使用“struct”-通常用于PInvoke。

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

第一:互操作场景或需要指定内存布局时

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

除了运行时直接使用的值类型和其他各种用于PInvoke目的的值类型之外,您应该只在两种情况下使用值类型。

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

我使用结构来打包或解包任何类型的二进制通信格式。这包括读取或写入磁盘、DirectX顶点列表、网络协议或处理加密/压缩数据。

你列出的三个准则在这种情况下对我没有用。当我需要按照特定顺序写出四百字节的东西时,我会定义一个四百字节的结构,我会用它应该具有的任何不相关的值填充它,并且我会以当时最有意义的方式设置它。(好吧,四百字节会很奇怪-但是当我以编写Excel文件为生时,我正在处理大约40字节的结构,因为这就是一些BIFF记录的大小。)

不-我不完全同意这些规则。它们是考虑性能和标准化的良好指导方针,但不是根据可能性。

正如您在回复中看到的,有很多创造性的方法可以使用它们。因此,这些指南需要做到这一点,始终是为了性能和效率。

在这种情况下,我使用类来表示更大形式的真实世界对象,我使用结构来表示具有更精确用途的较小对象。你说的方式,“更具凝聚力的整体”关键字是凝聚力。类将更面向对象元素,而结构可以具有其中的一些特征,尽管规模较小。IMO。

我在Treeview和Listview标签中经常使用它们,在这些标签中可以非常快速地访问常见的静态属性。我一直在努力以另一种方式获取这些信息。例如,在我的数据库应用程序中,我使用Treeview,其中我有Tables、SP、Functions或任何其他对象。我创建并填充我的结构,将其放入标签中,将其拉出来,获取选择的数据等等。我不会用类这样做!

我确实尝试将它们保持在较小的范围内,在单实例情况下使用它们,并防止它们发生变化。注意内存、分配和性能是谨慎的。测试是非常必要的。

我很少使用结构体。但这只是我。这取决于我是否需要对象为空。

如其他答案所述,我为现实世界的对象使用类。我也有结构用于存储少量数据的心态。

我不同意原帖子中给出的规则。以下是我的规则:

  1. 当存储在数组中时,您使用结构来提高性能。(另请参阅什么时候结构是答案?

  2. 在向/从C/C++传递结构化数据的代码中需要它们

  3. 不要使用结构体,除非你需要它们:

    • 它们的行为与赋值下的“正常对象”(引用类型)不同当作为参数传递时,这可能导致意外的行为;这是特别危险的,如果人看代码不知道他们正在处理一个结构。
    • 它们不能被继承。
    • 将结构作为参数传递比类更昂贵。

OP引用的来源有一些可信度……但是微软呢——结构使用的立场是什么?我寻找了一些额外的向微软学习,这是我发现的:

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

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

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

微软一直违反这些规则

好吧,不管怎样,#2和#3。我们心爱的字典有两个内部结构:

[StructLayout(LayoutKind.Sequential)]  // default for structsprivate 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. 每个结构EntryEnumerator代表单个值。
  2. 速度
  3. Entry永远不会作为Dicpedia类之外的参数传递。进一步的研究表明,为了满足IENumable的实现,Dicpedia使用Enumerator结构,每次请求枚举器时都会复制该结构……这是有道理的。
  4. Enumerator是公共的,因为Dicpedia是可枚举的,并且必须具有相同的可访问性,以实现接口实现——例如,ienomator getter。

更新-此外,请意识到,当结构实现一个接口时——就像枚举器一样——并被转换为该实现的类型时,该结构将成为引用类型并被移动到堆中。在字典类的内部,枚举器仍然是一个值类型。然而,一旦方法调用GetEnumerator(),就会返回一个引用类型IEnumerator

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

  1. 上面的结构中没有任何内容被声明为readonly-没有不可变
  2. 这些结构的大小可能超过16字节
  3. Entry具有不确定的生命周期(从Add()Remove()Clear()或垃圾回收机制);

还有…4.两个结构体都存储TKey和TValue,我们都知道它们很有能力成为引用类型(增加了额外的信息)

尽管有散列键,字典之所以快,部分原因是实例化结构比引用类型更快。在这里,我有一个Dictionary<int, int>,它存储了300,000个带有顺序递增键的随机整数。

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

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

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

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

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

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

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

显然,最大的区别在于调整大小。如果字典初始化为容量,有什么区别吗?不足以关注…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:结构设计中找到。简而言之:

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

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

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

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

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

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

除了“它是一个值”的答案之外,使用structs的一个特定场景是,当你知道有一组导致垃圾回收机制问题的数据,并且你有很多对象。例如,一个大型Person实例列表/数组。这里自然的隐喻是一个类,但是如果你有大量长期存在的Person实例,它们最终可能会堵塞Gen-2并导致GC停顿。如果场景需要,一种潜在的方法是使用Personstructs的数组(而不是列表),即Person[]。现在,在Gen-2中没有数百万个对象,而是在LOH上有一个块(我假设这里没有字符串等-即没有任何引用的纯值)。这对GC影响很小。

处理这些数据很尴尬,因为数据对于结构来说可能太大了,而且你不想一直复制胖值。然而,直接在数组中访问它不会复制结构——它是就地的(与列表索引器相比,它会复制)。这意味着大量的索引工作:

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

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

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

同样,这是就地的-我们没有复制值。

在非常具体的情况下,这种策略可以非常成功;然而,这是一个相当高级的方法,只有当你知道你在做什么和为什么时才应该尝试。

C#或其他. net语言中的结构类型通常用于保存应该表现为固定大小的值组的内容。结构类型的一个有用方面是,可以通过修改保存它的存储位置来修改结构类型实例的字段,而不是其他方式。以这样一种方式编写结构是可能的,即改变任何字段的唯一方法是构造一个全新的实例,然后使用结构赋值通过用新实例中的值覆盖它们来改变目标的所有字段,但是除非结构不提供创建其字段具有非默认值的实例的方法,否则如果结构本身存储在可变位置,则其所有字段都是可变的。

请注意,如果结构包含一个私有类类型字段,并将其自己的成员重定向到包装的类对象的成员,则可以设计一个结构类型,使其本质上表现得像一个类类型。例如,PersonCollection可能提供属性SortedByNameSortedById,两者都持有对PersonCollection的“不可变”引用(在其构造函数中设置),并通过调用creator.GetNameSortedEnumeratorcreator.GetIdSortedEnumerator实现GetEnumerator。此类结构的行为很像对PersonCollection的引用,只是它们的GetEnumerator方法将绑定到PersonCollection中的不同方法。也可以用一个结构包装数组的一部分(例如,可以定义一个SortedByName0结构,它包含一个名为SortedByName2的SortedByName1、一个intSortedByName3和一个intSortedByName4,它们具有索引属性,对于0到SortedByName6范围内的索引SortedByName5,将访问SortedByName7)。不幸的是,如果SortedByName8是这种结构的只读实例,当前的编译器版本将不允许像SortedByName9这样的操作,因为它们无法确定这些操作是否会尝试写入SortedByName8的字段。

也可以设计一个结构来表现得像一个值类型,它包含一个可变大小的集合(无论结构在哪里,它都会被复制),但实现这一功能的唯一方法是确保该结构包含引用的任何对象都不会暴露给任何可能改变它的东西。例如,一个类似数组的结构包含一个私有数组,其索引的“put”方法创建一个新数组,其内容与原始数组相似,除了一个更改的元素。不幸的是,让这样的结构高效执行可能有些困难。虽然有时结构语义学可能很方便(例如,能够将类似数组的集合传递给例程,调用者和被调用者都知道外部代码不会修改集合,可能比要求调用者和被调用者防御性地复制他们给出的任何数据要好),类引用指向永远不会改变的对象的要求通常是一个非常严格的约束。

. NET支持value typesreference types(在Java,您只能定义引用类型)。reference types的实例在托管堆中分配,并在没有对它们的未完成引用时被垃圾回收。另一方面,value types的实例在stack中分配,因此一旦它们的作用域结束,分配的内存就会被回收。当然,value types通过值传递,reference types通过引用传递。除了System. String之外,所有C#原始数据类型都是值类型。

当使用struct over class时,

在C#中,structsvalue types,类是reference types。在C#中,您可以使用enum关键字和struct关键字创建值类型。使用value type而不是reference type将导致托管堆上的对象更少,从而减少垃圾收集器(GC)的负载,减少GC循环的频率,从而提高性能。然而,value types也有其缺点。传递一个大的struct肯定比传递引用更昂贵,这是一个明显的问题。另一个问题是与boxing/unboxing相关的开销。如果你想知道boxing/unboxing是什么意思,请按照这些链接了解value types1和value types2的详细解释。除了性能之外,有时你只需要类型具有值语义学,如果你只有reference types,这将很难(或丑陋)实现。你应该只使用value types,当你需要复制语义学或需要自动初始化时,通常在这些类型中的value types5中。

C#语言规范

1.7结构

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

结构对于具有以下特性的小型数据结构特别有用价值语义学。复数,坐标系统中的点,或字典中的键值对都是结构的好例子对于小型数据结构使用结构而不是类可以使应用程序内存分配数量的巨大差异执行。例如,以下程序创建并初始化100个点的数组。将Point实现为类,101实例化单独的对象-一个用于数组,一个用于数组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);}}

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

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

现在,只实例化了一个对象——用于数组的对象——并且Point实例以内联方式存储在数组中。

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

使用类时,两个变量可能引用同一个对象,因此对一个变量的操作可能会影响另一个变量引用的对象。使用结构时,变量都有自己的数据副本,对一个变量的操作不可能影响另一个。例如,以下代码片段产生的输出取决于Point是类还是结构。

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

如果Point是一个类,则输出为20,因为a和b引用相同的对象。如果Point是一个结构,则输出为10,因为a对b的赋值会创建该值的副本,并且该副本不受后续对a. x的赋值的影响。

前面的例子强调了结构的两个局限性。首先,复制整个结构通常不如复制对象引用有效,因此与引用类型相比,结构的赋值和值参数传递可能更昂贵。其次,除了ref和out参数,无法创建对结构的引用,这排除了它们在许多情况下的使用。

结构可用于提高垃圾回收机制的性能。虽然你通常不必担心GC性能,但在某些情况下它可能是一个杀手。就像低延迟应用程序中的大型缓存一样。有关示例,请参阅这篇文章:

我的规则是

1、始终使用类;

2、如果有任何性能问题,我会尝试根据@I抽象提到的规则将一些类更改为struct,然后进行测试,看看这些更改是否可以提高性能。

这是一个基本规则。

  • 如果所有成员字段都是值类型,则创建一个struct

  • 如果任何一个成员字段是引用类型,请创建一个class。这是因为引用类型字段无论如何都需要堆分配。

例证

public struct MyPoint{public int X; // Value Typepublic int Y; // Value Type}
public class MyPointWithName{public int X; // Value Typepublic int Y; // Value Typepublic string Name; // Reference Type}

类是一种引用类型。当创建该类的对象时,分配对象的变量只保存对该内存的引用。当对象引用分配给新变量时,新变量引用原始对象。通过一个变量所做的更改会反映在另一个变量中,因为它们都引用相同的数据。结构是一种值类型。当创建结构时,分配结构的变量保存结构的实际数据。当结构分配给新变量时,它会被复制。因此,新变量和原始变量包含同一数据的两个单独副本。对一个副本所做的更改不会影响另一个副本。一般来说,类用于建模更复杂的行为,或者在创建类对象后打算修改的数据。结构最适合于主要包含创建结构后不打算修改的数据的小型数据结构。

类和结构(C#编程指南)

结构在大多数情况下都像类/对象。结构可以包含函数、成员并且可以被继承。但结构在C#中仅用于数据保存。结构比类做占用更少的RAM,是垃圾收集器更容易收集。但是当你在结构中使用函数时,编译器实际上将该结构与类/对象非常相似,所以如果你想要函数,然后使用类/对象的东西。

我刚刚在处理Windows通信基金会[WCF]命名管道,我确实注意到使用结构确实有意义,以确保数据交换是值类型而不是引用类型

简单地说,使用struct if:

  1. 您的对象属性/字段不需要更改。我的意思是您只想给它们一个初始值,然后读取它们。

  2. 对象中的属性和字段是值类型,它们不是那么大。

如果是这种情况,您可以利用结构来获得更好的性能和优化的内存分配,因为它们只使用堆栈而不是堆栈和堆(在类中)

struct是一种值类型。如果您将结构分配给新变量,新变量将包含原始变量的副本。

public struct IntStruct {public int Value {get; set;}}

执行以下结果会导致存储在内存中的结构的5个实例

var struct1 = new IntStruct() { Value = 0 }; // originalvar struct2 = struct1;  // A copy is madevar struct3 = struct2;  // A copy is madevar struct4 = struct3;  // A copy is madevar struct5 = struct4;  // A copy is made
// NOTE: A "copy" will occur when you pass a struct into a method parameter.// To avoid the "copy", use the ref keyword.
// Although structs are designed to use less system resources// than classes.  If used incorrectly, they could use significantly more.

是引用类型。当您将类分配给新变量时,该变量包含对原始类对象的引用。

public class IntClass {public int Value {get; set;}}

执行以下命令会导致内存中类对象的只有一个实例

var class1 = new IntClass() { Value = 0 };var class2 = class1;  // A reference is made to class1var class3 = class2;  // A reference is made to class1var class4 = class3;  // A reference is made to class1var class5 = class4;  // A reference is made to class1

结构可能会增加代码错误的可能性。如果将值对象视为可变引用对象,那么当所做的更改意外丢失时,开发人员可能会感到惊讶。

var struct1 = new IntStruct() { Value = 0 };var struct2 = struct1;struct2.Value = 1;// At this point, a developer may be surprised when// struct1.Value is 0 and not 1

我用基准数据点做了一个小基准测试,以更好地理解数字中的“struct”好处。我正在测试通过结构(或类)的数组(或列表)进行循环。创建这些数组或列表超出了基准测试的范围——很明显,“class”更重将占用更多内存,并将涉及GC。

所以结论是:要小心LINQ和隐藏结构的装箱/拆箱,并使用结构进行微优化,严格遵守数组。

附注:通过调用堆栈传递struct/class的另一个基准是https://stackoverflow.com/a/47864451/506147

BenchmarkDotNet=v0.10.8, OS=Windows 10 Redstone 2 (10.0.15063)Processor=Intel Core i5-2500K CPU 3.30GHz (Sandy Bridge), ProcessorCount=4Frequency=3233542 Hz, Resolution=309.2584 ns, Timer=TSC[Host] : Clr 4.0.30319.42000, 64bit RyuJIT-v4.7.2101.1Clr    : Clr 4.0.30319.42000, 64bit RyuJIT-v4.7.2101.1Core   : .NET Core 4.6.25211.01, 64bit RyuJIT

Method |  Job | Runtime |      Mean |     Error |    StdDev |       Min |       Max |    Median | Rank |  Gen 0 | Allocated |---------------- |----- |-------- |----------:|----------:|----------:|----------:|----------:|----------:|-----:|-------:|----------:|TestListClass |  Clr |     Clr |  5.599 us | 0.0408 us | 0.0382 us |  5.561 us |  5.689 us |  5.583 us |    3 |      - |       0 B |TestArrayClass |  Clr |     Clr |  2.024 us | 0.0102 us | 0.0096 us |  2.011 us |  2.043 us |  2.022 us |    2 |      - |       0 B |TestListStruct |  Clr |     Clr |  8.427 us | 0.1983 us | 0.2204 us |  8.101 us |  9.007 us |  8.374 us |    5 |      - |       0 B |TestArrayStruct |  Clr |     Clr |  1.539 us | 0.0295 us | 0.0276 us |  1.502 us |  1.577 us |  1.537 us |    1 |      - |       0 B |TestLinqClass |  Clr |     Clr | 13.117 us | 0.1007 us | 0.0892 us | 13.007 us | 13.301 us | 13.089 us |    7 | 0.0153 |      80 B |TestLinqStruct |  Clr |     Clr | 28.676 us | 0.1837 us | 0.1534 us | 28.441 us | 28.957 us | 28.660 us |    9 |      - |      96 B |TestListClass | Core |    Core |  5.747 us | 0.1147 us | 0.1275 us |  5.567 us |  5.945 us |  5.756 us |    4 |      - |       0 B |TestArrayClass | Core |    Core |  2.023 us | 0.0299 us | 0.0279 us |  1.990 us |  2.069 us |  2.013 us |    2 |      - |       0 B |TestListStruct | Core |    Core |  8.753 us | 0.1659 us | 0.1910 us |  8.498 us |  9.110 us |  8.670 us |    6 |      - |       0 B |TestArrayStruct | Core |    Core |  1.552 us | 0.0307 us | 0.0377 us |  1.496 us |  1.618 us |  1.552 us |    1 |      - |       0 B |TestLinqClass | Core |    Core | 14.286 us | 0.2430 us | 0.2273 us | 13.956 us | 14.678 us | 14.313 us |    8 | 0.0153 |      72 B |TestLinqStruct | Core |    Core | 30.121 us | 0.5941 us | 0.5835 us | 28.928 us | 30.909 us | 30.153 us |   10 |      - |      88 B |

代码:

[RankColumn, MinColumn, MaxColumn, StdDevColumn, MedianColumn][ClrJob, CoreJob][HtmlExporter, MarkdownExporter][MemoryDiagnoser]public class BenchmarkRef{public class C1{public string Text1;public string Text2;public string Text3;}
public struct S1{public string Text1;public string Text2;public string Text3;}
List<C1> testListClass = new List<C1>();List<S1> testListStruct = new List<S1>();C1[] testArrayClass;S1[] testArrayStruct;public BenchmarkRef(){for(int i=0;i<1000;i++){testListClass.Add(new C1  { Text1= i.ToString(), Text2=null, Text3= i.ToString() });testListStruct.Add(new S1 { Text1 = i.ToString(), Text2 = null, Text3 = i.ToString() });}testArrayClass = testListClass.ToArray();testArrayStruct = testListStruct.ToArray();}
[Benchmark]public int TestListClass(){var x = 0;foreach(var i in testListClass){x += i.Text1.Length + i.Text3.Length;}return x;}
[Benchmark]public int TestArrayClass(){var x = 0;foreach (var i in testArrayClass){x += i.Text1.Length + i.Text3.Length;}return x;}
[Benchmark]public int TestListStruct(){var x = 0;foreach (var i in testListStruct){x += i.Text1.Length + i.Text3.Length;}return x;}
[Benchmark]public int TestArrayStruct(){var x = 0;foreach (var i in testArrayStruct){x += i.Text1.Length + i.Text3.Length;}return x;}
[Benchmark]public int TestLinqClass(){var x = testListClass.Select(i=> i.Text1.Length + i.Text3.Length).Sum();return x;}
[Benchmark]public int TestLinqStruct(){var x = testListStruct.Select(i => i.Text1.Length + i.Text3.Length).Sum();return x;}}

C#struct是类的轻量级替代品。它可以做几乎与类相同的事情,但使用结构而不是类“昂贵”。这样做的原因有点技术性,但总而言之,类的新实例被放置在堆上,新实例化的结构被放置在堆栈上。此外,你不像处理类那样处理对结构的引用,而是直接使用结构实例。这也意味着当你将结构传递给函数时,它是按值传递的,而不是作为引用。在关于函数参数的章节中有更多关于这一点的信息。

因此,当你希望表示更简单的数据结构时,你应该使用结构,特别是如果你知道你将实例化很多结构。. NET框架中有很多例子,微软使用结构而不是类,例如Point、Rectangle和Color结构。

误区1:结构是轻量级的

这个神话有多种形式。有些人认为值类型不能或不应该有方法或其他重要的行为-它们应该使用简单数据搬迁类型,仅具有公共字段或简单属性很好的反例:它是一个值类型,就存在而言基本单位,如数字或字符,它也有意义能够根据其值进行计算。从另一个角度看事物方向,数据搬迁类型通常应该是引用类型-决策应该基于期望的值或参考类型语义学,而不是简单的的类型。其他人认为值类型在术语上比引用类型“更轻”事实是,在某些情况下,值类型更具性能-他们不需要垃圾回收机制,除非他们是盒装的,没有这种类型识别开销,并且不需要取消引用,例如。但在其他方法,引用类型更高性能-参数传递,将值分配给变量、返回值和类似的操作只需要4或8个字节(取决于您运行的是32位还是64位CLR)而不是复制所有数据。想象一下,如果ArrayList在某种程度上是一个“纯”值类型,并且将ArrayList表达式传递给方法涉及复制其所有数据!在几乎无论如何,性能并不是由这种决策决定的。瓶颈几乎从来都不是你认为的地方,在你根据性能做出设计决策之前,你应该衡量不同的选择。值得注意的是,这两种信念的结合也不起作用。它不管一个类型有多少个方法(不管它是一个类还是一个结构)每个实例占用的内存不受影响。(就内存而言,有成本####用于代码本身,但这是一次而不是每个实例。)

误区#2:引用类型活在堆上;值类型活在堆栈上

这通常是由于重复它的人懒惰造成的。第一个部分是正确的-引用类型的实例总是在堆上创建的。它是导致问题的第二部分。正如我已经指出的,变量的值存在于声明的任何地方,因此如果您有一个具有int类型实例变量的类,则该变量对任何给定对象的值将始终与该对象的其余数据相同是-在堆上。只有局部变量(在方法中声明的变量)和方法参数存在于堆栈中。在C#2及更高版本中,即使一些局部变量也不会真正生活在堆栈上,正如我们在第5章中查看匿名方法时您将看到的那样。这些概念现在相关吗?有争议的是,如果您正在编写托管代码,您应该让运行时担心如何最好地使用内存。事实上,语言规范并不能保证什么是生命;未来的运行时可能能够在堆栈上创建一些对象,如果它知道它可以逃脱惩罚,或者C#编译器可以生成几乎不使用堆栈。下一个神话通常只是一个术语问题。

误区#3:对象在C#中默认通过引用传递

这可能是传播最广的神话。同样,制作这个的人声称经常(尽管并不总是)知道C#的实际行为,但他们不知道“引用传递”的真正含义。不幸的是,这对于那些你知道这意味着什么。引用传递的形式定义相对复杂,涉及l值和类似的计算机科学术语,但重要的是,如果你通过了一个通过引用变量,您正在调用的方法可以通过更改其参数值来更改调用者变量的值。现在,请记住引用的值类型变量是引用,而不是对象本身。您可以更改参数引用的对象,而参数本身不通过引用传递。例如,以下方法更改StringBuilder的内容对象,但调用者的表达式仍将引用与之前:

void AppendHello(StringBuilder builder){builder.Append("hello");}

调用此方法时,参数值(对StringBuilder的引用)为按值传递。如果您要在方法-例如,使用语句Builder=null;-更改不会被来电者看到,与神话相反。有趣的是,不仅神话中的“通过引用”位不准确,而且“对象被传递”位也不准确。对象本身也永远不会被传递通过引用或值。当涉及引用类型时,变量要么是通过引用传递或参数(引用)的值通过值传递。除此之外,这回答了当null为空时会发生什么的问题作为一个按值参数-如果对象被传递,这将导致问题,因为没有要传递的对象!相反,空引用由值的方式与任何其他引用相同。如果这个快速的解释让你感到困惑,你可能想看看我的文章,“C#中的参数传递”(http://mng.bz/otVt),它涉及更多细节。这些神话并不是唯一存在的。拳击和拆箱是他们的#36825;的误解,我将在接下来尝试澄清。

参考: C#in Depth 3 Edition by Jon Skeet

以下是Microsoft网站上定义的规则:

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

❌避免定义结构,除非该类型具有以下所有特征:

它在逻辑上表示单个值,类似于原始类型(int、Double等)。

它的实例大小小于16字节。

它是不可变的。

它不需要经常被包装。

更多阅读

✔️考虑结构使用

  1. 创建一个对象或不需要创建对象(直接你可以赋值,它创建对象)
  2. 需要速度或性能改进
  3. 无需构造器和拆除器(静态承包商可用)
  4. 不需要类继承,但接口是可以接受的
  5. 小工作负载对象工作,如果它变高,内存问题会引起
  6. 您不能为变量设置默认值。
  7. 结构也可用方法、事件、静态构造函数、变量等
  8. 减少GC的工作量
  9. 不需要引用类型,只需要值类型(每次创建新对象时)
  10. 没有不可变对象(字符串是不可变对象,因为任何操作都不会在不更改原始字符串的情况下返回任何新字符串)

除了通常引用的性能差异之外,让我添加另一个方面,那就是揭示默认值使用的意图。

如果结构字段的默认值不代表建模概念的合理默认值,请不要使用结构。

例如。

  • 即使它们的所有字段都设置为默认值,颜色或点也有意义。RGB 0,0,0是一个非常好的颜色,(0,0)作为2D中的点也是如此。
  • 但是地址或人名没有合理的默认值。我的意思是,你能理解FirstName=null和LastName=null的人名吗?

如果你用类实现一个概念,那么你可以强制执行某些不变量,例如一个人必须有名字和姓氏。但是对于结构,总是可以创建一个实例,其所有字段都设置为默认值。

因此,当建模一个没有合理默认值的概念时,更喜欢一个类。您的类的用户会理解null意味着未指定个人名称,但如果您给他们一个所有属性都设置为null的个人名称结构实例,他们会感到困惑。

(通常的免责声明:性能考虑可能会覆盖此建议。如果您有性能问题,请务必在决定解决方案之前进行测量。尝试基准数据点它太棒了!)

类最适合将复杂的操作和数据分组在一起这将在整个程序中发生变化;结构是更好的选择在大多数情况下保持不变的简单对象和数据。除了它们的用途之外,它们在一个键上有根本的不同区域-即它们如何在变量之间传递或分配。类是引用类型,这意味着它们被传递给引用;结构是值类型,这意味着它们被传递给值。

  • 小心使用类。如果你有一些游戏对象引用相同的内存,修改一个会修改其他的。

创建struct对象时,其所有数据都存储在其没有引用或连接到其内存的相应变量位置。这使得结构对于创建需要快速有效地复制,同时仍保留其不同的身份

ExampleStruct struct1= new ExampleStruct()ExampleStruct struct2= struct1

修改struct2不会影响struct1

  • 基本上,创建结构是为了提高性能。但是,有时结构可能会更慢,因为涉及到所有的复制。如果你的结构有很多需要复制的变量,将其转换为一个类,只是传递引用可能会更快

  • 如果你有一个array of structs,则数组本身就是堆上的对象,并且数组中包含结构值。因此garbage collector只有一个对象需要考虑。如果数组超出范围,垃圾回收器可以一步释放数组内的所有结构。如果您的代码的任何其他部分使用此数组中的结构,则由于结构被复制,我们可以安全地释放数组本身及其内容。

  • 如果你有一个array of objects,数组本身和数组中的每个对象都是堆上的单独对象。每个对象都可以存储在堆的完全不同的部分,你的代码的另一部分可能有对这些对象的引用。所以当我们的数组超出范围时,我们不能马上释放数组。因为the garbage collector必须单独考虑每个对象,并确保在释放它们之前没有对每个对象的引用。