• CLR C#--引用类型和值类型


    引言

    引用类型总是从托管堆分配,值类型分配在栈中。

    1. 内存必须从托管堆分配。
    2. 堆上分配的每个对象都有一些额外成员,这些成员必须初始化。
    3. 对象中的其他字节(为字段而设)总是设为零。
    4. 从托管堆分配对象时,可能强制执行一次垃圾回收。

    区别

    //引用类型(因为'class')
    class SomeRef {   
    	public Int32 x; 
    } 
    //值类型(因为'struct')
    struct SomeVal {
    	public Int32 x;
    }
    static void ValueTypeDemo()
    {
    	//前半部分
    	SomeRef rl = new SomeRef(); //在堆上分配 
    	SomeVal vl = new SomeVal(); //在栈上分配 
    	rl.x = 5;					//提领指针 
    	vl.x = 5;					//在栈上修改 
    	Console.WriteLine(rl.x); 	//显示"5" 
    	Console.WriteLine(vl.x);	//同样显示”5”
    	
    	//后半部分
    	SomeRef r2 = rl; 			//只复制引用(指针)
    	SomeVal v2 = vl; 			//在栈上分配并复制成员
    	rl.x = 8;					//rl.x和r2.x都会更改
    	vl.x = 9;					//vl.x会更改,v2.x不变
    	Console.WriteLine(rl.x);	//显示"8"
    	Console.WriteLine(r2.x);	//显示"8"
    	Console.WriteLine(vl.x);	//显示"9"
    	Console.WriteLine(v2.x);	//显示"5"
    }
    

    在这里插入图片描述
    设计自己的类型时,要仔细考虑类型是否应该定义成值类型而不是引用类型。值类型有时
    能提供更好的性能。具体地说,除非满足以下全部条件,否则不应将类型声明为值类型。

    • 类型具有基元类型的行为。也就是说,是十分简单的类型,没有成员会修改类型的任
      何实例字段。如果类型没有提供会更改其字段的成员,就说该类型是不可变(immutable) 类型。
    • 类型不需要从其他任何类型继承。
    • 类型也不派生出其他任何类型。

    装箱

    //声明值类型 
    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中
    		}
    	}
    }
    
    public virtual Int32 Add(Object value);
    

    装箱步骤:

    1. 在托管堆中分配内存。分配的内存量是值类型各字段所需的内存量,还要加上托管堆
      所有对象都有的两个额外成员(类型对象指针和同步块索引)所需的内存量。
    2. 值类型的字段复制到新分配的堆内存。
    3. 返回对象地址。现在该地址是对象引用;值类型成了引用类型。

    拆箱

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

    Point p = (Point)a [0];
    
    1. 获取ArrayList的元素0包含的引用(或指针),试图将其放到Point值类型的实例p中。
    2. 己装箱Point对象中的所有字段都必须复制到值类型变量p中,后者在线程栈上。

    第一步获取己装箱Point对象中的各个Point字段的地址。这个过 程称为拆箱(unboxing)o第二步将字段包含的值从堆复制到基于栈的值类型实例中。

    拆箱的代价比装箱低得多。拆箱其实就是获取指针的过 程,该指针指向包含在一个对象中的原始值类型(数据字段)。其实,指针指向的是已装箱 实例中的未装箱部分。

    一次拆箱操作经常紧接着一次字段复制。以下C#代码演示了拆箱和复制:

    public static void Main()   { 
    	Point p;
    	p.x = p.y = 1;
    	Object o = p;       //对p装箱;o引用已装箱实例
    	p = (Point)o;      //对o拆箱.将字段从已装箱实例复制到栈变量中
    }
    

    最后一行,C#编译器生成一条1L指令对0拆箱(获取己装箱实例中的字段的地址),并生成另一条IL指令将这些字段从堆复制到基于栈的变量p中.

    IL

    public static void Main()
    {
    	Int32 v = 5;         //创建未装箱值类型变量
    	Object o = v; 		// o引用已装箱的、包含值5的Int32 
    	v = 123;			//将未装箱的值修改成123
    	Console.WriteLine(v + ", " + (Int32) o) ;   // 显示"123, 5"
    }
    
    .method public hidebysig static void Main ()  cil managed
    {
    	.entrypoint
    	//代码大小   45 (0x2d) 
    	.maxstack 3
    	.locals init ( [0]int32 v,[1]  object o)
    	//将5加载到v中 
    	IL_0000: ldc.i4.5 
    	IL_0001:  stloc.O
    	//对v装箱,将引用指针存储到o中 
    	IL_0002: ldloc.O
    	IL_0003:   box        [mscorlib]System.Int32
    	IL_0008:   stloc.1
    	//将123加载到v中 
    	IL_0009: ldc.i4.s 123 
    	IL_000b: stloc.O
    	//对v装箱,将指针保留在栈上以进行Concat (连接)操作 
    	IL_000c: ldloc.O
    	IL_000d:   box        [mscorlib)System.Int32 
    	//将字符串加载到栈上以执行Concat操作
    	IL_0012: ldstr  ・・,"
    	//对o拆箱:获取一个指针,它指向栈上的Int32字段 
    	IL_0017: ldloc.l
    	IL_0018:   unbox.any [mscorlib]System.Int32 
    	//对Int32装箱,将指针保留在栈上以进行Concat操作
    	IL_001d: box        [mscorlib]System.Int32 
    	// 调用 Concat
    	IL_0022: call string [mscorliblSystem.String::Conct(object,
    	object, 
    	object)
    	//将从Concat返回的字符串传给WriteLine
    	IL_0027: call void [mscorlib]System.Console::WriteLine(string)
    	//从Main返回,终止应用程序 
    	IL_002c: ret
    }   // end of method App::Main
    
    
    1. 首先在栈上创建一个Int32未装箱值类型实例(v),将其初始化为5
    2. 再创建Object类的 变量(o)并初始化,让它指向V。但由于引用类型的变量始终指向堆中的对象,所以C#生成正确的IL代码对v进行装箱,将v的已装箱拷贝的地址存储到o中。
    3. 接着,值123被放到未装箱值类型实例v中,但这个操作不会影响己装箱的Int32,后者的值依然为5。
    4. 接着调用WriteLine方法,WriteLine要求获取一个String对象,但当前没有String对象。相反,现在有三个数据项:一个未装箱的Int32值类型实例(v),—个String(它是引用类型),以及对己装箱lnt32值类型实例的引用(0),它要转型为未装箱的Int32,o必须以某种方式合并这些数据项来创建一个String为了创建一个String, C#编译器生成代码来调用String的静态方法Concat。该方法有几个重载版本,所有版本执行的操作都一样,只是参数的数量不同。由于需要连接三个数据项 来创建字符串所以编译器选择Concat方法的以下版本:public static String Concat(Object argO, Object argl, Object arg2);为第一个参数argO传递的是v。但v是未装箱的值参数,而argO是Object,所以必须对v 进行装箱,并将已装箱的v的地址传arg0。对于arg1参数,字符串",”作为String对象引用传递。对于arg2参数,o(—个Object引用)会转型为Int32o这要求执行拆箱(但不紧接着执行复制),从而获取包含在已装箱Int32中的未装箱lnt32的地址。这个未装箱的Int32 实例必须再次装箱,并将新的已装箱实例的内存地址传给Concat的arg2参数。
      Concat方法调用指定的每个对象的ToString方法,将每个对象的字符串形式连接起来。从
      Concat返回的String对象传给W riteLine方法以显示最终结果。
      应该指出,如果像下面这样写W riteLine调用,生成的IL代码将具有更高的执行效率:
    Console.WriteLine(v +","+ o) ;    // 显示"123, 5"
    

    未装箱值类型比引用类型更“轻”。这要归结于以下两个原因。

    • 不在托管堆上分配。
    • 没有堆上的每个对象都有的额外成员:“类型对象指针”和“同步块索引”。

    示例

    using System;
    internal struct Point : IComparable { 
    	private Int32 m_x, m_y;
    	//构造器负责初始化字段
    	public Point(Int32 x, Int32 y)
    	{
    		m_x = x; 
    		m_y = y;
    	}
    	// 觅写从 System.ValueType 继承的 ToString 方法
    	public override String ToString()
    	{
    		//将point作为字符串返回。注意:调用ToString以避免装箝
    		return String.Format(n({0}, (1})", m_x.ToString(), m_y.ToString());
    	}
    	//实现类型安全的Compare?o方法 
    	public Int32 CompareTo(Point other)
    	{
    		//利用勾股定理计算哪个point距离原点(0, 0)更远 
    		return Math.Sign(Math.Sqrt(m_x * m_x + m_y * m_y)- Math.Sqrt(other.ra_x * other,m_x + other.m_y * other,m_y));
    	}
    	// 实现 IComparable 的 CompareTo 方法 
    	public Int32 CompareTo(Object o) {
    		if (GetType() != o.GetType())  {
    			throw new ArgumentException(no is not a Point");
    		}
    		//调用类型安全的CompareTo方法 
    		return CompareTo((Point) o);
    	}
    }
    
    public static class Program
    {
    	public static void Main()
    	{
    		//在栈上创建两个Point实例 
    		Point pl = new Point(10r,10); 
    		Point p2 = new Point(20, 20);
    		//调用ToString (虚方法)不装箱pl
    		Console. WriteLine(pl.ToString() );     // 显示"(10, 10) n 
    		//调用GetType (非虚方法)时.要对pl进行装箱
    		Console. WriteLine(pl. GetType () ) ;  // 显示"Point" 
    		//调用CompareTo不装箱pl
    		//山于调用的是CompareTo(Point).所以p2不装箱 
    		Console. WriteLine(pl. CompareTo(p2) ) ;   // 显示"-1"
    		// pl要装箱,引用放到c中 
    		IComparable c = pl;
    		Console.WriteLine(c.GetType() ) ;     // 显示"Point" 
    		//调用CompareTo不装箱pl
    		//由于向CompareTo传递的不是Point变量,
    		//所以调用的是CompareTo(Object),它要求获取对己装箱Point的引用 
    		// c不装箱是因为它本来就引用已装箱Point
    		Console.WriteLine(pl. CompareTo(c) ) ;    // 显不"0" 
    		// c不装箱,因为它本來就引用己装箱Point
    		// p2要装箱,因为调用的是CompareTo(Object) 
    		Console.WriteLine(c.CompareTo(p2) ) ;    // 显不"-1"
    		//对c拆箱,字段复制到p2中 
    		p2 = (Point) c;
    		//证明字段己复制到p2中
    		Console.WriteLine (p2 .ToString () ) ;          // 显示"(10, 10)"
    	}
    }
    
    1. 调用ToString吋pl不必装箱。表面看pl似乎必须装箱,因为ToString是从基类System.ValueType继承的虛方法。由于pl是未装箱的值类型,所以不存在“类型对象指针”。但JIT编译器发现Point重写了 ToString方法,所以会生成代码来直接(非虛地)调用ToString方法,而不必进行任何装箱操作。编译器知道这里不存在多态性问题,因为Point是值类型,没有类型能从它派生以提供虚方法的另一个实现。但假如Point的ToString方法在内部调用base.ToString(),那么在调用System.ValueType的ToString方法时,值类型的实例会被装箱。
    2. 调用非虚方法GetType时pl必须装箱。Point的GetType方法是从System.Object继承的。所以,为了调用GetType, CLR必须使用指向类型对象的指针,而这个指针只 能通过装箱pl来获得。
    3. 第一次调用CompareTo时pl不必装箱,因为Point实现了 CompareTo方法,编译器能直接调用它。注意向CompareTo传递的是一个Point变量(p2),所以编译器调用的是获取一个Point参数的CompareTo重载版本。这意味着p2以传值方式传给 CompareTo,无需装箱。
    4. pl转型为接口类型的变量c时必须装箱,因为接口被定义为引用类型。装箱pl后,指向己装箱对象的指针存储到变量c中。后面对GetType的调用证明c确实引用堆上 的已装箱Point
    5. 第二次调用CompareTo时pl不必装箱,因为Point实现了 CompareTo方法,编译 器能直接调用。注意向CompareTo传递的是IComparable类型的变量c,所以编译器 调用的是获取一个Object参数的CompareTo重载版本。这意味着传递的实参必须是 指针,必须引用堆上一个对象。幸好,c确实引用一个己装箱Point,所以c中的内存 地址直接传给CompareTo,无需额外装箱。
    6. 第三次调用CompareTo时,c本来就引用堆上的已装箱Point对象,所以不装箱。由 c是IComparable接口类型,所以只能调用接口的获取一个Object参数的CompareTo方法。这意味着传递的实参必须是引用了堆h对象的指针。所以p2要装箱,指向这个已装箱对象的指针将传给CompareTo()
    7. 将c转型为Point时,c引用的堆上对象被拆箱,其字段从堆复制到p2。p2是栈上的 Point类型实例。
  • 相关阅读:
    信息系统项目管理师必背核心考点(六十)项目集管理
    kafka 消费者
    laravel的服务容器,服务提供者,门面的理解
    页面内Tab切换-工程问题
    windows下用Java跑通spark官方文档的quick-start
    前端学习之Babel转码器
    风投机构加持的NFT明星项目,是否值得追逐?
    STM32笔记-AD模数转换
    LeetCode高频题46. 全排列
    力扣2596
  • 原文地址:https://blog.csdn.net/huan13479195089/article/details/127111637