本丝花了近半年,终于将《CLR Via C#》这本书看完了(请不要BS本人的看书速度T_T),这确实是一本好书,大大们推荐的果然值得一读。

虽然很多东西还没有尽得其要,我常想在自己深刻掌握了某个知识点后再总结分享出来(不知道大家是不是这个心理),但现在我觉得应该在一个人成长的过程中就去做这件事情,所以有了本篇不成文的总结,文中知识点大量来自《CLR Via C#》这本书,在此对作者及翻译者表示感谢!另文中如有错误的地方,欢迎大家指出!

术语解释:

CLR: 公共语言运行时(Common Language Runtime)

FCL:Framework类库(Framework Class Library)

IL: 中间语言(Intermediate Language)

类型基础

CLR要求所有的类型最终都是从System.Object派生的。这符合在面向对象的语言和设计中,一切皆为对象!

从图中,我们可以看到,显式从System.Object派生的类ExplicitlyDerivedFromObject和没有显式指定其父类的类ImplicitlyDerivedFromObject,最终生成的IL中间都是一致的。如果我们没有指定一个类的父类,编译器会自动为我们的类加上System.Object父类。

基元类型

编译器直接支持的数据类型称为基元类型(Primitive type),每种基元类型都对应FCL中的某一类型。如下表格所示每种基元类型与对应的FCL类型:

我们也可以简单理解成,编译器会自动在我们的每个源码文件中,加上以下using指令(using在此的作用是给类型起一个别名):

using int = System.Int32;
using string = System.String;
……

理解了这一点,我们应该能知道在我们的代码中,写int还是Int32,用string还是用String本质上是一样的!从下面两种不同的写法生成的IL代码来看,结果也是符合预计的。

我们还应该知道,C#中的int永远是代表32位整型,long永远是64位整型等。这一点和其他编程语言可能是不一致的(比如C/C++可能会根据机器平台决定,比如int在16位机器上可能是16位,而在32位机器上可能是32位。对于C/C++本人早已记忆模糊,这里如果有错误,请指出!)。

引用类型、值类型

CLR支持两种类型:引用类型值类型,它们的区别是在内存分配方式上的差异:引用类型是从托管堆上分配的;值类型是在线程栈上分配的。而CLR的垃圾回收是针对托管堆的,因此值类型不受垃圾回收器的控制。

在FCL中,所有称为“结构”(struct)的类型都是值类型,所有称为“类”(class)的类型都是引用类型。所有的Struct都直接派生自抽象类System.ValueType,而System.ValueType直接从System.Object派生。所有的枚举都直接从System.Enum派生,而后者又派生自System.ValueType,所以枚举也是值类型。由于CLR的单继承规则,所以我们在定义值类型时,不能指定基类型,但可以实现接口。同时从下图生成的IL也可以看出,值类型是隐式密封的(sealed),也就是说也不能从值类型派生。

虽然引用类型与值类型实质只是内存分配上的差异,但这种差异会导致两种类型在行为表现上有着明显不同,比如下面的例子:

    struct ValType { public int x;}
class RefType { public int x;} class Program
{
static void Main(string[] args)
{
ValType v1 = new ValType(); //在栈上分配内存
RefType r1 = new RefType(); //在堆上分配内存 v1.x = ;
r1.x = ;
//执行到这里,内存结构请见图1 Console.WriteLine(v1.x); //
Console.WriteLine(r1.x); // ValType v2 = v1; //在栈上分配内存(v2),并把v1栈的内容复制到v2
RefType r2 = r1; //把r1的堆地址复制给r2 v2.x = ; //只改变v2栈的内容
r2.x = ; //由于r2和r1都引用同一个堆上的对象,改变r2也会改变r1
//执行到这里,内存结构请见图2 Console.WriteLine(v1.x); //
Console.WriteLine(r1.x); //5 注意这里变成了r2修改后的值
Console.WriteLine(v2.x); //
Console.WriteLine(r2.x); // Console.ReadKey();
}
}

