CLR via C#-----chapter 5 原型,引用类型,和值类型【原版翻译总结版】

原型,引用类型,和值类型。

  • 使用原型编程:

  某些数据类型是非常常见的,很多编译器都允许使用很简单的语法对其进行操作。例如你可以分配一个整数:

  System.Int32 a = new System.Int32();

  但是我想你对这种整数负值和初始化的过程是感动很笨重的。幸运的是,很多编译器都允许如下的赋值方式:

  Int a = 0;

  原型(Primitive types)定义的引入:任何编译器可以直接支持的类型就是原型。原型在FCLFramework Class Library)中直接映射到存在的类型。例如在C#中,int 直接映射到    System.Int32类型。因为如此,如下的4行代码都能正确的编译,并且产生相同的IL代码。

  Int a = 0;

  System.Int32 a = 0;

  Int a = new int();

  System.Int32 a = new System.Int32();

 

l  我发现很多开发者感到困惑,或者是不知道在代码中使用String或者是string,因为在C#string(关键字)映射到FCL中的类型就是String,使用哪个都没有区别。

l  C#中,long映射到System.Int64,但是在不同的编程语言中,long可能会映射到int16或者是int32。事实上,C++/CLI 并不把long当作int32来处理。有些人在读一种语言的源代码的时候可能会很容易误解代码的意图,如果他做过其他语言的编程的话。事实上,大多数的语言甚至都不把long当作关键字,用了这个关键字的地方不进行编译。

l  FCL中有很多方法,他们的类型名字是方法名字的一部分。例如,在BinaryReader 类型提供了几个方法是ReadBooleanReadInt32ReadSingle等等,并且 System.Convert 类型提供了一个ToBooleanToInt32, ToSingle等等。虽然,如下代码是合法的,但是float的那一行我感觉很不自然:

Float val = br.ReadSingle();

Single val = br.ReadSingle();

  • CheckedUnchecked 原型操作

不同的语言对溢出有不同的处理方式。C C++ 不考虑溢出是一种错误,并且允许类型包装。一个应用程序同样会继续执行。但是,VB一般认为溢出是错误并且抛出异常。

C# 允许程序员决定去决定,溢出应该如何被处理。默认情况下,溢出的处理是会关闭的。这意味着编译器在进行不同版本的加减乘除计算的生成IL代码的时候并不包括了类型的检查。其结果就是运行很快,但是,程序员必须保证他们写的东西不会有异常并且代码会接受溢出。

C#里面一个控制溢出的途径就是使用check 编译转换。转换告诉我们,编译器生成IL代码的时候会进行加减乘除溢出的检查。这样的话代码就会执行的慢一些了,因为CLR会检查这些操作会不会发生溢出。如果发生的话,CLR会抛出一个overflowException异常。

Byte b = 100;

b = Checked((Byte) (b + 200));

  上面这个例子就是,首相b200第一次转换到32位值,并且加到一起。结果是300,然后300 被外面转换到Byte ,这会产生一个Overflow异常。如果Byte转换在checked操作符之外就不会:

b = (Byte) checked(b + 200);

  除了checkedunchecked操作符之外,C#同样会提供Checkedunchecked状态。

checked{

Byte b = 100;

B = (Byte) (b + 200);

}

事实上,如果用了一个checked状态的程序块,就可以使用 += 操作符了。如下;

Checked{

         Byte b = 100;

         b += 200;

}

使用check unchecked 的最佳途径:

l  在这样一个情况下使用checked,程序块中会发生不想要的错误的输入导致的异常,比如一个终端用户的请求。

l  如果你清楚的知道,一个程序块内部的溢出是可以接受的,那么在这个程序块的外面使用unchecked

l  对于任何不使用uncheckedchecked的代码,假定你希望一个溢出的异常发生的话,比如计算一些未知的输入,并且溢出是bug

注意:

  System.Decimal类型是一个特殊的类型。虽然很多编程语言包括C# VB在内都认为Decimal是原型,但是CLR不是这样认为的。这就意味着CLR并没有IL结构,知道如何操作Decimal类型。如果你在SDK中查看Decimal类型,你会发现他有很多公共的静态方法,AddSubstrackMultiplyDivide等等。除此之外,Decimal类型提供了对+ -*/,操作符的重载。

  • 引用类型和值类型

  CLR提供了2中类型,一种是值类型一种是引用类型。当很多类型在FCL中是引用类型,程序到用到的最多的是值类型。引用类型大多在堆分配的时候或者是C#new操作符的使用上面。

  如果每一个类型都是引用的类型的话,那么一个应用程序会表现出来受到很大的影响。假想一下,如果每次都是用 Int32类型的话,那么内存的分配将是一个多么可怜的过程啊。为了提高这种表现,在类型的使用上面,CLR提供了一种轻量级的类型是值类型,值类型的内存分配通常在线程的栈上。一个值类型的变量代表了并不包含指向另一个实例的指针,通常的只是只包含实例本身。

  如果仔细的看SDK,你会发现所有的结构都是从立即从System.ValueType这个抽象的类型继承而来的,这个类型它自己也是从System.Object继承而来的,根据定义,所有的值类型必须从System.ValueType继承而来,而所有的枚举类型都要从System.Enum抽象类型继承而来。

尽管在你定义一个类型的时候不能够选择它的基类,但是一个值类型可以实现一个或者多个你可以自己选择的接口。除此之外,所有的值类型都是sealed(密封的),也就是禁止了其它的类型以它作为基类进行派生。也就是说,例如,不允许定义一些新的类型,以BooleanCharInt32Uint64SingleDoubleDecimal等等作为他们的基类。

如下的这段代码可以看出值类型和引用类型的区别:

Class SomeRef{ public Int32 x;}

Struct SomeVal{public Int32 x;}

Static void ValueTypeDemo()

{

         SomeRef r1 = new SomeRef();

         SomeVal v1 = new SomeVal();

         R1.x = 5; //dispays “5”

         V1.x = 5; //dispays “5”

         Console.writeLine(r1.x);

         Console.WriteLine(v1.x);

         SomeRef r2 = r1;

         SomeVal v2 = v1;

         R1.x = 8;

         V1.x = 9;

         Console.WriteLine(r1.x);//dispays “8”

         Console.WriteLine(r2.x);//dispays “8”

         Console.WriteLine(v1.x);//dispays “9”

         Console.WriteLine(v2.y);//dispays “5”

}

  这段代码中SomeVal类型定义为struct类型。在C#中,struct是值类型的,而class的定义是引用类型的。就像你看见的一样,值类型和引用的类型的表现差别还是不小的。

在设计你的代码的过程中,认真的考虑应该定义值类型还是引用类型。在某些情况下,值类型比应用类型表现的更好。特别是,对于以下的情况下。使用值类型更佳:

l  不会派生。

l  不是派生来的。

l  扮演着原型的角色。

        

     你的类型实例的大小也是一个需要考虑的一个条件,通常情况下,变量在传值的过程中,会引发值类型实例的拷贝,那么也会影响到效率的。同样的,在一个方法调用的过程中,实例会被建立并且在调用者copy的时候分配到内存中,也会有效率上的影响。

 

所以,以下情况下也是需要定义为值类型的:

l  实例类型比较小(大约是16byte或者更小)

l  实例类型可以大于16byte但是他不会被通过方法的参数传递而作为返回值。

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

l  值类型对象有2个表现类型:装箱和拆箱形式。而引用类型只有装箱的形式。

l  值类型是从System.ValueType继承而来的。

l  因为你不能定义一个新的值类型或者是一个引用类型他是把一个值类型作为基类的,所以你不能定义一个virtual的方法给一个值类型因为所有的abstract都是不能派生的,没有意义也。

l  引用类型的变量包含一个对象的在堆内内存的地址。缺省状态下,当一个引用类型被创建的时候,初始化的是null,那就是说,引用类型的对象当并不是指向一个有效的对象。当使用一个引用类型的时候会报出NullReferenceException异常。

l  当把一个值类型赋值给另一个值类型的时候,逐字段拷贝。当把一个引用类型赋值给另一个引用类型的时候,只有内存的地址将会被拷贝。

  • 类型的装箱和拆箱(Boxing && UnBoxing)

         较之于引用类型,值类型算是轻量级的了。因为在管理好的堆中没有被当作对象分配,没有垃圾回收机制,没有被一个指针进行引用。然而,在很多情况下,你必须要得到一个值类型实例的引用。例如,你要建立一个ArrayList的对象,来承载一系列的结构。

Struct Point

{

         Public Int 32 x,y;

}

