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