CLR笔记(一)

前端之家收集整理的这篇文章主要介绍了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- 未优化 优化 默认设置
/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命令检查程序集所有方法,指出其中的不安全代码方法

  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: 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不支持创建多文件程序集。

  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方法使用的是反射技术。

猜你在找的VB相关文章