Public sealed class Program

{

         Public static void Main()

         {

                   ArrayList a = new ArrayList();

                   Point p;

                   For(Int32 I = 0; i<10 ;i++)

                            {

                                     p.x = p.y = I;

                                     a.Add(p);

                            }

                   …………….

         }

}

         每一次循环的迭代,一个poinvalue类型的域将会被初始化,之后point将会呗存储的到ArrayList中去。我们停下来思考一下,被存在ArrayList中的到底是什么?为了得到答案,我们应该看看ArrayList 中的Add方法的变量的定义来看个究竟。Add 方法的原型定义如下:

         Public virtual AddObject value);

         从上我们可以看出,Add把一个对象看做是变量,也就意味着在堆管理上add需要一个对象的引用作为参数。但是在实际的过程中,我传递的是p,一个point,是一个值类型。对于这段代码的应用,必须吧一个值类型转换为一个真正的对管理中的对象,这个对象的引用也要包含在其中。

         用一个装箱的机制可以吧一个值类型转换为引用类型。当一个实例被从一个值类型装箱到引用类型的时候会发生如下的变化:

1.         堆管理的内存被分配。

2.         值类型会被复制到新分配的对内存中去。

3.         对象的地址将会被返回。地址现在是一个对象的引用。值类型现在是一个引用类型。

         对一个值类型的实例进行装箱的过程中,C#编译器自动的产生IL代码,但是你仍然需要明白,代码的内部发生了什么。

 

  • 对象等同和身份(Object Equality and Identity

         System.Object类型,提供了一个virtual方法Equals,它的主要意图就是当两个对象相等的时候返回trueObjectEquals方法的实现大概是这样的:

         Public class Object

         {

                   Public virtual Boolean Equals(Object obj)

                   {

                            If(this == obj) return true;

                            Return false;

                   }

         }

         这看起来是很合理的,因为Equals方法知道一个对象必须等同于他自己。然而,如果讨论一下参数涉及到不同对象的,如果对象包含一样的值的话Equals可能就不确定的,也许会返回false。换句话说,Equals默认的实现是实现了身份验证而不是相等。

1.         如果obj参数是null,返回false。显然的,当这个非静态的方法被调用的时候,因为当前的对象通过this很明显不是null就会返回false了。

2.         如果thisobj这两个变量涉及到不同的类型,返回false。明显的,检验string对象和FileStream对象是不是相等也会导致false

3.         调用基类的Equals方法,这样的话可以与任何定义的域进行比较。如果基类的Equals方法返回false,则返回false

所以,MSobjectEquals方法的实现大概是这个样子的:

Public class Object

{

         Public virtual Boolean Equals(Object obj)

         {

                   If(obj == null) return false;

                   If( this.GetType() != obj.GetType()) return false;

                   Return true;

         }

}

但是MS还是没有这样实现这个方法,实现Equals的柜子要远比你想想的复杂。当一个类型重写了Equals,那么它就必须要调用基类的Equals实现否则的话就是Object的实现了。

  • 对象哈希编码(Object Hash Code

         FCL的设计者认为如果任何一个对象的任何一个实例都放在一个hash表的集合当中去的话是一件很不可思议的事情。出于这个目的,System.Object提供了一个虚拟的方法,GetHashCode. 事实上,MS的编译器忽略了这样一个警告,就是如果你重写了一个类型的Equals方法而不重写GetHashCode方法。利于,编译如下代码会有一个警告:”warning CS0659”.

Public sealed class program

{

         Public override Boolean Equals(Object obj){…..}

}

 

当一个类型重新定义Equals的时候必须是要重写GetHashCode的,因为System.Collections.HashTable类型和Generic.Dictionary需要任何的两个对象相等必须是哈希编码相等。所以当你重写Equals方法的时候必须确保GetHashCode使用的是同样的算法来计算对象的哈希编码。

 

列举一个方法:(直接取两个的异或即可XOR)

Internal sealed calss Point

{

         Private Int32 m_x, m_y;

         Public override Int32 GetHashCode()

         {

                   Return m_x ^ m_y;

         }

}

时间仓促,难免有的地方我可能都不知道翻译过来的是什么。还忘海涵,E文功底有待于提高啊!

posted @ 2009-01-05 16:17  AlexLiu  阅读(502)  评论(0编辑  收藏  举报