C#类型基础Part1-值类型与引用类型

C#类型基础Part1-值类型与引用类型

参考资料

  • 《.NET之美–.NET关键技术深入与解析》

前言

C#中的类型一共分为两类,一类是值类型(Value Type),一类是引用类型(Reference Type)。值类型和引用类型是以它们在计算机内存中是如何被分配来划分的。值类型包括了 结构枚举,引用类型则包括了 接口委托 等。还有一种特殊的值类型,称为简单类型,比如byte、int等,这些简单类型实际上是BCL基类库类型的别名。比如,声明一个int类型,实际上是声明一个 System.Int32 结构类型。因此,在 Int32 类型中定义的方法或属性,都可以在int类型上调用,比如:

123.Equals(2);

所有的值类型都隐式地继承自 System.ValueType 类型(注意 System.ValueType 本身是一个类类型)。之所以说是“隐式地”,是因为在C#代码中,是看不到这个继承关系的,这个关系只有通过MSIL代码才能看到。 System.ValueType 类型和所有的引用类型都继承自 System.Object 基类。

C#不支持多重继承,因为结构已经隐式地继承自ValueType,所以结构不支持继承

说明:
栈(stack)是一种 后进先出的数据结构,在内存中,变量会被分配在栈上来进行操作。堆(heap)是用于为==引用类型的实例(对象)==分配空间的内存区域,在堆上创建一个对象,会将对象的地址传给栈上的变量(反过来叫变量指向此对象,或者变量引用此对象)。

值类型

当声明一个值类型的变量的时候,变量本身包含了值类型的全部字段,该变量会被分配在线程堆栈(Thread Stack)上。

假如有下面这个值类型,代表了直线上的一点:

public struct ValPoint{
	public int x;
	public ValPoint(int x){
		this.x=x;
	}
}

当在程序中声明一个变量:

ValPoint vPoint1;
vPoint1.x=10;
Console.WriteLine(vPoint1.x);//输出10

上面代码中,因为变量已经包含了值类型的所有字段,所以已经可以进行操作,并且只有对变量进行操作(vPoint1.x=10),才会进行入栈。对变量进行操作,实际上是一系列入栈、出栈操作
如果将ValPoint改为引用类型class,则会出现编译错误:使用了未赋值的局部变量“vPoint1”。除此之外,引用类型在运行时经常会抛出NullReferenceException异常。

如果不对vPoint1.x进行赋值,直接写Console.WriteLine(vPoint1.x),则会出现编译错误:使用了可能为赋值的字段x。这是因为.NET的一个约束:所有的元素使用前都必须初始化。比如下面语句也会引发这个错误:

int i;
Console.WriteLine(i);

虽然结构类型变量本身不需要像类一样使用new操作符创建一个实例(其本身就相当于一个实例),但如果要使用它的内部成员,则要在使用前对它进行赋值。结构还有一个特性:调用结构上的方法前,需要对其所有字段进行赋值。 修改ValPoint:

public struct ValPoint{
	public int x;
	public void Blank(){
	}       
}

那么下面的代码将会发生编译错误:

ValPoint vPoint1;
vPoint1.Blank();//使用了未赋值的变量vPoint1
Console.WriteLine(vPoint1);//使用了未赋值的变量vPoint1

解决上述问题可以通过这样一种方式:编译器隐式地为结构类型创建无参数的构造函数。在这个构造函数中会对结构成员进行初始化,所有的值类型成员被赋予0或者相当于0的值,所有的引用类型被赋予null值。(因此,Struct类型不可以自行声明无参数的构造函数)。所以,可以通过隐式声明的构造函数去创建一个ValPoint类型变量:

ValPoint vPoint1=new ValPoint();
Console.WriteLine(vPoint1.x);//输出0

引用类型

当声明一个引用类型变量,并使用new操作符创建引用类型实例的时候,该引用类型的变量会被分配到线程栈上,变量保存了位于堆上的引用类型的实例的内存地址。变量本身不包含任何类型所定义的数据。如果仅仅声明一个变量,但不使用new操作符,由于在堆上还没有创建类型的实例,因此,变量值为null,即不指向任何对象。

如果有这样一个类,它依然代表直线上的一点:

public class RefPoint{
	public int x;
	public RefPoint(int x){this.x=x}
	public RefPoint(){}
}

当仅仅写下

RefPoint rPoint1;

它会在线程栈上创建一个不包含任何数据,也不指向任何对象(不包含内存地址)的变量。
而当使用new操作符时:

rPoint1=new RefPoint(1)

则会完成下面几件事:

  • 在应用程序堆上创建一个引用类型对象的实例,并分配内存地址
  • 自动传递该实例的引用给构造函数。(正因为如此,才可以使用this来访问这个实例)
  • 调用该类型的构造函数。
  • 返回该实例的引用(内存地址),赋值给人Point1变量

装箱和拆箱

简单来说,装箱就是将一个值类型转换为等价的引用类型。它的过程分为这样几步:

  1. 在堆上为新生成的对象实例分配内存。该对象实例包含数据,但没有名称。
  2. 将栈上值类型变量的值复制到堆上的对象中。
  3. 将堆上创建的对象的地址返回给引用类型变量。

装箱实例代码如下:

int i=1;
Object boxed=i;
Console.WriteLine("Boxed Point:"+boxed);

拆箱则是将一个已装箱的引用类型转换为值类型:

int i=1;
Object boxed=i;
int j;
j=(int)boxed;
Console.WriteLine("UnBoxed Point: " + j);

需要注意的是:拆箱操作需要显示声明拆箱后转换的类型。它分为两步来完成

  1. 获取已装箱的对象的地址。
  2. 将值从堆上的对象中复制到堆栈上的值变量中。

可见,装箱和拆箱需要反复在堆上进行操作,因此,在程序中应该尽量避免无意义的装箱和拆箱。

  • 10
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值