【发布时间】:2020-11-16 15:04:21
【问题描述】:
.NET 中的结构和类有什么区别?
【问题讨论】:
-
好问题,对我也有帮助。
标签: .net class struct value-type reference-type
.NET 中的结构和类有什么区别?
【问题讨论】:
标签: .net class struct value-type reference-type
结构体和类的区别:
【讨论】:
我♥可视化,在这里我创建了一个来显示structs和classes之间的基本区别。
还有文本表示以防万一;)
+--------------------------------------------------+------+----------------------------------------------+
| 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. |
+--------------------------------------------------+------+----------------------------------------------+
更多信息请看下面:
【讨论】:
| Struct | Class | |
|---|---|---|
| Type | Value-type | Reference-type |
| Where | On stack / Inline in containing type | On Heap |
| Deallocation | Stack unwinds / containing type gets deallocated | Garbage Collected |
| Arrays | Inline, elements are the actual instances of the value type | Out of line, elements are just references to instances of the reference type residing on the heap |
| Al-Del Cost | Cheap allocation-deallocation | Expensive allocation-deallocation |
| Memory usage | Boxed when cast to a reference type or one of the interfaces they implement, Unboxed when cast back to value type (Negative impact because boxes are objects that are allocated on the heap and are garbage-collected) |
No boxing-unboxing |
| Assignments | Copy entire data | Copy the reference |
| Change to an instance | Does not affect any of its copies | Affect all references pointing to the instance |
| Mutability | Should be immutable | Mutable |
| Population | In some situations | Majority of types in a framework should be classes |
| Lifetime | Short-lived | Long-lived |
| Destructor | Cannot have | Can have |
| Inheritance | Only from an interface | Full support |
| Polymorphism | No | Yes |
| Sealed | Yes | When have sealed keyword (C#), or Sealed attribute (F#) |
| Constructor | Can not have explicit parameterless constructors | Any constructor |
| Null-assignments | When marked with nullable question mark | Yes (When marked with nullable question mark in C# 8+ and F# 5+ 1) |
| Abstract | No | When have abstract keyword (C#), or AbstractClass attribute (F#) |
| Member Access Modifiers |
public, private, internal
|
public, protected, internal, protected internal, private protected
|
1 在 F# 中不鼓励使用 null,而是使用 Option 类型。
【讨论】:
除了其他答案之外,还有一个值得注意的根本区别,那就是数据如何存储在数组中,因为这会对性能产生重大影响。
所以结构数组在内存中看起来像这样
[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}");
}
【讨论】:
除了其他答案中描述的所有差异:
如果您正在观看解释所有差异的视频,您可以查看 Part 29 - C# Tutorial - Difference between classes and structs in C#。
【讨论】:
有一个有趣的“类与结构”难题的案例——当您需要从方法返回多个结果时:选择使用哪个。如果你知道 ValueTuple 的故事 - 你知道 ValueTuple (struct) 被添加是因为它应该比 Tuple (class) 更有效。但它在数字上意味着什么?两个测试:一个是具有 2 个字段的结构/类,另一个是具有 8 个字段的结构/类(维度大于 4 - 就处理器滴答而言,类应该比结构更有效,但当然也应该考虑 GC 负载)。
附:特定案例“带有集合的结构或类”的另一个基准是: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中,有两类类型,引用类型和值类型。
结构是值类型,类是引用类型。
一般的区别是引用类型存在于堆上,而值类型存在于内联,也就是说,无论它是你定义的变量或字段。
包含值类型的变量包含整个值类型值。对于结构体,这意味着变量包含整个结构体及其所有字段。
一个包含引用类型的变量包含一个指针,或一个引用到内存中实际值所在的其他地方。
这有一个好处,首先:
在内部,引用类型被实现为指针,知道这一点,知道变量赋值是如何工作的,还有其他行为模式:
当您声明变量或字段时,这两种类型的区别如下:
【讨论】:
为了完整起见,使用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 坐标)。课程适合其他人。即使两个人的名字、身高、体重……相同,他们仍然是两个人。
【讨论】:
来自微软的Choosing Between Class and Struct ...
根据经验,框架中的大多数类型应该是 类。但是,在某些情况下, 值类型的特征使其更适合使用 结构。
✓ 考虑一个结构而不是一个类:
- 如果该类型的实例很小且通常寿命很短,或者通常嵌入在其他对象中。
X 避免使用结构体,除非该类型具有以下所有 特点:
- 它在逻辑上表示单个值,类似于原始类型(int、double 等)。
- 它的实例大小小于 16 个字节。
- 它是不可变的。 (无法更改)
- 不必经常装箱。
【讨论】:
如前所述:类是引用类型,而结构是具有所有后果的值类型。
作为规则框架设计指南建议在以下情况下使用结构而不是类:
【讨论】:
原始值类型或结构类型的每个变量或字段都包含该类型的唯一实例,包括其所有字段(公共和私有)。相比之下,引用类型的变量或字段可能为空,或者可能引用存储在其他地方的对象,也可能存在任何数量的其他引用。结构的字段将存储在与该结构类型的变量或字段相同的位置,它们可能在堆栈上,也可能是另一个堆对象的一部分。
创建原始值类型的变量或字段将使用默认值创建它;创建结构类型的变量或字段将创建一个新实例,并以默认方式创建其中的所有字段。创建引用类型的新实例首先会以默认方式在其中创建所有字段,然后根据类型运行可选的附加代码。
将一个原始类型的变量或字段复制到另一个变量或字段将复制该值。将结构类型的一个变量或字段复制到另一个会将前一个实例的所有字段(公共和私有)复制到后一个实例。将一个引用类型的变量或字段复制到另一个变量或字段将导致后者引用与前者相同的实例(如果有)。
请务必注意,在 C++ 等某些语言中,类型的语义行为与其存储方式无关,但对于 .NET 则不然。如果一个类型实现了可变值语义,则将该类型的一个变量复制到另一个变量会将第一个变量的属性复制到另一个实例,由第二个实例引用,并使用第二个实例的成员对其进行变异将导致第二个实例被更改,但不是第一个。如果一个类型实现了可变引用语义,将一个变量复制到另一个变量并使用第二个变量的成员来改变对象将影响第一个变量引用的对象;具有不可变语义的类型不允许突变,因此复制是否创建新实例或创建对第一个实例的另一个引用在语义上并不重要。
在 .NET 中,值类型可以实现上述任何语义,前提是它们的所有字段都可以这样做。然而,引用类型只能实现可变引用语义或不可变语义;具有可变引用类型字段的值类型仅限于实现可变引用语义或奇怪的混合语义。
【讨论】:
在类中声明的事件通过 lock(this) 自动锁定其 += 和 -= 访问权限,以使其线程安全(静态事件锁定在类的类型上)。在结构中声明的事件不会自动锁定其 += 和 -= 访问权限。结构的 lock(this) 不起作用,因为您只能锁定引用类型表达式。
创建结构实例不会导致垃圾回收(除非构造函数直接或间接创建引用类型实例),而创建引用类型实例会导致垃圾回收。
结构总是有一个内置的公共默认构造函数。
class DefaultConstructor
{
static void Eg()
{
Direct yes = new Direct(); // Always compiles OK
InDirect maybe = new InDirect(); // Compiles if constructor exists and is accessible
//...
}
}
这意味着结构总是可实例化的,而类可能不是,因为它的所有构造函数都可能是私有的。
class NonInstantiable
{
private NonInstantiable() // OK
{
}
}
struct Direct
{
private Direct() // Compile-time error
{
}
}
结构不能有析构函数。析构函数只是对象的覆盖。变相的终结,而结构作为值类型,不受垃圾回收的影响。
struct Direct
{
~Direct() {} // Compile-time error
}
class InDirect
{
~InDirect() {} // Compiles OK
}
And the CIL for ~Indirect() looks like this:
.method family hidebysig virtual instance void
Finalize() cil managed
{
// ...
} // end of method Indirect::Finalize
结构是隐式密封的,类不是。
结构不能是抽象的,类可以。
结构不能在其构造函数中调用 : base() 而没有显式基类的类可以。
一个结构不能扩展另一个类,一个类可以。
结构不能声明类可以声明的受保护成员(例如,字段、嵌套类型)。
结构不能声明抽象函数成员,抽象类可以。
结构不能声明虚函数成员,类可以。
结构不能声明密封函数成员,类可以。
结构不能声明覆盖函数成员,类可以。
此规则的一个例外是结构可以覆盖 System.Object 的虚拟方法,即 Equals()、GetHashCode() 和 ToString()。
【讨论】:
Object 的初始为空隐藏字段,它将包含对该结构的盒装副本的引用。跨度>
结构与类
结构是值类型,所以存储在栈中,而类是引用类型,存储在堆中。
结构不支持继承和多态,但类支持两者。
默认情况下,所有结构成员都是公共的,但类成员默认情况下是私有的。
由于结构体是一种值类型,我们不能将null赋给结构体对象,但对于类则不然。
【讨论】:
在 .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.
类可以什么都不是——引用可以指向一个空值。
结构是实际值——它们可以是空的,但不能为空。由于这个原因,结构总是有一个没有参数的默认构造函数——它们需要一个“起始值”。
【讨论】:
除了访问说明符的基本区别和上面提到的很少之外,我想补充一些主要的区别,包括上面提到的几个与输出的代码示例,这将使参考和价值更加清晰
结构:
类:
代码示例
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;
}
}
输出
Struct Object的初始值为:10
内部结构方法 Struct Object 的内部 Method 值为:20
Struct Object的方法调用后值为:10
类对象的初始值为:10
内部类方法 Class Object的Inside Method值为:20
Class Object的方法调用后值为:20
在这里你可以清楚地看到按值调用和按引用调用的区别。
【讨论】:
每个的简短摘要:
仅限类:
仅限结构:
类和结构:
【讨论】:
c# struct memory overhead,发现 Hans Passant 的 this answer 说不,也不是这样。那么你的意思是做什么?
class 的实例是托管内存(已处理由垃圾收集器),而 struct 的实例不是。
结构是实际值 - 它们可以为空,但不能为空
确实如此,但请注意,从 .NET 2 开始,结构支持 Nullable 版本,并且 C# 提供了一些语法糖以使其更易于使用。
int? value = null;
value = 1;
【讨论】:
(object)(default(int?)) == null,您无法使用任何其他值类型,因为这里不仅仅是糖。唯一的糖是int? for Nullable<int>。
类的实例存储在托管堆上。所有“包含”实例的变量都只是对堆上实例的引用。将对象传递给方法会导致传递引用的副本,而不是对象本身。
结构(技术上是值类型)存储在任何使用它们的地方,就像原始类型一样。运行时可以随时复制内容,而无需调用自定义的复制构造函数。将值类型传递给方法涉及复制整个值,同样无需调用任何可自定义的代码。
C++/CLI 名称可以更好地区分:“ref class”是第一个描述的类,“value class”是第二个描述的类。 C# 使用的关键字“class”和“struct”只是必须学习的东西。
【讨论】:
嗯,对于初学者来说,结构是通过值而不是通过引用传递的。结构适用于相对简单的数据结构,而从架构的角度来看,通过多态性和继承,类具有更大的灵活性。
其他人可能会给你比我更多的细节,但是当我想要的结构很简单时,我会使用结构。
【讨论】: