C# 中的基元类型、值类型和引用类型

1. 基元类型(Primitive Type)

  编译器直接支持的类型称为基元类型。基元类型可以直接映射到 FCL 中存在的类型。例如,int a = 10 中的 int 就是基元类型,其对应着 FCL 中的 System.Int32,上面的代码你完全可以写作System.Int32 a = 10,编译器将生成完全形同的 IL,也可以理解为 C# 编译器为源代码文件中添加了 using int = System.Int32

1.1 基元类型的算术运算的溢出检测

  对基元类型的多数算术运算都可能发生溢出,例如

byte a = 200;
byte b = (Byte)(a + 100);//b 现在为 4

  上面代码生成的 IL 如下

  从中我们可以看出,在计算之前两个运算数都被扩展称为了32位,然后加在一起是一个32位的值(十进制300),该值在存到b之前又被转换为了Byte。C# 中的溢出检查默认是关闭的,所以上面的运算并不会抛出异常或产生错误,也就是说编译器生成 IL 时,默认选择加、减、乘以及转换操作的无溢出检查版本(如上图中的 add 命令以及conv.u1都是没有进行溢出检查的命令,其对应的溢出检查版本分别为add.ovf和conv.ovf),这样可以使得代码快速的运行,但前提是开发人员必须保证不发生溢出,或者代码能够预见溢出。

  C#中控制溢出,可以通过两种方式来实现,一种全局设置,一种是局部控制。全局设置可以通过编译器的 /checked 开关来设置,局部检查可以使用 checked/unchecked 运算符来对某一代码块来进行设置。进行溢出检查后如果发生溢出,会抛出 System.OverflowException 异常。通过上述设置后编译器编译代码时会使用加、减、乘和转换指令的溢出检查版本。这样生成的代码在执行时要稍慢一些,因为 CLR 要检查这些运算是否发生溢出。

  使用溢出检查

checked{
byte a = 200;
byte b = (Byte)(a + 100);
}
//亦可以通过下面的方式来实现
// byte b = checked((Byte)(a + 100));

最佳实践: 在开发程序时打开 /checked+ 开关进行调试性生成,这样系统会对没有显式标记为 checkedunchecked 的代码进行溢出检查,此时发生异常便可以轻松捕捉到,及时修正代码中的错误 ,正式发布时使用编译器的 /checked- 开关,确保代码能够快速运行,不会产生溢出异常。

2. 值类型和引用类型

  CLR 支持两种类型:值类型和引用类型,下面引用 MSDN 对两者的定义:

  

2.1 值类型

  值类型直接包含它的数据,值类型的实例要么在堆栈上,要么在内联结构中。与引用类型相比,值类型更为"轻",因为它们不需要在托管堆上分配内存,亦不受垃圾回收器的控制,无需进行垃圾回收,C#中的值类型都派生自System.ValueType ,值类型主要包括两种类型:结构枚举, 结构可以分为以下几类:

  1. 数值类型
  2. bool 类型
  3. char 类型
  4. 用户自定义的结构

  值类型的特点:

  1. 所有的值类型都直接或间接的派生自 System.ValueType
  2. 值类型都是隐式密封的,即不能从其它任何类型继承,也不能派生出任何的类型,目的是防止将值类型用作其它引用类型的基类型。
  3. 将值类型赋值给另外一个值类型的变量时,会逐字段进行复制。
  4. 每种值类型都有一个默认的构造函数来初始化该类型的默认值。

  自定义类型时,什么情况下适合将类型定义为值类型?

  1. 类型具有基元类型的特点,即该类型十分简单,没有成员会修改类型的任何实例字段
  2. 类型不需要从其它类型继承,亦不派生出任何的类型
  3. 类型的实例字段较小(16字节或更小)
  4. 类型的实例较大(大于16字节),但不作为方法的实参传递,也不从方法返回。

  对于后两点是因为实参默认以传值的方式进行传递,造成对值类型中的字段进行复制,造成性能上的损害。被定义为返回一个值类型的方法返回时,实例中的字段会复制到调用者的分配的内存中,对性能造成损害。

