.NET (C#) Internals: Struct and Class

简介:

引言

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

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

一、直观印象

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

以下是Struct代码:

Structusing System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace StructVSClass
{
    public struct Person
    {
        //fields
        public string _Name;
        public int _Age;

        //constructors
        public Person(string name, int age)
        {
            _Name = name;
            _Age = age;
        }
        //public Person()//struct can't declare default constructor ,which has no parameters 
        //{ }

        //properties 
        public string Name
        {
            get { return _Name; }
            set { _Name = value; }
        }
        public int Age
        {
            get { return _Age; }
            set { _Age = value; }
        }

        //methods
        public void Print()
        {
            System.Console.WriteLine("Person Name: " + _Name + "\tPerson Age: " + _Age);
        }

        //indexers,operators and even other structure types
    }

    class Program
    {
        static void Main(string[] args)
        {
            Person p0 = new Person();//using the default constructor.
            p0.Print();

            Person p1 = new Person("skynet", 23);//using our custom constructor.
            p1.Print();

            Person p2;//Declare a struct object without "new."
            p2._Name = "wuqin";
            p2._Age = 23;
            p2.Print();

            /*
            Person p3;
            p3.Name = "abc";//Compile error,because if you use properties or methods ,you have to declare a struct with new.
            p3.Age=23;
            p3.Print();
             */
        }
    }
}

以下是class代码:

Classusing System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace StructVSClass
{
    public class Person
    {
        //fields
        public string _Name;
        public int _Age;

        //constructors
        public Person()
        {
 
        }
        public Person(string name, int age)
        {
            _Name = name;
            _Age = age;
        }

        //properties 
        public string Name
        {
            get { return _Name; }
            set { _Name = value; }
        }
        public int Age
        {
            get { return _Age; }
            set { _Age = value; }
        }

        //methods
        public void Print()
        {
            System.Console.WriteLine("Person Name: " + _Name + "\tPerson Age: " + _Age);
        }

        //indexers,operators and even other structure types
    }

    class Program
    {
        static void Main(string[] args)
        {
            Person p0 = new Person();//using the default constructor.
            p0.Print();

            Person p1 = new Person("skynet", 23);//using our custom constructor.
            p1.Print();

            //Person p2;//compile error.Declare a class object must use new operator.
            //p2._Name = "wuqin";
            //p2._Age = 23;
            //p2.Print();
        }
    }
}

有没有感觉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一样,它阻止你修改对象的值。这意味着你不能再重新分配,因为这将导致所有的字段重新初始化。下面的代码证明了这个:

代码class MyReferenceType
    {
        int state;

        public int State
        {
            get
            {
                return state;
            }
            set
            {
                state = value;
            }
        }
    }

    struct MyValueType
    {
        int state;

        public int State
        {
            get
            {
                return state;
            }
            set
            {
                state = value;
            }
        }
    }

    class Program
    {
        readonly MyReferenceType myReferenceType = new MyReferenceType();
        readonly MyValueType myValueType = new MyValueType();
        
        public void SomeMethod()
        {
            myReferenceType = new MyReferenceType(); // Compiler Error

            myReferenceType.State = 1234; // Ok


            myValueType = new MyValueType(); // Compiler Error

            myValueType.State = 1234; // Compiler Error

        }
    }

注意:在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



  • 本文转自吴秦博客园博客,原文链接:http://www.cnblogs.com/skynet/archive/2010/04/02/1703378.html,如需转载请自行联系原作者
相关文章
|
3天前
|
开发框架 .NET C#
【Azure Developer】C# / .NET 静态函数中this关键字的作用
在C#中,`this`关键字用于扩展方法,允许向已有类型添加功能而不修改其源代码。扩展方法必须在静态类中定义,且第一个参数使用`this`修饰,如`public static XElement AcquireElement(this XContainer container, string name, bool addFirst = false)`。这种方式增强了代码的可读性和类型的安全性,尤其在处理第三方库时。
|
1月前
|
开发框架 前端开发 .NET
LIMS(实验室)信息管理系统源码、有哪些应用领域?采用C# ASP.NET dotnet 3.5 开发的一套实验室信息系统源码
集成于VS 2019,EXT.NET前端和ASP.NET后端,搭配MSSQL 2018数据库。系统覆盖样品管理、数据分析、报表和项目管理等实验室全流程。应用广泛,包括生产质检(如石化、制药)、环保监测、试验研究等领域。随着技术发展,现代LIMS还融合了临床、电子实验室笔记本和SaaS等功能,以满足复杂多样的实验室管理需求。
36 3
LIMS(实验室)信息管理系统源码、有哪些应用领域?采用C# ASP.NET dotnet 3.5 开发的一套实验室信息系统源码
|
1月前
|
Java C# 数据安全/隐私保护
|
1月前
|
Cloud Native API C#
C#的现代化:.NET Core引领的技术革命
【6月更文挑战第9天】`.NET Core引领C#现代化,实现跨平台革命,提升性能并支持云原生应用。异步编程模型优化体验,统一API简化开发流程。C#应用场景扩展,开发效率提高,技术创新加速,预示其未来在技术领域将持续发挥关键作用。`
35 10
|
13天前
|
人工智能 开发框架 调度
C#/.NET这些实用的技巧和知识点你都知道吗?
C#/.NET这些实用的技巧和知识点你都知道吗?
|
1月前
|
存储 编解码 算法
C#.NET逃逸时间算法生成分形图像的毕业设计完成!晒晒功能
该文介绍了一个使用C#.NET Visual Studio 2008开发的程序,包含错误修复的Julia、Mandelbrot和优化过的Newton三种算法,生成色彩丰富的分形图像。作者改进了原始算法的效率,将内层循环的画点操作移至外部,提升性能。程序提供五种图形模式,支持放大缩小及颜色更新,并允许用户自定义画布大小以调整精度。还具备保存为高质JPG的功能。附有四张示例图片展示生成的分形效果。
417 3
|
21天前
|
开发框架 .NET Nacos
使用 Nacos 在 C# (.NET Core) 应用程序中实现高效配置管理和服务发现
使用 Nacos 在 C# (.NET Core) 应用程序中实现高效配置管理和服务发现
45 0
|
1月前
|
前端开发 Java C#
GitHub突破5k Star!这件事情我坚持了3年,努力打造C#/.NET/.NET Core全面的学习、工作、面试指南知识库
GitHub突破5k Star!这件事情我坚持了3年,努力打造C#/.NET/.NET Core全面的学习、工作、面试指南知识库
|
22天前
|
存储 IDE C#
C#入门:在JetBrains Rider中创建.Net Framework控制台应用程序,输出“Hello, World!”
C#入门:在JetBrains Rider中创建.Net Framework控制台应用程序,输出“Hello, World!”
58 0
|
1月前
|
XML 开发框架 人工智能
C#/.NET/.NET Core拾遗补漏合集(24年5月更新)
C#/.NET/.NET Core拾遗补漏合集(24年5月更新)