什么时候应该在C#中使用结构而不是类?我的概念模型是,当项只是值类型的集合时,使用结构。一种将它们逻辑地结合在一起的方法。
我在这里遇到了这些规则:
结构应表示单个价值结构应具有内存占用空间小于16字节。结构不应在之后更改创造
这些规则有效吗?结构在语义上意味着什么?
什么时候应该在C#中使用结构而不是类?我的概念模型是,当项只是值类型的集合时,使用结构。一种将它们逻辑地结合在一起的方法。
我在这里遇到了这些规则:
结构应表示单个价值结构应具有内存占用空间小于16字节。结构不应在之后更改创造
这些规则有效吗?结构在语义上意味着什么?
当前回答
C#或其他.net语言中的结构类型通常用于保存应该表现为固定大小的值组的内容。结构类型的一个有用方面是,可以通过修改保存结构类型实例的存储位置来修改该实例的字段,而不是以其他方式。可以以这样的方式对结构进行编码,即变异任何字段的唯一方法是构造一个完整的新实例,然后使用结构赋值通过用新实例中的值覆盖目标的所有字段来对其进行变异,但除非结构不提供创建其字段具有非默认值的实例的方法,如果结构本身存储在可变位置,则其所有字段都是可变的。
请注意,如果结构包含一个私有类类型字段,那么可以设计一个结构类型,使其基本上表现为类类型,并将其自身成员重定向到包装的类对象的成员。例如,PersonCollection可能提供财产SortedByName和SortedById,这两个属性都持有对PersonCollection的“不可变”引用(在其构造函数中设置),并通过调用creator.GetNameSortedEnumerator或creator.GetIdSortedEnumerator来实现GetEnumerater。此类结构的行为与对PersonCollection的引用非常相似,除了它们的GetEnumerator方法将绑定到PersonCollection中的不同方法。也可以有一个结构来包裹数组的一部分(例如,可以定义一个ArrayRange<T>结构,该结构将保存一个称为Arr的T[]、一个int Offset和一个int Length,以及一个索引属性,对于范围0到Length-1的索引idx,该属性将访问Arr[idx+Offset])。不幸的是,如果foo是这种结构的只读实例,当前的编译器版本将不允许像foo[3]+=4这样的操作;因为它们无法确定这些操作是否会尝试写入foo的字段。
也可以设计一个结构,使其行为类似于一个值类型,该值类型包含一个可变大小的集合(无论何时该结构都会被复制),但唯一可行的方法是确保该结构包含引用的对象不会暴露于任何可能使其发生变异的对象。例如,可以有一个类似数组的结构,它保存一个私有数组,其索引的“put”方法创建一个新数组,其内容与原始数组的内容相似,只有一个元素发生了更改。不幸的是,要使这种结构有效地执行可能有些困难。虽然有时结构语义可能很方便(例如,能够将类似数组的集合传递给例程,调用方和被调用方都知道外部代码不会修改集合,这可能比要求调用方和受调用方防御性地复制它们所提供的任何数据要好),类引用指向永远不会变异的对象的要求通常是一个相当严格的约束。
其他回答
神话1:结构是轻量级类
这个神话有多种形式。有些人认为价值类型不能或不应该有方法或其他重要的行为,它们应该简单地使用数据传输类型,只有公共字段或简单的财产。DateTime类型是这是一个很好的反例:就存在而言,它是一种价值类型是有意义的一个基本单位,如数字或字符能够基于其值执行计算。从另一个角度看问题方向,数据传输类型通常应该是引用类型应该基于期望的值或引用类型语义,而不是类型。其他人认为价值类型比参考类型“更轻”性能。事实是,在某些情况下,价值类型更有表现力-它们不需要垃圾收集,除非它们是盒装的,没有类型例如,识别开销,并且不需要取消引用。但在其他方面方法,引用类型是性能更高的参数传递,将值分配给变量、返回值和类似操作只需要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为如果正在传递对象,则用作by-value参数,这将导致问题,因为不会有对象要通过!相反,空引用由传递值的方式与任何其他引用相同。如果这个简短的解释让您感到困惑,那么您可能想看看我的文章“在C#中传递参数”(http://mng.bz/otVt),其中包含更多内容细节这些神话并不是唯一的神话。拳击和拆箱是他们的我会努力澄清这一误解。
参考:Jon Skeet的C#深度第三版
我用BenchmarkDotNet做了一个小的基准测试,以更好地理解数字中的“结构”好处。我正在测试遍历结构(或类)数组(或列表)的循环。创建这些数组或列表超出了基准测试的范围——很明显,“类”更重会占用更多内存,并且会涉及GC。
因此,结论是:小心LINQ和隐藏结构装箱/拆箱,并使用结构进行微优化严格遵守数组。
P.S.关于通过调用堆栈传递结构/类的另一个基准是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=4
Frequency=3233542 Hz, Resolution=309.2584 ns, Timer=TSC
[Host] : Clr 4.0.30319.42000, 64bit RyuJIT-v4.7.2101.1
Clr : Clr 4.0.30319.42000, 64bit RyuJIT-v4.7.2101.1
Core : .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;
}
}
当您需要值语义而不是引用语义时,请使用结构。
如果需要引用语义,则需要类而不是结构。
无论何时:
不需要多态性,want值语义,以及希望避免堆分配和相关的垃圾收集开销。
然而,需要注意的是,结构(任意大)传递比类引用(通常是一个机器字)更昂贵,因此类在实践中可能会更快。
我很少对事物使用结构。但这只是我,这取决于我是否需要对象为空。
如其他答案所述,我对真实世界的对象使用类。我也有结构用于存储少量数据的想法。