Answers:
在.NET中,有两类类型,引用类型和值类型。
结构是值类型,类是引用类型。
通常的区别是,引用类型驻留在堆上,而值类型驻留在内联中,也就是说,无论它在何处定义了变量或字段。
包含值类型的变量包含整个值类型值。对于结构,这意味着变量包含整个结构及其所有字段。
包含引用类型的变量包含指针或对实际值所在的内存中其他位置的引用。
首先有一个好处:
在内部,引用类型 s被实现为指针,并且知道了这一点,并且知道了变量赋值的工作原理,还有其他行为模式:
声明变量或字段时,以下是两种类型的区别:
每个的简短摘要:
仅课程:
仅结构:
类和结构:
c# struct memory overhead
并找到了汉斯·帕桑特(Hans Passant)的答案,说不是,也不是。那么,什么做你的意思是?
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.
类不能为空-引用可以指向null。
结构是实际值-它们可以为空,但不能为null。因此,结构始终具有不带参数的默认构造函数-它们需要一个“起始值”。
结构和类之间的区别:
除了其他答案中描述的所有差异之外:
如果您正在观看解释所有差异的视频,则可以查看第29部分-C#教程-C#中的类和结构之间的差异。
+------------------------+------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------+
| | 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 |
| Aldel Cost | Cheap allocation-deallocation | Expensive allocation-deallocation |
| Memory usage | Boxed when cast to a reference type or one of the interfaces they implement, | No boxing-unboxing |
| | Unboxed when cast back to value type | |
| | (Negative impact because boxes are objects that are allocated on the heap and are garbage-collected) | |
| 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 |
| 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+) |
| Abstract | No | When have abstract keyword |
| Member Access Modifiers| public, private, internal | public, protected, internal, protected internal, private protected |
+------------------------+------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------+
结构与类别
结构是值类型,因此它存储在堆栈中,而类是引用类型,并存储在堆中。
结构不支持继承和多态,但是类同时支持。
默认情况下,所有结构成员都是公共的,但类成员默认情况下是私有的。
由于结构是值类型,因此我们不能将NULL赋给struct对象,但类并非如此。
除了其他答案外,还有一个基本区别值得注意,那就是数据在数组内的存储方式,因为这会对性能产生重大影响。
所以内存中的结构数组看起来像这样
[struct][struct][struct][struct][struct][struct][struct][struct]
而一组类看起来像这样
[pointer][pointer][pointer][pointer][pointer][pointer][pointer][pointer]
对于一组类,您感兴趣的值不会存储在数组中,而是会存储在内存中的其他位置。
对于绝大多数应用程序而言,这种区别并不重要,但是,在高性能代码中,这将影响内存中数据的位置,并严重影响CPU缓存的性能。在可以/应该使用结构的情况下使用类将大大增加CPU上的高速缓存未命中数。
现代CPU最慢的操作不是处理数字,而是从内存中获取数据,一级缓存命中率比从RAM中读取数据快许多倍。
这是您可以测试的一些代码。在我的机器上,遍历类数组花费的时间比struct数组长约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坐标)。而且班级适合其他人。即使2个人的名字,身高,体重...相同,他们仍然是2个人。
除了访问说明符的基本区别之外,我还想补充一些主要区别,包括上面提到的少量区别和带有输出的代码示例,这将使参考和值的概念更加清晰。
结构:
类:
代码样例
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对象的初始值为:10
内部结构方法内部结构方法的内部方法值是:20
结构对象的Method调用值之后:10
类对象的初始值为:10
内部类方法内部类方法的内部方法值是:20
在Class Object的Method调用值之后为:20
在这里,您可以清楚地看到按值调用和按引用调用之间的区别。
在类中声明的事件具有通过锁自动锁定的+ =和-=访问权限,从而使它们成为线程安全的(静态事件在类的类型上锁定)。在结构中声明的事件没有自动锁定其+ =和-=访问。由于只能锁定引用类型表达式,因此结构的锁(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,viz,Equals()和GetHashCode()和ToString()的虚拟方法。
Object
,将举行一个参考结构的盒装拷贝。
有一种有趣的“类与结构”难题的情况-当您需要从该方法返回多个结果时的情况:选择要使用的结果。如果您了解ValueTuple的故事-您知道添加了ValueTuple(结构)是因为它比Tuple(类)更有效。但是,这在数字上是什么意思呢?两项测试:一种是具有2个字段的struct / class,另一种是具有8个字段的struct / class(维数大于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;
}
}
}
}
结构是实际值-它们可以为空,但不能为null
的确如此,但是还请注意,从.NET 2开始,结构支持Nullable版本,并且C#提供了一些语法糖以使其易于使用。
int? value = null;
value = 1;
(object)(default(int?)) == null
您不能使用任何其他值类型,因为这里不仅有糖。唯一的糖是int?
对Nullable<int>
。
基本值类型或结构类型的每个变量或字段都具有该类型的唯一实例,包括其所有字段(公共字段和私有字段)。相比之下,引用类型的变量或字段可以为null,或者可以引用存储在其他位置的对象,也可以存在许多其他引用。结构的字段将与该结构类型的变量或字段存储在同一位置,该变量或字段可以在堆栈上,也可以是另一个堆对象的一部分。
创建原始值类型的变量或字段将使用默认值创建它;创建结构类型的变量或字段将创建一个新实例,并以默认方式在其中创建所有字段。创建引用类型的新实例将首先以默认方式在其中创建所有字段,然后根据类型运行可选的附加代码。
将一个基本类型的变量或字段复制到另一个将复制该值。将一个变量或结构类型的字段复制到另一个变量或字段将把前一个实例的所有字段(公共和私有)都复制到后一个实例。将一个变量或引用类型的字段复制到另一个变量或字段将导致后者引用与前者相同的实例(如果有)。
需要特别注意的是,在某些语言(如C ++)中,类型的语义行为与存储方式无关,但是.NET并非如此。如果某个类型实现了可变值语义,则将该类型的一个变量复制到另一个变量中,会将第一个实例的属性复制到另一个实例,该实例由第二个实例引用,并使用第二个实例的成员进行变异,这将导致第二个实例被更改,但不是第一个。如果一个类型实现了可变的引用语义,则将一个变量复制到另一个变量并使用第二个变量的成员来对该对象进行变异将影响第一个变量所引用的对象;具有不可变语义的类型不允许突变,因此在语义上复制是创建新实例还是创建对第一个实例的另一个引用都无关紧要。
在.NET中,值类型可以实现上面的任何语义,条件是它们的所有字段都可以照此执行。但是,引用类型只能实现可变的引用语义或不可变语义。具有可变引用类型字段的值类型仅限于实现可变引用语义或奇怪的混合语义。