.NET 中的结构和类有什么区别?
在 .NET 中,有两类类型,引用类型和值类型。
结构是值类型,类是引用类型。
一般的区别是引用类型存在于堆上,而值类型存在于内联,也就是说,无论它是你的变量或字段的定义。
包含值类型的变量包含整个值类型值。对于结构,这意味着变量包含整个结构及其所有字段。
包含引用类型的变量包含一个指针,或对内存中实际值所在的其他位置的引用。
这有一个好处,首先是:
值类型总是包含一个值
引用类型可以包含一个空引用,这意味着它们现在根本不引用任何东西
在内部,引用类型被实现为指针,并且知道并知道变量赋值是如何工作的,还有其他行为模式:
将值类型变量的内容复制到另一个变量中,将整个内容复制到新变量中,使两者不同。换句话说,在复制之后,对一个的更改不会影响另一个
将引用类型变量的内容复制到另一个变量中,复制引用,这意味着您现在有两个对相同的引用在其他地方存储实际数据。换句话说,在复制之后,更改一个引用中的数据似乎也会影响另一个引用,但这只是因为您实际上只是在两个地方查看相同的数据
当您声明变量或字段时,这两种类型的区别如下:
变量:值类型存在于堆栈中,引用类型存在于堆栈中,作为指向实际内存所在的堆内存中某处的指针(尽管请注意 Eric Lipperts 文章系列:堆栈是一个实现细节。)
class/struct-field:值类型完全存在于类型内部,引用类型存在于类型内部,作为指向实际内存所在的堆内存中某处的指针。
每个的简短摘要:
仅限课程:
可以支持继承
是引用(指针)类型
引用可以为空
每个新实例都有内存开销
仅结构:
不支持继承
是值类型
按值传递(如整数)
不能有空引用(除非使用 Nullable)
每个新实例都没有内存开销 - 除非“装箱”
类和结构:
复合数据类型通常用于包含一些具有某种逻辑关系的变量吗
可以包含方法和事件
可支持接口
c# struct memory overhead
,发现 Hans Passant 的 this answer 说不,也不是这样。那么你的意思是做什么?
class
的实例是托管内存(由垃圾收集器处理),而实例struct
不是。
结构和类之间的区别:
结构是值类型,而类是引用类型。
结构存储在堆栈上,而类存储在堆上。
值类型将它们的值保存在声明它们的内存中,但引用类型保存对内存中对象的引用。
值类型在范围丢失后立即销毁,而引用类型仅在范围丢失后销毁变量。该对象稍后被垃圾收集器销毁。
当您将一个结构复制到另一个结构中时,会创建该结构的一个新副本。修改后的结构不会影响另一个结构的值。
当您将一个类复制到另一个类时,它只复制引用变量。
两个引用变量都指向堆上的同一个对象。对一个变量所做的更改将影响另一个参考变量。
结构不能有析构函数,但类可以有析构函数。
结构不能有显式的无参数构造函数,而类可以。结构不支持继承,但类支持。两者都支持从接口继承。
结构为密封型。
在 .NET 中,结构和类声明区分引用类型和值类型。
当你传递一个引用类型时,只有一个实际存储的。访问该实例的所有代码都在访问同一个。
当您传递一个值类型时,每个值类型都是一个副本。所有代码都在自己的副本上工作。
这可以用一个例子来展示:
struct MyStruct
{
string MyProperty { get; set; }
}
void ChangeMyStruct(MyStruct input)
{
input.MyProperty = "new value";
}
...
// Create value type
MyStruct testStruct = new MyStruct { MyProperty = "initial value" };
ChangeMyStruct(testStruct);
// Value of testStruct.MyProperty is still "initial value"
// - the method changed a new copy of the structure.
对于一个班级,这将是不同的
class MyClass
{
string MyProperty { get; set; }
}
void ChangeMyClass(MyClass input)
{
input.MyProperty = "new value";
}
...
// Create reference type
MyClass testClass = new MyClass { MyProperty = "initial value" };
ChangeMyClass(testClass);
// Value of testClass.MyProperty is now "new value"
// - the method changed the instance passed.
类可以什么都不是——引用可以指向一个空值。
结构是实际值——它们可以为空,但不能为空。出于这个原因,结构总是有一个没有参数的默认构造函数——它们需要一个“起始值”。
来自 Microsoft 的 Choosing Between Class and Struct ...
根据经验,框架中的大多数类型应该是类。然而,在某些情况下,值类型的特性使其更适合使用结构。 ✓ 考虑一个结构而不是一个类:如果该类型的实例很小并且通常是短暂的,或者通常嵌入在其他对象中。 X 避免使用结构,除非该类型具有以下所有特征: 它在逻辑上表示单个值,类似于原始类型(int、double 等)。它的实例大小小于 16 个字节。它是不可变的。 (不能更改)不必经常装箱。
https://i.stack.imgur.com/M9c18.png
以及文本表示以防万一;)
+--------------------------------------------------+------+----------------------------------------------+
| Struct | | Class |
+--------------------------------------------------+------+----------------------------------------------+
| - 1 per Thread. | | - 1 per application. |
| | | |
| - Holds value types. | | - Holds reference types. |
| | | |
| - Types in the stack are positioned | | - No type ordering (data is fragmented). |
| using the LIFO principle. | | |
| | | |
| - Can't have a default constructor and/or | | - Can have a default constructor |
| finalizer(destructor). | | and/or finalizer. |
| | | |
| - Can be created with or without a new operator. | | - Can be created only with a new operator. |
| | | |
| - Can't derive from the class or struct | VS | - Can have only one base class and/or |
| but can derive from the multiple interfaces. | | derive from multiple interfaces. |
| | | |
| - The data members can't be protected. | | - Data members can be protected. |
| | | |
| - Function members can't be | | - Function members can be |
| virtual or abstract. | | virtual or abstract. |
| | | |
| - Can't have a null value. | | - Can have a null value. |
| | | |
| - During an assignment, the contents are | | - Assignment is happening |
| copied from one variable to another. | | by reference. |
+--------------------------------------------------+------+----------------------------------------------+
有关更多信息,请查看以下内容:
类和结构(官方文档)。
在 Class 和 Struct 之间进行选择(官方文档)。
除了其他答案中描述的所有差异之外:
结构不能具有显式无参数构造函数,而类可以 结构不能具有析构函数,而类可以 结构不能从另一个结构或类继承,而类可以从另一个类继承。 (结构和类都可以从接口实现。)
如果您正在观看解释所有差异的视频,您可以查看Part 29 - C# Tutorial - Difference between classes and structs in C#。
结构类类型值类型引用类型在堆栈/包含类型中的内联在堆释放堆栈展开/包含类型被释放垃圾收集数组内联,元素是值类型的实际实例线外,元素只是引用驻留在堆上的引用类型的实例 Al-Del 成本 廉价的分配-释放 昂贵的分配-释放 内存使用 转换为引用类型或它们实现的接口之一时装箱,转换回值类型时取消装箱(负面影响,因为盒子是在堆上分配并被垃圾回收的对象)无装箱拆箱分配复制整个数据复制引用更改到实例不影响其任何副本影响指向实例的所有引用可变性应该是不可变的可变种群在某些情况 框架中的大多数类型应该是类 Lifetime Short-lived Long-lived Destructor Cannot have Can have In继承 仅来自接口 完全支持 多态 否 是 密封 是 有密封关键字 (C#) 或密封属性 (F#) 构造函数 不能有显式无参数构造函数 任何构造函数 空赋值 当标记为可为空的问号 是(当标记为可为空时C# 8+ 和 F# 5+ 中的问号 1) Abstract No 何时具有抽象关键字 (C#) 或 AbstractClass 属性 (F#) 成员访问修饰符 public、private、internal public、protected、internal、protected internal、private protected
1 在 F# 中不鼓励使用 null
,而是使用 Option 类型。
类的实例存储在托管堆上。所有“包含”实例的变量都只是对堆上实例的引用。将对象传递给方法会导致传递引用的副本,而不是对象本身。
结构(技术上是值类型)存储在任何使用它们的地方,就像原始类型一样。运行时可以随时复制内容,而无需调用自定义的复制构造函数。将值类型传递给方法涉及复制整个值,同样无需调用任何可定制的代码。
通过 C++/CLI 名称可以更好地区分:“ref class”是第一个描述的类,“value class”是第二个描述的类。 C# 使用的关键字“class”和“struct”只是必须学习的东西。
结构与类
结构是值类型,因此它存储在堆栈中,而类是引用类型并存储在堆中。
结构不支持继承和多态,但类支持两者。
默认情况下,所有结构成员都是公共的,但类成员默认情况下是私有的。
由于结构是值类型,我们不能将 null 分配给结构对象,但对于类则不是这样。
为了补充其他答案,有一个值得注意的根本区别,那就是数据如何存储在数组中,因为这会对性能产生重大影响。
使用结构,数组包含结构的实例
对于类,数组包含指向内存中其他位置的类实例的指针
所以一个结构数组在内存中看起来像这样
[struct][struct][struct][struct][struct][struct][struct][struct]
而一组类看起来像这样
[pointer][pointer][pointer][pointer][pointer][pointer][pointer][pointer]
对于类数组,您感兴趣的值不会存储在数组中,而是存储在内存中的其他位置。
对于绝大多数应用程序来说,这种差异并不重要,但是,在高性能代码中,这会影响内存中数据的局部性,并对 CPU 缓存的性能产生很大影响。当您可以/应该使用结构时使用类将大大增加 CPU 上缓存未命中的数量。
现代 CPU 做的最慢的事情不是处理数字,而是从内存中获取数据,并且 L1 缓存命中比从 RAM 中读取数据快很多倍。
这是您可以测试的一些代码。在我的机器上,遍历类数组需要比结构数组长约 3 倍的时间。
private struct PerformanceStruct
{
public int i1;
public int i2;
}
private class PerformanceClass
{
public int i1;
public int i2;
}
private static void DoTest()
{
var structArray = new PerformanceStruct[100000000];
var classArray = new PerformanceClass[structArray.Length];
for (var i = 0; i < structArray.Length; i++)
{
structArray[i] = new PerformanceStruct();
classArray[i] = new PerformanceClass();
}
long total = 0;
var sw = new Stopwatch();
sw.Start();
for (var loops = 0; loops < 100; loops++)
for (var i = 0; i < structArray.Length; i++)
{
total += structArray[i].i1 + structArray[i].i2;
}
sw.Stop();
Console.WriteLine($"Struct Time: {sw.ElapsedMilliseconds}");
sw = new Stopwatch();
sw.Start();
for (var loops = 0; loops < 100; loops++)
for (var i = 0; i < classArray.Length; i++)
{
total += classArray[i].i1 + classArray[i].i2;
}
Console.WriteLine($"Class Time: {sw.ElapsedMilliseconds}");
}
好吧,对于初学者来说,结构是通过值而不是通过引用传递的。结构适用于相对简单的数据结构,而从架构的角度来看,类通过多态性和继承具有更大的灵活性。
其他人可能会给你比我更多的细节,但是当我想要的结构很简单时,我会使用结构。
为了完整起见,使用 Equals
方法时还有另一个区别,该方法由所有类和结构继承。
假设我们有一个类和一个结构:
class A{
public int a, b;
}
struct B{
public int a, b;
}
在 Main 方法中,我们有 4 个对象。
static void Main{
A c1 = new A(), c2 = new A();
c1.a = c1.b = c2.a = c2.b = 1;
B s1 = new B(), s2 = new B();
s1.a = s1.b = s2.a = s2.b = 1;
}
然后:
s1.Equals(s2) // true
s1.Equals(c1) // false
c1.Equals(c2) // false
c1 == c2 // false
因此,结构适用于类似数字的对象,例如点(保存 x 和 y 坐标)。课程适合其他人。即使两个人的名字、身高、体重……相同,他们仍然是两个人。
在类中声明的事件通过 lock(this) 自动锁定其 += 和 -= 访问,以使它们成为线程安全的(静态事件被锁定在类的类型上)。在结构中声明的事件不会自动锁定其 += 和 -= 访问权限。结构的 lock(this) 不起作用,因为您只能锁定引用类型表达式。创建结构实例不会导致垃圾回收(除非构造函数直接或间接创建引用类型实例),而创建引用类型实例会导致垃圾回收。结构始终具有内置的公共默认构造函数。类 DefaultConstructor { static void Eg() { Direct yes = new Direct(); // 总是编译 OK InDirect 也许 = new InDirect(); // 如果构造函数存在且可访问,则编译 //... } } 这意味着结构始终是可实例化的,而类可能不是,因为它的所有构造函数都可能是私有的。 class NonInstantiable { private NonInstantiable() // OK { } } struct Direct { private Direct() // 编译时错误 { } } 结构不能有析构函数。析构函数只是 object.Finalize 变相的覆盖,而作为值类型的结构不受垃圾回收的影响。 struct Direct { ~Direct() {} // 编译时错误 } class InDirect { ~InDirect() {} // 编译正常 } ~Indirect() 的 CIL 如下所示: .method family hidebysig virtual instance void Finalize () cil managed { // ... } // 方法结束 Indirect::Finalize 结构是隐式密封的,而类不是。结构不能是抽象的,类可以。结构不能在其构造函数中调用 : base() 而没有显式基类的类可以。一个结构不能扩展另一个类,一个类可以。结构不能声明类可以声明的受保护成员(例如,字段、嵌套类型)。结构不能声明抽象函数成员,抽象类可以。结构不能声明虚函数成员,类可以。结构不能声明密封函数成员,类可以。结构不能声明覆盖函数成员,类可以。此规则的一个例外是结构可以覆盖 System.Object 的虚拟方法,即 Equals()、GetHashCode() 和 ToString()。
Object
的初始为空的隐藏字段,它将包含对该结构的盒装副本的引用。
如前所述:类是引用类型,而结构是具有所有后果的值类型。
作为规则框架设计指南建议在以下情况下使用结构而不是类:
它的实例大小小于 16 字节
它在逻辑上表示单个值,类似于原始类型(int、double 等)
它是不可变的
不必经常装箱
除了访问说明符的基本区别,以及上面提到的几个,我想补充一些主要的区别,包括上面提到的几个和输出的代码示例,这将使参考和价值更加清晰
结构:
是值类型,不需要堆分配。
内存分配不同,存放在栈中
适用于小型数据结构
影响性能,当我们将值传递给方法时,我们将整个数据结构传递给堆栈。
构造函数只返回结构值本身(通常在堆栈上的临时位置),然后根据需要复制该值
每个变量都有自己的数据副本,对一个变量的操作不可能影响另一个变量。
不支持用户指定的继承,它们隐式继承自类型对象
班级:
引用类型值
存储在堆中
存储对动态分配对象的引用
使用 new 运算符调用构造函数,但不会在堆上分配内存
多个变量可能引用同一个对象
对一个变量的操作可能会影响另一个变量引用的对象
代码示例
static void Main(string[] args)
{
//Struct
myStruct objStruct = new myStruct();
objStruct.x = 10;
Console.WriteLine("Initial value of Struct Object is: " + objStruct.x);
Console.WriteLine();
methodStruct(objStruct);
Console.WriteLine();
Console.WriteLine("After Method call value of Struct Object is: " + objStruct.x);
Console.WriteLine();
//Class
myClass objClass = new myClass(10);
Console.WriteLine("Initial value of Class Object is: " + objClass.x);
Console.WriteLine();
methodClass(objClass);
Console.WriteLine();
Console.WriteLine("After Method call value of Class Object is: " + objClass.x);
Console.Read();
}
static void methodStruct(myStruct newStruct)
{
newStruct.x = 20;
Console.WriteLine("Inside Struct Method");
Console.WriteLine("Inside Method value of Struct Object is: " + newStruct.x);
}
static void methodClass(myClass newClass)
{
newClass.x = 20;
Console.WriteLine("Inside Class Method");
Console.WriteLine("Inside Method value of Class Object is: " + newClass.x);
}
public struct myStruct
{
public int x;
public myStruct(int xCons)
{
this.x = xCons;
}
}
public class myClass
{
public int x;
public myClass(int xCons)
{
this.x = xCons;
}
}
输出
结构对象的初始值为:10
Inside Struct Method Inside Method Struct Object 的值为:20
结构对象的方法调用后值为:10
类对象的初始值为:10
类对象内部方法的内部方法值为:20
类对象的方法调用后值为:20
在这里您可以清楚地看到按值调用和按引用调用之间的区别。
有一个有趣的“类 vs 结构”难题的案例 - 当您需要从方法返回多个结果时:选择使用哪个。如果你知道 ValueTuple 的故事 - 你知道 ValueTuple (struct) 被添加是因为它应该比 Tuple (class) 更有效。但它在数字上意味着什么?两个测试:一个是具有 2 个字段的结构/类,另一个是具有 8 个字段的结构/类(维度大于 4 - 就处理器滴答而言,类应该比结构更有效,但当然也应该考虑 GC 负载)。
PS 特定案例“带有集合的结构或类”的另一个基准是:https://stackoverflow.com/a/45276657/506147
BenchmarkDotNet=v0.10.10, OS=Windows 10 Redstone 2 [1703, Creators Update] (10.0.15063.726)
Processor=Intel Core i5-2500K CPU 3.30GHz (Sandy Bridge), ProcessorCount=4
Frequency=3233540 Hz, Resolution=309.2586 ns, Timer=TSC
.NET Core SDK=2.0.3
[Host] : .NET Core 2.0.3 (Framework 4.6.25815.02), 64bit RyuJIT
Clr : .NET Framework 4.7 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.2115.0
Core : .NET Core 2.0.3 (Framework 4.6.25815.02), 64bit RyuJIT
Method | Job | Runtime | Mean | Error | StdDev | Min | Max | Median | Rank | Gen 0 | Allocated |
------------------ |----- |-------- |---------:|----------:|----------:|---------:|---------:|---------:|-----:|-------:|----------:|
TestStructReturn | Clr | Clr | 17.57 ns | 0.1960 ns | 0.1834 ns | 17.25 ns | 17.89 ns | 17.55 ns | 4 | 0.0127 | 40 B |
TestClassReturn | Clr | Clr | 21.93 ns | 0.4554 ns | 0.5244 ns | 21.17 ns | 23.26 ns | 21.86 ns | 5 | 0.0229 | 72 B |
TestStructReturn8 | Clr | Clr | 38.99 ns | 0.8302 ns | 1.4097 ns | 37.36 ns | 42.35 ns | 38.50 ns | 8 | 0.0127 | 40 B |
TestClassReturn8 | Clr | Clr | 23.69 ns | 0.5373 ns | 0.6987 ns | 22.70 ns | 25.24 ns | 23.37 ns | 6 | 0.0305 | 96 B |
TestStructReturn | Core | Core | 12.28 ns | 0.1882 ns | 0.1760 ns | 11.92 ns | 12.57 ns | 12.30 ns | 1 | 0.0127 | 40 B |
TestClassReturn | Core | Core | 15.33 ns | 0.4343 ns | 0.4063 ns | 14.83 ns | 16.44 ns | 15.31 ns | 2 | 0.0229 | 72 B |
TestStructReturn8 | Core | Core | 34.11 ns | 0.7089 ns | 1.4954 ns | 31.52 ns | 36.81 ns | 34.03 ns | 7 | 0.0127 | 40 B |
TestClassReturn8 | Core | Core | 17.04 ns | 0.2299 ns | 0.2150 ns | 16.68 ns | 17.41 ns | 16.98 ns | 3 | 0.0305 | 96 B |
代码测试:
using System;
using System.Text;
using System.Collections.Generic;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Attributes.Columns;
using BenchmarkDotNet.Attributes.Exporters;
using BenchmarkDotNet.Attributes.Jobs;
using DashboardCode.Routines.Json;
namespace Benchmark
{
//[Config(typeof(MyManualConfig))]
[RankColumn, MinColumn, MaxColumn, StdDevColumn, MedianColumn]
[ClrJob, CoreJob]
[HtmlExporter, MarkdownExporter]
[MemoryDiagnoser]
public class BenchmarkStructOrClass
{
static TestStruct testStruct = new TestStruct();
static TestClass testClass = new TestClass();
static TestStruct8 testStruct8 = new TestStruct8();
static TestClass8 testClass8 = new TestClass8();
[Benchmark]
public void TestStructReturn()
{
testStruct.TestMethod();
}
[Benchmark]
public void TestClassReturn()
{
testClass.TestMethod();
}
[Benchmark]
public void TestStructReturn8()
{
testStruct8.TestMethod();
}
[Benchmark]
public void TestClassReturn8()
{
testClass8.TestMethod();
}
public class TestStruct
{
public int Number = 5;
public struct StructType<T>
{
public T Instance;
public List<string> List;
}
public int TestMethod()
{
var s = Method1(1);
return s.Instance;
}
private StructType<int> Method1(int i)
{
return Method2(++i);
}
private StructType<int> Method2(int i)
{
return Method3(++i);
}
private StructType<int> Method3(int i)
{
return Method4(++i);
}
private StructType<int> Method4(int i)
{
var x = new StructType<int>();
x.List = new List<string>();
x.Instance = ++i;
return x;
}
}
public class TestClass
{
public int Number = 5;
public class ClassType<T>
{
public T Instance;
public List<string> List;
}
public int TestMethod()
{
var s = Method1(1);
return s.Instance;
}
private ClassType<int> Method1(int i)
{
return Method2(++i);
}
private ClassType<int> Method2(int i)
{
return Method3(++i);
}
private ClassType<int> Method3(int i)
{
return Method4(++i);
}
private ClassType<int> Method4(int i)
{
var x = new ClassType<int>();
x.List = new List<string>();
x.Instance = ++i;
return x;
}
}
public class TestStruct8
{
public int Number = 5;
public struct StructType<T>
{
public T Instance1;
public T Instance2;
public T Instance3;
public T Instance4;
public T Instance5;
public T Instance6;
public T Instance7;
public List<string> List;
}
public int TestMethod()
{
var s = Method1(1);
return s.Instance1;
}
private StructType<int> Method1(int i)
{
return Method2(++i);
}
private StructType<int> Method2(int i)
{
return Method3(++i);
}
private StructType<int> Method3(int i)
{
return Method4(++i);
}
private StructType<int> Method4(int i)
{
var x = new StructType<int>();
x.List = new List<string>();
x.Instance1 = ++i;
return x;
}
}
public class TestClass8
{
public int Number = 5;
public class ClassType<T>
{
public T Instance1;
public T Instance2;
public T Instance3;
public T Instance4;
public T Instance5;
public T Instance6;
public T Instance7;
public List<string> List;
}
public int TestMethod()
{
var s = Method1(1);
return s.Instance1;
}
private ClassType<int> Method1(int i)
{
return Method2(++i);
}
private ClassType<int> Method2(int i)
{
return Method3(++i);
}
private ClassType<int> Method3(int i)
{
return Method4(++i);
}
private ClassType<int> Method4(int i)
{
var x = new ClassType<int>();
x.List = new List<string>();
x.Instance1 = ++i;
return x;
}
}
}
}
结构是实际值 - 它们可以为空但绝不为空
这是真的,但也请注意,从 .NET 2 开始,结构支持 Nullable 版本,并且 C# 提供了一些语法糖以使其更易于使用。
int? value = null;
value = 1;
(object)(default(int?)) == null
,这是任何其他值类型都无法做到的,因为这里不仅仅是糖。 Nullable<int>
的唯一糖是 int?
。
原始值类型或结构类型的每个变量或字段都包含该类型的唯一实例,包括其所有字段(公共和私有)。相比之下,引用类型的变量或字段可能为空,或者可能引用存储在其他地方的对象,也可能存在任何数量的其他引用。结构的字段将与该结构类型的变量或字段存储在同一位置,这些变量或字段可能位于堆栈上,也可能是另一个堆对象的一部分。
创建原始值类型的变量或字段将使用默认值创建它;创建结构类型的变量或字段将创建一个新实例,并以默认方式创建其中的所有字段。创建引用类型的新实例将首先以默认方式创建其中的所有字段,然后根据类型运行可选的附加代码。
将原始类型的一个变量或字段复制到另一个将复制该值。将结构类型的一个变量或字段复制到另一个会将前一个实例的所有字段(公共和私有)复制到后一个实例。将引用类型的一个变量或字段复制到另一个将导致后者引用与前者相同的实例(如果有)。
需要注意的是,在某些语言(如 C++)中,类型的语义行为与其存储方式无关,但对于 .NET 而言并非如此。如果一个类型实现了可变值语义,则将该类型的一个变量复制到另一个变量会将第一个变量的属性复制到另一个实例,由第二个实例引用,并使用第二个实例的成员对其进行变异将导致第二个实例被更改,但不是第一个。如果一个类型实现了可变引用语义,将一个变量复制到另一个变量并使用第二个变量的成员来改变对象将影响第一个变量引用的对象;具有不可变语义的类型不允许突变,因此复制是否创建新实例或创建对第一个实例的另一个引用在语义上并不重要。
在 .NET 中,值类型可以实现上述任何语义,前提是它们的所有字段都可以这样做。然而,引用类型只能实现可变引用语义或不可变语义;具有可变引用类型字段的值类型仅限于实现可变引用语义或奇怪的混合语义。