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

引用类型与值类型

在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 界的经典之作,读的过程写点笔记跟大家分享,我也推荐大家看英文版,能够直接领会原意 

作者:张雪飞
出处:https://zhangxuefei.site/p/103
版权说明:欢迎转载,但必须注明出处,并在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

发表评论

电子邮件地址不会被公开。 必填项已用*标注