《CLR Via C#》5.2值类型和引用类型

一、CLR支持两种类型:值类型(线程栈分配)和引用类型(托管堆分配)

几个基本事实:
1、内存必须从托管堆分配。
2、堆上分配的每个对象都有一些额外成员,这些成员必须初始化。(类型对象指针和同步块索引)
3、对象中的其他字节(为字段而设)总是设为零。
4、从托管堆分配对象时,可能强制执行一次垃圾回收。(0代对象容量不足时)

任何可称为“类”的类型都是引用类型,比如System.Exception类
所有值类型都成为结构或者枚举,比如System.Int32结构

所有的结构都是抽象类型System.ValueType的直接派生类,System.ValueType又直接从Syste.Object派生。而所有枚举都从System.Enum抽象类型派生,后者又从System.ValueType派生。

虽然不能在定义值类型时为它选择基类型,但如果愿意,值类型可实现一个或多个接口。
值类型都是隐式密封,目的时放着将值类型用作其他引用类型或者值类型的基类。

注意:c#new操作符虽然返回对象内存地址,但并不代表new的一定是从托管堆分配内存。
new 值类型,也是值类型,在线程栈上分配。
原因后面讲,先看一段代码的运行时:

class MyClass{ public int x;}
struct MyStruct{ public int x;}

static void Main()
{
    MyClass mc1 = new MyClass();  //在堆上分配
    MyStruct ms1 = new MyStruct(); //在栈上分配
    mc1.x = 5;  // 提领指针
    ms1.x = 5;  // 直接在栈上修改
    
    -------------------------------------
    MyClass mc2 = mc1;  // 只复制引用(指针)
    MyStruct ms2 = ms1;  // 在栈上分配并复制成员
    mc1.x = 8;   // mc1.x  mc2.x 都会修改
    ms1.x = 9;   // ms1.x会修改, ms2.x不会修改
}

在这里插入图片描述

接着说上面的

MyStruct ms1 = new MyStruct();   //在栈上分配

乍一看,似乎要在托管堆上分配一个MyStruct的实例,但是C#编译器直到MyStruct是值类型,所以会生成正确的IL代码,在线程栈上分配一个MyStruct实例,还会确保值类型中的所有字段都初始化为零。
当然上面的代码也可以这样写

MyStruct ms1;

这样写生成的IL代码也会在线程栈上分配实例,并将字段初始化为零。唯一的区别在于,如果使用new操作符,C#会认为实例已经初始化。

MyStruct ms1 = new MyStruct();
int posX = ms1.x;  // ok,C#认为ms1的字段已初始化为0(比如字段x)


MyStruct ms1;
int posX = ms1.x;  // Error,C#不认为ms1的字段已初始化为0,报错“使用了可能未赋值的字段x”

结构初始化后才能使用。

如果要设计自己的类型,那么只有当满足以下条件时,才应该声明为值类型:
1、类型具有基元类型的行为。换句话说,十分简单的类型,成员不会修改类型的任何实例字段。如果类型没有提供会更改其字段的成员,那么就说该类型是不可变类型。事实上,对于多数值类型,我们都建议将全部字段标记为readonly。
2、类型不需要从其他任何类型继承。
3、类型也不派生出其他任何类型。
考虑到值类型实参默认以传值方式传递,造成对值类型实例中的字段进行复制,对性能造成损害,同样如果返回一个值类型的方法在返回时,实例中的字段会复制到调用者分配的内存中,也会对性能造成损害,所以值类型实例大小要注意满足以下任意一条:
1、类型的实例较小(16字节或更小)
2、类型的实例较大(大于16字节),但不作为方法实参传递,也不从方法返回。

二、值类型和引用类型的区别:

1、关于表示形式:值类型对象有两种表示形式:未装箱和已装箱。而引用类型总是处于已装箱形式。
2、关于继承:值类型派生自Syste.ValueType,该类型提供了与System.Object相同的方法。但前者重写了Equals方法,能在两个对象的字段值完全匹配的情况下返回true。也重写了GetHashCode,生成哈希码时,这个重写方法所用的算法会将对象的实例字段中的值考虑进去。由于这个默认实现存在性能问题,所以定义自己的值类型时应重新Equals和GetHashCode方法,并提供它们的显示实现。
3、关于多态:由于不能将值类型作为基类型来定义新的值类型或者新的引用类型,所以不应再值类型中引入任何新的虚方法。所有方法都不能是抽象的,所有方法都隐式密封(不可重写)。
4、关于初始化:引用类型的变量包含堆中对象的地址。引用类型的变量创建时默认初始化null,表明当前不指向有效地址。而值类型的变量总是包含其基础类型的一个值,而且值类型的所有成员都初始化为0,值类型变量不是指针,访问值类型不会抛出空指针异常。
5、关于赋值:值类型赋值给另一个值类型,逐字段复制。引用类型只复制内存地址。
6、关于赋值:修改值类型的值不会影响其他值类型的值,而引用类型,由于可能多个引用类型引用同一个对象,所以对某个对象的操作会影响到另一个引用这个对象的变量。
7、关于GC:由于未装箱的值类型不在堆上分配,一旦定义了该类型的一个实例的方法不再活动,为他们分配的存储就会被释放,而不像引用类型那样等待进行垃圾回收。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值