我遇到了这些规则(链接):
- 结构体应该代表单个值。
- 结构体的内存占用应小于16字节。
- 结构体在创建后不应更改。
OP提供的来源有一定可信度,但是微软的态度如何 - 对结构使用有何立场?我从Microsoft中寻找了一些额外的学习材料,以下是我发现的:
如果类型的实例很小且通常是短暂的或通常嵌入其他对象中,请考虑定义一个结构而不是类。
除非该类型具备以下所有特征,请勿定义结构:
- 它在逻辑上表示单个值,类似于原始类型(整数、双精度等)。
- 它的实例大小小于16个字节。
- 它是不可变的。
- 它不需要经常装箱。
好吧,至少第2和第3条。我们心爱的字典有2个内部结构:
[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个结构体中有2个是值类型(struct),这是有道理的,因为它们只表示单个值并且速度快。如果你发现自己要对结构体进行装箱(boxing),重新思考你的架构。为什么微软会使用这些结构体呢?让我们来看看:
Entry
和Enumerator
)都表示单个值。Entry
从不在Dictionary类外部作为参数传递。进一步调查显示,为了满足IEnumerable的实现,Dictionary使用枚举器(Enumerator)结构体,并在每次请求枚举器时复制该结构体……很合理。Enumerator
是public的,因为Dictionary可枚举并且必须具有相等的IEnumerator接口实现的可访问性——例如IEnumerator getter。更新-此外,请意识到当一个结构体实现一个接口(如Enumerator所做的那样),并被转换为该实现的类型时,结构体变成了引用类型并被移动到堆(heap)上。在Dictionary类内部,Enumerator仍然是值类型。然而,一旦一个方法调用GetEnumerator()
,就会返回一个引用类型的IEnumerator。
这里我们看不到任何试图保持结构体不可变或保持实例大小为16字节或更小的要求或证明:
readonly
——也不是不可变的(li)Entry
的生命周期是未确定的(从Add(), Remove(), Clear()或垃圾回收开始);并且... 4.这两个结构体都存储TKey和TValue,我们都知道它们可以是引用类型(附带信息)
尽管有哈希键,但字典之所以快速,部分原因在于实例化结构体比引用类型快。这里我有一个Dictionary<int, int>
,存储300,000个随机整数和递增的键。
容量: 312874
MemSize: 2660827 bytes
完成调整: 5ms
填充总时间: 889ms
Capacity:内部数组必须调整大小之前可用的元素数。
MemSize:通过将字典序列化到MemoryStream并获取字节长度来确定(对于我们的目的而言足够准确)。
完成的调整大小: 调整内部数组从150862个元素到312874个元素所需的时间。考虑到每个元素都是通过Array.CopyTo()
进行顺序复制的,这还不错。
填充总时间: 因为记录日志和添加到源代码的OnResize
事件而导致偏差;但在操作期间调整大小15次并填充300k整数仍然很令人印象深刻。出于好奇,如果我已经知道容量,填充的总时间会是多少?13毫秒
那么,如果Entry
是一个类会发生什么情况呢?这些时间或度量值真的会有很大的差异吗?
容量: 312874
MemSize: 2660827字节
完成的调整大小:26毫秒
填充总时间:964毫秒
显然,最大的区别在于调整大小。如果使用Capacity初始化Dictionary,会有什么不同吗?不足以引起担忧……12毫秒。
由于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#编译器)不允许结构具有默认构造函数。
事实上,它非常简单,我们可以借鉴阿西莫夫的三大法则:
...我们从中得到什么启示:简而言之,要对值类型的使用负责。它们快速高效,但如果未经妥善维护(即意外复制),可能会导致许多意外行为。
Decimal
或DateTime
],那么如果它不遵守其他三个规则,就应该用类来替换它。如果一个结构体包含一组固定的变量,每个变量都可以保存其类型有效的任何值[例如Rectangle
],那么它应该遵守不同的规则,其中一些与“单值”结构相反。 - supercatDictionary
条目类型仅为内部类型,性能比语义更重要,或者提出其他借口来为其辩护。我的观点是,像Rectangle
这样的类型应该将其内容作为可单独编辑的字段公开,不是因为性能优势超过了结果上的语义缺陷,而是因为该类型在语义上表示一组固定且独立的值,因此可变结构既具有更高的性能,也具有更好的语义优越性。 - supercat每当您:
然而,需要注意的是,结构体(任意大小)在传递过程中比类引用(通常为一个机器字)更昂贵,因此在实践中可能会导致类更快。
(Guid)null
这样的情况(将null强制转换为引用类型是可以的),以及其他一些情况。 - user166390我不同意原帖中给出的规则。以下是我的规则:
在数组中存储时,您可以使用结构体来提高性能。(另请参见何时使用结构体?)
需要在代码中传递结构化数据到/从C/C++
除非需要,否则不要使用结构体:
如果你想要值语义而不是引用语义,可以使用结构体。
如果需要引用语义,需要使用类而不是结构体。
除了“它是一个值”的答案之外,使用结构体的一个特定场景是当你知道你有一组数据会导致垃圾回收问题,并且你有大量对象。例如,一个大型的Person实例列表/数组。自然的比喻是一个类,但如果你有大量长期存在的Person实例,它们可能会堵塞GEN-2并导致GC停顿。如果情况需要,这里有一个潜在的方法是使用一个Person结构体的数组(而不是列表),即Person[]
。现在,你只有一个块在LOH上,而不是在GEN-2中拥有数百万个对象(我假设这里没有字符串等 - 即一个纯值而没有任何引用)。这对GC影响很小。
与此数据一起工作很麻烦,因为这个数据可能对于一个结构体来说过大,你不想一直复制大块的值。然而,在数组中直接访问它不会复制结构体 - 它是原地的(与列表索引器相反,后者会复制)。这意味着需要使用索引进行大量的操作:
int index = ...
int id = peopleArray[index].Id;
需要注意的是保持值本身不可变将有所帮助。对于更复杂的逻辑,请使用一个具有按引用传递参数的方法:
void Foo(ref Person person) {...}
...
Foo(ref peopleArray[index]);
再次强调,这是原地操作——我们没有复制该值。
在非常特定的情况下,这种策略可能非常成功;然而,这是一个相当高级的场景,只有在您知道自己在做什么以及为什么时才应该尝试。在这里,默认使用类。
ICustomer
,并有一个实现该接口的CustomerRef
结构,它持有单个int
索引,并根据数组项适当地执行操作,那么性能会如何?我认为,如果将接受Customer
的方法通用地改为接受ICustomer
,则应该可以获得与当前方法相当的性能,而无需广泛暴露底层数组。 - supercatICustomer
方法,如果需要,可以通过各种方式迁移离开使用单块数组(2GB的限制是其中一个原因,但不是唯一的原因)。顺便说一句,您的博客没有提到您的方法的“成本”之一是GC无法知道哪些数组插槽有对它们的引用。这减少了GC的成本,但意味着应用程序可能需要自己跟踪这些东西。 - supercatList
在幕后使用了一个 Array
,不是吗? - Royi Namir来自C#语言规范:
1.7 结构体
结构体与类一样,都是可以包含数据成员和函数成员的数据结构, 但与类不同的是,结构体是值类型,不需要进行堆分配。结构体类型 直接存储结构体的数据,而类类型的变量则存储对动态分配对象的引用。 结构体类型不支持用户指定继承,并且所有结构体类型都隐式继承自object类型。
结构体特别适用于具有值语义的小型数据结构,例如复数、坐标系中的点或字典中的键值对。 对于小型数据结构而言,使用结构体而不是类可以大大减少应用程序执行的内存分配次数。 例如,以下程序创建并初始化了一个包含100个点的数组。如果Point实现为类,则会实例化101个不同的对象——一个用于数组,每个元素一个。
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
传递给外部方法,并知道外部方法对其执行的任何更改都将在其返回之前完成。遗憾的是,.net 没有任何关于短暂参数和函数返回值的概念,因为... - supercat这里是一个基本规则。
如果所有成员字段都是值类型,请创建一个 struct。
如果任何一个成员字段是引用类型,请创建一个 class。这是因为引用类型字段将需要堆分配。
示例
public struct MyPoint
{
public int X; // Value Type
public int Y; // Value Type
}
public class MyPointWithName
{
public int X; // Value Type
public int Y; // Value Type
public string Name; // Reference Type
}
string
这样的不可变引用类型在语义上等同于值,将对不可变对象的引用存储到字段中并不需要堆分配。公开字段的结构体和公开字段的类对象之间的区别在于,给定代码序列var q = p; p.X = 4; q.X = 5;
,如果a
是结构体类型,则p.X
将具有值4,而如果它是类类型,则为5。如果希望方便地修改类型的成员,则应根据是否希望更改q
以影响p
来选择“class”或“struct”。 - supercatArraySegment<T>
封装了一个始终为类类型的 T[]
。结构类型 KeyValuePair<TKey,TValue>
经常与类类型一起用作泛型参数。 - supercatstruct
实际上可以提高性能,但必须通过分析和运行单元测试来验证其可行性,并显示程序功能未受影响。如果您想在答案中包含此评论的一部分作为编辑,请随意这样做^^ - CoffeDeveloper结构体适合原子数据的表示,其中所述数据可以被代码多次复制。通常情况下,克隆一个对象比复制一个结构体更昂贵,因为它涉及到分配内存、运行构造函数以及在使用完后进行撤销/垃圾回收。
第一点:Interop场景或者需要指定内存布局时。
第二点:当数据的大小几乎等于一个引用指针时。
我使用BenchmarkDotNet制作了一个小型基准测试,以更好地了解“结构体”在数字方面的优势。我正在测试循环遍历结构体(或类)的数组(或列表)。创建这些数组或列表不在基准测试范围内 - 很明显,“类”更加沉重,会利用更多内存,并涉及GC。
所以结论是:要小心使用LINQ和隐藏的结构体装箱/拆箱,并严格使用数组来进行微观优化。
P.S. 关于通过调用堆栈传递结构体/类的另一个基准测试,请参见https://dev59.com/UHVD5IYBdhLWcg3wVKEb#47864451
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;
}
}
System.Drawing.Rectangle
违反了这三条规则。 - ChrisW