值类型的装箱和拆箱

  装箱:将值类型转换为引用类型的过程称为 装箱(Box).

  对值类型实例进行装箱时所发生的事情如下所示:

  1. 在托管堆中分配内存。分配的内存量是值类型各字段所需的内存量,还要加上托管堆所有对象都有的两个额外成员(类型对象指针和同步块索引)所需的内处量。

  2. 值类型的字段复制到新分配的堆内存中

  3. 返回对象的地址。现在该地址是对象的引用;值类型变成了引用类型。

注意

  由于值类型的装箱需要在托管堆上分配内存,因此是较为耗费性能的,应尽量避免进行过多的装箱操作。因此许多的方法会有多个重载,目的就是减少常用值类型的发生装箱的次数;如果知道自己的代码造成编译器对一个值类型进行多次重复的装箱,可以采用手动方式进行装箱,这样的代码会更小、更快;在定义自己的类型时,可以将类型中的方法定义为泛型,这样方法便可以获取所有的类型,从而不必对值类型进行装箱。

  下面通过例子对装箱进行说明

    int v = 20;//创建未装箱值类型变量
object o = v;//v 引用已装箱、包含值20的int32
v = 123;//将未装箱的值修改为123
Console.WriteLine(v + "," + (int)o);//输出 "123,20"
正常情况下这里不应该这么写,因为会导致编译器发生一次多余的拆箱和装箱操作,而应该
Console.WriteLine(v+","+o);

上面代码编译出的 IL 如下所示:

     .entrypoint
.maxstack 3
.locals init (
[0] int32 num,
[1] object obj2)
L_0000: nop
L_0001: ldc.i4.s 20
L_0003: stloc.0
L_0004: ldloc.0
L_0005: box [System.Runtime]System.Int32
L_000a: stloc.1
L_000b: ldc.i4.s 0x7b
L_000d: stloc.0
L_000e: ldloc.0
L_000f: box [System.Runtime]System.Int32
L_0014: ldstr ","
L_0019: ldloc.1
L_001a: unbox.any [System.Runtime]System.Int32
L_001f: box [System.Runtime]System.Int32
L_0024: call string [System.Runtime]System.String::Concat(object, object, object)
L_0029: call void [System.Console]System.Console::WriteLine(string)
L_002e: nop
L_002f: ret

  通过观察上述 IL 可以看出 box 指令出现了三次,说明上述代码在编译过程中发生了三次装箱。  

  首先在栈上创建一个 Int 32 的未装箱值类型实例v,将其初始化为20,再创建 object 类型的变量o,让它指向v,但由于引用类型的变量始终指向堆中的对象,因此 C# 会生成代码对v进行装箱,将v装箱的副本的地址存储到o中。这里进行了第一次装箱。

   接着调用 WriteLine 方法,该方法要求一个 string 类型的参数,但这里没有 string 对象,只有三个数据项:未装箱的 Int32 值类型的实例v,一个字符串,一个对已装箱 Int 32 值类型实例的引用o,它要转换为值类型的 Int32,为了创建一个 string 对象,C#编译器调用 StringConcat 方法,由于具有三个参数,因此编译器调用 Concat 方法的如下版本的重载:Concat(Object arg0,Object arg1,Object arg2),为第一个参数传递的是v,这是一个未装箱的值参数,因此必须对v进行装箱,这是第二次装箱,第二个参数传递的是“,”,作为String 对象引用传递,对于第三个参数 arg2,o 会被转型为 Int 32,这要求进行拆箱操作,从而获取包含在已装箱的 Int 32 中未装箱的 Int 32 的地址,然后这个未装箱的值类型必须再次被装箱,这是第三次装箱。

