• C#中的值类型和引用类型


    值类型和引用类型

     本篇笔记结合了《CLR Via C#》和《C# in Depth》两本书中讲述的值类型和引用类型的区别和特性、值类型的装箱和拆箱这两部分内容。

    但我根据装箱部分的理解所整理出来的配图可能会有错误和遗漏,希望能有人来指正。


    现实世界中的值和引用

    报纸与值类型

    先假设你正在读的是一份真正的报纸。为了给朋友一份,需要影印报纸的全部内容并交给他。届时,他将获得属于他自己的一份完整的报纸。

    在这种情况下,我们处理的是值类型的行为。所有信息都在你的手上,不需要从任何其他地方获得。制作了副本之后,你的这份信息和朋友的那份是各自独立的。可以在自己的报纸上添加一些注解,他的报纸根本不会改变。

    网址与值类型

    再假设你正在读的是一个网页。你需要给朋友的就是网页的URL。这是引用类型的行为,URL代替引用。为了真正读到文档,必须在浏览器中输入URL,并要求它加载网页来导航引用。

    另一方面,假如网页由于某种原因发生了变化,你和你的朋友下次载入页面时,都会看到那个改变。

    在C#和.NET中,值类型和引用类型的差异与现实世界中的差别类似。

    谁是值类型或引用类型

    .NET中的大多数类型都是引用类型,除了以下总结的特殊情况,类是引用类型,而结构是值类型。特殊情况包括如下方面:

    ①数组类型是引用类型,即使元素类型是值类型(所以即便 int 是值类型, int[] 仍是引用类型);

    ②枚举(使用 enum 来声明)是值类型;

    ③委托类型(使用 delegate 来声明)是引用类型;

    ④接口类型(使用 interface 来声明)是引用类型,但可由值类型实现。


    值类型和引用类型的基础知识

    值类型

    大多数表达式都有与其相关的静态类型。对于值类型的表达式,它的值就是表达式的值。

    值类型都隐式密封,目的是防止将值类型用作其他引用类型或值类型的基类型。

    虽然不能在定义值类型时为他选择基类型,但如果愿意,值类型可实现一个或多个接口。

    引用类型

    对于引用类型的表达式,它的值是一个引用,是new操作符返回对象内存地址——即指向对象数据的内存地址。

    使用引用类型的四个事实

    使用引用类型必须留意性能问题。首先要认清楚以下四个事实。

    1.内存必须从托管堆分配。

    2.堆上分配的每个对象都有一些额外成员,这些成员必须初始化。

    3.对象中的其他字节(为字段而设)总是设为零。

    4.从托管堆分配对象时,可能强制执行一次垃圾回收。


    二者在内存中的区别

    Point 类型可以实现为结构或类。

    Point p1 = new Point(10, 20);
    Point p2 = p1;

    左部分指出当 Point 是引用类型时所涉及的值,右部分展示了当Point 是一个值类型时的情形。

    ①在 Point 是引用类型的情况下,那个值是引用: p1 和 p2 都引用同一个对象。

    ②在 Point 是值类型的情况下, p1 的值是一个完整的数据,也就是 x 和 y 值。将 p1 的值赋给 p2 ,会复制 p1 的所有数据。


    声明值类型的条件

    除非满足以下全部条件,否则不应将类型声明为值类型

    ①类型没有提供会更改其字段的成员,也就是说该类型是不可变类型,建议将绝大多数的值类型的字段都编辑为readonly。

    ②类型不需要从其他任何类型继承,类型也不派生出其他任何类型。

    ③类型的实例较小(16字节或更小);或者类型的实例较大(大于16字节),但不作为方法实参传递,也不从方法返回。

     


    值类型和引用类型的区别

    值类型的主要优势是不作为对象在托管堆上分配。与引用类型相比,值类型也存在自身的一些局限。下面列出了二者的一些区别。

    ①值类型对象有两种表示形式:未装箱和已装箱。引用类型则总是处于已装箱形式。

    ②定义自己的值类型时应重写Equals和GetHashCode,并提供他们的显式实现。

    ③不应在值类型中引入任何新的虚方法。所有方法都不能是抽象的,所有方法都隐式密封。

    ④引用类型的变量包含堆中对象的地址。引用类型的变量创建时默认初始化为null。值类型的变量总是包含其基础类型的一个值,所有成员都初始化为0。

    ⑥值类型变量赋值给另一个值类型变量,逐字段地复制。引用类型赋值,只复制地址。

    ⑦未装箱的值类型不在堆上分配。一旦定义了该类型的一个实例的方法不再活动,
    为他们分配的存储就会被释放,而不是等着进行垃圾回收。


    值类型的装箱和拆箱

    值类型比引用类型轻,是他们不作为对象在托管堆中分配,不被垃圾回收,也不通过指针进行引用。但许多时候都需要获取对值类型实例的引用。

    举个例子

    例如假定要创建ArrayList对象来容纳一组Point结构。

    struct Point{
        public Int32 x,y;
    }
    //测试类
    ArrayList list = new ArrayList();
    Point p;//分配一个Point,不再堆中分配
    for(Int32 i = 0;i<10;i++){
        p.x = p.y = i;//初始化值类型中的成员
        list.Add(p);//对值类型装箱,将引用添加到ArrayList中
    }
    //本例的Add方法原型
    public virtual Int32 Add(Object value);

    可以看出Add获取的是一个Object参数,也就是说Add获取对托管堆上的一个对象的引用来作为参数。

    但代码传递的是Point是值类型。为了使代码正确工作,Point值类型必须转换成真正的、在堆中托管的对象,而且必须获取对该对象的引用。

    装箱机制

    将值类型转换成引用类型要使用装箱机制。下面总结了对值类型的实例进行装箱时发生的事情。

    ①在托管堆中分配内存。分配的内存量是值类型各字段所需的内存量,还要加上托管堆所有对象都有的两个额外成员所需的内存量。

    ②值类型的字段复制到新分配的堆内存。

    ③返回对象地址。现在该地址是对象引用,值类型成了引用类型。

    在运行时,当前存在于Point值类型实例p中的字段复制到新分配的Point对象中。已装箱Point对象的地址返回并传给Add方法。

    Point对象一直存在于堆中,直到被垃圾回收。Point值类型变量可被重用,因为ArrayList不知道关于他的任何事情。

    在这种情况,已装箱值类型的生存期超过了未装箱值类型的生存期。

    拆箱

    假定要用以下代码获取ArrayList的第一个元素。

    Point p=(Point) a[0];

    获取ArrayList的元素0包含的引用或指针,试图将其放到Point值类型的实例p中。

    为此已装箱Point对象中的所有字段都必须赋值到值类型变量p中,后者在线程栈上。CLR分两步完成复制。

    ①获取已装箱Point对象中的各个Point字段的地址。这个过程称为拆箱。

    ②第二步将字段包含的值从堆复制到基于栈的值类型实例中。

    拆箱不是直接将装箱过程倒过来,其代价比装箱低得多。拆箱就是获取指针的过程,该指针指向包含在一个对象中的原始值类型(数据字段)。

    指针指向的是已装箱实例中的未装箱部分。所以和装箱不同,拆箱不要求在内存中复制任何字节。往往紧接着拆箱发生一次字段复制。

    已装箱值类型实例在拆箱时,内部发生这些事。

    ①如果包含“对已装箱值类型实例的引用”的变量变为null,抛出NullReferenceException异常。

    ②如果引用的对象不是所需值类型的已装箱实例,抛出InvalidCastException异常。

    第二条意味着在对象进行拆箱时,只能转型为最初未装箱的值类型。

    //错误
    Int32 x=5;
    Object o=x;//对x装箱,o引用已装箱对象
    Int16 y=(Int16)o;//抛出InvalidCastExcrption异常
    //正确
    Int32 x=5;
    Object o=x;//对x装箱,o引用已装箱对象
    Int16 y=(Int16)(Int32)o;//先拆箱为争取类型,再转型
  • 相关阅读:
    Python基础数据类型二
    集合
    SourceInsight打开的工程中中文字体显示乱码的问题
    3、U-boot的环境变量: bootcmd 和bootargs
    2、qq物联环境搭建
    FTP、SSH、NFS等环境工具的安装
    1、基本概念介绍
    7、从系统角度考虑电源管理,我们要学习更多
    6、修改应用程序数码相框以支持自动关闭LCD
    5、regulator系统的概念及测试
  • 原文地址:https://www.cnblogs.com/errornull/p/10022846.html
Copyright © 2020-2023  润新知