.NET 中的结构和类有什么区别?
-
08-06-2019 - |
题
.NET 中的结构和类有什么区别?
解决方案
在 .NET 中,有两类类型, 参考类型 和 值类型.
结构是 值类型 和课程是 参考类型.
一般的区别是 参考类型 存在于堆上,并且 值类型 内嵌,即无论变量或字段定义在何处。
一个变量包含 值类型 包含整个 值类型 价值。对于结构体,这意味着变量包含整个结构体及其所有字段。
一个变量包含 参考类型 包含一个指针,或者一个 参考 到内存中实际值所在的其他位置。
首先,这有一个好处:
- 值类型 总是包含一个值
- 参考类型 可以包含一个 无效的-引用,这意味着它们目前根本不引用任何内容
在内部, 参考类型s 被实现为指针,并且知道这一点,并且知道变量赋值如何工作,还有其他行为模式:
- 复制a的内容 值类型 变量到另一个变量,将整个内容复制到新变量中,使两个变量不同。换句话说,复制后,其中一个的更改不会影响另一个
- 复制a的内容 参考类型 变量到另一个变量,复制引用,这意味着你现在有两个对同一个变量的引用 别的地方 实际数据的存储。换句话说,复制后,更改一个引用中的数据似乎也会影响另一个引用,但这只是因为您实际上只是在两个地方查看相同的数据
当您声明变量或字段时,这两种类型的区别如下:
- 多变的: 值类型 存在于栈中, 参考类型 作为指向堆内存中实际内存所在的某个位置的指针存在于堆栈中(尽管请注意 埃里克·利珀茨文章系列:堆栈是一个实现细节.)
- 类/结构体字段: 值类型 完全存在于类型内部, 参考类型 作为指向堆内存中实际内存所在的某个位置的指针存在于类型内部。
其他提示
每个的简短摘要:
仅课程:
- 可以支持继承
- 是引用(指针)类型
- 引用可以为空
- 每个新实例都有内存开销
仅结构:
- 不支持继承
- 是值类型
- 按值传递(如整数)
- 不能有空引用(除非使用 Nullable)
- 每个新实例没有内存开销 - 除非“装箱”
类和结构:
- 复合数据类型通常用于包含一些具有某种逻辑关系的变量
- 可以包含方法和事件
- 可支持接口
在 .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.
类可以什么都没有——引用可以指向空值。
结构是实际值 - 它们可以为空,但决不能为空。因此,结构体总是有一个不带参数的默认构造函数——它们需要一个“起始值”。
来自微软的 在类和结构之间进行选择 ...
根据经验,框架中的大多数类型都应该是类。但是,在某些情况下,值类型的特征使使用结构更合适。
✓ 考虑一个结构体 而不是一个类:
- 如果类型的实例很小并且通常寿命很短或者通常嵌入到其他对象中。
X 避免使用结构体 除非类型有 全部 以下特征:
- 它在逻辑上表示单个值,类似于基本类型(int、double 等)。
- 它的实例大小小于 16 字节。
- 它是不可改变的。 (无法更改)
- 它不必经常装箱。
除了其他答案中描述的所有差异之外:
- 结构体 不能有显式的无参数构造函数 而一个类可以
- 结构体 不能有析构函数, ,而一个类可以
- 结构体 不能继承 从另一个结构或类,而一个类可以从另一个类继承。(结构和类都可以从接口实现。)
如果您想观看解释所有差异的视频,您可以查看 第 29 部分 - C# 教程 - C# 中类和结构之间的区别.
类的实例存储在托管堆上。“包含”实例的所有变量都只是对堆上实例的引用。将对象传递给方法会导致传递引用的副本,而不是对象本身。
结构(从技术上讲,值类型)存储在任何使用的地方,就像原始类型一样。运行时可以随时复制内容,而无需调用自定义的复制构造函数。将值类型传递给方法涉及复制整个值,同样不调用任何可自定义的代码。
C++/CLI 名称可以更好地区分:“引用类”是第一个描述的类,“值类”是第二个描述的类。C# 使用的关键字“class”和“struct”是必须学习的东西。
结构体和类之间的区别:
- 结构体是值类型 然而 类是引用类型.
- 结构体存储在栈中 然而 课程存储在堆上.
- 值类型将其值保持在声明的内存中,但参考类型具有对对象内存的引用。
- 值类型立即销毁 范围丢失后,而参考类型仅在范围丢失后变量破坏。该物体后来被垃圾收集器摧毁。
- 当您将struct复制到另一个结构中时,该结构的新副本被修改为一个结构,不会影响另一个结构的值。
- 当您将类复制到另一个类中时,它仅复制参考变量。
- 两个引用变量都指向堆上的同一个对象。对一个变量的更改将影响另一个参考变量。
- 结构体不能有析构函数, ,但类可以有析构函数。
- 结构不能有显式的无参数构造函数 班级可以结构构建不支持继承,但是类可以。两者都支持从界面继承。
- 结构为密封型.
结构与类别
结构体是值类型,因此存储在堆栈中,而类是引用类型,存储在堆中。
结构不支持继承和多态性,但类支持两者。
默认情况下,所有结构成员都是公共的,但类成员默认情况下是私有的。
由于结构体是值类型,因此我们不能将 null 分配给结构体对象,但类则不然。
为了使其完整,使用时还有另一个区别 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个人。
为了补充其他答案,有一个值得注意的根本区别,那就是它在内存中的存储方式。这会对阵列的性能产生重大影响。结构是值类型,因此它们将值存储在它们指向的内存区域中,类是引用类型,因此它们引用它们指向的内存区域中的类,实际值存储在其他地方。
- 对于结构体,内存在包含的类中分配用于存储数据。
- 对于一个类,包含类将仅包含指向不同内存区域中的新类的指针。
对于数组也是如此,因此结构体数组在内存中看起来像这样
[struct][struct][struct][struct][struct][struct][struct][struct]
类数组看起来像这样
[pointer][pointer][pointer][pointer][pointer][pointer][pointer][pointer]
您感兴趣的实际值实际上并不存储在数组中,而是存储在内存中的其他位置。
对于绝大多数应用程序来说,这种差异并不重要,但是,在高性能代码中,这将影响内存中数据的局部性,并对 CPU 缓存的性能产生很大影响。当您可以/应该使用结构时使用类将大大增加 CPU 上缓存未命中的数量。
现代 CPU 做的最慢的事情不是处理数字,而是从内存中获取数据,L1 缓存命中比从 RAM 读取数据快很多倍。
好吧,对于初学者来说,结构体是按值传递的,而不是按引用传递的。结构适用于相对简单的数据结构,而从体系结构的角度来看,类通过多态性和继承具有更大的灵活性。
其他人可能可以为您提供比我更多的细节,但是当我想要的结构很简单时,我会使用结构。
除了访问说明符的基本区别和上面提到的一些区别之外,我想添加一些主要区别,包括上面提到的一些区别和带有输出的代码示例,这将给出更清晰的参考和价值
结构:
- 是值类型,不需要堆分配。
- 内存分配不同,存储在栈中
- 对于小型数据结构很有用
- 影响性能,当我们向方法传递值时,我们传递整个数据结构并且全部传递到堆栈。
- 构造函数只是返回结构值本身(通常位于堆栈上的临时位置),然后根据需要复制该值
- 每个变量都有自己的数据副本,对一个变量的操作不可能影响另一个变量。
- 不支持用户指定的继承,它们隐式继承自类型object
班级:
- 参考类型值
- 存储在堆中
- 存储对动态分配对象的引用
- 使用 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;
}
}
输出
Struct 对象的初始值为:10
内部结构方法内部方法的结构对象的值是:20
Struct Object 的方法调用后值为:10
类对象的初始值为:10
类的内部方法内部方法值的class对象的值是:20
类对象的方法调用后值为:20
在这里您可以清楚地看到按值调用和按引用调用之间的区别。
类中声明的事件的 += 和 -= 访问通过锁(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 { } }
结构体不能有析构函数。析构函数只是对 object.Finalize 的变相覆盖,而结构作为值类型,不受垃圾回收的影响。
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()。
就像之前提到的:类是引用类型,而结构是具有所有后果的值类型。
作为规则框架设计指南建议在以下情况下使用结构而不是类:
- 它的实例大小小于 16 字节
- 它在逻辑上表示单个值,类似于基本类型(int、double 等)
- 它是不可变的
- 不必经常装箱
+-----------------------+------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------+
| | 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 |
| Access Modifiers | public, private, internal | public, protected, internal, protected internal, private protected |
+-----------------------+------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------+
结构是实际值 - 它们可以为空,但决不能为空
这是事实,但还要注意,从 .NET 2 开始,结构支持 Nullable 版本,并且 C# 提供了一些语法糖以使其更易于使用。
int? value = null;
value = 1;
有一个有趣的“类与结构”难题案例 - 当您需要从方法返回多个结果时的情况:选择使用哪个。如果您了解 ValueTuple 的故事 - 您就会知道添加 ValueTuple(结构)是因为它应该比 Tuple(类)更有效。但这在数字上意味着什么呢?两个测试:一种是具有 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;
}
}
}
}
原始值类型或结构类型的每个变量或字段都保存该类型的唯一实例,包括其所有字段(公共和私有)。相比之下,引用类型的变量或字段可以保持为空,或者可以引用存储在别处的对象,该对象还可以存在任意数量的其他引用。结构体的字段将存储在与该结构体类型的变量或字段相同的位置,该位置可能位于堆栈上,也可能位于 部分 另一个堆对象。
创建原始值类型的变量或字段将使用默认值创建它;创建结构类型的变量或字段将创建一个新实例,并以默认方式创建其中的所有字段。创建一个新的 实例 引用类型的引用类型将首先以默认方式创建其中的所有字段,然后根据类型运行可选的附加代码。
将基本类型的一个变量或字段复制到另一个变量或字段将会复制该值。将结构类型的一个变量或字段复制到另一个变量或字段会将前一个实例的所有字段(公共和私有)复制到后一个实例。将引用类型的一个变量或字段复制到另一个变量或字段将导致后者引用与前者相同的实例(如果有)。
需要注意的是,在 C++ 等某些语言中,类型的语义行为与其存储方式无关,但 .NET 并非如此。如果某个类型实现可变值语义,则将该类型的一个变量复制到另一个变量会将第一个变量的属性复制到另一个实例,由第二个实例引用,并且使用第二个实例的成员来改变它将导致第二个实例发生更改,但不是第一个。如果一个类型实现了可变引用语义,则将一个变量复制到另一个变量并使用第二个变量的成员来改变对象将影响第一个变量引用的对象;具有不可变语义的类型不允许突变,因此复制是否创建新实例或创建对第一个实例的另一个引用在语义上并不重要。
在 .NET 中,值类型可以实现上述任何语义,前提是它们的所有字段都可以这样做。然而,引用类型只能实现可变引用语义或不可变语义;具有可变引用类型字段的值类型仅限于实现可变引用语义或奇怪的混合语义。