注意:虽然未装箱的值类型没有类型对象指针,但仍然可以调用由类型继承或重写的虚方法(如ToString,GetHashCode,Equals),并且此时并不会对值类型进行装箱操作。但在调用非虚的、继承的方法(GetType 或 MemberwiseClone) 时,无论如何都会对值类型进行装箱。因为这些方法由System.Object 定义,要求 this 实参是一个指向堆对象的指针。此外,将值类型转换为类型的某个接口时要对实例进行装箱。因为接口变量必须包括对堆对象的引用。

  拆箱: Object 向值类型或接口类型向实现了该接口的值类型的显式转换称为拆箱(UnBox)

  相对装箱,拆箱的代价要比装箱低的多。注意,拆箱并不是装箱的逆过程,拆箱就是获取指针(地址)的过程,该指针指向对象中的原始值类型(数据字段).拆箱时内部发生了如下的事情:

  1. 如果包含“对已装箱值类型实例的引用”的变量为 Null 时,抛出 NullReferenceException 的异常。

  2. 如果引用的对象不是值类型的已装箱实例,抛出 InvalidCaseException 的异常。

  3. 如果前面两步都没有问题,那么将该值从实例复制到值类型的变量中。 

  

2.2  引用类型

  C# 中所有的引用类型总是从托管堆分配(初始化新进程时,CLR会为进程保留一个连续的地址空间区域,该区域称为托管堆),C#的 new 运算符返回对象的内存地址-即指向对象数据的内存地址。使用new运算符创建对象的过程如下:

  1. 计算类型及其所有基类型(直到System.Object)中定义的所有的实例字段所需的字节数。堆上的对象都需要一些额外的成员(OverHead),包括类型对象指针(Type Object Pointer)和同步块索引(sync block index),CLR 利用这些成员管理对象。额外成员的字节数要记入对象的大小。
  2. 从托管堆中分配对象所需要的字节数。从而分配对象的内存,分配的所有字节都设为0
  3. 初始化对象的类型对象指针和同步块索引。
  4. 调用类型的实例构造器。传递在调用new中指定的实参(如果有的话),大多数编译器会在构造器中自动生成代码调用基类的构造器。每个类型的构造器都负责初始化该类型定义的实例字段。最终调用 System.Object 的构造器,该构造器什么都不做,只是简单的返回。

  执行完上诉的过程后 new 操作符会返回一个新建对象的引用或指针。

  那么创建引用类型的实例,是否必需调用构造函数呢?答案是否定的,在调用对象的 MemberwiseClone 方法进行对象的复制时,并不会调用类型的构造方法。该方法的作用在于分配内存,初始化附加字段,然后将原类型的字节数据复制到新的对象中。另外,在进行反序列化操作生成对象的实例时,也不会调用类型的构造函数。

