CLR via C# (第3版) – Jeffrey Richter 著 – 讀書筆記

來源:互聯網
上載者:User

 

 

第1章 CLR的執行模型

託管模組的各個組成部分:PE32或PE32+頭,CLR頭,中繼資料,IL(中繼語言)代碼。

 

進階語言通常只公開了CLR的所有功能的一個子集。然而,IL組合語言允許開發人員訪問CLR的所有功能。

 

JITCompiler函數負責將一個方法的IL代碼編譯成本地CPU指令。由於IL是“即時”(just in time)編譯的,所以通常將CLR的這個組件稱為JITter或者JIT編譯器。

 

Microsoft定義了一個“Common Language Specification”(Common Language Specification, CLS),它定義了所有語言都必須支援的一個最小功能集。

 

第2章 產生、打包、部署和管理應用程式及類型

回應檔(response file)是一個文字檔,其中包含一組編譯器命令列開關。

除了在命令列上顯式指定的檔案,編譯器還會自動尋找兩個名為CSC.rsp的檔案(本地的和全域的) 。

本地和全域回應檔中的某個設定發生衝突,將以本地檔案的設定為準。類似地,命令列上顯式指定的設定將覆蓋本地回應檔中的設定。

 

為了增加趣味性,在ILDasm中選擇“視圖”|“統計”,會顯示有趣的資訊。

 

AssemblyVersion 這個版本號碼非常重要,它唯一性地標識了一個程式集。一個程式集將與所引用的程式集的一個特定的版本緊密綁定到一起。

 

第3章 共用組件和強命名程式集

顯然,只根據檔案名稱來區分程式集是不夠的。CLR必須提供對程式集進行唯一性標識的機制。這正是“強命名程式集”的來曆。一個強命名的程式集具有4個重要attributes,它們共同對程式集進行了唯一性標識:一個檔案名稱(不計副檔名)、一個版本號碼、一個語言文化(culture)標識以及一個公開金鑰。

 

由於公開金鑰是非常大的數字,所以經常使用從公開金鑰派生的一個小的雜湊值。這個雜湊值稱為 公開金鑰標記(public key token)。

 

CLR在做出安全或信任決策時,永遠都不會使用公開金鑰標記,因為幾個公開金鑰可能在雜湊處理之後得到同一個公開金鑰標記。

 

如果一個程式集要由多個應用程式訪問,必須把它放到一個已知的目錄中,而且CLR在檢測到對該程式集的一個引用時,必須知道自動檢查該目錄。這個已知的位置稱為全域組件快取(Global Assembly Cache, GAC).

 

將一個強命名的程式集安裝到GAC時,系統會執行一次檢查,核實含有清單的那個檔案沒有被篡改。這個檢查只在安裝時執行一次。相反,從非GAC的一個目錄載入強命名程式集時,CLR會校正程式集的資訊清單檔,核實檔案的內容未被篡改,造成該檔案每次載入都會帶來額外的效能開銷。

 

第4章 類型基礎

is操作符、as操作符永遠不會拋出異常。

is操作符使用如下時:

if (o is Employee) 
{
    Employee e = (Employee) o;
}

CLR實際會檢查兩次對象的類型,無疑對效能造成一定影響。C#專門提供了as操作符,目的就是簡化這種代碼的寫法,同時提升其效能。

Employee e = o as Employee;
if (e != null) { }

如果o不相容於Employee類型,as操作符會返回null,這隻造成CLR校正一次對象的類型。if語句只是檢查e是否為null,這個檢查的速度比校正對象的類型快得多。

 

C#編譯器提供了一個名為外部別名(extern alias)的功能,它解決了這個雖然罕見但仍有可能發生的問題:通過編程來區分不同的程式集,而非僅能區分不同的命名空間。外部別名還允許從同一個程式集的兩個(或更多)不同的版本中訪問一個類型。欲知外部別名的詳情,請參見C#語言規範。

 

除了全域性地開啟或關閉溢出檢查,程式員還可在代碼的特定地區控制溢出檢查。C#通過提供checkedunchecked操作符來實現這種靈活性。

 

第5章 基元類型、參考型別和實值型別

如果你定義的一個類型重寫了Equals方法,那麼還應重寫GetHashCode方法,確保相等性演算法和對象雜湊碼演算法是一致的。這是因為在System.Collections.Hashtable類型、System.Collections.Generic.Dictionary類型以及其他一些集合的實現中,要求兩個對象為了相等,必須具有相同的雜湊碼。

 

不要混淆dynamicvar。用var聲明一個局部變數只是一種簡化文法,它要求編譯器根據一個運算式推斷具體的資料類型。var關鍵字只能用於聲明方法內部的局部變數,而dynamic關鍵字可用於局部變數、欄位和參數。運算式不能轉型為var,但能轉型為dynamic。必須顯式初始化用var聲明的變數,但無需初始化用dynamic聲明的變數。

 

代碼使用dynamic運算式/變數來調用一個成員時,編譯器會產生特殊的IL代碼來描述所需的操作。這種特殊的代碼稱為payload(有效載荷)。

雖然能用動態功能簡化文法,但也要看是否值得。畢竟,載入所有這些程式集以及額外的記憶體消耗,會對效能產生額外的影響。

 

第6章 類型和成員基礎

構建程式集時,可以使用在System.Runtime.CompilerServices命名空間中定義的一個名為InternalsVisibleTo的attribute來標明它認為是“友元”的其他程式集。

 

設計一個類型時,應盡量減少所定義的虛方法的數量。 首先,調用虛方法的速度比調用非虛方法慢。其次,JIT編譯器不能內嵌(inline)虛方法,這進一步影響了效能。第三,虛方法使組件的版本控制變得更脆弱。第四,定義一個基底類型時,經常需要提供一組重載的簡便方法(convenience method)。如果希望這些方法是多態的,最好的辦法就是使最複雜的方法成為虛方法,使所有重載的簡便方法成為非虛方法。

 

第8章 方法
不要在構造器中調用會影響所構造對象的任何虛方法。原因是假如這個虛方法在當前要執行個體化的類型的衍生類別型中進行了重寫,就會調用重寫的實現。但在繼承階層中,欄位尚未完全初始化。所以,調用虛方法將導致無法預測的行為。

 

關於類型構造器的效能:對於進行了內聯初始化的靜態欄位(會在類的類型定義表中產生一個添加了 BeforeFieldInit中繼資料標記的記錄項),只要在訪問之前初始化就可以了,具體什麼時間無所謂。而顯式類型構造器可能包含具有副作用的代碼,所以需要在精確拿捏啟動並執行時間。

 

擴充方法有潛在的版本控制問題。如果Microsoft未來為他們的StringBuilder類添加了一個IndexOf執行個體方法,而且和My Code試圖調用的原型一樣,那麼在重新編譯My Code時,編譯器會綁定到Microsoft的IndexOf執行個體方法。這樣一來,我的程式就會有不同的行為。這個版本控制問題是使用擴充方法時必須謹慎的另一個原因。

 

關於分部方法,有一些額外的規則和原則需要謹記:

  • 它們只能在部分類別或結構中聲明。
  • 分部方法的傳回型別始終是void,任何參數都不能用out修飾符來標記。分部方法可以有ref參數,可以是泛型方法,可以是執行個體或靜態方法,而且可標記為unsafe
  • 如果兩者都應用了定製attribute,那麼attribute會合并到一起。
  • 分部方法總是被視為private方法。

 

第9章 參數

注意,如果方法是從模組的外部調用的,更改參數的預設值具有潛在的危險性。call site在它的調用中嵌入預設值。如果以後更改了參數的預設值,但沒有重新編譯call site所在的代碼,它在調用你的方法時就會傳遞舊的預設值。可考慮將預設值0/null作為哨兵值使用:

//不要這樣做:
private static String MakePath(String filename = "Untitled") {
    return String.Format(@"C:\{0}.txt", filename);
}

//而是要這樣做:
private static String MakePath(String filename = null) {
    return String.Format(@"C:\{0}.txt", filename ?? "Untitled");
}

這裡使用了空接合操作符(??)

對於以傳引用的方式傳給方法的變數,它的類型必須與方法簽名中聲明的類型相同,原因是保障型別安全。

 

參數和傳回型別的指導原則:聲明方法的參數類型時,應盡量指定最弱的類型,最好是介面而不是基類(偶理解為基本類型)。例如,如果要寫一個方法來處理一組資料項目,最好是用介面(比如IEnumerable<T>)來聲明方法的參數,而不要用強資料類型(比如List<T>)或者更強的介面類型(比如ICollection<T>IList<T>)。

 

第10章 屬性

匿名型別

利用C#的匿名型別功能,可以使用非常簡潔的文法來聲明一個不可變(immutable)的元群組類型(tuple)。元群組類型是含有一組屬性的類型,這些屬性通常以某種方式相互關聯。

var o1 = new { Name = "Jeff", Year = 1964 };

這行代碼建立了一個匿名型別,我沒有在new關鍵字後指定類型名稱,所以編譯器會為我自動建立一個類型名稱,而且不會告訴我這個名稱具體是什麼。

 

編譯器在定義匿名型別時是非常“善解人意”的。如果它看到你在原始碼中定義了多個匿名型別,而且這些類型具有相同的結構,那麼它只會建立一個匿名型別定義,但建立該類型的多個執行個體。 所謂“相同的結構”,是指在這些匿名型別中,每個屬性都有相同的類型和名稱,而且這些屬性的指定順序相同。這樣可以檢查兩個對象是否包含相等的值,並將對一個對象的引用賦給正在指向另一個對象的變數。還可以建立一個隱式類型的數組:

var people = new[] {
    o1, //o1參見上一節
    new { Name = "Kristin", Year = 1970 },
    new { Name = "Aidan", Year = 2003 },
    new { Name = "Grant", Year = 2008 }
};

匿名型別的執行個體不能泄露到一個方法的外部。如果想傳遞一個元組,應考慮System.Tuple類型。

 

和匿名型別相似,一旦建立好一個Tuple,它就不可變了(所有屬性都唯讀)。

當然,非常重要的一點在於,Tuple的生產者(寫它的人)和消費者(用它的人)必須對Item#屬性返回的內容有一個清楚的理解。對於匿名型別,屬性的實際名稱是根據定義匿名型別的原始碼來確定的。對於Tuple類型,屬性一律被Microsoft稱為Item#,我們無法對此進行人任何改變。

 

第11章 事件

如果定義一個事件成員,意味著類型要提供以下能力。

  • 方法可登記它對該事件的關注。
  • 方法可登出它對該事件的關注。
  • 該事件發生時,登記了的方法會收到通知。

關於設計要公開事件的類型這一節,寫得很詳細,其中有安全執行緒的考慮,書P228-233建議反覆閱讀。

 

第13章 介面

介面繼承的一個重要特點是,凡是能使用具名介面類型的執行個體的地方,都能使用實現了介面的一個類型的執行個體。

 

在運行時,可將變數從一種介面類型轉型為另一種,只要該對象的類型實現了這兩個介面。 

String s = "Jeffrey";
IComparable comparable = s;
IEnumerable enumerable = (IEnumerable)comparable;

 

在C#中定義一個顯示介面方法時,不允許指定可訪問性(比如publicprivate)。但是,編譯器產生方法的中繼資料時,其可訪問性會被自動設為private,防止其他代碼在使用類的執行個體時直接調用介面方法。要調用介面方法,只能通過介面類型的一個變數來進行。

 

第14章 字元、字串和文本處理

System.Security.SecureString類可用它保護敏感的字串資料,比如密碼和信用卡資料。

 

構造一個SecureString對象時,它會在內部分配一個非託管記憶體塊,其中包含一個字元數組。之所以要使用非託管記憶體,是為了避開記憶體回收行程的“羅網”。

 

使用一個SecureString,編譯時間要為C#編譯器指定 /unsafe 開關選項。

 

第16章 數組

如果只是需要把數組中的某些元素複製到另一個數組,可以選擇System.BufferBlockCopy方法,它的執行速度比ArrayCopy方法快。不過,BufferBlockCopy方法只支援基元類型,它不提供像ArrayCopy方法那樣的轉型能力。

 

如果需要可靠地將一個數組中的元素複製到另一個數組,應該使用System.ArrayConstrainedCopy方法。該方法保證在不破壞目標數組中的資料的前提下完成複製,或者拋出一個異常。

 

注意,Array.Copy方法執行的是淺拷貝。換言之,如果數組元素是參考型別,新數組將引用現有的對象。

 

