什么时候应该在 C# 中使用 struct 而不是 class?我的概念模型是当项目仅仅是值类型的集合时使用结构。一种在逻辑上将它们组合成一个有凝聚力的整体的方法。
我遇到了这些规则here:
一个结构应该代表一个单一的值。
结构的内存占用应小于 16 字节。
创建后不应更改结构。
这些规则有效吗?结构在语义上意味着什么?
System.Drawing.Rectangle
违反了所有这三个规则。
OP 引用的来源具有一定的可信度……但是 Microsoft 呢?对 struct 使用的立场是什么?我寻找了一些额外的 learning from 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' 来源获得了 4 分中的 3 分——这是可以原谅的,因为 #4 可能不会成为问题。如果您发现自己在装箱结构,请重新考虑您的架构。
让我们看看微软为什么会使用这些结构:
每个结构,Entry 和 Enumerator,代表单个值。 Speed Entry 永远不会作为 Dictionary 类之外的参数传递。进一步的调查表明,为了满足 IEnumerable 的实现,Dictionary 使用了 Enumerator 结构,每次请求枚举数时它都会复制它......这是有道理的。 Dictionary 类的内部。 Enumerator 是公共的,因为 Dictionary 是可枚举的,并且必须对 IEnumerator 接口实现具有相同的可访问性 - 例如 IEnumerator getter。
更新 - 此外,要意识到当一个结构实现一个接口时 - 就像 Enumerator 所做的那样 - 并被强制转换为该实现的类型,该结构成为一个引用类型并被移动到堆中。在 Dictionary 类的内部,Enumerator is 仍然是一个值类型。但是,只要方法调用 GetEnumerator()
,就会返回引用类型 IEnumerator
。
我们在这里没有看到任何保持结构不可变或保持实例大小仅为 16 字节或更少的尝试或要求证明:
上面的结构中没有任何内容被声明为只读 - 不是不可变的 这些结构的大小可能远远超过 16 个字节 条目具有未确定的生命周期(从 Add() 到 Remove()、Clear() 或垃圾收集);
并且... 4. 两个结构都存储 TKey 和 TValue,我们都知道它们非常有能力成为引用类型(添加了奖励信息)
尽管有散列键,但字典速度很快,部分原因是实例化结构比引用类型更快。在这里,我有一个 Dictionary<int, int>
,它存储 300,000 个随机整数和顺序递增的键。
容量:312874 MemSize:2660827 字节完成调整大小:5ms 总填充时间:889ms
容量:必须调整内部数组大小之前可用的元素数。
MemSize:通过将字典序列化为 MemoryStream 并获取字节长度来确定(对于我们的目的来说足够准确)。
完成调整大小:将内部数组的大小从 150862 个元素调整为 312874 个元素所需的时间。当您发现每个元素都通过 Array.CopyTo()
顺序复制时,这还不算太简陋。
填充的总时间:由于日志记录和我添加到源的 OnResize
事件,确实存在偏差;但是,在操作期间调整 15 次大小时填充 300k 整数仍然令人印象深刻。只是出于好奇,如果我已经知道容量,那么填充的总时间是多少? 13 毫秒
那么,现在,如果 Entry
是一个类呢?这些时间或指标真的会有那么大的不同吗?
容量:312874 内存大小:2660827 字节完成调整大小:26 毫秒总填充时间:964 毫秒
显然,最大的区别在于调整大小。如果 Dictionary 是用容量初始化的,有什么区别吗?不足以关注... 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: Structure Design 中找到。简而言之:
不要为结构提供默认构造函数。如果结构定义了默认构造函数,则在创建结构的数组时,公共语言运行库会自动对每个数组元素执行默认构造函数。某些编译器(例如 C# 编译器)不允许结构具有默认构造函数。
它实际上很简单,我们将借用 Asimov's Three Laws of Robotics:
结构必须可以安全使用 结构必须有效地执行其功能,除非这会违反规则 #1
...我们从中得到什么:简而言之,负责使用值类型。它们快速高效,但如果维护不当(即无意复制),可能会导致许多意外行为。
每当您:
不需要多态性,需要值语义,并希望避免堆分配和相关的垃圾收集开销。
然而,需要注意的是,结构(任意大)比类引用(通常是一个机器字)更昂贵,因此在实践中类最终可能会更快。
(Guid)null
(可以将 null 转换为引用类型)等等。
我不同意原帖中给出的规则。这是我的规则:
当存储在数组中时,您可以使用结构来提高性能。 (另请参阅结构何时是答案?)您在将结构化数据传递给/从 C/C++ 的代码中需要它们 除非您需要它们,否则不要使用结构:它们在赋值和传递时的行为与“普通对象”(引用类型)不同作为参数,可能导致意外行为;如果查看代码的人不知道他们正在处理一个结构,这尤其危险。它们不能被继承。将结构作为参数传递比类更昂贵。
struct
才能知道它的行为方式,但如果某物是具有暴露字段的 struct
,那么这就是人们所需要知道的。如果一个对象公开了一个暴露字段结构类型的属性,并且如果代码将该结构读取到一个变量并进行了修改,那么可以安全地预测,除非或直到该结构被写入,否则这种操作不会影响读取其属性的对象背部。相比之下,如果属性是可变类类型,读取并修改它可能会按预期更新基础对象,但是......
当您需要值语义而不是引用语义时,请使用结构。
编辑
不知道为什么人们不赞成这一点,但这是一个有效的观点,并且使before操作澄清了他的问题,这是结构的最基本原因。
如果您需要引用语义,则需要一个类而不是结构。
除了“它是一个值”的答案之外,使用结构的一个特定场景是当您知道您有一组导致垃圾收集问题的数据时,你有很多对象。例如,大量的 Person 实例列表/数组。这里的自然比喻是一个类,但如果你有大量长寿命的 Person 实例,它们最终可能会阻塞 GEN-2 并导致 GC 停顿。如果情况允许,这里一种可能的方法是使用 Person structs 的数组(不是列表),即 Person[]
。现在,不是在 GEN-2 中拥有数百万个对象,而是在 LOH 上有一个块(我假设这里没有字符串等 - 即没有任何引用的纯值)。这对 GC 影响很小。
处理这些数据很尴尬,因为数据对于结构来说可能过大,而且您不想一直复制胖值。但是,直接在数组中访问它不会复制结构 - 它是就地的(与复制的列表索引器形成对比)。这意味着大量的索引工作:
int index = ...
int id = peopleArray[index].Id;
请注意,保持值本身不可变将对此有所帮助。对于更复杂的逻辑,使用带有 by-ref 参数的方法:
void Foo(ref Person person) {...}
...
Foo(ref peopleArray[index]);
同样,这是就地的——我们没有复制该值。
在非常具体的场景中,这种策略可以非常成功;但是,这是一个相当高级的场景,只有在您知道自己在做什么以及为什么做的情况下才应该尝试。这里的默认值是一个类。
List
我相信,在幕后使用 Array
。不 ?
1.7 结构体与类一样,结构体是可以包含数据成员和函数成员的数据结构,但与类不同的是,结构体是值类型,不需要堆分配。结构类型的变量直接存储结构的数据,而类类型的变量存储对动态分配对象的引用。结构类型不支持用户指定的继承,所有结构类型都隐式继承自类型对象。结构对于具有值语义的小型数据结构特别有用。复数、坐标系中的点或字典中的键值对都是结构的好例子。对小型数据结构使用结构而不是类可以对应用程序执行的内存分配数量产生很大影响。例如,以下程序创建并初始化一个包含 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 会创建一个值的副本,并且该副本不受后续赋值给 ax 的影响
前面的示例突出了结构的两个限制。首先,复制整个结构通常比复制对象引用效率低,因此结构的赋值和值参数传递可能比引用类型更昂贵。其次,除了 ref 和 out 参数,不能创建对结构的引用,这排除了它们在许多情况下的使用。
ref
赋予可变结构,并且知道外部方法将对它执行的任何突变都将在它返回之前完成。太糟糕了.net没有任何临时参数和函数返回值的概念,因为......
ref
传递的结构的有利语义。本质上,局部变量、参数和函数返回值可以是持久的(默认)、可返回的或短暂的。代码将被禁止将短暂的东西复制到任何超出当前范围的东西。可返回的东西就像短暂的东西,除了它们可以从函数中返回。函数的返回值将受到适用于其任何“可返回”参数的最严格的限制。
结构适用于数据的原子表示,其中所述数据可以通过代码多次复制。克隆对象通常比复制结构更昂贵,因为它涉及分配内存、运行构造函数以及在完成后解除分配/垃圾收集。
这是一个基本规则。
如果所有成员字段都是值类型,则创建一个结构。
如果任何一个成员字段是引用类型,则创建一个类。这是因为无论如何引用类型字段都需要堆分配。
示例
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
来选择“类”或“结构”。
ArraySegment<T>
封装了一个 T[]
,它始终是一个类类型。结构类型 KeyValuePair<TKey,TValue>
通常与类类型一起用作泛型参数。
第一:互操作场景或需要指定内存布局时
第二:当数据的大小与引用指针几乎相同时。
在要使用 StructLayoutAttribute 显式指定内存布局的情况下,您需要使用“结构” - 通常用于 PInvoke。
编辑:评论指出您可以将类或结构与 StructLayoutAttribute 一起使用,这当然是正确的。在实践中,您通常会使用结构 - 它分配在堆栈上而不是堆上,如果您只是将参数传递给非托管方法调用,这很有意义。
我使用结构来打包或解包任何类型的二进制通信格式。这包括读取或写入磁盘、DirectX 顶点列表、网络协议或处理加密/压缩数据。
在这种情况下,您列出的三个准则对我没有用。当我需要按特定顺序写出 400 字节的内容时,我将定义一个 400 字节的结构,并用它应该具有的任何不相关的值填充它,然后我会去以当时最有意义的方式进行设置。 (好吧,四百字节会很奇怪——但是当我以编写 Excel 文件为生时,我正在处理多达大约四十字节的结构,因为这就是一些 BIFF 记录的大小。)
除了运行时直接使用的值类型和其他各种用于 PInvoke 目的的值类型外,您应该只在 2 个场景中使用值类型。
当您需要复制语义时。当您需要自动初始化时,通常在这些类型的数组中。
this
参数除外);类允许重复引用。
我用 BenchmarkDotNet 做了一个小型基准测试,以更好地理解“结构”在数字上的好处。我正在测试遍历结构(或类)的数组(或列表)。创建这些数组或列表超出了基准测试的范围 - 很明显,“类”更重将占用更多内存,并涉及 GC。
所以结论是:小心 LINQ 和隐藏结构的装箱/拆箱,以及使用结构进行微优化严格使用数组。
PS 关于通过调用堆栈传递结构/类的另一个基准是 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;
}
}
.NET 支持 value types
和 reference types
(在 Java 中,您只能定义引用类型)。 reference types
的实例在托管堆中分配,并在没有对它们的未完成引用时进行垃圾收集。另一方面,value types
的实例在 stack
中分配,因此一旦它们的作用域结束,分配的内存就会被回收。当然,value types
通过值传递,reference types
通过引用传递。除 System.String 外,所有 C# 原始数据类型都是值类型。
何时在类上使用结构,
在 C# 中,structs
是 value types
,类是 reference types
。您可以在 C# 中使用 enum
关键字和 struct
关键字创建值类型。使用 value type
而不是 reference type
将导致托管堆上的对象减少,从而减少垃圾收集器 (GC) 上的负载,减少 GC 周期,从而提高性能。但是,value types
也有其缺点。传递一个大的 struct
肯定比传递一个引用更昂贵,这是一个明显的问题。另一个问题是与 boxing/unboxing
相关的开销。如果您想知道 boxing/unboxing
的含义,请点击这些链接以获得关于 boxing
和 unboxing
的详细说明。除了性能之外,有时您只需要类型具有值语义,如果您只有 reference types
,这将很难(或难看)实现。您应该只使用 value types
,当您需要复制语义或需要自动初始化时,通常在这些类型的 arrays
中。
ref
传递结构一样便宜。通过 ref
传递任何大小结构的成本与通过值传递类引用的成本相同。复制任何大小的结构或按值传递比执行类对象的防御性副本并存储或传递对它的引用要便宜。大时代类比结构更好的存储值是(1)当类是不可变的(以避免防御性复制),并且创建的每个实例将被传递很多,或者......
readOnlyStruct.someMember = 5;
的最佳方法不是将 someMember
设为只读属性,而是将其设为字段。
结构是一种值类型。如果将结构分配给新变量,则新变量将包含原始变量的副本。
public struct IntStruct {
public int Value {get; set;}
}
执行以下结果会导致 5 个结构实例存储在内存中:
var struct1 = new IntStruct() { Value = 0 }; // original
var struct2 = struct1; // A copy is made
var struct3 = struct2; // A copy is made
var struct4 = struct3; // A copy is made
var 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 class1
var class3 = class2; // A reference is made to class1
var class4 = class3; // A reference is made to class1
var 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
误区一:结构是轻量级的类
这个神话有多种形式。有些人认为值类型不能或不应该有方法或其他重要行为——它们应该用作简单的数据传输类型,只有公共字段或简单属性。 DateTime 类型是一个很好的反例:它是一个值类型,作为一个基本单位,如数字或字符,它是有意义的,它能够执行基于计算的计算也是有意义的。它的价值。从另一个方向来看,数据传输类型通常应该是引用类型——决定应该基于所需的值或引用类型语义,而不是类型的简单性。其他人认为值类型在性能方面比引用类型“更轻”。事实是,在某些情况下,值类型的性能更高——例如,它们不需要垃圾收集,除非它们被装箱,没有类型识别开销,并且不需要取消引用。但在其他方面,引用类型的性能更高——参数传递、为变量赋值、返回值和类似操作只需要复制 4 或 8 个字节(取决于您运行的是 32 位还是 64 位 CLR ) 而不是复制所有数据。想象一下,如果 ArrayList 是某种“纯”值类型,并且将 ArrayList 表达式传递给涉及复制其所有数据的方法!几乎在所有情况下,性能都不是由这种决定决定的。瓶颈几乎永远不会出现在您认为会出现的地方,在您根据性能做出设计决策之前,您应该衡量不同的选项。值得注意的是,这两种信念的结合也不起作用。一个类型有多少方法(无论是类还是结构)并不重要——每个实例占用的内存不受影响。 (就代码本身占用的内存而言,这是有代价的,但这是一次而不是每个实例。)
误区二:引用类型在堆上;价值类型存在于堆栈中
这通常是由于重复它的人的懒惰造成的。第一部分是正确的——总是在堆上创建一个引用类型的实例。这是导致问题的第二部分。正如我已经指出的,一个变量的值存在于它被声明的任何地方,所以如果你有一个具有 int 类型实例变量的类,那么任何给定对象的该变量的值将始终是该对象的其余数据所在的位置——在堆上。只有局部变量(在方法中声明的变量)和方法参数存在于堆栈中。在 C# 2 及更高版本中,甚至一些局部变量也并不真正存在于堆栈中,正如您将在第 5 章中查看匿名方法时看到的那样。这些概念现在是否相关?有争议的是,如果您正在编写托管代码,您应该让运行时担心如何最好地使用内存。事实上,语言规范并不能保证什么存在于哪里。如果未来的运行时知道它可以摆脱它,那么它可能能够在堆栈上创建一些对象,或者 C# 编译器可以生成几乎不使用堆栈的代码。下一个神话通常只是一个术语问题。
误区 #3:对象在 C# 中默认通过引用传递
这大概是流传最广的神话了。同样,经常(尽管并非总是)提出这种说法的人知道 C# 的实际行为方式,但他们不知道“通过引用传递”的真正含义。不幸的是,这让知道这意味着什么的人感到困惑。引用传递的正式定义比较复杂,涉及左值和类似的计算机科学术语,但重要的是,如果你通过引用传递一个变量,你调用的方法可以改变调用者变量的值通过更改其参数值。现在,请记住引用类型变量的值是引用,而不是对象本身。您可以更改参数引用的对象的内容,而无需通过引用传递参数本身。例如,以下方法更改了相关 StringBuilder 对象的内容,但调用者的表达式仍将引用与以前相同的对象:
void AppendHello(StringBuilder builder)
{
builder.Append("hello");
}
调用此方法时,参数值(对 StringBuilder 的引用)按值传递。如果您要在方法中更改 builder 变量的值——例如,使用语句 builder = null;——调用者不会看到该更改,这与神话相反。有趣的是,不仅神话中的“通过引用”位不准确,“对象被传递”位也是如此。对象本身永远不会通过引用或值传递。当涉及引用类型时,要么变量通过引用传递,要么参数(引用)的值通过值传递。除此之外,这还回答了当 null 用作按值参数时会发生什么的问题——如果对象被传递,那会导致问题,因为没有对象可以传递!相反,空引用以与任何其他引用相同的方式按值传递。如果这个快速的解释让您感到困惑,您可能想看看我的文章“C# 中的参数传递”(http://mng.bz/otVt),它更详细。这些神话并不是唯一的。装箱和拆箱是因为它们存在相当大的误解,接下来我将尝试澄清。
参考:C# in Depth 3rd Edition by Jon Skeet
C# 或其他 .net 语言中的结构类型通常用于保存应该表现得像固定大小的值组的东西。结构类型的一个有用方面是结构类型实例的字段可以通过修改它所在的存储位置来修改,而没有其他方式。可以以这样一种方式对结构进行编码,即改变任何字段的唯一方法是构造一个全新的实例,然后使用结构赋值来改变目标的所有字段,方法是用新实例中的值覆盖它们,但是除非结构没有提供创建其字段具有非默认值的实例的方法,否则如果结构本身存储在可变位置,则其所有字段都将是可变的。
请注意,如果结构包含私有类类型字段,并且将其自己的成员重定向到包装的类对象的成员,则可以设计一个结构类型,使其本质上表现得像一个类类型。例如,PersonCollection
可能提供属性 SortedByName
和 SortedById
,它们都持有对 PersonCollection
的“不可变”引用(在其构造函数中设置)并通过调用 creator.GetNameSortedEnumerator
或creator.GetIdSortedEnumerator
。此类结构的行为与对 PersonCollection
的引用非常相似,只是它们的 GetEnumerator
方法将绑定到 PersonCollection
中的不同方法。也可以有一个结构来包装数组的一部分(例如,可以定义一个 ArrayRange<T>
结构,该结构将包含一个名为 Arr
的 T[]
、一个 int Offset
和一个 int Length
,带有一个索引属性,对于 0 到 Length-1
范围内的索引 idx
,将访问 Arr[idx+Offset]
)。不幸的是,如果 foo
是这种结构的只读实例,当前的编译器版本将不允许像 foo[3]+=4;
这样的操作,因为它们无法确定此类操作是否会尝试写入 foo
的字段。
也可以设计一个结构,使其表现得像一个值类型,它包含一个可变大小的集合(只要结构存在,它就会被复制),但完成这项工作的唯一方法是确保没有对象struct 持有一个引用将永远暴露给任何可能改变它的东西。例如,可以有一个类似数组的结构,该结构包含一个私有数组,其索引“put”方法创建一个新数组,其内容与原始数组相同,只是有一个更改的元素。不幸的是,要使这样的结构有效地执行可能有些困难。虽然有时结构语义很方便(例如,能够将类似数组的集合传递给例程,调用者和被调用者都知道外部代码不会修改集合,但可能比同时要求调用者和被调用者更好)被调用者防御性地复制他们给出的任何数据),类引用指向永远不会变异的对象的要求通常是一个非常严格的约束。
不——我不完全同意这些规则。它们是考虑性能和标准化的好指南,但不是考虑到可能性。
正如您在回复中看到的那样,有很多创造性的方式可以使用它们。因此,为了性能和效率,这些指导方针必须如此。
在这种情况下,我使用类来以更大的形式表示现实世界的对象,我使用结构来表示具有更精确用途的较小对象。就像你说的那样,“一个更有凝聚力的整体”。关键字具有凝聚力。这些类将是更多面向对象的元素,而结构可以具有其中一些特征,尽管规模较小。国际海事组织。
我在 Treeview 和 Listview 标签中经常使用它们,可以非常快速地访问常见的静态属性。我一直在努力以另一种方式获取这些信息。例如,在我的数据库应用程序中,我使用 Treeview,其中包含表、SP、函数或任何其他对象。我创建并填充我的结构,将其放入标签中,将其拉出,获取选择的数据等等。我不会在课堂上这样做!
我确实尝试让它们保持小,在单一实例情况下使用它们,并防止它们改变。谨慎注意内存、分配和性能。测试是如此必要。
double
值的任意组合创建实例,那么这样的规范将强制它在语义上与暴露字段结构相同,除了多线程行为的一些细节(不可变类在某些情况下会更好,而暴露字段结构在其他情况下会更好;所谓的“不可变“ struct 在每种情况下都会更糟)。
我的规则是
1、始终使用类;
2,如果有任何性能问题,我尝试根据@IAbstract 提到的规则将一些类更改为struct,然后进行测试,看看这些更改是否可以提高性能。
Foo
的变量来封装一个固定的独立值集合(例如,一个点的坐标)时,人们有时希望将其作为一个组传递,有时希望独立更改.我还没有找到任何使用类的模式,它结合了这两个目的几乎和一个简单的暴露字段结构一样好(它是一个固定的自变量集合,完全符合要求)。
public readonly
字段,因为创建只读属性的工作量太大,实际上没有任何好处。)
MyListOfPoint[3].Offset(2,3);
转换为 var temp=MyListOfPoint[3]; temp.Offset(2,3);
,这种转换在应用时是虚假的......
Offset
方法。防止这种虚假代码的正确方法不应该是使结构不必要地不可变,而是允许像 Offset
这样的方法被标记为禁止上述转换的属性。如果可以对隐式数字转换进行标记,以便仅在其调用显而易见的情况下适用,那么隐式数值转换也可能会好得多。如果 foo(float,float)
和 foo(double,double)
存在重载,我认为尝试使用 float
和 double
通常不应该应用隐式转换,而应该是错误。
double
值直接分配给 float
,或将其传递给可以采用 float
参数但不能采用 double
的方法,几乎总是符合程序员的意图。相比之下,在没有显式类型转换的情况下将 float
表达式分配给 double
通常是错误的。唯一允许隐式 double->float
转换会导致问题的情况是它会导致选择不太理想的重载。我认为防止这种情况的正确方法不应该是禁止隐式 double->float,而是用属性标记重载以禁止转换。
类是引用类型。当创建类的对象时,分配给该对象的变量只保存对该内存的引用。当对象引用分配给新变量时,新变量引用原始对象。通过一个变量所做的更改会反映在另一个变量中,因为它们都引用相同的数据。结构是一种值类型。创建结构时,分配给结构的变量保存结构的实际数据。当结构被分配给一个新变量时,它被复制。因此,新变量和原始变量包含相同数据的两个单独副本。对一个副本所做的更改不会影响另一个副本。通常,类用于建模更复杂的行为,或在创建类对象后要修改的数据。结构最适合主要包含在创建结构后不打算修改的数据的小型数据结构。
Classes and Structs (C# Programming Guide)
我只是在处理 Windows Communication Foundation [WCF] Named Pipe,我确实注意到使用 Structs 来确保数据交换是值类型而不是引用类型是有意义的。
C# 结构是类的轻量级替代方案。它可以做的几乎与类相同,但使用结构而不是类更“昂贵”。这样做的原因有点技术性,但总而言之,一个类的新实例放在堆上,新实例化的结构放在堆栈上。此外,您不是像处理类那样处理对结构的引用,而是直接使用结构实例。这也意味着当您将结构传递给函数时,它是按值传递的,而不是作为引用。在关于函数参数的章节中有更多关于这一点的内容。
因此,当您希望表示更简单的数据结构时,您应该使用结构,尤其是当您知道您将实例化大量它们时。 .NET 框架中有很多示例,其中 Microsoft 使用结构而不是类,例如 Point、Rectangle 和 Color 结构。
简而言之,在以下情况下使用 struct:
您的对象属性/字段不需要更改。我的意思是你只想给它们一个初始值,然后读取它们。对象中的属性和字段是值类型,它们不是那么大。
如果是这种情况,您可以利用结构来获得更好的性能和优化的内存分配,因为它们只使用堆栈而不是堆栈和堆(在类中)
我认为一个好的第一个近似值是“从不”。
我认为一个好的第二个近似值是“从不”。
如果您迫切需要性能,请考虑它们,但要始终衡量。
结构可用于提高垃圾收集性能。虽然您通常不必担心 GC 性能,但在某些情况下它可能会成为杀手。就像低延迟应用程序中的大型缓存一样。有关示例,请参见此帖子:
http://00sharp.wordpress.com/2013/07/03/a-case-for-the-struct/
以下是 Microsoft 网站上定义的规则:
✔️ 如果类型的实例很小且通常短暂存在或通常嵌入在其他对象中,请考虑定义结构而不是类。
❌ 避免定义结构,除非该类型具有以下所有特征:
它在逻辑上表示单个值,类似于原始类型(int、double 等)。
它的实例大小小于 16 个字节。
它是不可变的。
它不必经常装箱。
进一步reading
除了通常提到的性能差异之外,让我添加另一个方面,那就是揭示默认值使用的意图。
如果结构的字段的默认值不代表建模概念的合理默认值,则不要使用结构。
例如。
即使所有字段都设置为默认值,颜色或点也是有意义的。 RGB 0,0,0 是一种非常好的颜色, (0,0) 作为 2D 中的点也是如此。
但是 Address 或 PersonName 没有合理的默认值。我的意思是你能理解 FirstName=null 和 LastName=null 的 PersonName 吗?
如果你用一个类实现一个概念,那么你可以强制执行某些不变量,例如。一个人必须有名字和姓氏。但是对于结构,总是可以创建一个实例,并将其所有字段设置为默认值。
因此,在对没有合理默认值的概念进行建模时,更喜欢使用类。您的类的用户将理解 null 意味着未指定 PersonName,但如果您将其所有属性都设置为 null 的 PersonName 结构实例交给他们,他们会感到困惑。
(通常的免责声明:性能考虑可能会覆盖此建议。如果您有性能问题,请始终在决定解决方案之前进行测量。试试BenchmarkDotNet,这太棒了!)
我很少使用结构来做事情。但这只是我。这取决于我是否需要对象可以为空。
如其他答案所述,我将类用于真实世界的对象。我也有结构用于存储少量数据的心态。
✔️ 考虑结构用法
创建对象或不需要创建对象(直接可以赋值,它创建对象) 需要速度或性能改进 不需要构造函数和析构函数(静态承包商可用) 不需要类继承,但可以接受接口 工作量小对象工作,如果它变高,内存问题将引发您不能为变量设置默认值。 Struct 还有可用的方法、事件、静态构造函数、变量等 GC 的工作量更少 不需要引用类型,只有值类型(每次创建新对象时) 没有不可变对象(字符串是不可变对象,因为任何操作都不返回任何每次新字符串不改变原来的)
结构在大多数方面类似于类/对象。结构可以包含函数、成员并且可以被继承。但是结构在 C# 中仅用于数据保存。结构确实比类占用更少的 RAM,并且更容易被垃圾收集器收集。但是当您在结构中使用函数时,编译器实际上将该结构与类/对象非常相似,因此如果您想要带有函数的东西,请使用类/对象。
Decimal
或DateTime
],则如果它不遵守其他三个规则,则应将其替换为一个类。如果一个结构包含一个固定的变量集合,每个变量都可以包含对其类型有效的任何值 [例如Rectangle
],那么它应该遵守 不同 规则,其中一些规则是与“单值”结构相反。Dictionary
条目类型只是内部类型,性能被认为比语义或其他借口更重要。我的观点是,像Rectangle
这样的类型应该将其内容公开为可单独编辑的字段,而不是“因为”性能优势超过了由此产生的语义缺陷,而是因为 该类型在语义上表示一组固定的独立值 i>,因此可变结构在性能和语义上都优越。