第五章 基元类型,引用类型和值类型

5.1 编程语言的基元类型

编译器直接支持的数据类型称为基元类型。基元类型直接映射到Framework类库中存在的类型。

只有在转换“安全(不会发生数据丢失)”的时候,C#才允许隐式转型。(Int32转换为Int64)如果不安全,C#要求显示转型。(Int64转换成Int32)

基本类型还能写成字面值。字面值可以被看成是类型本身的实例。如(123.ToString();)

checked和unchecked基元类型 可以在代码特定区域控制溢出检查。

5.2 引用类型和值类型

CLR支持两种类型:引用类型和值类型

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

1. 内存必须从托管堆分配。

2. 堆上分配的每个对象都有一些额外成员,这些成员必须初始化。

3. 对象中的其他字节(为字段而设)总是设为零。

4. 从托管堆分配对象时,可能强制执行一次垃圾回收。

值类型:值类型的实例一般在线程栈上分配(也可作为字段嵌入引用类型的对象中)。在代表值类型实例的变量中不包含指向实例的指针。变量中包含了实例本身的字段,所以操作实例中的字段不需要提领指针。值类型的实例不受垃圾回收器的控制。

类是引用类型:从Object.Object派生。

结构和枚举是值类型:所有结构从System.ValueType派生,枚举从System.Enum派生。System.ValueType和System.Enum从System.Object派生。

所有值类型都是隐式密封,目的是防止将值类型用作其他引用类型或值类型的基类型。

设计类型时,满足以下条件可以定义值类型:

1. 类型具有基元类型的行为。(没有成员会修改类型的任何实例字段,如果类型没有提供会更改其字段的成员,就说该类型时不可变类型,建议将全部字段标记为readonly)

2. 类型不需要从其他任何类型继承。

3. 类型也不派生出其他任何类型。

满足以上全部条件,还必须满足以下任意条件:

类型的实例较小(16字节或更小)。

类型的实例较大(大于16字节),但不作为方法实参传递,也不从方法返回。

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

值类型对象有两种表示形式:未装箱和已装箱。引用类型总是处于已装箱形式。

值类型从System.ValueType派生。该类型重写了Equals方法,能在两个对象的字段完全匹配的前提下返回true。也重写了GetHashCode方法。

由于不能将值类型来定义新的值类型或者新的引用类型,所以不应再值类型中引入任何新的虚方法。所有方法不能使抽象的,所有方法都隐式密封(不可重写)。

引用类型的变量包含堆中对象的地址,变量创建时默认初始化为null,表明当前不指向有效对象。值类型的变量总是包含其基础类型的一个值,而且值类型的所有成员都初始化为0.

将值类型变量赋给另一个类型变量,会执行逐字段的复制。将引用类型的变赋给另一个引用类型的变量只复制内存地址。

两个或多个引用类型的变量能引用堆中同一个对象,所有对一个变量执行的操作可能影响到另一个变量引用的对象。值类型变量自成一体,对值类型变量执行的操作不可能影响到另一个值类型变量。

由于未装箱的值类型不在堆上分配,一旦定义了该类型的一个实例的方法不再活动,为它们分配的存储就会被释放,而不是等着进行垃圾回收。

5.3 值类型的装箱和拆箱

将值类型转换成引用类型要使用装箱机制。对值类型的实例进行装箱时会发生以下事情:

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

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

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

拆箱不是直接将装箱过程倒过来。拆箱其实是获取指针的过程,该指针指向包含在一个对象中的原始值类型(数据字段)。其实,指针指向的是已装箱实例中的未装箱部分。

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

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

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

如果值类型重写了其中任何虚方法(比如Equals,GetHashCode,ToString),那么CLR可以以非虚地调用该方法,因为值类型隐士密封,不可能有类型从它们派生,而且调用虚方法的值类型实例没有装箱。如果重写的虚方法要调用方法在基类中的实现,那么在调用基类的实现时,值类型实例会装箱,以便能通过this指针将对一个堆对象的引用传给基方法。

