第二部分 设计类型:第5章 基元类型、引用类型和值类型
5.1编程语言的基元类型
基元类型:编译器直接支持的数据类型。
基元类型直接映射到Framework类库中存在的类型。如C#中,int->System.Int32
简化过程:
System.Int32 a = new System.Int32();
int a = new int();
System.Int32 a = 0;
int a = 0;
编译器自动在所有源代码文件中添加using指令:
using int = System.Int32;
...
C#语言规范推荐使用关键字,而不是完整类型名称。
作者认为应该使用完整类型名称,原因如下:
1.写法的困惑。string与String
2.语言的差异。C#中long映射到System.Int64,但其他语言可能不一样。
3.FCL(Framework Class Library)许多方法都将类型名称作为方法名的一部分。
BinaryReader br = new BinaryReader(...);
float val = br.ReadSingle();//正确,但感觉不自然。
Single val = br.ReadSingle();//正确,而且让人一目了然。
4.c#程序员的习惯问题。
显示和隐式转换:
不丢失精度,转化为范围更大的为隐式转换。
丢失精度,转化为范围更小的为显示转换。
C#编译器采用截断处理:6.8最终将放6到Int32中。其他编译器可能是向上取整。
基本类型能写成文本常量(literal),文本常量可被看成是类型本身的一个实例。
Console.WriteLine(123.ToString() + 456.ToString()); //"123456"
如果一个表达式由文本常量构成,编译器能在编译时完成表达式的求值,增强程序性能:
Boolean found = false; //生成的代码将found设为0
Int32 x = 100 + 20 + 3; //生成的代码将x设为123
String s = "a " + "bc"; //生成的代码将s设为"a bc"
编译器知道顺序解析操作符(+,-,++,--等)
checked和unchecked基元类型操作
UInt32 invalid = unchecked(UInt32) (-1); //OK 不检查是否溢出
Byte b = 100;
b = checked((Byte)(b + 200)); //抛出OverflowException异常
语句:
checked{
Byte b = 100;
b = (Byte)(b+200); //该表达式会进行溢出检查
SomeMethod(400); //调用方法不会对方法造成任何影响
}
checked、unchecked操作符和语句唯一作用为:决定生成哪一个版本的加减乘和数据转换IL指令。
建议:
1.尽量使用有符号数值类型(如,Int32和Int64),不要使用无符号数值类型(UInt32、Uint64),无符号数值类型不相容于CLS。类库的多个部分被硬编码为返回有符号的值(如Array和String的Length属性)。
2.代码可能发生你不希望的溢出,把这些代码放到checked块中,同时捕捉OverflowException,从容得体地从错误中恢复。
3.将允许发生溢出的代码放到unchecked块中,比如计算一个校验和的时候。
4.对于没有使用checked或unchecked的任何代码,都假定你希望在发生溢出时抛出一个异常,比如在输入是已知的前提下计算一些东西(比如质素),此时的溢出应被记为bug。
开发时,打开编译器/checked+开关来调试性生成。生成 - 高级 - 检查运算上溢/下溢
正式发布时,改为/checked-
5.2引用类型和值类型
CLR支持两种类型:引用类型 + 值类型
引用类型总是从托管堆上分配的。
值类型的实例一般分配在线程栈上(虽然也可作为字段嵌入引用类型的对象中)。
值类型的变量包含了实例本身的字段。
值类型=结构+枚举
System.Object=>System.ValueType=>结构
System.Object=>System.ValueType=>System.Enum抽象类=>枚举
所有值类型必须从System.ValueType派生
一个值类型可以实现一个或多个接口。
值类型都是隐式密封的(sealed),防止作为其他类型的基类型。
一般值类型的性能更好,定义类型的时候可以先考虑定义成值类型,不过要求比较严格,大部分情况还是定义为引用类型。
5.3 值类型的装箱和拆箱
装箱:值类型=>引用类型
装箱操作内部发生的事:
1.在托管堆中分配好内存。内存=值类型字段需要内存+托管堆所有对象都有的两个额外成员(类型对象指针、同步块索引)需要的内存
2.值类型的字段复制到新分配的堆内存。
3.返回对象的地址(对象的引用)。值类型现在是一个引用类型。
拆箱:引用类型=>值类型 获取指针的过程,指针指向对象中的原始值类型(数据字段),指向的是已装箱实例中的未装箱部分。
已装箱的值类型实例在拆箱时内部发生的事情:
1.如果包含了“对已装箱值类型实例的引用”的变量为null,就抛出NullReferenceException异常。
2.如果引用指向的对象不是所期待的值类型的一个已装箱实例,就抛出一个InvalidCastException异常。
未装箱值类型比引用类型更“轻型”的原因:
1.它们不在托管堆上分配。
2.它们没有堆上的每个对象的额外成员(类型对象指针、同步块索引)。
5.4 对象哈希码
System.Object提供了虚方法GetHashCode获取任意对象的Int32哈希码。
如果重写了Equals那么还应该重写下GetHashCode,因为两个对象是否相等是看哈希码是否一样。
•集合中添加一个键值对时,首先获取键对象的一个哈希码,这个哈希码指出键值对应该存储到哪个哈希桶中。
•集合查找键时,会获取指定的键对象的哈希码,这个哈希码标识了现在要搜索的目标哈希桶,在其中查找与指定键对象相等的一个键对象。
•一旦修改了集合中的一个键对象,集合就再也找不到对象了。所以,要修改一个哈希表中的键值对象时,正确的做法:移除原来的键值对,修改键对象,再将新的键值对添加回哈希表中。
5.5 dynamic基元类型
C#是一种类型安全的语言,所有表达式都解析成某个类型的实例,编译器生成的代码中,只会执行对这个类型来说有效的操作。
•为了方便开发人员使用反射或与基本组件通信,C#编译器允许将一个表达式的类型标记为dynamic,还可将表达式结果放到变量中,并将变量的类型标记为dynamic。
•用这个dynamic表达式/变量调用一个成员,如字段、属性/索引器、方法、委托、一元/二元/转换操作符。
•代码使用dynamic表达式/变量调用一个成员时,编译器会生成特殊的IL代码(payload有效载荷)来描述所需的操作,在运行时,payload代码根据当前由dynamic表达式/变量引用的对象的实际类型来决定具体执行的操作。