C#中的基元类型、值类型和引用类型的更多相关文章

  1. 无法将类型“System.Nullable`1”强制转换为类型“System.Object”。LINQ to Entities 仅支持强制转换 EDM 基元或枚举类型。

    在一个项目中使用LINQ和EF时出现了题目所示的异常,搜索了很多资料都找不到解决办法,主要是因为EF方面的知识欠缺. 先将情况记录如下,以供以后参考. 查询主要设计两张表,由外键关联: 在进行下面的查 ...

  2. [你必须知道的.NET] 第八回:品味类型---值类型与引用类型(上)-内存有理

    原文地址:http://kb.cnblogs.com/page/42318/ 系列文章导航: [你必须知道的.NET] 开篇有益 [你必须知道的.NET] 第一回:恩怨情仇:is和as [你必须知道的 ...

  3. ASP.NET Core中的Action的返回值类型

    在Asp.net Core之前所有的Action返回值都是ActionResult,Json(),File()等方法返回的都是ActionResult的子类.并且Core把MVC跟WebApi合并之后 ...

  4. c语言中的结构体为值类型,当把一个结构体赋值给另一个结构体时,为值传递

    #include <stdio.h> int main() { struct person { int age; }; }; //值传递,将p1中所有成员变量的值赋值个p2中对应的成员变量 ...

  5. CLR via C#(02)-基元类型、引用类型、值类型

    http://www.cnblogs.com/qq0827/p/3281150.html 一. 基元类型 编译器能够直接支持的数据类型叫做基元类型.例如int, string等.基元类型和.NET框架 ...

  6. 《CLR via C#》读书笔记(5)基元类型、引用类型和值类型

    5.1 基元类型 编译器直接支持的数据类型称为基元类型(primitive type). 以下4行到吗生成完全相同的IL int a = 0; //最方便的语法 System.Int32 b = 0; ...

  7. <NET CLR via c# 第4版>笔记 第5章 基元类型、引用类型和值类型

    5.1 编程语言的基元类型 c#不管在什么操作系统上运行,int始终映射到System.Int32; long始终映射到System.Int64 可以通过checked/unchecked操作符/语句 ...

  8. NET中的引用类型和值类型 zt

    .NET中的类型分为值类型和引用类型,他们在内存布局,分配,相等性,赋值,存储以及一些其他的特性上有很多不同,这些不同将会直接影响到我们应用程序 的效率.本文视图对.NET 基础类型中的值类型和引用类 ...

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

    为何要转载 一来是最近面试了几家公司,发现问的还都是这些的基础知识,二来是为了复习对.NET技术的基础拾遗达到温故知新的效果. 为什么有人说,不动笔不读书.我现在也是深有体会了,看过的东西不一定会记得 ...

随机推荐

  1. win10 音频服务未响应的解决方法

    最近在调试usb audio设备,由于使用的是自己的audio 设备,所以要频繁的更换采样率,可是 在win10中经常出现一些莫名其妙的问题,今天这个问题就是折腾了我好久才搞定的. 当把usb aud ...

  2. NPOI 2.0 教程

    NPOI2.0帮助官方地址 目录 1. 前言 1.1 NPOI 2.0与NPOI 1.x的区别 1.2 NPOI 2.0模块简介 1.3 自动识别并打开Excel 2003和Excel 2007文件 ...

  3. 自定义类型转换器Convert

    //自己指定一个类型转换器(将String转成Date) ConvertUtils.register(new Converter() { @Override public Object convert ...

  4. java基础只关键字final

    final关键字简述 final关键字是在编写java程序中出现频率和很高的关键字,如果想要更好的编写java程序,那么掌握final关键字的运用是非常必要的.让我们先看一下final关键字可以修饰的 ...

  5. vexx 邀请码 送3个比特龙

    错过了比特币的行情,注册获取3个原始比特币分叉币,比特龙. 目前10元一个,送3个币.类似于股票IPO,第一天一般会冲高十几倍,建议第一天就卖. 如果看好就继续持有吧. 放心是送的不用钱的. 注册网址 ...

  6. 2018年手机应用UI设计趋势预测

    用户需求瞬息万变,而手机软件UI设计为适应变化的用户需求,也相应的发生着变化.但是,这并不意味着用户需求和UI设计趋势就是无迹可寻的.事实上,根据前几年的手机app界面设计变化的特点,尤其是2017年 ...

  7. Java框架之Hibernate(二)

    本文主要介绍: 1 Criteria 接口 2 用 myeclipse 反向生成 3 hibernate  主键生成策略 4 多对一 5 一对多 6 使用List 集合的一对多 7 多对多 一.Cri ...

  8. Java学习笔记27(集合框架一:ArrayList回顾、Collection接口方法)

    集合:集合是java中提供的一种容器,可以用来存储多个数据 集合和数组的区别: 1.数组的长度是固定的,集合的长度是可变的 2.集合中存储的元素必须是引用类型数据 对ArrayList集合的回顾 示例 ...

  9. JAXB应用实例

    过往的项目中数据存储都离不开数据库,不过最近做的一个项目的某些数据(比如人员信息.菜单.权限等等)却完全没有涉及任何数据库操作,直接XML搞定.这里无意比较优劣,因为数据库存储和XML存储本就有不同的 ...

  10. bzoj:1685 [Usaco2005 Oct]Allowance 津贴

    Description As a reward for record milk production, Farmer John has decided to start paying Bessie t ...