.NET中class和struct的区别

本文探讨了C#中Class与Struct的区别与联系,重点分析了二者在内存分配、使用特性及适用场景等方面的不同,通过具体示例帮助理解。
1、引言

  提起class和struct,我们首先的感觉是语法几乎相同,待遇却天壤之别。历史将接力棒由面向过程编程传到面向对象编程,class和struct也背负着各自的命运前行。在我认为,struct英雄迟暮,class天下独行,最本质的区别是class是引用类型,而struct是值类型,它们在内存中的分配情况有所区别。由此产生的一系列差异性,本文将做以全面讨论。

  2、基本概念

  2.1、什么是class?

  class(类)是面向对象编程的基本概念,是一种自定义数据结构类型,通常包含字段、属性、方法、属性、构造函数、索引器、操作符等。因为是基本的概念,所以不必在此详细描述,读者可以查询相关概念了解。我们重点强调的是.NET中,所有的类都最终继承自System.Object类,因此是一种引用类型,也就是说,new一个类的实例时,对象保存了该实例实际数据的引用地址,而对象的值保存在托管堆(managed heap)中。

  2.2、什么是struct?

  struct(结构)是一种值类型,用于将一组相关的信息变量组织为一个单一的变量实体 。所有的结构都继承自System.ValueType类,因此是一种值类型,也就是说,struct实例分配在线程的堆栈(stack)上,它本身存储了值,而不包含指向该值的指针。所以在使用struct时,我们可以将其当作int、char这样的基本类型类对待。

  

  图1

  3、相同点,不同点

  相同点:语法类似。

  不同点:

  ◆class是引用类型,继承自System.Object类;struct是值类型,继承自System.ValueType类,因此不具多态性。但是注意,System.ValueType是个引用类型。

  ◆从职能观点来看,class表现为行为;而struct常用于存储数据。

  ◆class支持继承,可以继承自类和接口;而struct没有继承性,struct不能从class继承,也不能作为class的基类,但struct支持接口继承。

◆class可以声明无参构造函数,可以声明析构函数;而struct只能声明带参数构造函数,且不能声明析构函数。因此,struct没有自定义的默认无参构造函数,默认无参构造器只是简单地把所有值初始化为它们的0等价值。

  ◆实例化时,class要使用new关键字;而struct可以不使用new关键字,struct在声明时就进行了初始化过程,所有的成员变量均默认为0或null。

  ◆class可以实抽象类(abstract),可以声明抽象函数;而struct为抽象,也不能声明抽象函数。

  ◆class可以声明protected成员、virtual成员、sealed成员和override成员;而struct不可以,但是值得注意的是,struct可以重载System.Object的3个虚方法,Equals()、ToString()和GetHashTable()。

  ◆class的对象复制分为浅拷贝和深拷贝,必须经过特别的方法来完成复制;而struct创建的对象复制简单,可以直接以等号连接即可。

  ◆class实例由垃圾回收机制来保证内存的回收处理;而struct变量使用完后立即自动解除内存分配。

  ◆作为参数传递时,class变量是以按址方式传递;而struct变量是以按值方式传递的。

  我们可以简单的理解,class是一个可以动的机器,有行为,有多态,有继承;而struct就是个零件箱,组合了不同结构的零件。其实,class和struct最本质的区别就在于class是引用类型,内存分配于托管堆;而struct是值类型,内存分配于线程的堆栈上。由此差异,导致了上述所有的不同点,所以只有深刻的理解内存分配的相关内容,才能更好的驾驭。本系列将再以后的内容中,将引用类型和值类型做以深入的比较和探讨,敬请关注。当然正如本文标题描述的一样,使用class基本可以替代struct的任何场合,class后来居上。虽然在某些方面struct有性能方面的优势,但是在面向对象编程里,基本是class横行的天下。

  那么,有人不免会提出,既然class几乎可以完全替代struct来实现所有的功能,那么struct还有存在的必要吗?答案是,至少在以下情况下,鉴于性能上的考虑,我们应该考虑使用struct来代替class:

  ◆实现一个主要用于存储数据的结构时,可以考虑struct。

  ◆struct变量占有堆栈的空间,因此只适用于数据量相对小的场合。

  ◆结构数组具有更高的效率。

  ◆提供某些和非托管代码通信的兼容性。

  所有这些是struct有一席之地的理由,当然也许还有其他的更多说法,只是我不知道罢了。

  4、经典示例

  4.1、小菜一碟

  下面以示例为说明,来阐述本文的基本规则,详细见注释内容。

  (1)定义接口

  

      interface IPerson

  {

  void GetSex();

  }

