. NET中的struct和class有什么区别?

. NET中的struct和class有什么区别?

485407 次浏览

首先,结构是按值传递的,而不是按引用传递的。结构适合相对简单的数据结构,而类通过多态性和继承从架构的角度来看具有更大的灵活性。

其他人可能会给你比我更详细的信息,但是当我想要的结构很简单时,我会使用结构。

在. NET中,结构和类声明区分引用类型和值类型。

当您循环传递引用类型时,实际只存储了一个。访问实例的所有代码都在访问同一个。

当您传递一个值类型时,每个值类型都是一个副本。所有代码都在自己的副本上工作。

这可以用一个例子来说明:

struct MyStruct{string MyProperty { get; set; }}
void ChangeMyStruct(MyStruct input){input.MyProperty = "new value";}
...
// Create value typeMyStruct 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 typeMyClass testClass = new MyClass { MyProperty = "initial value" };
ChangeMyClass(testClass);
// Value of testClass.MyProperty is now "new value"// - the method changed the instance passed.

类可以什么都不是——引用可以指向一个null。

结构是实际值-它们可以为空但绝不能为空。因此,结构总是有一个没有参数的默认构造函数-它们需要一个“起始值”。

类的实例存储在托管堆上。所有“包含”实例的变量只是对堆上实例的引用。将对象传递给方法会导致传递引用的副本,而不是对象本身。

结构(技术上是值类型)存储在使用它们的任何地方,就像原始类型一样。内容可以随时由运行时复制,而无需调用自定义的复制构造函数。将值类型传递给方法涉及复制整个值,同样无需调用任何可自定义代码。

/CLI名称C++更好地区分了这一点:“ref class”是首先描述的类,“value class”是第二个描述的类。C#使用的关键字“class”和“struct”只是必须学习的东西。

结构是实际值-它们可以为空,但绝不能为空

这是真的,但是也请注意,从. NET 2结构开始支持Nullable版本,并且C#提供了一些语法糖以使其更易于使用。

int? value = null;value  = 1;

在. NET中,有两类类型,引用类型值类型

结构是值类型,类是引用类型

一般的区别是引用类型存在于堆中,而值类型存在于内联中,也就是说,无论它是您定义的变量或字段。

包含值类型的变量包含整个值类型值。对于结构,这意味着该变量包含整个结构及其所有字段。

包含引用类型的变量包含一个指针,或指向实际值所在内存中其他位置的参考

这有一个好处,首先:

  • 值类型总是包含一个值
  • 引用类型可以包含null引用,这意味着它们目前根本不引用任何东西

在内部,引用类型被实现为指针,并且知道变量赋值是如何工作的,还有其他行为模式:

  • 值类型变量的内容复制到另一个变量中,将整个内容复制到新变量中,使两者不同。换句话说,复制后,对一个的更改不会影响另一个
  • 引用类型变量的内容复制到另一个变量中,复制引用,这意味着您现在有两个引用指向相同的去别的地方实际数据存储。换句话说,复制后,更改一个引用中的数据似乎也会影响另一个引用,但这只是因为您实际上只是在两个地方查看相同的数据

当您声明变量或字段时,这两种类型有何不同:

  • 变量:值类型存在于堆栈中,引用类型作为指向堆内存中实际内存所在位置的指针存在于堆栈中(尽管注意Eric Lipperts文章系列:堆栈是一个实现细节)。
  • class/struct-field:值类型完全位于类型内部,引用类型位于类型内部,作为指向堆内存中实际内存所在位置的指针。

每个人的简短摘要:

仅类:

  • 支持继承
  • 是引用(指针)类型
  • 引用可以为空
  • 每个新实例有内存开销

仅结构:

  • 无法支持继承
  • 是值类型
  • 按值传递(如整数)
  • 不能有空引用(除非使用Nullable)
  • 每个新实例没有内存开销-除非“装箱”

类和结构:

  • 复合数据类型通常用于包含一些具有某种逻辑关系的变量吗
  • 可以包含方法和事件
  • 可以支持接口

结构vs类

结构是值类型,因此它存储在堆栈上,但类是引用类型,存储在堆上。

结构不支持继承和多态,但类支持两者。

默认情况下,所有结构成员都是公共的,但类成员默认是私有的。