如果效能是首要目標,請避免在堆上分配託管的數組對象。相反,應線上程棧上分配數組,這是通過C#的stackalloc語句來完成的。stackalloc語句只能建立一維0基、由實值型別元素構成的數組,而且實值型別絕對不能包含任何參考型別的欄位。

unsafe {
    const Int32 width = 20;
    Char* pc = stackalloc Char[width];  //在棧上分配數組
    ...
}

 

通常,因為數組是參考型別,所以在一個結構中定義的數組欄位實際只是指向數組的一個指標或引用;數組本身在結構的記憶體的外部。不過,也可像下面代碼中的CharArray結構那樣,直接將數組嵌入結構中。這樣就是在棧上分配數組。

internal unsafe struct CharArray {
   //這個數組以內聯的方式嵌入結構   內聯(內嵌)數組
   public fixed Char Characters[20];
}

 

第18章 定製attribute

應用一個attribute時,C#允許用一個首碼明確指定attribute要應用於的目標元素。但在一些情況下,必須指定首碼向編譯器清楚表明我們的意圖。

[return: SomeAttr]
public int SomeMethod()
{ return SomeValue; }

 

為了告訴編譯器自訂attribute的合法應用範圍,需要向attribute類應用System.AttributeUsageAttribute類的一個執行個體。

AttributeUsageAttribute的其中一個屬性是Inherited,它指出attribute在應用於基類時,是否同時應用於衍生類別和重寫的方法。注意,.NET Framework只認為類、方法、屬性、事件、欄位、方法傳回值和參數等目標元素是可繼承的。

 

調用AttributeGetCustomAttribute或者GetCustomAttributes方法時,這些方法會在內部調用attribute類的構造器,而且可能調用屬性的set訪問器方法。這樣一來,就相當於允許未知的代碼在AppDomain中運行,所以是一個潛在的安全隱患。

使用System.Reflection.CustomAttributeData類,可以在尋找attribute的同時禁止執行attribute類中的代碼。

 

第20章 異常和狀態管理

如果僅僅使用throw關鍵字本身來重新拋出一個異常對象,CLR就不會重設堆棧的起點。

 

在你的代碼中,如果確定狀態已損壞到無法修複的程度,就應銷毀所有損壞的狀態,防止它造成更多的傷害。然後,重新啟動應用程式,將狀態初始化到一個良好的狀態,並寄希望於狀態不再損壞。這時,可以調用AppDomainUnload方法來卸載整個AppDomain。

如果覺得狀態過於糟糕,以至於整個進程都應該終止,那麼應該調用Environment的靜態FailFast方法。

 

為了在異常中添加額外的資料或上下文,可在異常對象的Data屬性(一個鍵/值對的集合)中添加資料,然後重新拋出(only throw) 。

 

關於約束執列區域(CER)RuntimeHelpers.PrepareConstrainedRegions是一個非常特別的方法。JIT編譯器如果發現在一個try塊前調用了這個方法,就會提前編譯與try關聯的catchfinally塊中的代碼。JIT編譯器會載入任何程式集,建立任何類型對象,調用任何靜態構造器,並對任何方法進行JIT編譯。如果其中任何操作造成異常,這個異常會線上程進入try塊之前發生。

 

第21章 自動記憶體管理(記憶體回收)

使用System.Runtime.ConstrainedExecution.CriticalFinalizerObject 類型確保終結。CLR以一種非常特殊的方式對待該類及其衍生類別:1.對Finalize方法進行提前編譯;2.在調用了非CriticalFinalizerObject 衍生類別型的Finalize方法之後,才調用CriticalFinalizerObject 衍生類別型的Finalize方法。

 

System.Runtime.InteropServices中的CriticalHandle類,除了不提供引用計數器功能,其他方面與SafeHandle類相同。CriticalHandle類及其衍生類別通過犧牲安全性來換取更好的效能。

 

所有定義了Finalize方法的類型都應同時實現本節描述的Dispose模式,使類型的使用者對資源的生存期有更多的控制。但是,類型也可實現Dispose模式,但不定義Finalize方法。

 

如果類定義了一個欄位,而且該欄位的類型實現了Dispose模式,那麼類本身也應實現Dispose模式。Dispose方法應dispose欄位引用的對象。

 

建議只有在以下兩種情況下才調用DisposeClose:確定必須清理資源(例如,為了刪除一個已開啟的檔案);或者確定可以安全地調用DisposeClose,並希望將對象從終結列表中刪除,禁止對象提升(到另一代),從而提升效能。

 