首先我们定义一个一值类型与一个引用类型,内部都只有一个字段。用new操作符分配内存时,值类型v1的内存分配在了线程栈上,引用类型r1的内存分配在了托管堆上,在程序运行到第一次WriteLine输出时,看到的结果是一致的。但接下来声明两个新的对象并执行赋值时,这里的发生的事明显不同:虽然赋值操作都是拷贝线程栈上变量的内容,但由于值类型变量v1的栈内容就是ValType类型实例本身,而引用类型r1的栈内容是RefType对象实例在堆上的地址。所以赋值后的结果就是,v1和v2各保存了一份ValType类型实例,而r1和r2保存了同一块堆内存的地址。所以改变r2对象导致了r1对象的随同改变。下面是内存示意图:

图1

图2

虽然值类型实例不需要垃圾回收,但由于值类型在传递时,传递的是内容本身,所以并不适合将所一些实例较大的类型定义为值类型。实现上除非满足以下所有条件,否则不应该将一个类型声明为值类型。

  • 没有更改其字段的成员,即该类型是不可变的。(建议所有字段为readonly)
  • 类型不需要从其他任何类型继承。(值类型不能选择基类)
  • 类型也不会派生出其他任何类型。(所有的值类型都是隐式密封sealed的)
  • 实例较小(约<=16Byte)或较大但不作为方法实参传递,也不从方法返回。

值类型的装箱与拆箱

将值类型转换成一个引用类型的过程叫装箱,整个过程看起来是这样的:

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

拆箱仅是获取一个指针的过程,该指针指向包含在一个对象中的原始值类型(数据字段)。虽然拆箱比装箱代价低,但实际在拆箱之后往往紧接着就是赋值操作(内存复制)。显然装箱和拆箱/复制会对应用程序的速度与内存消耗上产生不利影响,所以应该了解到这一点,并尽量避免装箱和拆箱操作。那么什么时候会发生装箱和拆箱,最直观的方法就是看生成的IL代码(IL对应指令是分别是box与unbox),比如下面的例子:

示例中ArrayList的Add方法参数是Object类型,也就是说一个引用类型(在堆上分配的内存),当我们传递int类型时,这里便会将int实例装箱,以返回一个堆上的地址。在将array[0]强制转型为int时,由于值类型int的对象是在线程栈上分配的,所以这里拆箱并紧接着发生赋值(内存复制)操作。同时为了对比,我加了引用类型的reference,可以看出引用类型是不会发生装箱与拆箱的。

那么如何避免(或减少)装箱与拆箱:

  • 尽量使用泛型集合。
  • 尽量将装箱与拆箱操作移到循环体之外。
  • 定义一个方法如果可接收引用类型或值类型时,尽量不要将参数定义为object,可以考虑通过重载定义多个版本或定义泛型方法。

总结

本文只能算是对自己看书的一点小结,分享出来的目的一是希望如果对某些知识理解有误,能及时得到大家指正;同时如果您感觉这篇博文有一点小价值,那我的第二个目的也就达到了。

C#基础之基本类型的更多相关文章

  1. JavaScript学习笔记-基础语法、类型、变量

    基础语法.类型.变量   非数字值的判断方法:(因为Infinity和NaN他们不等于任何值,包括自身) 1.用x != x ,当x为NaN时才返回true; 2.用isNaN(x) ,当x为NaN或 ...

  2. 【转】Java基础笔记 – 枚举类型的使用介绍和静态导入--不错

    原文网址:http://www.itzhai.com/java-based-notes-introduction-and-use-of-an-enumeration-type-static-impor ...

  3. redis基础的字符串类型

    redis —— 第二篇 基础的字符串类型 我们都知道redis是采用C语言开发,那么在C语言中表示string都是采用char[]数组的,然后你可能会想,那还不简单,当我执行如下命令,肯定是直 接塞 ...

  4. 【.Net基础一】 类型、对象、线程栈、托管堆运行时的相互关系

    目前在看CLR via C#,把总结的记下来,索性就把他写成一个系列吧. 1.[.Net基础一] 类型.对象.线程栈.托管堆运行时的相互关系 2.[.Net基础二]浅谈引用类型.值类型和装箱.拆箱 J ...

  5. C# 构造基础返回值类型-BaseResponse

    学无止境,精益求精 十年河东,十年河西,莫欺少年穷 用于基础返回值类型,如下: using System; using System.Collections.Generic; using System ...

  6. Java基础之枚举类型Enum的使用

    Java基础之枚举类型Enum的使用 定义 public enum AccruedCleanEnum { SPREAD("1","发票"),OTHER(&quo ...

  7. java基础04-数据类型扩展及面试题

    java基础04-数据类型扩展及面试题讲解 public class demo02 { public static void main(String[] args){ // 一.整数拓展: 进制 二进 ...

  8. python基础之序列类型的方法——字符串方法

    python基础之序列类型的方法--字符串方法 Hello大家好,我是python学习者小杨同学,经过一段时间的沉淀(其实是偷懒不想更新),我终于想起了自己的博客账号,所以这次带来的是序列方法的后半部 ...

  9. c#1所搭建的核心基础之值类型和引用类型

    这个主题很重要,在.NET中做的一切其实都是在和一个值类型或者引用类型打交道. 现实世界中的值和引用 假定你在读一份非常棒的东西,希望一个朋友也去读他.于是你到复印室里复印了一份.这个时候他获得了属于 ...

  10. 一起写框架-MVC框架-基础功能-Date类型数据绑定(七)

    实现功能 表单请求传递的数据,格式为以下格式的日期时间数据. (1):yyyy-MM-dd hh:mm:ss (2):yyyy-MM-dd 执行方法可以使用Date类型接收. 实现思路 1.获得表单字 ...

随机推荐

  1. 向vivi中加入命令

    在vivi的lib/command.c中添加自己的命令 核心数据结构user_command. typedef struct user_command { const char *name;      ...

  2. Oracle 12C 新特性之在线重命名、迁移活跃的数据文件

    Oracle 数据库 12c 版本中对数据文件的迁移或重命名不再需要太多繁琐的步骤,可以使用 ALTER DATABASE MOVE DATAFILE 这样的 SQL 语句对数据文件进行在线重命名和移 ...

  3. Parallel Programming-Parallel.Invoke

    本文主要介绍Parallel.Invoke的使用. 一.使用例子 class ParallelInvoke { public void Action1() { Thread.Sleep(); Cons ...

  4. 洛谷【P1177】【模板】基数排序

    题目传送门:https://www.luogu.org/problemnew/show/P1177 我对计数排序的理解:https://www.cnblogs.com/AKMer/p/9649032. ...

  5. 用遗传算法解决TSP问题

    浅谈遗传算法:https://www.cnblogs.com/AKMer/p/9479890.html Description \(小m\)在踏上寻找\(小o\)的路程之后不小心碰到了大魔王\(fat ...

  6. 洛谷 P4546 & bzoj 5020 在美妙的数学王国中畅游 —— LCT+泰勒展开

    题目:https://www.luogu.org/problemnew/show/P4546 先写了个55分的部分分,直接用LCT维护即可,在洛谷上拿了60分: 注意各处 pushup,而且 spla ...

  7. Azure Public IP DNS域名

    在某些环境下,PIP是Azure上的一种比较好的解决方案处理一些特殊的环境.比如大量的端口需要打开.向外部的访问非常多等等. 但目前,Azure的Reserved IP address不用应用到PIP ...

  8. tar 排除某个目录

    tar -zcvf tomcat.tar.gz --exclude=tomcat/logs --exclude=tomcat/libs tomcat

  9. 1.如何绕过WAF(Web应用防火墙)

    一:大小写转换法: 看字面就知道是什么意思了,就是把大写的小写,小写的大写.比如: SQL:sEleCt vERsIoN(); ‍‍XSS:)</script> 出现原因:在waf里,使用 ...

  10. hdu1084

    #include<iostream> #include<algorithm> using namespace std; #define N 101 struct node { ...