1.CLR的执行模型
术语: CLR :Common Language Runtime 公共语言运行期,有多种不同编程语言使用的运行库
托管模块:Managed Module,一个标准的MS Window可移植执行体文件(32位PE32或64位PE32+)
IL:Intermediate Language 中间语言,又叫托管代码(由CLR管理它的执行)
元数据:Metadata,一系列特殊的数据表
程序集:Assembly,抽象的
JIT:just-in-time 即时编译,将IL编译成本地cpu指令(本地代码)
FCL:Framework Class Library,Framework 类库
CTS:Common Type System,通用类型系统,描述了类型的定义及其行为方式
CLI:Common Language Infrastructure,公共语言基础结构,这是MS提交给ECMA的一个标准,由CTS和其他Framwork组件构成
CLS:Common Language Specfication,公共语言规范,详细规定了一个最小特性集
1.1 将源代码编译成托管模块
CLR编译过程: C#源码文件——C#编译器编译——托管模块(IL和元数据)
托管模块的各个部分:
1.PE32或PE32+头
标志了文件类型,GUI/CUI/DLL,文件生成时间,在32位还是64位上运行
2.CLR头
3.元数据
3种类型的表
4.IL代码
元数据包括:
1.描述了模块中定义的内容,比如类及其成员
2.指出了托管模块引用的内容,比如导入的类及其成员
3.清单manifest,描述了构成Assembly的文件,由Assembly中的文件实现的公共导出类型,与Assembly相关联的资源/数据文件
元数据总是嵌入到与代码相同的EXE/DLL中,始终与IL保持同步。元数据用途:
1.消除了对头/库文件的依赖,直接从托管模块中读取
2.智能感知,从元数据中解析
4.正反序列化
5.垃圾收集器跟踪对象的生存期以及对象的类型
1.2 将托管模块合并成程序集
程序集:一个或多个托管模块/资源文件的逻辑分组,是最小的重用,安全性以及版本控制单元。
既可以生成但文件程序集,也可以生成多文件程序集,这由编译器工具决定。
CLR是和程序集一起工作的,而不是和托管模块
1.3 加载CLR
CLRVer命令,查看机器上所有CLR版本
csc的 /plattform开关,决定生成什么样的程序集:Anycpu,x86,x64,Itanium
1.4 执行Assembly代码
ILAsm命令,将IL编译成Assembly;ILDasm将Assembly编译成IL。
高级语言(C#)只是CLR的一个子集,IL则允许访问CLR的所有功能。
JITCompiler函数,又名JIT编译器(JITter)
在方法首次执行时,CLR检测出Main的代码引用的所有类型,于是CLR分配一个内部数据结构,用于管理对引用类型的访问。
在这个内部结构中,每个方法都有一条对应的纪录以及地址。
对此结构进行初始化时,CLR将每条纪录都设置为CLR内部包含的一个未文档化的函数,即 JITCompiler函数。
JITCompiler函数被调用时,找到相应方法的IL,编译成本地cpu指令,并保存到一个动态内存块中,将该内存地址存入内部结构中,最后JITCompiler函数会跳转到内存块中的代码,执行。
JIT将本地代码保存在动态内存中,一旦程序终止,本地代码会被丢弃。
csc命令有2个开关会影响代码的优化:/optimize ,/debug
/optimize- ,/debug- 未优化 优化 默认设置
/optimize- ,/debug(+/full/pdbonly) 未优化 未优化 VS2005 Degug状态
/optimize+ ,/debug(-/full/pdbonly) 优化 优化 VS2005 Release状态
生成未优化的IL时,会在IL中生成NOP指令用于调试,设置断点。
IL是基于堆栈的。所有指令都是:将操作数压栈,结果则从栈中弹出
IL有安全验证机制,保证每一行IL代码是正确的,不会非法访问内存,每个托管EXE都在独自的AppDomain中运行。
不安全代码:允许C#直接操作内存字节,在COM互操作时使用,csc以/unsafe开关标记包含不安全代码,其中所有方法都使用unsafe关键字。
PEVerify命令检查程序集所有方法,指出其中的不安全代码方法。
NGEN.exe将IL预先编译到硬盘文件中,可以加快程序的启动速度,减小程序的工作集(所有加载该程序集的AppDomain不再copy其副本,因为该程序集已经与编译到文件中,是代码共享的)。
缺点是:
不能保护IL外泄
因为在文件中要计算首选基地址,而NGEN是静态计算好的,所以要修改基地址,速度会慢下来
如果不能使用NGEN生成的文件,会自动加载JITCompiler。
1.7 CTS
CTS的一些规定:
1.一个类型可以包含0个或多个成员
2.类型可视化以及类型成员的访问规则
4.所有类型最终都从预定义的System.Object继承
1.8 CLS
如果在C#中定义的类型及其方法,可以在VB中使用,那么,就不能在C#中定义CLS外的任何public/protected特性,privated的类型及其成员不受限制。
C#可以有仅大小写不同的两个方法——不符合CLS,所以不能是public的。
使用[assembly:CLSComplant(true)]标志程序集,告诉编译器检查该程序集的CLS相容性。书上写得不明白,我这里做了一个测试:
using System;
[assembly: CLSCompliant(true)]
namespace ClassLibrary2
{
public class Class1
{
public void A()
{
}
public void a()
{
}
}
}
注意,[assembly:CLSComplant(true)]要写在namespace外。
我定义了两个不同方法A和a,编译器会有警告,说这样的语法不兼容CLS;如果去掉[assembly:CLSComplant(true)]声明,那么不会有这个警告;如果将a方法改为private,则不会有警告。
中途我使用了ILDasm观察这个dll,发现两个方法A和a都存在于IL中,说明IL的语法范围也大于CLS。
在VB中,我添加了对此dll的引用:
Imports ClassLibrary2
Module Module1Module Module1
Public Class TClass T
Public Function A()Function A() As Integer
Dim c1 As Class1 = New Class1()
End Function
End Class
End Module
发现,在c1.后面不会有A或a方法的智能感知,说明VB不能识别不符合CLS的语法。如果修改了dll中的a方法为private或者删除a方法,则在VB中可以智能感知到A方法。
可以得出结论,不符合CLS的语法,在另一种语言中是看不到的。
1.9 COM互操作
3种互操作情形:
1.托管代码可以调用DLL中包含的非托管函数,如Kernal32.dll,User32.dll
2.托管代码可以使用现成的COM组件
3.非托管代码可以使用托管类型(C#写的ActiveX控件或shell扩展)
2.生成,打包,部署,管理
2.1 .NET Framework部署目标
非.NET程序的问题:
1.DLL hell
2.安装复杂。目录分散,注册表,快捷方式
3.安全性。悄悄下载恶意代码
2.2 将类型集成到模块中——编译器工具csc
csc /out:Program.exe /t:exe /r:Mscorlib.dll Program.cs
由于C#会自动引用Mscorlib.dll,可以省略 /r:Mscorlib.dll
C#默认生成exe(CUI), 所以/t:exe可以省略;dll(程序集 /t:library)和GUI(可视化应用程序 /t:winexe)时不可以省略
C#默认编译成Program.exe,所以/out:Program.exe可以省略
最后精简为:
csc Program.cs
如果不希望默认引用Mscorlib.dll,使用/nostdlib开关
csc /nostdlib Program.cs
注:/t可以写为/target,/r可以写为/reference
/reference:指定引用的dll,可以使用完整路径;如果是不完整的,在以下目录依次查找:
1.工作目录(要编译的cs文件所在)
2.系统目录(csc.exe所在)
3./lib开关指定的目录
4.LIB系统变量指定的目录
应答文件(Response File)
包括一系列编译器命令行开关,执行csc时会将其打开,例如MyProject.rsp中有以下文本:
/out:Program.exe
/t:exe
/r:Mscorlib.dll
那么调用如下:csc @MyProject.rsp Program.cs
这个应答文件的位置,运行csc命令时,先在当前目录(Program.cs所在)查找;后在系统目录(csc.exe所在)查找,如果都有就以前者为准
使用/noconfig开关指定忽略rsp文件
2.3 元数据概述
3种类别的表:定义表,引用表,清单表
1.常见的定义表:ModuleDef,TypeDef,MethodDef,FieldDef,ParamDef,PropertyDef,EventDef
2.常见的引用表:AssemblyRef,ModuleRef,TypeRef,MemberRef
3.常见的清单表:AssemblyDef,FileDef,ManifestResourceDef,ExportedTypesDef
2.4 合并模块以构成一个程序集
CLR总是首先加载包含清单表的文件,然后使用这个清单,加载其他文件。
使用多文件程序集的3个理由:
1.按类别划分类型,放到不同的程序集中
2.可以添加资源/数据文件,使用AL.exe,使其成为程序集的一部分
3.程序集的各个类型可以使用不同的语言来实现,然后使用ILAsm生成IL
csc /t:module A.cs 指示编译器生成不含清单表的清单文件,一般总是一个DLL,生成的文件为A.netmodule
接下来,要把这个netmodule文件附加到一个有清单表的程序集中,使用addmodule开关:
csc /out:FinalAssmbly.dll /t:library /addmodule:A.netmodule B.cs 这里B.cs包含清单表,最终生成FinalAssmbly.dll,如果A.netmodule不存在,便一起会报错。但是运行程序时,A.netmodule可以不存在,仅在调用其中的方法时,才会加载A.netmodule
VS2005中添加引用的“.NET选项”,对应注册表中 HKEY_LOCAL_MACHINESOFTAREMicrosoft.NETFrameworkAssemblyFolders,动态添加键值,VS2005可以在对应的目录下找到dll,并加载到“.NET选项”中。
IL中 Token:0x26000001,000001代表行号,0x26代表FileRef,此外0x01=TypeRef,0x02=TypeDef,0x03=AssemblyRef,0x27=ExportedType。
AL.exe程序集链接器
生成一个DLL,只包括一个清单文件,不包含IL代码,以下生成的是FinalAssmbly.dll:
AL /out:FinalAssmbly.dll /t:library /addmodule:A.netmodule B.netmodule
还可以生成CUI或GUI,但很少这么做,因为要添加/main开关,指定入口方法:
AL /out:FinalAssmbly.dll /t:exe /main:Program.Main /addmodule:A.netmodule B.netmodule
在程序集中包含资源文件,书上讲到了3个开关:
/embled[resource] 嵌入到程序集中,更新清单表的ManifestResourceDef——对应csc的/resource开关
/link[resource] 并不嵌入到程序集中,更新清单表的ManifestResourceDef和FileDef,对应csc的/linkresource开关
/win32res 嵌入标准的Win32文件
/win32icon 嵌入ico文件
2.5 程序集版本资源信息
使用System.Diagnostics.FileVersionInfo的静态方法GetVersionInfo获取这些信息。在VS2005中,这些信息存放在AsseblyInfo.cs中。
使用AL生成程序集时,可以指定开关,来配置这些信息,表从略(见书)
2.6 语言文化
附属程序集satellite assembly,使用一种具体的语言文化来标记的程序集。
使用AL时,通过/[culture]: text来指定语言文化,这里text为en-US,zh-CN等等。也可以直接写在程序集中,使用自定义属性:
[assembly:AssemblyCulture("en-US")]
使用System.Resource.ResourceManager来访问附属程序集的资源。
2.7 简单应用程序部署
这一节讲的是私有部署方式(private deployed assembly),即部署到和应用程序相同的目录中的程序集
2.8 简单管理控制
CLR定位程序集A时,
对于中性neatual语言文化,按照配置文件privatePath属性顺序,先后扫描privatePath指定的目录,直到找到所需:先找A.dll,如下:
AppDirAsmName.dll
AppDirAsmNameAsmName.dll
AppDirfirstPrivatePathAsmName.dll
AppDirfirstPrivatePathAsmNameAsmName.dll
AppDirsecondPrivatePathAsmName.dll
AppDirsecondPrivatePathAsmNameAsmName.dll
如果没有,重头再来找A.exe
附属程序集遵循同样规则,只是目录变为privatePath+"文化名称(如en-US,先找dll,再找exe;如果没有找到,就把文化名称改为en,重头再来)"
3.共享程序集合强命名程序集
3.1 两种程序集,两种部署
CLR有两种程序集,弱命名程序集和强命名程序集,二者基本一样,区别:强命名程序集时用发布者的公钥/私钥对 进行了签名,唯一性的标识了程序集的发布者。弱命名程序集只能私有部署,强命名程序集可以使用全局部署,也可以私有部署。
3.2 为程序集指派强名称
一个强命名的程序集包括4部分重要属性,标志唯一:一个无扩展名的程序集,一个版本号,一个语言文化标志,一个公钥publickey。此外,还使用发布者的私钥进行签名
MyTypes,Version=1.0.8123.0,Culture=neatral,PublicKeyToken=xxxxxxxxxxxxxxxx(公钥标记)
MS使用公钥/私钥加密技术,这样,没有两家公司有相同的公钥/私钥对(除非他们共享公钥/私钥对)。
使用反射获取强命名程序集的PublicKeyToken
创建强命名程序集的步骤:
1.生成公钥/私钥对:使用SN命令,这个命令所有开关都区分大小写
SN -k MyCompany.keys
——这里MyCompany.keys是创建的文件名
2.将原有程序集升级为强命名程序集
csc /keyfile:MyCompany.keys app.cs
——这里,app.cs是包含清单表的文件,不能对不包含清单表的文件签名。C#编译器会打开MyCompany,使用私钥对程序集进行签名,并在清单中嵌入公钥。
用私钥签名一个文件:是指生成一个强命名程序集时,程序集的FileDef清单中列出了包含的所有本件,将每个文件名称添加到清单中,文件的内容都会根据私钥进行哈希处理,得到的哈希值与文件名一起存入FileDef中。这个哈希值称为RSA数字签名。
最终,生成的包含清单的PE32文件,其中会含有RSA数字签名和公钥
补充1:签名默认使用SHA-1算法,也可以使用别的算法,通过AL命令的/algid开关指定。
补充2,还可以使用SN命令,在原有基础上,得到只含公钥的文件并显示:
SN -p MyCompany.keys MyCompany.PublicKey
——这里MyCompany.PublicKey是创建的公钥文件名
SN -pt MyCompany.PublicKey
补充3:在IL中,Local对应于Culture
补充4:公钥标记是公钥的最后8个字节。
AssemblyRef中存的是公钥标记,AssemblyDef存的是公钥。
3.3 GAC 全局程序集缓存
GAC一般在C:WindowsAssembly,结构化的,有很多子目录。
使用Windows Explorer shell扩展来浏览GAC目录,这个工具是在安装Framework时附带的。
不能使用手动方法复制程序集文件到GAC,要使用GACUtil命令。
只能安装强命名程序集到GAC中,而且要有Admin/PowerUser权限。
GAC的好处是可以容纳一个程序集的多个版本。每个版本都有自己的目录。缺点是违反了简单安装的原则。
3.4 在生成的程序集中引用一个强命名程序集
第2章有讲到,对于不完整路径,csc编译时目录查找顺序:
1.工作目录(要编译的cs文件所在)
2.系统目录(csc.exe所在,同时也包括CLR DLL)
3./lib开关指定的目录
4.LIB系统变量指定的目录
安装Framework时,会安装.NET程序集两套副本,一套在编译器/CLR目录——便于生成程序集,另一套在GAC子目录——便于在运行时加载它们。编译时并不去GAC中查找。
3.5 强命名程序集能防范篡改
在安装强命名程序集到GAC时,系统对包含清单的文件内容进行哈希处理,并将这个值与PE32文件中嵌入的RSA数字签名进行比较,如果一致,就再去比较其他文件内容(也是哈希处理在比RSA签名)。一旦有一个不一致,就不能安装到GAC。
如果强命名程序集安装在GAC以外的目录,则会在加载时比较签名。
3.6 延迟签名(部分签名) delayed signing
开发阶段会使用到这个功能
允许开发人员只用公钥来生成一个程序集,而不需要私钥。
编译时,会预留一定空间来存储RSA数字签名,不对文件内容进行哈希处理。CLR会跳过对哈希值的检查。以后可以再对其进行签名。
步骤如下:
1.生成程序集:csc /keyfile: MyCompany.PublicKey /delaysign: MyAssembly.cs
2.跳过对哈希值的检查: SN.exe -Vr MyAssembly.dll
3.准备私钥,再次进行签名: SN.exe -R MyAssembly.dll MyCompany.PrivateKey
4.再次延迟签名: SN.exe -Vu MyAssembly.dll
3.7 私有部署强命名程序集
强命名程序集如果不在GAC中,每次加载都要进行验证,有性能损失。
还可以设计为局部共享强命名程序集,指定配置文件的codeBase即可。
3.8 运行库如何解析类型引用
在TypeRef中查找类型引用的纪录,发现其强签名,然后定位这个程序集的所在位置:会在以下三个地方查找:
1.同一个文件:编译时就能发现(早期绑定)
2.不同的文件,但同一个程序集:在FileRef表中
3.不同的文件,不同的程序集:这时要加载被引用的程序集,从中查找
注:AssemblyRef使用不带扩展名的文件名来引用程序集。绑定程序集时,系统通过探测xx.dll和xx.exe来定位文件。
ModuleDef,ModuleRef,FileDef使用文件名及其扩展名来引用文件。
注:在GAC中查找程序集时,除了名称,版本,语言文化和公钥,还需要cpu体系结构,而且是先根据这个体系结构查找。
4.类型基础
4.1 所有类型都派生自System.Object
System.Object提供的方法:GetType(),ToString(),GetHashCode(),Equals(),MemberwiseClone(),Finalize()
所有对象都是用new操作符创建,创建过程:
1. 计算对象大小,包括“类型对象指针”和“同步块索引”
2.从托管堆分配对象的内存
3.初始化对象的“类型对象指针”和“同步块索引”
4.调用ctor,传入相应参数——最终会调用到System.Object的ctor,该ctor是空操作
5.返回新对象的引用/指针
4.2 强制类型转换
类型安全,CLR的最重要特性之一。
1.对象转成其基类,不需要任何特殊语法,默认为安全隐式转换
Object o = new Employee(); ——将new Employee转为Object基类,可以看作:
Employee e = new Employee();
Object o = e;
2.对象转成其子类,要显示转换 Employee e = (Employee)o;
基于以上原则,有 类型安全性检测:http://www.cnblogs.com/Jax/archive/2007/08/05/844159.html
is和as操作符
is:检查一个对象是否兼容于指定的类型,并返回一个bool值——即使类型不对,仅返回false,不会抛出异常;null对象则返回false
if (o is Employee)
{
Employee e = (Employee)o;
}
上述代码检测两次对象类型,一次在if中的is,另一次在显示转型时——会影响性能,使用as代替。
as:用来简化上述代码:永远不会抛出异常,如果对象不能转型,就返回null:
Employee e = o as Employee;
if (e != null)
{
//执行操作
}
4.3 命名空间和程序集
CLR不知道namespace概念,using是C#的语法,CLR只认识类型的全称
C#会自动在MSCorLib.dll中查找所有核心FCL类型,如Object,Int32,String
记住以下语法:using System = NameSpaceAnotherName;
5.基元,引用和值类型
5.1基元类型
编译器(C#)直接支持的任何数据类型都称为基元类型(primitive type),基元类型直接映射到FCL中存在的类型。可以认为 using string = System.String;自动产生。
FCL中的类型在C#中都有相应的基元类型,但是在CLS中不一定有,如Sbyte,UInt16等等。
C#允许在“安全”的时候隐式转型——不会发生数据丢失,Int32可以转为Int64,但是反过来要显示转换,显示转换时C#对结果进行截断处理。
unchecked和check控制基元类型操作
C#每个运算符都有2套IL指令,如+对应Add和Add.ovf,前者不执行溢出检查,后者要检查并抛出System.OverflowException异常。
溢出检查默认是关闭的,即自动对应Add这样的指令而不是Add.ovf。
控制C#溢出的方法:
1.使用 /check+编译器开关
2.使用操作符checked和unchecked:
int b = 32767; // Max short value
//b = checked((short)(b + 32767)); throw System.OverflowException
b = (short)checked(b + 32767); //return -2
这里,被注释掉的语句肯定会检查到溢出,运行期抱错;而第二句是在Int32中检查,所以不会溢出。注意这两条语句只是为了说明check什么时候发挥作用,是两条不同语义的语句,而不是一条语句的正误两种写法。
3.使用 checked和unchecked语句,达到与check操作符相同的效果:
int b = 32767; // Max short value
checked
{
b = b + 32767;
}
return (short)b;
System.Decimal类型在C#中是基元,但在CLR中不是,所以check对其无效。
5.2 引用类型和值类型
引用类型从托管堆上分配内存,值类型从一个线程堆栈分配。
值类型不需要指针,值类型实例不受垃圾收集器的制约
struct和enum是值类型,其余都是引用类型。这里,Int32,Boolean,Decimal,TimeSpan都是结构。
struct都派生自System.ValueType,后者是从System.Object派生的。enum都派生自System.Enum,后者是从System.ValueType派生的。
值类型都是sealed的,可以实现接口。
new操作符对值类型的影响:C#确保值类型的所有字段都被初始化为0,如果使用new,则C#会认为实例已经被初始化;反之也成立。
SomeVal v1 = new SomeVal();
Int32 a1 = v1.x; //已经初始化为0
SomeVal v2;
Int32 a2 = v2.x; //编译器报错,未初始化
使用值类型而不是引用类型的情况:
1.类型具有一个基元类型的行为:不可变类型,其成员字段不会改变
2.类型不需要从任何类型继承
3.类型是sealed的
4.类型大小:或者类型实例较小(<16k);或者类型实例较大,但不作为参数和返回值使用
值类型有已装箱和未装箱两种形式;引用类型总是已装箱形式。
System.ValueType重写了Equals()方法和GetHashCode()方法;自定义值类型也要重写这两个方法。
引用类型可以为null;值类型总是包含其基础类型的一个值(起码初始化为0),CLR为值类型提供相应的nullable。
copy值类型变量会逐字段复制,从而损害性能,copy引用类型只复制内存地址。
值类型的Finalize()方法是无效的,不会在垃圾自动回收后执行——就是说不会被垃圾收集。
CLR控制类型字段的布局:System.Runtime.InteropServices.StructLayoutAttribute属性,LayoutKind.Auto为自动排列(默认),CLR会选择它认为最好的排列方式;LayoutKind.Sequential会按照我们定义的字段顺序排列;LayoutKind.Explicit按照偏移量在内存中显示排列字段。
[System.Runtime.InteropServices.StructLayout(LayoutKind.Auto)]
struct SomeVal
{
public Int32 x;
public Byte b;
}
Explicit排列,一般用于COM互操作
[StructLayout(LayoutKind.Explicit)]
struct SomeVal
{
[FieldOffset(0)]
public Int32 x;
[FieldOffset(0)]
public Byte b;
}
5.3 值类型的装箱和拆箱
Boxing机制:
1.从托管堆分配内存,包括值类型各个字段的内存,以及两个额外成员的内存:类型对象指针和同步块索引。
2.将值类型的字段复制到新分配的堆内存。
3.返回对象的地址。
——这样一来,已装箱对象的生存期 超过了 未装箱的值类型生存期。后者可以重用,而前者一直到垃圾收集才回收。
unBoxing机制:
1.获取已装箱对象的各个字段的地址。
2.将这些字段包含的值从堆中复制到基于堆栈的值类型实例中。
——这里,引用变量如果为null,对其拆箱时抛出NullRefernceException异常;拆箱时如果不能正确转型,则抛出InvalidCastException异常。
装箱之前是什么类型,拆箱时也要转成该类型,转成其基类或子类都不行,所以以下语句要这么写:
Int32 x = 5;
Object o = x;
Int16 y = (Int16)(Int32)o;
拆箱操作返回的是一个已装箱对象的未装箱部分的地址。
大多数方法进行重载是为了减少值类型的装箱次数,例如Console.WriteLine提供多种类型参数的重载,从而即使是Console.WriteLine(3);也不会装箱。注意,也许WriteLine会在内部对3进行装箱,但无法加以控制,也就默认为不装箱了。我们所要做的,就是尽可能的手动消除装箱操作。
可以为自己的类定义泛型方法,这样类型参数就可以为值类型,从而不用装箱。
最差情况下,也要手动控制装箱,减少装箱次数,如下:
Int32 v = 5;
Console.WriteLine("{0},{1},{2}",v,v); //要装箱3次
Object o = v; //手动装箱
Console.WriteLine("{0},o,o); //仅装箱1次
由于未装箱的值类型没有同步块索引,所以不能使用System.Threading.Monitor的各种方法,也不能使用lock语句。
值类型可以使用System.Object的虚方法Equals,GetHashCode,和ToString,由于System.ValueType重写了这些虚方法,而且希望参数使用未装箱类型。即使是我们自己重写了这些虚方法,也是不需要装箱的——CLR以非虚的方式直接调用这些虚方法,因为值类型不可能被派生。
值类型可以使用System.Object的非虚方法GetType和MemberwiseClone,要求对值类型进行装箱
值类型可以继承接口,并且该值类型实例可以转型为这个接口,这时候要求对该实例进行装箱
5.4使用接口改变已装箱值类型
interface IChangeBoxedPoint
{
void Change(int x);
}
struct Point : IChangeBoxedPoint
{
int x;
public Point(int x)
{
this.x = x;
}
public void Change(int x)
{
this.x = x;
}
public override string ToString()
{
return x.ToString();
}
class Program
{
static void Main(string[] args)
{
Point p = new Point(1);
Object obj = p;
((Point)obj).Change(3);
Console.WriteLine(obj); //输出1,因为change(3)的对象是一个临时对象,并不是obj
((IChangeBoxedPoint)p).Change(4);
Console.WriteLine(p); //输出1,因为change(4)的对象是一个临时的装箱对象,并不是对p操作
((IChangeBoxedPoint)obj).Change(5);
Console.WriteLine(obj); //输出5,因为change(5)的对象是(IChangeBoxedPoint)obj装箱对象,于是使用接口方法,修改引用对象obj
}
}
}
5.5 对象相等性和身份标识
相等性:equality
同一性:identity
System.Object的Equal方法实现的是同一性,这是目前Equal的实现方式,也就是说,这两个指向同一个对象的引用是同一个对象:
public class Object
{
public virtual Boolean Equals(Object obj)
{
if (this == obj) return true; //两个引用,指向同一个对象
return false;
}
}
但现实中我们需要判断相等性,也就是说,可能是具有相同类型与成员的两个对象,所以我们要重写Equal方法:
public class Object
{
public virtual Boolean Equals(Object obj)
{
if (obj == null) return false; //先判断对象不为null
if (this.GetType() != obj.GetType()) return false; //再比较对象类型
//接下来比较所有字段,因为System.Object下没有字段,所以不用比较,值类型则比较引用的值
return true;
}
}
如果重写了Equal方法,就又不能测试同一性了,于是Object提供了静态方法ReferenceEquals()来检测同一性,实现代码同重写前的Equal()。
检测同一性不应使用C#运算符==,因为==可能是重载的,除非将两个对象都转型为Object。
System.ValueType重写了Equals方法,检测相等性,使用反射技术——所以自定义值类型时,还是要重写这个Equal方法来提高性能,不能调用base.Equals()。
重写Equals方法的同时,还需要:
让类型实现System.IEquatable<T>接口的Equals方法。
运算符重载==和!=
如果还需要排序功能,那额外做的事情就多了:要实现System.IComparable的CompareTo方法和System.IComparable<T>的CompareTo方法,以及重载所有比较运算符<,>,<=,>=
5.6 对象哈希码
重写Equals方法的同时,要重写GetHashCode方法,否则编译器会有警告。
——因为System.Collection.HashTable和Generic.Directory的实现中,要求Equal的两个对象要具有相同的哈希码。
HashTable/Directory原理:添加一个key/value时,先获取该键值对的HashCode;查找时,也是查找这个HashCode然后定位。于是一旦修改key/value,就再也找不到这个键值对,所以修改的做法是,先移除原键值对,在添加新的键值对。
不要使用Object.GetHashCode方法来获取某个对象的唯一性。FCL提供了特殊的方法来做这件事:
using System.Runtime.CompilerServices;
RuntimeHelpers.GetHashCode(Object o)
这个GetHashCode方法是静态的,并不是对System.Object的GetHashCode方法重写。
System.ValueType实现的GetHashCode方法使用的是反射技术。