如果一個類要封裝數量有限的本地資源,就應該使用System.Runtime.InteropServicesHandleCollector類的一個執行個體來提示記憶體回收行程它實際需要消耗資源的多少個執行個體。該類的對象會在內部監視這個計數,當計數變大時,就強制執行記憶體回收。

 

在內部,GC.AddMemoryPressureHandleCollector.Add方法會調用GC.Collect方法,強迫記憶體回收在第0代達到預算前啟動。

 

System.Runtime命名空間提供了一個MemoryFailPoint類,它允許在開始一個記憶體消耗巨大的演算法之前檢查是否有充裕的記憶體。但要注意,請求的記憶體尚未物理性地分配。這意味著演算法只是更有可能獲得所需的記憶體並成功運行。這個類的目的只是協助你寫更健壯的應用程式。

 

一個很出色的工具可供監視應用程式的對象分配:CLR Profiler。

 

第22章 CLR寄宿和AppDomain

如果線程正在執行類型的類構造器、catch塊或finally塊中的代碼、CER中的代碼或者Unmanaged 程式碼,線程就不在安全點。

 

第23章 程式集載入和反射

反射的4種方式:

1.利用TypeInvokeMember來綁定並調用一個成員。

2.利用FieldInfo、MethodInfo、PropertyInfo等類可以一次綁定,多次調用。這個技術可以產生效能更好的代碼。

3.綁定到一個對象或成員,然後建立一個委託來引用該對象或成員。這個技術能產生比上一個技術還要快的代碼。

4.使用C#的dynamic基元類型來簡化訪問成員時使用的文法。

 

使用綁定控制代碼(RuntimeTypeHandle, RuntimeFieldHandle, RuntimeMethodHandle)來減少進程的記憶體耗用。

 

第24章 運行時序列化

可利用序列化建立對象的一個深拷貝,或者說一個複製體。

 

序列化一個對象時,類型的全名和類型的定義程式集的名稱會被寫入流。預設情況下,BinaryFormatter會輸出程式集的完整標識,其中包括程式集的檔案名稱(無副檔名)、版本號碼、語言文化以及公開金鑰資訊。還原序列化一個對象時,格式化器首先擷取程式集標識資訊,並通過Assembly.Load確保程式集載入到正在執行的AppDomain中。

 

控制序列化和還原序列化過程的最佳方式就是使用OnSerializing, OnSerialized, OnDeserializing, OnDeserialized, NonSerializedOptionalField等attribute。

 

可利用ISerializationSurrogate介面來接管一個特定的類型的序列化和還原序列化工作(非常罕見)。

 

可利用System.Runtime.Serialization.SerializationBinder類,可以非常簡單地將一個對象還原序列化成一個不同的類型。為此,首先要定義自己的類型,讓它從抽象類別SerializaitonBinder派生。

 

第26章 計算限制的非同步作業

如果回調方法的執行時間很長,計時器可能再次觸發。這可能造成多個線程池線程同時執行你的回調方法。為解決這個問題,我的建議是:構造Timer時,為period參數指定Timeout.Infinite。這樣,計時器就只觸發一次。然後,在你的回調方法中,調用Change方法來指定一個新的dueTime,並再次為period參數指定Timeout.Infinite

 

第28章 基元線程同步構造

當線程通過共用記憶體相互連信時,調用VolatileWrite來寫入最後一個值,調用VolatileRead來讀取第一個值。

 

自旋鎖只應該用於保護那些會執行得非常快的代碼地區。

 

有一個泛型方法Morph封裝了Interlocked Anything 模式。它主要解決在多線程的情況下,原子方法的實現(參見書P720-721)。

        delegate int Morpher<TResult, TArgument>(int startValue, TArgument argument, out TResult morphResult);        static TResult Morph<TResult, TArgument>(ref int target, TArgument argument, Morpher<TResult, TArgument> morpher)        {            TResult morphResult;            int currentVal = target, startVal, desiredVal;            do            {                startVal = currentVal;                desiredVal = morpher(startVal, argument, out morphResult);                currentVal = Interlocked.CompareExchange(ref target, desiredVal, startVal);            } while (startVal != currentVal);            return morphResult;        }

 

 書已讀完,不再更新。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.