`

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, v);    //要装箱3次

                Object o = v;   //手动装箱
                Console.WriteLine("{0}, {1}, {2}", o, 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方法使用的是反射技术。

 

分享到:
评论

相关推荐

    CLR Via C# 读书笔记

    这是国外一位程序员写的CLR Via C#读书笔记,写得挺好,拿出来跟大家分享

    重温C# clr 笔记总结

    本篇文章是对以前学习C# clr做的一些笔记,现在拿出来和大家分享下,希望需要的朋友能参考一下

    NICK笔记.CLR

    Nick 的学习CLR的总结, 其中使用大量图片说明CLR内部实现机制.

    c#学习笔记.txt

    c#学习笔记(1) 51099在线学习网发布 文章来源:网络收集 发布时间:2006-05-25 字体: [大 中 小] 51099在线学习网 http://www.51099.com 1, 结构(struct) 与 类(class) [attributes] [modifiers] struct ...

    带着问题读CLR via C#(笔记一)CLR的执行模型

    A1: CLR (Common Language Runtime) 是一个可以由多种编程语言使用的“运行时”。 Q2: CLR的核心功能有哪些? A2: 1)内存管理;2)程序集加载;3)安全性;4)异常处理;5)线程同步 Q3: CLR与使用的编程语言有关吗...

    麻省理工学院算法导论中英文版习题笔记全\solution to CLR(算法导论习题答案).pdf

    麻省理工学院算法导论中英文版习题笔记全\solution to CLR(算法导论习题答案).pdf

    带着问题读CLR via C#(笔记二)类型基础

    A2: 1)计算对象所需字节数,包括该类型及其基类型定义的所有实例字段所需的字节数和类型对象指针、同步块索引所需字节数,类型指针和同步块索引是CLR用来管理对象的;2)在托管堆上分配该对象所需内存空间;3)初始...

    联想昭阳K46序列笔记本添加SLIC 2.1的Marker工具

    请在纯DOS分别执行 CLR.BAT 和 FLASH.BAT,出现active……即成功。 注: 此SLIC 2.1的ID为"CB-01", 是官方原生版本. 此工具适合联想昭阳K46全系列笔记本(如果是SLIC 2.1,则不必Marker),以及同一代工厂出的其它序号...

    net学习笔记及其他代码应用

    1. 简述 private、 protected、 public、 internal 修饰符的访问权限。 答 . private : 私有成员, 在类的内部才可以访问。...47.当一个线程进入一个对象的一个synchronized方法后,其它线程是否可...

    c#手写笔记文档 适用于c#实战开发

    C# 程序在 .NET 上运行,而 .NET 是名为公共语言运行时 (CLR) 的虚执行系统和一组类库。 CLR 是 Microsoft 对公共语言基础结构 (CLI) 国际标准的实现。 CLI 是创建执行和开发环境的基础,语言和库可以在其中无缝地...

    c#笔记总结

    C#从入门到精通学习笔记.1.C#语言特性: 不可以直接操作内存,去掉了指针操作。用CLR中的GC来管理 面向对象:封装、继承、多态 支持HTML,XML,SOAP等 2.C#与.net框架的关系: C#只是.net框架所支持的一种语言。C#...

    C#OOP笔记大全

    1.深入.NET框架 ... CLR : Common Language Runtime 公共语言运行时 1.CTS Common Type System 通用类型系统 2.CLS Common Language Specification 公共语言规范 FCL : Framework Class Libary 框架类库

    一键恢复4.65.0

    用光盘启动进入DOS,提示A:\在这里输入d:\(新硬盘会提示这个盘符的,如果是已经有分区的硬盘,找到光盘的盘符就OK),然后在这里输入hpatool /view,查看以前是否安装,如果安装则输入:hpatool/clr然后在命令提示符下...

    联想笔记本 Lenovo E46 Marker_TOOL slic 2.1开通

    联想E46 Marker TOOL 工具 dos下运行 先运行clr.bat 再运行flash.bat

    ASP.NET预备知识学习笔记

    是一套应用程序开发框架,主要目的提供一个开发模型。 主要的两个组件:  公共语言运行时(Common Language Runtime)(CLR): 提供内存管理、线程管理和远程处理等核心服务,并且还强制实施严格的安全类型,提高...

    ExceptionBreaker:Visual Studio扩展提供了一种快速切换所有异常中断的方法

    概述ExceptionBreaker是VS扩展,它提供了一种方法来快速切换所有CLR异常。 可通过“ Debug工具栏和“ Debug顶层菜单使用。安装可以从安装扩展。 支援的VS版本:2010、2012、2013、2015。细节在“ Common Language ...

Global site tag (gtag.js) - Google Analytics