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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章