由于结构是一种值类型,我们不能将null分配给struct对象,但类不是这样。

  1. 类中声明的事件的+=和-=访问通过锁(this)自动锁定,使它们成为线程安全的(静态事件锁定在类的类型上)。结构中声明的事件不会自动锁定其+=和-=访问。结构的锁(this)将不起作用,因为你只能锁定引用类型表达式。

  2. 创建结构实例不会导致垃圾回收机制(除非构造函数直接或间接创建引用类型实例),而创建引用类型实例会导致垃圾回收机制。

  3. 结构总是有一个内置的公共默认构造函数。

    class DefaultConstructor{static void Eg(){Direct     yes = new   Direct(); // Always compiles OKInDirect maybe = new InDirect(); // Compiles if constructor exists and is accessible//...}}

    这意味着结构总是可实例化的,而类可能不是,因为它的所有构造函数都可以是私有的。

    class NonInstantiable{private NonInstantiable() // OK{}}
    struct Direct{private Direct() // Compile-time error{}}
  4. A struct cannot have a destructor. A destructor is just an override of object.Finalize in disguise, and structs, being value types, are not subject to garbage collection.

    struct Direct{~Direct() {} // Compile-time error}class InDirect{~InDirect() {} // Compiles OK}
    And the CIL for ~Indirect() looks like this:
    .method family hidebysig virtual instance voidFinalize() cil managed{// ...} // end of method Indirect::Finalize
  5. A struct is implicitly sealed, a class isn't.
    A struct can't be abstract, a class can.
    A struct can't call : base() in its constructor whereas a class with no explicit base class can.
    A struct can't extend another class, a class can.
    A struct can't declare protected members (for example, fields, nested types) a class can.
    A struct can't declare abstract function members, an abstract class can.
    A struct can't declare virtual function members, a class can.
    A struct can't declare sealed function members, a class can.
    A struct can't declare override function members, a class can.
    The one exception to this rule is that a struct can override the virtual methods of System.Object, viz, Equals(), and GetHashCode(), and ToString().

基本值类型或结构类型的每个变量或字段都保存该类型的唯一实例,包括其所有字段(公共和私有)。相比之下,引用类型的变量或字段可能持有null,或者可能引用存储在其他地方的对象,对该对象也可能存在任意数量的其他引用。结构的字段将与该结构类型的变量或字段存储在同一位置,该变量或字段可能在堆栈上,也可能是的一部分另一个堆对象。

创建原始值类型的变量或字段将使用默认值创建它;创建结构类型的变量或字段将创建一个新实例,并以默认方式创建其中的所有字段。创建引用类型的新实例将首先以默认方式创建其中的所有字段,然后根据类型运行可选的附加代码。

将一个基本类型的变量或字段复制到另一个将复制该值。将一个结构类型的变量或字段复制到另一个将前一个实例的所有字段(公共和私有)复制到后一个实例。将一个引用类型的变量或字段复制到另一个将导致后者引用与前者相同的实例(如果有)。

重要的是要注意,在C++等某些语言中,类型的语义行为与它的存储方式无关,但对于. NET来说并非如此。如果一个类型实现了可变值语义学,将该类型的一个变量复制到另一个变量将第一个实例的属性复制到第二个实例引用的另一个实例,并使用第二个实例的成员进行突变,它将导致第二个实例被更改,而不是第一个实例。如果一个类型实现了可变引用语义学,将一个变量复制到另一个变量并使用第二个变量的成员来突变对象将影响第一个变量引用的对象;具有不可变语义学的类型不允许突变,因此复制是否创建新实例或创建对第一个实例的另一个引用在语义上并不重要。

在. NET中,值类型可以实现上述任何语义学,前提是它们的所有字段都可以这样做。然而,引用类型只能实现可变引用语义学或不可变语义学;具有可变引用类型字段的值类型仅限于实现可变引用语义学或奇怪的混合语义学。

除了其他答案中描述的所有差异之外:

  1. 结构不能有显式的无参数构造函数,而类可以
  2. 结构不能有析构函数,而类可以
  3. 结构不能继承来自另一个结构或类,而类可以从另一个类继承。(结构和类都可以从接口实现。)

如果您正在观看解释所有差异的视频,您可以查看第29部分-C#教程-C#中类和结构之间的区别

除了访问说明符的基本区别之外,上面提到的很少,我想添加一些主要的区别,包括上面提到的几个带有输出的代码示例,这将更清楚地了解参考和价值

结构:

  • 是值类型,不需要堆分配。
  • 内存分配不同,存储在堆栈中
  • 适用于小型数据结构
  • 影响性能,当我们将值传递给方法时,我们传递整个数据结构,所有都传递给堆栈。
  • 构造函数简单地返回结构值本身(通常位于堆栈上的临时位置),然后根据需要复制此值
  • 每个变量都有自己的数据副本,对一个变量的操作不可能影响另一个变量。
  • 不支持用户指定的继承,它们隐式继承自类型对象

班级:

  • 引用类型值
  • 存储在堆
  • 存储对动态分配对象的引用
  • 使用new运算符调用构造函数,但这不会在堆上分配内存
  • 多个变量可能对同一个对象有引用
  • 对一个变量的操作可能会影响另一个变量引用的对象

代码示例

    static void Main(string[] args){//StructmyStruct 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();
//ClassmyClass 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

内部结构方法结构对象的内部方法值为:20

结构对象的After方法调用值为:10

类对象的初始值为:10

内部类方法类对象的内部方法值为:20

类对象的后方法调用值为:20

在这里,您可以清楚地看到按值调用和按引用调用之间的区别。

除了其他答案之外,还有一个值得注意的基本区别,那就是数据如何存储在数组中,因为这会对性能产生重大影响。

  • 对于结构,数组包含结构的实例
  • 对于类,数组包含一个指向内存中其他位置的类实例的指针

一个数组的结构在内存中看起来像这样

[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}");}

如前所述:类是引用类型,而结构是具有所有后果的值类型。

框架设计指南建议在以下情况下使用结构而不是类:

  • 它的实例大小小于16字节
  • 它在逻辑上表示单个值,类似于原始类型(int、Double等)。
  • 它是不变的
  • 它不需要经常被装箱

从微软的在类和结构之间进行选择

作为经验法则,框架中的大多数类型应该是然而,在某些情况下值类型的特性使其更适合使用结构。

考虑一个struct而不是类:

  • 如果该类型的实例很小并且通常是短暂的,或者通常嵌入在其他对象中。

X避免结构。除非该类型具有以下所有特点:

  • 它在逻辑上表示单个值,类似于原始类型(int、Double等)。
  • 它的实例大小小于16字节。
  • 它是不可变的。(无法更改)
  • 它将不必经常被装箱。

为了使其完整,使用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) // trues1.Equals(c1) // falsec1.Equals(c2) // falsec1 == c2 // false

所以,结构适合类似数字的对象,比如点(保存x和y坐标)。类适合其他类。即使两个人有相同的名字、身高、体重…,他们仍然是两个人。

有一个有趣的“class vs struct”难题案例——当你需要从方法返回几个结果时的情况:选择使用哪个。如果你知道ValueTuple的故事——你就会知道ValueTuple(struct)被添加是因为它应该比Tuple(class)更有效。但是它在数字中意味着什么?两个测试:一个是有2个字段的struct/class,另一个是有8个字段的struct/class(在处理器滴答声方面,类应该比struct更有效,但当然GC负载也应该被考虑在内)。

附注:特定案例“sturct或类与集合”的另一个基准是: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=4Frequency=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 RyuJITClr    : .NET Framework 4.7 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.2115.0Core   : .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;}}}}

结构和类的区别:

  • 结构是值类型类是引用类型
  • 结构存储在堆栈上类存储在堆
  • 值类型在声明它们的内存中保存它们的值,但是引用类型保存对内存中对象的引用。
  • 值类型立即被销毁范围丢失后,而引用类型仅在范围丢失后销毁变量。该对象稍后由垃圾收集器销毁。
  • 当您将一个结构复制到另一个结构时,该结构的新副本被创建。修改后的结构不会影响其他结构。
  • 当你复制一个类到另一个类时,它只复制引用变量。
  • 两个引用变量都指向堆上的同一个对象。对一个变量所做的更改将影响另一个引用变量。
  • 结构不能有析构函数,但是类可以有析构函数。
  • 结构不能有显式的无参数构造函数,而类可以。结构不支持继承,但类支持。两者支持从接口继承。
  • 结构是密封型的
结构
类型值类型引用类型
哪里堆栈上/内联在包含类型中堆上
解除分配堆栈展开/包含类型被释放垃圾回收
数组内联,元素是值类型的实际实例出格的是,元素只是对驻留在堆上的引用类型实例的引用
Al-Del成本廉价分配-解除分配昂贵的分配-解除分配
内存使用当转换为引用类型或它们实现的接口之一时装箱,
当转换回值类型
时取消装箱(负面影响,因为盒子是在堆上分配的对象并且是垃圾收集的)
不要拆箱
作业复制整个数据复制参考资料
更改为实例不会影响它的任何副本影响指向实例的所有引用
可变性应该是不可变的可变
人口在某些情况下框架中的大多数类型应该是类
一生短暂的长寿
析构函数不能有可以有
继承仅从接口全力支持
多态
密封的当有sealed关键字(C#)或Sealed属性(F#)时
构造函数不能有显式的无参数构造函数任何构造函数
空赋值当用可空问号标记时是(在C#8+和F#5+1中用可空问号标记时)
摘要当有abstract关键字(C#)或AbstractClass属性(F#)时
成员访问修饰符publicprivateinternalpublicprotectedinternalprotected internalprivate protected

1在F#中不鼓励使用null,请改用选项类型。

我♥可视化,在这里我创建了一个可视化来显示structs之间的基本差异。输入图片描述


和文本表示只是以防万一;)

+--------------------------------------------------+------+----------------------------------------------+|                      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.                              |+--------------------------------------------------+------+----------------------------------------------+

更多信息请看下面: