码迷,mamicode.com
首页 > 其他好文 > 详细

第5章 基元类型、引用类型和值类型

时间:2016-06-26 18:19:54      阅读:305      评论:0      收藏:0      [点我收藏+]

标签:

5.1编程语言的基元类型

编译器(Compiler)直接支持的数据类型称为基元类型(primitive type)。

 技术分享

 技术分享

我希望编译器根本不要提供基元类型名称,强制开发人员使用FCL(Framework类库)类型名称:

许多开发人员都困惑于到底应该使用string还是String。由于C#的string直接映射到System.String,所以两者是没有区别的。int始终映射到System.Int32,所以不管在什么操作系统上运行,代表的都是32位整数。

5.2引用类型和值类型

虽然FCL中大多数类型是引用类型,但程序员用的最多的还是值类型。

值类型:原类型(Sbyte、Byte、Short、Ushort、Int、Uint、Long、Ulong、Char、Float、Double、Bool、Decimal)、枚举(enum)、结构(struct)。

为提升简单的常用的类型的性能,CLR提供了“值类型”的轻量级类型。值类型的实例一般在线程堆栈上分配。

引用类型总是从托管堆上分配的,C#的new操作符会返回对象的内存地址——也就是指向对象数据的内存地址。

引用类型共有四种:类类型、接口类型、数组类型和委托类型。所有引用类型变量所引用的对象,其内存都是在托管堆中分配的。

使用引用类型时必须考虑性能问题,首先考虑以下事实:

  • 内存必须从托管堆上分配
  • 堆上分配的每个对象都有一些额外的成员(类型对象指针、同步块索引),这些成员必须初始化
  • 对象中的其他字节(为字段而设)总是设为零
  • 从托管堆中分配一个对象时,可能强制执行一次垃圾收集操作

在代表值类型实例的一个变量中,并不包含一个指向实例的指针。相反,变量包含了实例本身的字段。由于变量已经包含了实例的字段,所以为了操作实例中的字段,不再需要提领一个指针,值类型的实例不受垃圾回收器的控制,超出了作用范围,系统就会自动释放。因此值类型的使用缓解了托管堆中的压力,并减少了一个应用程序在其生存期内需要进行的垃圾回收次数。

在.Net Framework SDK文档中,任何称为“类”的类型都是引用类型。例如:System.Exception类、System.IO.FileStream类以及System.Random类都是引用类型。

相反,文档中将所有值类型称为结构或枚举。例如:System.Int32结构、System.TimeSpan结构、System.DayofWeek枚举。所有结构都是抽象类型System.ValueType的直接派生类。所有枚举都从System.Enum 抽象类型派生。System.Enum又是从System.ValueType派生。

所有值类型都是隐式密封的(sealed),目的是防止将一个值类型用作其他任何引用类型或值类型的基类型。

 技术分享

在代码中使用类型时,必须注意该类型是引用类型还是值类型。

SomeVal v1 = new SomeVal ();

上面一行代码似乎要在托管堆上分配一个SomeVal实例。然而,C#编译器知道SomeVal是一个值类型,所以会生成相应的IL代码,在线程堆栈上分配一个SomeVal实例。C#还会确保值类型中所有字段都初始化为零。

以下条件都满足时,才应该将一个类型声明为值类型:

  • 类型具有基元类型的行为。类型中没有成员会修改类型的任何实例字段。事实上,对于许多值类型来说,都建议将它们的字段都标记为readonly
  • 类型不需要从其他任何类型继承
  • 类型也不会派生出其他任何类型

声明为值类型除了满足上面3个条件外,还必须满足一下任何一个条件:

  • 类型是实例较小(约为16字节或者更小)
  • 类型的实例较大, 但不作为方法的实参传递,也不从方法返回

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

  • 值类型有两种表示形式:未装箱(unboxed)形式和已装箱(boxed)形式。相反,引用类型总是处于已装箱形式
  • 值类型是从System.ValueType派生的。该方法提供了与System.Object定义的相同的方法。然而,System.ValueType重写了Equals方法,能在两个对象的字段完全匹配的前提下返回true。System.ValueType重写了GetHashCode方法,生成哈希码时,这个重写方法所用的算法会将对象的实例字段的值考虑在内。
  • 由于不能将一个值类型作为基类型来定义一个新的值类型或者一个新的引用类型,所以不能在值类型中引入任何虚方法。所有方法都不能是抽象的,都隐式地为密封方法
  • 引用类型的变量指向的是堆上的一个对象的地址。默认情况下,在创建一个引用类型的变量时,它被初始化为null,表明引用类型的变量,当前不指向一个有效的对象。试图使用一个为null的引用类型变量,会抛出NullReferenceException异常。值类型的变量总是包含其基础类型的一个值,而且值类型的所有成员都初始化为0 。由于值类型的变量不是指针,所有在访问一个值类型时,不可能抛出NullReferenceException异常。CLR为值类型提供了一个特殊的特性,能为值类型添加“可空性”
  • 将一个值类型的变量赋给另一个值类型变量,会执行一次逐字段的复制(在线程堆栈上重新分配并复制成员)。将引用类型的变量赋给另一个引用类型变量时,只复制内存地址(堆中同一个类型的实例/对象。)
  • 基于上一条,两个或多个引用类型的变量能引用堆中同一个对象,所有对一个变量执行的操作可能影响到另一个变量引用的对象。相反,值类型的变量是自成一体的对象,对一个值类型变量的操作不可能影响另一个值类型变量
  • 由于未装箱的值类型不在堆上分配,所以一旦定义了该类型的一个实例的方法不再处于活动状态,为它们分配的存储就会被释放。这意味着值类型的实例在其内存被回收时,不会通过Finalize方法接收到一个通知

 

 

private static void Main()

{

int i;

MyClass mc;

i = 5;

mc = new MyClass();

}

 

当一个局部变量声明之后,就会在栈的内存中分配一块内存给这个变量,至于这块内存多大,里面存放什么东西,就要看这个变量是值类型还是引用类型了。

  • 值类型

如果是值类型,为变量分配这块内存的大小就是值类型定义的大小,存放值类型自身的值(内容)。比如,对于上面的整型变量 i,这块内存的大小就是 4个字节(一个 int型定义的大小),如果执行 i = 5;这行代码,则这块内存的内容就是 5(如图 -1)。

 技术分享

对于任何值类型,无论是读取还是写入操作,可以一步到位,因为值类型变量本身所占的内存就存放着值。

  • 引用类型

如果是引用类型,为变量分配的这块内存的大小,就是一个内存指针(实例引用、对象引用)的大小(在 32位系统上为 4字节,在 64位系统上为 8字节)。因为所有引用类型的实例(对象、值)都是创建在托管堆上的,而这个为变量分配的内存就存放变量对应在堆上的实例(对象、值)的内存首地址(内存指针),也叫实例(对象)的引用。

 技术分享

由图 -2可知,变量 mc中存放的是 MyClass实例(对象)的对象引用,如果需要访问 mc实例,系统需要首先从 mc变量中得到实例的引用(在堆中的地址),然后用这个引用(地址)找到堆中的实例,再进行访问。需要至少 2步操作才可以完成实例访问。

5.3 值类型的装箱和拆箱

值类型是比引用类型更“轻型”的一种类型,因为它不作为对象在托管堆中分配,不会被垃圾回收,也不通过指针来引用。但在许多情况下,都需要获取对值类型的一个实例的引用,即将值类型转换成引用类型。

 技术分享

如上面代码,创建一个ArrayList对象(System.Collections命名空间中定义的一个类型)来容纳一组Point结构。

每一次循环迭代都会初始化值类型字段(x和y)。然后这个Point会存储到ArrayList中。但ArrayList中究竟存储的是什么?是Point结构,还是其他什么东西。我们必须研究ArrayList的Add方法,了解它的参数被定义成什么类型。

 技术分享

Add需要获取一个Object参数。换言之,Add需要获取对托管堆上的一个对象的引用(指针)来作为参数。但在之前的代码中,传递的是p,也就是一个Point,是一个值类型。为了将一个值类型转换成引用类型,要使用一个名为装箱(boxing)的机制。

对值类型的一个实例进行装箱操作时在内部发生的事情:

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

C#编译器会自动生成对一个值类型的实例进行装箱所需的IL代码。

在上述代码中,C#编译器检测到是向一个需要引用类型的方法传递一个值类型,所以会自动生成代码对对象进行装箱。在运行时,当前存在于Point值类型实例p中字段会复制到新分配的Point对象中。已装箱的Point对象(现在是一个引用类型)的地址会返回给Add方法。Point对象会一直存在于堆中,直到被垃圾回收。Point值类型变量p可以重用,因为ArrayList根本不知道关于它的任何事情。在这种情况下,已装箱的值类型的生存期超过了未装箱的值类型的生存期。

在知道装箱如何进行之后,接着谈谈拆箱。

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

 技术分享

现在是要获取ArrayList的元素0中包含的引用(或指针),并试图将其放到一个Point值类型的实例p中。包含在已装箱Point对象中的所有字段都必须复制到值类型变量p中,后者在线程栈上。

CLR分两步完成这个复制操作(拆箱/复制)。

第一步,获取已装箱的Point对象中的各个Point字段的地址。这个过程称为拆箱(unboxing)

第二步,将这些字段包含的值从堆中复制到基于栈的值类型实例中

拆箱不是直接将装箱过程倒过来。拆箱的代价比装箱低的多。拆箱其实就是获取一个指针的过程,该指针指向包含在一个对象中的原始值类型(数据字段)。所以,和装箱不同,拆箱不要求在内存中复制任何字节,往往会紧接着拆箱操作后发生一次字段的复制操作。

一个已装箱的值类型实例在拆箱时,内部会发生下面这些事情:

  • 如果包含了“对已装箱的值类型实例的引用”的变量为null,就抛出一个NullReferenceException异常
  • 如果引用指向的对象不是所期待的值类型的一个已装箱实例,就抛出一个InvalidCastException异常

 技术分享

以上代码从逻辑上说,完全可以获取o所引用一个已装箱的Int32,然后将其强制转换为一个Int16。然而,在对一个对象进行拆箱的时候,只能将其转型为原先未装箱时的值类型---本例即为Int32

下面的代码是正确的写法:

 技术分享

overload:重载指的是同一个类中有两个或多个名字相同但是参数不同(参数个数和参数类型)的方法。

由于未装箱的值类型没有同步块索引,所以不能使用System.Threading.Monitor类型的各种方法,让多个线程同步对这个实例的访问。

5.3.2对象相等性和同一性(以后重看)

有时需要将对象放到一个集合中,并编写代码对集合中的对象进行排序、搜索或比较。

对于Object的Equals方法的默认实现来说,它实现的实际是同一性(identity),而非相等性(equality)。

5.4对象哈希码(以后再看)

哈希:通过将哈希算法应用到任意数量的数据所得到的固定大小的结果。如果输入数据中有变化,则哈希也会发生变化。哈希可用于许多操作,包括身份验证和数字签名。也称为“消息摘要”。

哈希表:根据设定的哈希函数和处理冲突方法将一组关键字映象到一个有限的地址区间上,并以关键字在地址区间中的象作为记录在表中的存储位置,这种表称为哈希表或散列,所得存储位置称为哈希地址或散列地址。作为线性数据结构与表格和队列等相比,哈希表无疑是查找速度比较快的一种。

在.NetFramework中,HashTable是System.Collections命名空间提供的容器,用来处理和表现类似keyvalue的键\值对。其中key区分大小写,通常用来快速查找。value用来储存对应于key的值。Hashtable中keyvalue键\值对均为object类型,所以Hashtable可以支持任何类型的keyvalue键\值对。

Hashtable是非泛型的集合,所以在检索和存储值类型时通常会发生装箱与拆箱的操作。

在哈希表中添加一个keyvalue键\值对:HashtableObject.Add(key,value);

在哈希表中去除某个keyvalue键\值对:HashtableObject.Remove(key);

从哈希表中移除所有元素:HashtableObject.Clear();

判断哈希表是否包含特定键key:HashtableObject.Contains(key);

哈希算法:将任意长度的二进制值映射为固定长度的较小二进制值,这个小的二进制值称为哈希值。哈希值是一段数据唯一且极其紧凑的数值表示形式。

hashcode标识对象的地址,用于区别不同的对象。

普通的查找慢是因为要一个一个比, Hash就是让把比较的次数降低  而降低的办法就是靠计算。

哈希算法会根据你要存入的数据,先通过该算法计算出一个地址值,这个地址值就是你需要存入到集合当中的数据的位置,而不会像数组那样一个个的进行挨个存储,挨个遍历一遍后面有空位就存这种情况了。而你查找的时候也是根据这个哈希算法来的。将你的要查找的数据进行计算,得出一个地址,这个地址会映射到集合当中的位置,这样就能够直接到这个位置上去找了,而不需要像数组那样,一个个遍历,一个个对比去寻找,这样自然增加了速度,提高了效率了。

 

如果能将任何对象的任何实例放到一个哈希表集合中,会带来很多好处。为此,System.Object提供了虚方法GetHashCode,它能获取任意对象的Int32哈希码。

如果你定义的一个类型重写了Equals方法,那么还应重写GetHashCode方法,确保相等性算法和对象哈希码算法是一致的。是因为在System.Collections.Hashtable类型、System.Collections.Generic.Dictionary类型以及其他一些集合的实现中,要求两个对象为了相等,必须具有相同的哈希码。

简单地说,在一个集合中添加一个键\值对时,首先会获取键对象的一个哈希码。这个哈希码指出键\值对应该储存到哪一个哈希桶(bucket)中。集合需要查找一个键时,会获取指定的键对象的哈希码。这个哈希码标识了现在要搜索的目标哈希桶,要在其中查找与指定键对象相等的一个键对象。采用这种算法来储存和查找键,意味中一旦修改了集合中的一个键对象,集合就再也找不到对象。所以,需要修改一个哈希表中的键对象时,正确的做法是移除原来的键\值对,修改键对象,再将新的键\值对添加回哈希表。

5.5 dynamic基元类型

C#是一种类型安全的编程语言。这意味着所有表达式都解析成某个类型的一个实例,在编译器生成的代码中,只会执行对这个类型来说有效的操作。

从面向对象的角度来看, 对象的实例表示的是 个体, 而static的属性和方法则表示 全体所共有的方法和属性 , 如“会员张三”、“会员李四”是“会员”的两个个体, 昵称、等级是他们各自不同的属性,而 会员总数、注册新会员 则是 全体会员所共享的属性和方法。 双比如 “圆”这个class, 半径、面积、周长是 个体的属性,而圆周率PI则是共性。

从应用的角度来看,本质就是为了节省内存,在内存中只有一个引用。

静态类的主要功能如下:

  • 它们仅包含静态成员。
  • 它们不能被实例化。
  • 它们是密封的。
  • 它们不能包含实例构造函数,不能使用 new 关键字创建静态类的实例。

在许多时候,程序仍需处理一些运行时才会知晓的消息。如果你写的是一个纯C#应用程序,那么只有在使用反射的时候,才会在运行时才能确定的信息打交道。然而,许多开发者在使用C#时,都要和一些不是和C#实现的组件进行通信。有的组件是.Net动态语言,比如Python或Ruby,有的是HTML文档对象模型(DOM)对象。

 技术分享

 

C#编译器允许将一个表达式的类型标记为dynamic。还可以将一个表达式的结果放到一个变量中,并将变量的类型标记为dynamic。然后可以用这个dynamic表达式/变量调用一个成员,比如字段、属性/索引器、方法、委托以及一元/二元/转换操作符。

代码使用dynamic表达式/变量调用一个成员时,编译器会生成一个特殊的IL代码来描述所需的操作。这种特殊的代码称为payload(有效载荷)。在运行是payload代码会根据当前由dynamic表达式/变量引用的对象的实际类型来决定具体执行的操作。

这些Payload代码使用了一个称为运行时绑定器(runtime binder)的类。C#的运行时绑定器的代码在Microsoft.CSharp.dll程序集中,构建使用dynamic关键字的项目时,必须引用该程序集。

Plus方法将参数的类型申明为dynamic,在方法内部,实参作为二元+操作符的两个操作数使用。由于arg是dynamic,所以C#编译器会生成payload代码,以便在运行时检查arg的实际类型,并决定+操作符实际要做的事情。

第一次调用Plus时,传递的是5(一个Int32),所以Plus向它的调用者返回值10。结果放到result变量(一个dynamic类型)中。然后调用M方法,将result传给它。针对对M的调用,编译器会生成payload代码,以便在运行时检查传给M的值的实际类型,以及应该调用M方法的重载版本。

第二次调用Plus时,同第一次的原理一样。

在字段类型、方法参数或方法返回类型被指定为dynamic的前提下,编译器会将这个类型转换为System.Object,并在元数据中向字段、参数或方法类型应用System.Runtime.ComplierServices.DynamicAttribute的一个实例。如果是一个局部变量被指定为dynamic,变量类型也会变成Object,但不会向局部变量应用DynamicAttribute,因为它的使用限制在方法之内。

在运行时,Microsoft.CSharp.dll程序集必须加载到AppDomain中,这会损坏应用程序的性能,并增大内存耗用。虽然能用动态功能简化语法,但也要看是否值得。

第5章 基元类型、引用类型和值类型

标签:

原文地址:http://www.cnblogs.com/chrisghb8812/p/5618275.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!