引用类型和值类型,装箱与拆箱,对象的相等与同一

引用类型与值类型

在FCL里面的大多数类型是引用类型,引用类型从托管堆上分配内存,值类型分配在线程栈里。new运算符返回对象的内存地址——指向对象的二进制位,使用引用类型时需要清楚下面几点:

1.从托管堆上分配内存     2.每一个堆上的对象都有额外的成员需要初始化     3.在对象里的其他字节通常置为0    4.从堆分配的对象会导致GC的出现

值类型有装箱和拆箱两种呈现形式,引用类型始终是装箱的形式。值类型从System.ValueType派生,这个类型提供了跟System.Object里面定义的相同的方法。并且,System.ValueType重写了Equals方法,当两个对象的值相等时返回true,另外System.ValueType也重写了GetHashCode方法。

装箱与拆箱

值类型相对于引用类型会轻量级一些,因为不用在托管堆上分配内存,没有GC,也没有指向对象的引用(也就是指针)。但是在多数情况下,我们需要获取值类型实例的一个引用。例如我们创建一个ArrayList对象来存放Point结构的集合。如下:

// 定一个值类型
struct  Point
{
    public  Int32 x, y;
}  

public  sealed  class Program
{
    public  static  void Main()
    {
       ArrayList a = new  ArrayList();
       Point p;            // 分配一个Point内存
       for  (Int32  i = 0; i < 10;  i++) {
         p.x  = p.y  = i;    // 初始化值类型的成员
         a.Add(p);         // 装箱,并添加引用到ArrayList里面
    }
       ...
}

每一次循环,一个Point的值类型字段被初始化,Point是存储在ArrayList里面的,那么究竟在ArrayList里面存放的是什么呢,有这样几种情况:
①存放的是Point结构  ②Point结构的地址   ③都有
到底存放的是什么?答案从ArrayList的Add方法来,Add方法定义如下:public  virtual Int32 Add(Object value);这样很明显,这里会有装箱的操作。

装箱的过程发生了什么? 

1.在堆上分配内存,内存的大小是值类型字段本身需要的内存加上类型对象指针和同步块索引需要的总的大小

2.值类型的字段会被复制到堆上

3.返回对象的地址,该地址指向一个对象,现在该值类型就是一个新的的引用类型

这里值类型的实例p会复制到Point对象里,装箱后的引用类型Point对象的地址会返回并且传递给Add方法,值类型的Point实例p还可以继续使用,因为ArrayList根本不知道它。有一点,装箱后的值类型的生存周期比未装箱的值类型长。

拆箱

接着看看怎么获取ArrayList的第一个元素,代码如下:

Point p=(Point)a[0];

这里所有包含在引用类型的Point的字段的值必须复制到到值类型Point的实例p里面,CLR通过两个步骤来完成复制工作:

1.获取引用类型Point对象的地址   2.包含在字段里面的值从堆复制到值类型实例的栈上  ——这个过程称为拆箱

拆箱并不是严格跟装箱相对,拆箱操作比装箱花费代价更少。拆箱操作实际上仅仅是获取一个指向包含在引用类型里面的原始值类型(数据字段)的指针,而实际上这个指针就是装箱的实例的一部分(还记得装箱的时候额外创建的两个东东吧:对象指针,同步块索引)。所以,拆箱没有涉及任何内存字节的复制,澄清一个部分就是,拆箱操作是典型的遵循字段复制的。

很明显,装箱和拆箱操作都会影响性能,无论是从时间还空间。

通过一些例子来说明装箱,拆箱:

public  static  void Main()
{
    Int32  v = 5;
    Object  o = v;//一次装箱
    v = 123;
    Console.WriteLine(v + ", " + (Int32) o); //这里有两次,分别是:
    //1.因为这里使用的Console.WriteLine(String arg)版本,所以v有一次装箱;2.因为o被拆箱了,所以有一次装箱
}

上面的代码出现了几次装箱呢?3次。

对象的相等与同一

System.Object提供了一个Equals的虚方法,当两个对象包含相同的值时返回true,Object的Equals方式实现如下:

public  class Object
{
    public  virtual Boolean Equals(Object  obj)
   {
       if (this == obj) return  true;
       return  false;
    }
}

看起来这个实现非常合理,因为同一个对象的引用肯定指向相同的值。但是如果引用指向的是不同的对象,这个时候就不能确定是否包含相同的值了,换句话说,默认的Equals实现的是判断两个对象同一而不是包含相同的值。如果要比较恰当的重写Equals方法,可以遵循下面几条:

1.如果obj为null,返回false,当前的对象很容易判断是否为null。

2.如果this和obj指向同一个对象,返回true

3.如果this和obj指向不同类型的对象,返回false

4.遍历每一个实例字段,比较this对象的值和obj对象的值,相等返回true

5.调用基类的Equals方法,通过它可以比较任何字段,如果基类的Equals返回false,则返回false;否则返回true

这样做以后不能判断两个对象的同一了,所以Object提供了一个静态的ReferenceEquals方法,实现如下:

public  class Object
{
    public  static  Boolean ReferenceEquals(Object objA, Object  objB) {
       return  (objA == objB);
    }
}

判断两个对象同一最好使用ReferenceEquals方法而不是使用“==”运算符。

System.ValueType重写了Object的Equals方法并且遵循了上面的几条。如果要自己重写Equals,必须确保符合下面几个附加的条件:

1.自反的,x.Equals(x)必须返回true

2.对称,x.Equals(y)必须跟y.Equals(x)返回相同的结果

3.传递性,如果x.Equals(y)返回true,y.Equals(z)返回true,那么x.Equals(z)一定返回true

4.持久的,在两个没改变的对象之间比较结果一定是true或false

对象的哈希编码(Object Hash Codes)

System.Object提供了一个GetHashCode方法,可以获取任何对象的Int32类型的哈希编码。如果我们定义的类型重写了Equals方法,也应该重写GetHashCode方法。因为有一些集合需要两个对象必须具有相同的哈希编码。Object的GetHashCode方法在一个应用程序域返回唯一的值,这个值在对象的生存周期不会改变,如果对象被GC了,这个值可以被其他的对象使用,也就是说哈希编码是可重用的。

今天是七夕,祝全体园友有情人终成卷薯(卷在一起的薯条),o(∩_∩)o

 

注   《CLR via C#》(Jeffrey Richter著)——.NET 界的经典之作,读的过程写点笔记跟大家分享,我也推荐大家看英文版,能够直接领会原意 

时间: 2024-10-23 02:12:51

引用类型和值类型,装箱与拆箱,对象的相等与同一的相关文章

.NET中的六个重要概念:栈、堆、值类型、引用类型、装箱和拆箱

内容导读 •概述 •当你声明一个变量背后发生了什么? •堆和栈 •值类型和引用类型 •哪些是值类型,哪些是引用类型? •装箱和拆箱 •装箱和拆箱的性能问题一.概述 本文会阐述六个重要的概念:堆.栈.值类型.引用类型.装箱和拆箱.本文首先会通过阐述当你定义一个变量之后系统内部发生的改变开始讲解,然后将关注点转移到存储双雄:堆和栈.之后,我们会探讨一下值类型和引用类型,并对有关于这两种类型的重要基础内容做一个讲解. 本文会通过一个简单的代码来展示在装箱和拆箱过程中所带来的性能上的影响,请各位仔细阅读

[CLR via C#]5.3 值类型的装箱和拆箱

原文:[CLR via C#]5.3 值类型的装箱和拆箱 在CLR中为了将一个值类型转换成一个引用类型,要使用一个名为装箱的机制. 下面总结了对值类型的一个实例进行装箱操作时内部发生的事: 1)在托管堆中分配好内存.分配的内存量是值类型的各个字段需要的内存量加上托管堆上的所有对象都有的两个额外成员(类型对象指针和同步块索引)需要的内存量. 2)值类型的字段复制到新的分配的堆内存. 3)返回对象的地址.现在,这个地址是对一个对象的引用,值类型现在是一个引用类型. 拆箱不是直接将装箱过程倒过来.拆箱

C# 引用类型、值类型

CLR支持两种类型:引用类型和值类型,它们的区别是在内存分配方式上的差异:引用类型是从托管堆上分配的:值类型是在线程栈上分配的.而CLR的垃圾回收是针对托管堆的,因此值类型不受垃圾回收器的控制. 在FCL中,所有称为"结构"(struct)的类型都是值类型,所有称为"类"(class)的类型都是引用类型.所有的Struct都直接派生自抽象类System.ValueType,而System.ValueType直接从System.Object派生.所有的枚举都直接从Sy

C# 装箱和拆箱的知识回顾_C#教程

装箱是将值类型转换为 object 类型或由此值类型实现的任何接口类型的一个过程. 当 CLR 对值类型进行装箱时,会将该值包装到 System.Object 内部,再将后者存储在托管堆上. 拆箱将从对象中提取值类型. 装箱是隐式的:拆箱是显式的. 装箱和拆箱的概念是类型系统 C# 统一视图的基础,其中任一类型的值都被视为一个对象. 在下面的示例中,将整型变量 i 进行了装箱并分配给对象 obj. static void Main(string[] args) { var i = 123; //

C#高级(七)类型强制转换,拆箱,装箱

一.装箱和拆箱 装箱就是 将值类型转换为引用类型.拆箱就是 将引用类型转换为值类型. 比如我们非常常用的 .ToString() 方法,就是典型的一个装箱的过程. 再如下面的例子 int i=10; object y=(object) i ; //这就是装箱 int x=(int)y; //这是拆箱. 二.对象的相等比较 在C#中,有四种比较相等的方法. 1.ReferenceEquals() 该方法是一个静态方法,用来判断两个引用是否指向同一个实例.也就是是否 指向同一个内存地址,如果是,则返

关于Visual C#装箱与拆箱的研究

visual 关于Visual C#装箱与拆箱的研究2004-09-15 作者: 出处: CSDN 在对这个问题展开讨论之前,我们不妨先来问这么几个问题,以系统的了解我们今天要探究的主题. 观者也许曾无数次的使用过诸如System.Console类或.NET类库中那些品种繁多的类.那么,我想问的是它们究竟源自何处?C#又是如何联系它们?有没有支持我们个性化扩展的机制或类型系统?又有哪些类型系统可供我们使用呢?如果我们这些PL们连这些问题都不知其然,更不知其所以然的话,C#之门恐怕会把我们拒之门外

C#的装箱和拆箱

到目前为止,我们为大家讲解了有关C#语言中的值类型和引用类型数据. 这一节我们来了解一下C#语言类型系统提出的一个核心概念:装箱(boxing)和拆箱(unboxing).装箱和拆箱机制使得在C#类型系统中,任何值类型.引用类型和object(对象)类型之间进行转换,我们称这种转化为绑定连接. 简单地说,有了装箱和拆箱的概念,对任何类型的值来说最终我们都可以看作是object类型. 4.3.1 装箱转换 4.3.2 拆箱转换

《Effective C#》之减少装箱和拆箱

为了便于文章的开展,首先介绍装箱(Boxing)和拆箱(Unboxing)这两个名词 ..Net的类型分为两种,一种是值类型,另一种是引用类型.这两个类型的本质 区别,值类型数据是分配在栈中,而引用类型数据分配在堆上.那么如果要把一 个值类型数据放到堆上,就需要装箱操作;反之,把一个放在堆上的值类型数据 取出来,则需要进行拆箱操作. 例如,对于如下简单的装箱和拆箱操作 语句. int i = 123; object obj = i;//Boxing if( obj is int ) int j

Effective C#原则17:装箱和拆箱的最小化

值类型是数据的容器,它们不具备多太性.另一方面就是说,.Net框架被设 计成单一继承的引用类型,System.Object,在整个继承关系中做为根对象存在 .设计这两种类型的目的是截然不同的,.Net框架使用了装箱与拆箱来链接两种 不同类型的数据.装箱是把一个值类型数据放置在一个无类型的引用对象上,从 而使一个值类型在须要时可以当成引用类型来使用.拆箱则是额外的从" 箱"上拷贝一份值类型数据.装箱和拆箱可以让你在须要使用 System.Object对象的地方使用值类型数据.但装箱与拆箱