(2)定义类

  

      public class Person

  {

  public Person()

  {

  }

  public Person(string name, int age)

  {

  _name = name;

  _age = age;}

  private string _name;

  public string Name

  {

  get { return _name; }

  set { _name = value; }

  }

  private int _age;

  public int Age

  {

  get { return _age; }

  set { _age = value; }

  }

  }

(3)定义结构

  //可以继承自接口,不可继承类或结构

  

      struct Family: IPerson

  {

  public string name;

  public int age;

  public bool sex;

  public string country;

  public Person person;

  //不可以包含显式的无参构造函数和析构函数

  public Family(string name, int age, bool sex, string country, Person person)

  {

  this.name = name;

  this.age = age;

  this.sex = sex;

  this.country = country;

  this.person = person;

  }

  //不可以实现protected、virtual、sealed和override成员

  public void GetSex()

  {

  if (sex)

  Console.WriteLine(person.Name + " is a boy.");

  else

  Console.WriteLine(person.Name + " is a girl.");

  }

  public void ShowPerson()

  {

  Console.WriteLine("This is {0} from {1}", new Person(name, 22).Name, country);

  }

  //可以重载ToString虚方法

  public override string ToString()

  {

  return String.Format("{0} is {1}, {2} from {3}", person.Name, age, sex ? "Boy" : "Girl", country);

  }

  }

(4)测试结构和类

  

  图2

  猜猜运行结果如何,可以顺便检查检查对这个概念的认识。

  4.2、.NET研究

  在.NET框架中,System.Drawing命名空间中的有些元素,如System.Drawing.Point就是实现为struct,而不是class。其原因也正在于以上介绍的各方面的权衡,大家可以就此研究研究,可以体会更多。另外,还有以struct实现的System.Guid。

  5、结论

  对基本概念的把握,是我们进行技术深入探索的必经之路,本系列的主旨也是能够从基本框架中,提供给大家一个通向高级技术的必修课程。本文关于class和struct的讨论就是如此,在.NET框架中,关于class和struct的讨论将涉及到对引用类型和值类型的认识,并且进一步将触角伸向变量内存分配这一高级主题,所以我们有必要来了解其运行机制,把握区别和应用场合,以便在平常的系统设计中把握好对这一概念层次的把握。

<think>我们正在讨论C#中classstruct区别。根据用户提供的引用,我们可以总结出以下几点核心区别: 1. **类型本质**: - `class` 是引用类型(reference type),存储在堆(heap)上,通过引用来访问。 - `struct` 是值类型(value type),通常存储在栈(stack)上(但注意:当struct作为class的成员时,它随class存储在堆上),直接存储数据。 2. **内存分配**: - `class` 实例在堆上分配,由垃圾回收器(GC)管理内存。 - `struct` 实例通常在栈上分配(或者在包含它的类型中内联分配),因此分配释放速度更快,避免了GC开销。 3. **赋值行为**: - `class` 赋值时复制引用(即两个变量指向同一个对象)。 - `struct` 赋值时复制整个值(创建一个新的副本)。 4. **继承**: - `class` 支持继承(可以继承自另一个类,实现多态)。 - `struct` 不能继承(隐式继承自System.ValueType,但不可被继承,也不能继承其他类或struct,只能实现接口)。 5. **默认值**: - `class` 的默认值为`null`。 - `struct` 的默认值是其所有字段的默认值(数值类型为0,布尔型为false,引用类型为null)。 6. **性能考虑**: - `struct` 适用于小型数据结构(通常小于16字节),因为复制成本低,且避免了堆分配GC开销。 - `class` 适用于较大的对象或需要复杂行为的场景,因为引用传递避免了复制的开销。 7. **构造器**: - `class` 可以有显式的无参构造器(可以自定义)。 - `struct` 不能自定义无参构造器(编译器自动生成,将所有字段初始化为默认值)。 8. **修改性**: - `class` 可以声明为`sealed`以防止继承。 - `struct` 隐式密封(不能被继承)。 9. **使用场景**: - 使用`class`:当需要表示具有复杂行为、较大数据、需要继承或多态的对象时。 - 使用`struct`:当需要轻量级的数据结构,且数据不可变(或很少改变)时,例如坐标点(Point)、矩形(Rectangle)等。 注意:引用[3][4]都提到了使用场景的选择:class用于复杂的对象、需要继承或引用语义;struct用于轻量级数据结构、按值传递或避免GC开销。 另外,引用[2]提到了C#中refstruct的改进,这涉及到如何避免结构体复制带来的性能开销(通过使用ref struct或in、ref等关键字),但这属于高级主题,不在基本区别范围内。 下面我们用一个表格来总结主要区别: | **特性** | **class (类)** | **struct (结构体)** | |------------------|----------------------------------------|----------------------------------------| | **类型** | 引用类型 | 值类型 | | **存储位置** | 堆 | 栈(或内联在包含类型中) | | **赋值行为** | 复制引用(指向同一对象) | 复制整个值(创建新副本) | | **继承** | 支持(可被继承,可继承其他类) | 不支持(隐式继承自ValueType,不可被继承) | | **默认值** | null | 所有字段为默认值(数值为0,布尔为false) | | **构造器** | 可自定义无参构造器 | 不能自定义无参构造器(自动生成) | | **是否可密封** | 可以(使用sealed) | 隐式密封(不可被继承) | | **适用场景** | 大型对象、复杂行为、需要继承或多态 | 小型数据结构、轻量级、避免GC开销 | 示例代码: ```csharp // class示例 public class PointClass { public int X { get; set; } public int Y { get; set; } // 可以有自定义的无参构造器 public PointClass() { X = 0; Y = 0; } } // struct示例 public struct PointStruct { public int X { get; set; } public int Y { get; set; } // 不能有无参构造器(编译错误) // 但可以有带参构造器 public PointStruct(int x, int y) { X = x; Y = y; } } class Program { static void Main() { // class使用 PointClass p1 = new PointClass(); // 在堆上分配 PointClass p2 = p1; // 复制引用,p1p2指向同一个对象 p1.X = 10; // p2.X也变为10 // struct使用 PointStruct s1 = new PointStruct(0, 0); // 在栈上分配 PointStruct s2 = s1; // 复制值,s1s2是独立的 s1.X = 10; // s2.X仍为0 } } ``` 注意:在C#中,所有结构体都隐式继承自`System.ValueType`,而`ValueType`又继承自`System.Object`。因此,结构体也可以重写`ValueType`的方法,例如`ToString()`。 最后,关于如何选择class还是struct,我们可以参考以下原则: - 如果需要数据在传递时被修改且希望原始数据也被修改(引用语义),使用class。 - 如果希望数据在传递时被复制(值语义),且数据量小(通常小于16字节),使用struct。 - 如果数据需要频繁装箱(转换为object),则避免使用struct(因为值类型的装箱会产生性能开销)。 - 如果需要继承,必须使用class。 另外,注意结构体应该尽量设计为不可变(immutable),即一旦创建就不能修改。这样可以避免因值复制而导致的修改副本不影响原始数据的问题,也符合结构体的设计初衷。 例如,.NET中的内置结构体`DateTime`、`TimeSpan`等都是不可变的。 引用[1]中提到的比喻:class像一台机器,有行为;struct像零件箱,组合了不同零件。这个比喻形象地说明了二者在概念上的区别。 希望这个解释能帮助你理解C#中classstruct区别。 §§相关问题§§ 1. 在C#中,结构体(struct)何时会分配到堆上? 2. 如何决定在C#中使用结构体还是类? 3. C#中结构体(struct)是否可以实现接口? 4. 为什么结构体(struct)不能自定义无参构造器?</think>### C#中classstruct的核心区别分析 #### 1. **类型本质与内存分配** 🧠 | 特性 | **class(类)** | **struct(结构体)** | |--------------|-----------------------------------------|------------------------------------------| | **类型** | 引用类型(Reference Type) | 值类型(Value Type) | | **存储位置** | 堆(Heap) | 栈(Stack)或包含它的类型内联存储[^3] | | **内存管理** | 由GC(垃圾回收器)管理 | 超出作用域自动释放 | | **赋值行为** | 复制引用(指向同一对象) | 复制整个值(创建独立副本)[^4] | **示例说明**: ```csharp // class示例(引用传递) class PointClass { public int X, Y; } var p1 = new PointClass { X=1, Y=2 }; var p2 = p1; // 复制引用 p2.X = 99; // p1.X也变为99 // struct示例(值传递) struct PointStruct { public int X, Y; } var s1 = new PointStruct { X=1, Y=2 }; var s2 = s1; // 复制值 s2.X = 99; // s1.X仍为1 ``` #### 2. **设计哲学与使用场景** ⚖️ | **特性** | **class** | **struct** | **设计依据** | |----------------|----------------------------------------|-----------------------------------------|----------------------------------| | **继承** | 支持继承多态 | 不能继承(隐式密封)[^1] | 行为封装 vs 数据容器 | | **默认构造器** | 可自定义无参构造器 | 不能自定义无参构造器[^4] | 强制完全初始化 | | **空值** | 可为`null` | 不可为`null`(需用`Nullable<T>`包装) | 值类型完整性 | | **适用场景** | GUI控件、业务逻辑实体等复杂对象 | 坐标点、颜色值等轻量级数据结构[^3] | 行为需求 vs 数据存储效率 | > 💡 **形象比喻**: > - **class** 是可执行复杂操作的机器(如`Car`类含`Drive()`方法)[^1] > - **struct** 是零件箱(如`Point`仅存储X/Y坐标)[^1] #### 3. **性能关键差异** ⚡ | **指标** | **class** | **struct** | |----------------|----------------------------------------|-----------------------------------------| | **内存开销** | 对象头+方法表指针(约12-24字节额外) | 仅包含数据字段 | | **访问速度** | 需指针跳转(间接访问) | 直接内存访问(CPU缓存友好) | | **GC压力** | 产生GC碎片回收开销 | 无GC开销(栈自动释放) | | **大对象代价** | 引用传递成本固定 | 复制成本随尺寸线性增长 | **性能公式**: 设结构体尺寸为$S$字节,传递次数为$N$: - **class内存开销**:$O(1)$ $$ \text{开销} \approx 24 + S \text{ bytes} $$ - **struct内存开销**:$O(N)$ $$ \text{开销} \approx N \times S \text{ bytes} $$ #### 4. **最佳实践指南** ✅ ```csharp // ✅ 推荐使用struct的场景 public readonly struct Vector3d // 声明为readonly确保不可变 { public double X { get; } public double Y { get; } public double Z { get; } // 只读轻量级数据(16字节以下) } // ✅ 推荐使用class的场景 public abstract class Shape // 需要继承体系 { public abstract double Area { get; } // 多态行为 public virtual void Draw() { /*...*/ } // 复杂行为 } // ⚠️ 避免陷阱 struct BigStruct { byte[1000] data; } // 大尺寸结构体复制成本高 class PointClass { int x,y; } // 轻量级数据用class浪费内存 ``` #### 5. **现代C#的改进特性** 🚀 1. **`ref struct`**: 完全栈分配,禁止装箱到堆(如`Span<T>`) ```csharp ref struct StackOnlyStruct { /*...*/ } // 无法放入class字段 ``` 2. **`readonly struct`**: 强制不可变性(编译器优化保证) ```csharp readonly struct ImmutablePoint { public int X { get; } // 禁止setter public int Y { get; } } ``` 3. **`in`参数**: 按只读引用传递结构体(避免复制) ```csharp double Distance(in Vector3d a, in Vector3d b) { // 只读访问a/b(不复制) } ``` > ⚠️ **黄金法则**: > - 当尺寸 **> 16字节** 或需要 **继承/多态** 时选`class` > - 当尺寸 **≤ 16字节** 且 **无行为** 时选`struct`[^4] > - 优先设计为 **`readonly struct`** 保证线程安全 --- ### 内存布局对比(图示) ``` class实例内存布局: [对象头] [方法表指针] [字段X] [字段Y] ↑ 堆分配,GC管理 struct实例内存布局: [字段X] [字段Y] ↑ 栈分配,自动回收 ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值