在调用非虚的,继承的方法时,无论如何都要对值类型进行装箱。因为这些方法由System.Object定义,要求this实参是指向堆对象的指针。

将值类型的未装箱实例转型为类型的某个接口时要对实例进行装箱。这是因为接口变量必须包含对堆对象的引用。

5.3.1使用接口更改已装箱值类型中的字段(以及为什么不应该这样做)

建议将值类型的字段都标记为readonly。这样就不用操心什么时候发生装箱和拆箱/字段复制了。

5.3.2 对象相等性和同一性

对于Object的Equals方法的默认实现,它实现的是同一性,而非相等性。

Object的Equals方法的默认实现并不合理,而且永远不应该像这样实现。下面展示了Equals方法应该如何正确地实现。

1. 如果obj实参为null,就放回false,因为调用非静态Equals方法时,this所标识的当前对象显然不为空。

2. 如果this和obj实参引用同一个对象,就返回true。在比较包含大量字段的对象时,这一步有助于提升性能。

3. 如果this和obj对象引用不同类型的对像,就返回false。

4. 针对类型定义的每个实例字段,将this对象中的值与obj对象中的值进行比较。任何字段不相等,就返回false。

5. 调用基类的Equlas方法来比较它定义的任何字段。

同一性:两个引用是否指向同一个对象 调用ReferenceEquals方法。

定义自己的类型时,你重写的Equals要符合相等性的4个特征。

Equals必须自反;x.Equals(x)肯定返回true。

Equals必须对称;x.Equals(y)和y.Equals(x)返回相同的值。

Equals必须可传递;x.Equals(y)返回true,y.Equals(z)返回true,则x.Equals(z)肯定返回true。

Equals必须一致。比较的两个值不变。Equals返回值(true或false)也不能变。

重写Equals方法时,可能还需要做下面几件事情。

让类型实现System.IEquatable<T>接口的Equals方法。

重载==和!=操作符方法。

5.4 对象哈希码

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

  选择算法来计算类型实例的哈希码时,请遵守以下规则。

这个算法要提供良好的随机分布,使哈希表获得最佳性能。

可在算法中调用基类的GetHashCode方法,并包含它的返回值。

算法至少使用一个实例字段。

理想情况下,算法使用的字段应该不可变。

算法执行速度尽量快。

包含相同值得不同对象应返回相同的哈希码。

5.5 dynamic基元类型

类型安全安全的语言优势:

程序员会犯的许多错误都能在编译时检测到,确保代码在尝试执行前时正确的。

能编译出更小,更快的代码,因为能在编译时进行更多预设,并在生成的IL和元数据中落实预设。

为了方便开发人员使用反射或者与其他组件通信,C#编译器允许将表达式的类型标记为dynamic。还可将表达式的结果放到变量中,并将变量类型标记为dynamic。然后,可以用这个dynamic表达/变量调用成员。

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

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

泛型类,结构,接口,委托或方法的泛型类型实参也可以是dynamic类型。使用的泛型代码是已近编译好的,会将类型视为Ojbect;编译器不在泛型代码中生成paload代码,所以不会执行动态调度。

为COM对象生成可由“运行时”调用的包装程序集时,COM方法中使用的任何VARIANT实际都要转换成dynamic;这称为动态化(dynamicfication)。

paload代码使用了称为“运行时绑定器”的类。paload代码执行时,会在运行时生成动态代码;这些代码进入驻留于内存的程序集,即“匿名寄宿的DynamicMethods程序集”,作用时当特定call site 使用具有相同运行时类型的动态实参发出大量调用时增强动态调度性能。

dynamic的一个限制是只能访问对象的实例成员,因为dynamci变量必须引用对象。

每天学习一丢丢
原文地址:https://www.cnblogs.com/terry-1/p/9744430.html