zoukankan      html  css  js  c++  java
  • .NET (C#) Internals: Struct and Class

    引言

    Struct与Class的异同?本是一个老生常谈话题,前几天看帖就看到了Struct 与Class辨析,其中也提到了《[你必须知道的.NET] 第四回:后来居上:class和struct》(虽然在园子里看了这个系列,但仍然买了本书看),回帖也特别热闹。我也躺下这个浑水!希望能给您带来不一样的视觉,欢迎评论。本文主题如下:

    • 直观印象
    • 深入分析 
    • 刨根问底(刨祖坟)
    • 特别之处ReadOnly
    • 浅出

    一、直观印象

    Struct与Class的异同,到底什么是什么呢?首先来两段代码,给个直观印象。

    以下是Struct代码:

    Struct

    以下是class代码:

    Class

    有没有感觉Struct跟Class很像呢。他们基本上包含同样的成员,下面列出了可在类结构中声明的所有不同种类的成员:字段、常量、属性、方法、构造函数、析构函数、事件、索引器、运算符、嵌套类型。

      二、深入分析

      从上面的代码可以知道:以下几点:

      • Struct可以有构造器,但必须带参数;Class可以声明不带参数的构造器
      • 声明对象时,虽然我们没有声明不带参数的构造器,但Struct可用调用不带参数的默认构造器
      • Struct可以不用new操作符声明对象,但是当需要用到属性、方法等时必须要用new操作符;class一定要用new操作符声明对象

      我们现在能看到的就这么多。他们之间就这么点区别吗?什么造成了这些区别呢?

      首先我们考虑以下事实:

      • Struct是值类型(Value Type)
      • Class是引用类型(Reference Type)

      那他们的异同是否可以归结为值类型与应用类型的差异呢?且听一一道来。

      值类型:

      1. 值类型实例通常分配在线程的堆栈(Thread Stack)上。为什么是通常呢?因为当值类型嵌套到引用类型对象时会分配在托管堆中。
      2. 值类型实例存储它的字段的值。
      3. 值类型实例不受垃圾收集器(GC)的控制。

      引用类型:

      1. 引用类型实例必须从托管堆(Managed Heap)中分配。
      2. 引用类型实例存储的是对象的引用及一些额外附加成员。
      3. 引用类型实例受垃圾收集器的控制。

      关于堆栈与托管堆的简单说明:

      Windows使用一个系统——虚拟寻址系统,该系统把程序可用的内存地址映射到硬件内存的实际地址上,这些任务完全由Windows在后台完成。这样在32位的处理器上每个程序可以拥有4GB(=2^32bit)内存(64位的处理器上每个程序可以拥有8GB(=2^64bit)内存),而无论计算机上有多少硬盘空间,这个4GB称为虚拟地址空间或虚拟内存。

      这个4GB内存实际上包含了程序的所有部分,包含可执行代码、代码加载的所有DLL、以及程序运行时使用的所有变量的内容。4GB的每个存储单元都是从0开始往上排序的,要访问存储在其中的某个空间的一个值,就需要提供表示该存储单元的数字。

      在进程的这个4GB空间可分为多个区域,其中有两个区域:线程堆栈(thread stack)和托管堆(managed heap)。线程的堆栈比托管堆要小很多。

      • 线程堆栈:是一个先进后出的结构,存储对象成员的数据。在调用方法使,也使用堆栈存储传递给方法的所有参数的副本。我们不知道堆栈在地址空间的什么地方,这些信息在进行C#开发的时候也不需要知道。堆栈是自上向下填充的,即从高内存地址向低内存地址填充。
      • 托管堆:另一个存储内存区域,跟堆栈不同它在GC的控制下工作。托管堆是自下向上填充的,这与堆栈是不一样的。

      了解更多,请查阅相关资料。

      我认为正是由于值类型与引用类型的上述特性,造成了Struct与Class的一系列差异:

      1. 值类型分配在堆栈中,不受垃圾收集器的控制,这也意味着给垃圾收集器减压,减少它的回收周期,一旦值类型超出使用范围就会被回收(这也是值类型的优势之所在吧)。堆栈的空间也比较小,所以Struct一般用于存储小型数据结构。
      2. 当传递大对象或频繁地用于方法的参数传递时,用class比较好,因为无需拷贝大量数据,只是拷贝引用;而Struct则要拷贝字段,从而损伤性能。(注意:可以用ref和out使Struct用于传递引用)
      3. 由于Struct是值类型,如果频繁地应用于诸如ArrayList、Hashtable之类的集合,这会导致频繁地装箱拆箱,从而损失性能,故此时用class较好。
      4. 值类型是是不可以被继承的,引用类型却可以。所有当你想用继承或多态特性的话就得用class。Struct中不能有抽象成员,因为他不能被继承。
      5. Struct是值类型,故不可以声明无参的构造器,因为为值类型编译器默认既不生产一个默认的构造器,也不会调用默认的构造器。所以,即使你定义了一个默认额构造器,他也不会被调用,为了避免这些问题,C#编译器不允许由用户定义一个默认的构造器。
      6. Struct不能继承类,但可以继承接口,因为当继承接口时必须实现该接口定义的所有方法,而如果继承抽象类的话没有要求必须实现所有的抽象方法,但Struct本身又不能被继承,所以C#编译器就不允许Struct继承类却可以继承接口。
      7. Struct不能定义析构函数,class可以。但Struct仍然可以继承IDisposable接口,所以你还是可以使用dispose模式的(有点曲线救国的味道)。
      8. ”this”指针指向值类型的实例的一个字段的地址,用一个实例方法可以通过“this”+偏移(offset)访问指定字段;“this”指针指向引用类型实例,由于方法表的指针存在,所以要通过“this”+4个字节+偏移(offset)访问指定字段。(注意:这个偏移(offset)根据CLR加载的类型来决定)

      三、刨根问底(刨祖坟)

      值类型继承自System.ValueType,而他又继承自System.Object。任何继承自System.ValueType的类型CLR都认为是值类型。引用类型直接或间接继承自System.Object,但继承链中不能包含System.ValueType

      所以虽说值类型不能继承类,但System.Object是一个特例。Struct可以调用System.Object中的所有方法,甚至是重写。特别需要注意的是System.ValueType只重写了System.Object的Equals方法和GetHashCode方法,没有添加任何新的东西。因此,Struct调用Equals方法时比较的是struct中字段是否相等,而Class执行的是比较两个引用指向的是否是同一个对象(除非类或其祖先类重写了Equals方法)。

      Class不能继承Struct可以说是,因为他们的在内存中的存储结构不一样。但为什么Struct不能继承Struct呢?我想着大概是因为:Struct在内存中只存储它的字段数据值,没有方法表指针,因此实现不了多态(polymorphism),不能正确决定方法的调用。正是由于这点,如果没有运行时多态,继承是一个不完整的面向对象,所以所有的从System.ValueType继承的值类型都被标记为sealed。

      四、特别之处ReadOnly

      对于一个引用类型,ReadOnly阻止你重新分配一个引用指向其他对象。但是它不阻止你修改引用对象的值。对于值类型,ReadOnly就像C++中的const一样,它阻止你修改对象的值。这意味着你不能再重新分配,因为这将导致所有的字段重新初始化。下面的代码证明了这个:

      代码

      注意:在foreach语句声明的变量和using语言隐式的表示ReadOnly,因此如果您使用的是结构体,您将无法改变其状态。

      五、浅出

      上面讲了那么多了,现在我们总结一下。class和struct是 .NET Framework 中的通用类型系统的两种基本构造。两者在本质上都属于数据结构,封装着一组整体作为一个逻辑单位的数据和行为。数据和行为是该类或结构的“成员”,它们包含各自的方法、属性和事件等。

      class或struct的声明类似于蓝图,用于在运行时创建实例或对象。如果定义一个名为 Person 的class或struct,则 Person 为类型名称。如果声明并初始化 Person 类型的变量 p,则 p 称为 Person 的对象或实例。可以创建同一 Person 类型的多个实例,每个实例在其属性和字段中具有不同的值。

      class是一种引用类型。创建class的对象时,对象赋值到的变量只保存对该内存的引用。将对象引用赋给新变量时,新变量引用的是原始对象。通过一个变量做出的更改将反映在另一个变量中,因为两者引用同一数据。

      struct是一种值类型。创建struct时,struct赋值到的变量保存该结构的实际数据。将结构赋给新变量时,将复制该结构。因此,新变量和原始变量包含同一数据的两个不同的副本。对一个副本的更改不影响另一个副本。

      类通常用于对较为复杂的行为建模,或对要在创建类对象后进行修改的数据建模。结构最适合一些小型数据结构,这些数据结构包含的数据以创建结构后不修改的数据为主。

      何时该用struct、何时该用class:

      • 该类型的行为类似于基于类型,否则用class
      • 该类型不要继承自任何类型,否则用class
      • 该类型不会被继承,否则用class
      • 该类型的实例不会频繁地用于方法的参数传递,否则用class
      • 该类型的实例不会被频繁地用于诸如ArrayList、Hashtable之类的集合中,否则用class
      • 当struct变得很大时,应该用class
    • 相关阅读:
      AtCoder Regular Contest 093
      AtCoder Regular Contest 094
      G. Gangsters in Central City
      HGOI 20190711 题解
      HGOI20190710 题解
      HGOI 20190709 题解
      HGOI 20190708 题解
      HGOI20190707 题解
      HGOI20190706 题解
      HGOI 20190705 题解
    • 原文地址:https://www.cnblogs.com/skynet/p/1703378.html
    Copyright © 2011-2022 走看看