CLR笔记(一)
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头 CLR版本,入口方法,模块元数据,资源,强名称 3.元数据 3种类型的表 4.IL代码 元数据包括: 1.描述了模块中定义的内容,比如类及其成员 2.指出了托管模块引用的内容,比如导入的类及其成员 3.清单manifest,描述了构成Assembly的文件,由Assembly中的文件实现的公共导出类型,与Assembly相关联的资源/数据文件 元数据总是嵌入到与代码相同的EXE/DLL中,始终与IL保持同步。元数据用途: 1.消除了对头/库文件的依赖,直接从托管模块中读取 2.智能感知,从元数据中解析 3.代码验证,使用元数据确保代码只执行安全操作 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 开关设置 IL代码质量 JIT本地代码质量 /optimize- ,/debug- 未优化 优化 默认设置 生成未优化的IL时,会在IL中生成NOP指令用于调试,设置断点。 IL是基于堆栈的。所有指令都是:将操作数压栈,结果则从栈中弹出 IL有安全验证机制,保证每一行IL代码是正确的,不会非法访问内存,每个托管EXE都在独自的AppDomain中运行。 不安全代码:允许C#直接操作内存字节,在COM互操作时使用,csc以/unsafe开关标记包含不安全代码,其中所有方法都使用unsafe关键字。 PEVerify命令检查程序集所有方法,指出其中的不安全代码方法。 1.5 本地代码生成器 NGEN.exe NGEN.exe将IL预先编译到硬盘文件中,可以加快程序的启动速度,减小程序的工作集(所有加载该程序集的AppDomain不再copy其副本,因为该程序集已经与编译到文件中,是代码共享的)。 缺点是: 不能保护IL外泄 生成的文件可能失去同步 因为在文件中要计算首选基地址,而NGEN是静态计算好的,所以要修改基地址,速度会慢下来 较差的执行性能,NGEN生成的代码没有JIT好。 如果不能使用NGEN生成的文件,会自动加载JITCompiler。 1.7 CTS CTS的一些规定: 1.一个类型可以包含0个或多个成员 2.类型可视化以及类型成员的访问规则 3.定义了继承,虚方法,对象生成期的管理规则 4.所有类型最终都从预定义的System.Object继承 1.8 CLS 如果在C#中定义的类型及其方法,可以在VB中使用,那么,就不能在C#中定义CLS外的任何public/protected特性,privated的类型及其成员不受限制。 C#可以有仅大小写不同的两个方法——不符合CLS,所以不能是public的。 使用[assembly:CLSComplant(true)]标志程序集,告诉编译器检查该程序集的CLS相容性。书上写得不明白,我这里做了一个测试: using System; 注意,[assembly:CLSComplant(true)]要写在namespace外。 我定义了两个不同方法A和a,编译器会有警告,说这样的语法不兼容CLS;如果去掉[assembly:CLSComplant(true)]声明,那么不会有这个警告;如果将a方法改为private,则不会有警告。 中途我使用了ILDasm观察这个dll,发现两个方法A和a都存在于IL中,说明IL的语法范围也大于CLS。 在VB中,我添加了对此dll的引用: Imports ClassLibrary2 发现,在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不支持创建多文件程序集。 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开关 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 如果没有,重头再来找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(); 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) 上述代码检测两次对象类型,一次在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 这里,被注释掉的语句肯定会检查到溢出,运行期抱错;而第二句是在Int32中检查,所以不会溢出。注意这两条语句只是为了说明check什么时候发挥作用,是两条不同语义的语句,而不是一条语句的正误两种写法。 3.使用 checked和unchecked语句,达到与check操作符相同的效果: int b = 32767; // Max short value checked 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(); SomeVal v2; 使用值类型而不是引用类型的情况: 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)] Explicit排列,一般用于COM互操作 [StructLayout(LayoutKind.Explicit)] [FieldOffset(0)] 5.3 值类型的装箱和拆箱 boxing机制: 1.从托管堆分配内存,包括值类型各个字段的内存,以及两个额外成员的内存:类型对象指针和同步块索引。 2.将值类型的字段复制到新分配的堆内存。 3.返回对象的地址。 ——这样一来,已装箱对象的生存期 超过了 未装箱的值类型生存期。后者可以重用,而前者一直到垃圾收集才回收。 unboxing机制: 1.获取已装箱对象的各个字段的地址。 2.将这些字段包含的值从堆中复制到基于堆栈的值类型实例中。 ——这里,引用变量如果为null,对其拆箱时抛出NullRefernceException异常;拆箱时如果不能正确转型,则抛出InvalidCastException异常。 装箱之前是什么类型,拆箱时也要转成该类型,转成其基类或子类都不行,所以以下语句要这么写: Int32 x = 5; 拆箱操作返回的是一个已装箱对象的未装箱部分的地址。 大多数方法进行重载是为了减少值类型的装箱次数,例如Console.WriteLine提供多种类型参数的重载,从而即使是Console.WriteLine(3);也不会装箱。注意,也许WriteLine会在内部对3进行装箱,但无法加以控制,也就默认为不装箱了。我们所要做的,就是尽可能的手动消除装箱操作。 可以为自己的类定义泛型方法,这样类型参数就可以为值类型,从而不用装箱。 最差情况下,也要手动控制装箱,减少装箱次数,如下: Int32 v = 5; Object o = v; //手动装箱 由于未装箱的值类型没有同步块索引,所以不能使用System.Threading.Monitor的各种方法,也不能使用lock语句。 值类型可以使用System.Object的虚方法Equals,GetHashCode,和ToString,由于System.ValueType重写了这些虚方法,而且希望参数使用未装箱类型。即使是我们自己重写了这些虚方法,也是不需要装箱的——CLR以非虚的方式直接调用这些虚方法,因为值类型不可能被派生。 值类型可以使用System.Object的非虚方法GetType和MemberwiseClone,要求对值类型进行装箱 值类型可以继承接口,并且该值类型实例可以转型为这个接口,这时候要求对该实例进行装箱 5.4使用接口改变已装箱值类型 interface IChangeBoxedPoint struct Point : IChangeBoxedPoint public Point(int x) public void Change(int x) public override string ToString() class Program Object obj = p; ((Point)obj).Change(3); ((IChangeBoxedPoint)p).Change(4); ((IChangeBoxedPoint)obj).Change(5); 5.5 对象相等性和身份标识 相等性:equality 同一性:identity System.Object的Equal方法实现的是同一性,这是目前Equal的实现方式,也就是说,这两个指向同一个对象的引用是同一个对象: public class Object return false; 但现实中我们需要判断相等性,也就是说,可能是具有相同类型与成员的两个对象,所以我们要重写Equal方法: public class Object 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方法使用的是反射技术。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |