16.2 【C# 5】呼叫者資訊特性
16.2.1 基本行為
.NET 4.5引入了三個新特性(attribute),即 CallerFilePathAttribute 、 CallerLineNumber- Attribute 和 CallerMemberNameAttribute 。 三 者 均 位 於 System.Runtime.Compiler- Services 名稱空間下。和其他特性一樣,在應用時可以省略 Attribute 字尾。鑑於這是最常見的 特性用法,本書後續內容會進行適當地縮寫。 這三個特性都只能應用於引數,並且只有在應用於可選引數時才有用。其理念非常簡單:如 果呼叫點沒有提供實參,則編譯器可使用當前檔案、行數或成員名來作為實參,而不使用常規的 預設值。如果呼叫者提供了實參,編譯器則將忽略這些特性。
1 static void Main(string[] args) 2 { 3 ShowInfo(); 4 ShowInfo("fileName", -10); 5 Console.ReadKey(); 6 } 7 static void ShowInfo([CallerFilePath] string file = null, [CallerLineNumber] int line = 0, [CallerMemberName]stringmember = null) 8 { 9 Console.WriteLine("{0}:{1} - {2}", file, line, member); 10 }
當然,並不需要總是為這些引數提供虛擬值,但顯式傳遞還是很有用的,尤其是想使用同樣的特性來記錄當前方法呼叫者的時候。成員名特型適用於所有成員 ,但下列成員將使用特殊的名稱:
靜態建構函式: .cctor ;
建構函式: .ctor ;
解構函式: Finalize 。
當欄位初始化器與欄位名稱相同時,該名稱將作為方法呼叫的一部分。
在兩種情況下呼叫者成員資訊不會生效。其一是特性初始化。程式碼清單16-3給出了一個特性 示例,希望可以得到其應用到的成員名稱,但遺憾的是編譯器在這種情況下不會自動完成任何資訊的填充。
1 public class MemberDescriptionAttribute : Attribute 2 { 3 public string Member { get; set; } 4 public MemberDescriptionAttribute([CallerMemberName]string member = null) 5 { 6 Member = member; 7 } 8 }
這本可以很有用。我曾多次見過開發者通過反射得到特性後,卻不得不自己維護一個數據結 構,以儲存成員名和特性之間對映的例子,而這本可以由編譯器自動完成。 特性對動態型別無效,這是可以原諒的。程式碼清單16-4展示了不能生效的情況。
1 static void Main(string[] args) 2 { 3 dynamic x = new TypeUsedDynamically(); 4 x.ShowCaller(); 5 Console.ReadKey(); 6 } 7 class TypeUsedDynamically 8 { 9 internal void ShowCaller([CallerMemberName] string caller = "Unknown") 10 { 11 Console.WriteLine("Called by: {0}", caller); 12 } 13 }
程式碼清單16-4只打印出了 Called by: Unknown ,仿若應用特性不存在一般。儘管看上去有點遺憾,但要想讓它生效,編譯器需在每個可能需要呼叫者資訊的動態呼叫處都內嵌上成員名、檔名和行數。總的來說,這對大多數開發者來說都是得不償失的。
16.2.2 日誌
呼叫者資訊最明顯的用途莫過於寫入日誌檔案。以前記日誌時,通常需要構造一個堆疊跟蹤 (如使用 System.Diagnostics.StackTrace )來查詢日誌資訊的出處。雖然它通常隱藏在日誌 框架的後臺,但依然無法改變其醜陋的存在。此外,它還可能存在效能問題,並且在JIT編譯器 內聯時十分脆弱。
不難想象日誌框架會如何使用這個新特性,來低廉地記錄呼叫者資訊,即使某些程式集可能 通過剝離除錯資訊或混淆操作來保護行數和成員名也無妨。當然,想記錄完整的堆疊跟蹤時,由 於該特性起不到什麼作用,因此需各位自行實現這一操作。
截至本書編寫之時,還沒有日誌框架使用過該特性。首先它需要面向.NET 4.5進行構建, 或者像16.2.4節介紹的那樣,需要顯式宣告這些特性。不過為自己喜歡的日誌框架編寫一個包 裝類,並提供呼叫者資訊還是很容易的。隨著時間的推移,我敢肯定所有日誌框架最終都會提 供此種功能。
1 [AttributeUsage(AttributeTargets.All)] 2 public class MemberDescriptionAttribute : Attribute 3 { 4 public MemberDescriptionAttribute([CallerMemberName] string member = null) 5 { 6 Member = member; 7 } 8 9 public string Member { get; set; } 10 } 11 12 [Description("Listing 16.3")] 13 [MemberDescription] 14 class MemberNames 15 { 16 static MemberNames() 17 { 18 Log("Static constructor"); 19 } 20 21 public event EventHandler DummyEvent 22 { 23 add { Log("Event add"); } 24 remove { Log("Event remove"); } 25 } 26 27 static string foo = Log("Static variable initializer (foo)"); 28 29 string bar = Log("Instance variable initializer (bar)"); 30 31 private string this[int x] { get { return Log("Indexer"); } } 32 33 private string Property 34 { 35 get { return Log("Property get"); } 36 set { Log("Property set"); } 37 } 38 39 private void Method() { Log("Method"); } 40 41 MemberNames() 42 { 43 Log("Constructor"); 44 } 45 46 ~MemberNames() 47 { 48 Log("Finalizer"); 49 } 50 51 static void Main() 52 { 53 var instance = new MemberNames(); 54 instance.Property = instance[10] + instance.Property; 55 EventHandler lambda = (sender, args) => Log("Lambda expression"); 56 lambda(null, EventArgs.Empty); 57 instance.DummyEvent += lambda; 58 instance.DummyEvent -= lambda; 59 var attribute = (MemberDescriptionAttribute) typeof(MemberNames).GetCustomAttributes(typeof(MemberDescriptionAttribute), false)[0]; 60 Console.WriteLine("Attribute on type: {0}", attribute.Member); 61 62 instance = null; 63 GC.Collect(); 64 GC.WaitForPendingFinalizers(); 65 } 66 67 static string Log(string message, [CallerMemberName] string member = null) 68 { 69 Console.WriteLine("{0}: {1}", message, member); 70 return null; // Just for the variable initializers 71 } 72 }View Code
16.2.3 實現 INotifyPropertyChanged
三大特性之一的 [CallerMemberName] 還有一個不太明顯的用途,不過如恰好需要經常實 現 INotifyPropertyChanged 的話,這種用法就顯而易見了。
該介面十分簡單,只包含一個型別為 PropertyChangedEventHandler 的事件。其委託類 型簽名如下:
public delegate void PropertyChangedEventHandler(object sender, PropertyChangedEventArgs e);
PropertyChangedEventArgs 包含單一的建構函式:
public PropertyChangedEventArgs(string propertyName);
在C# 5之前,通常按以下方式實現 INotifyPropertyChanged 。
1 class OldPropertyNotifier : INotifyPropertyChanged 2 { 3 public event PropertyChangedEventHandler PropertyChanged; 4 5 private int firstValue; 6 public int FirstValue 7 { 8 get { return firstValue; } 9 set 10 { 11 if (value != firstValue) 12 { 13 firstValue = value; 14 NotifyPropertyChanged("FirstValue"); 15 } 16 } 17 } 18 19 // Other properties with the same pattern 20 21 private void NotifyPropertyChanged(string propertyName) 22 { 23 PropertyChangedEventHandler handler = PropertyChanged; 24 if (handler != null) 25 { 26 handler(this, new PropertyChangedEventArgs(propertyName)); 27 } 28 } 29 }
輔助方法可避免在每個屬性中都加入空驗證。當然,也可以將其實現為擴充套件方法,以避免在 每個實現類中都重複一遍。
這不僅冗長(此點沒有改變),而且脆弱。問題在於屬性的名稱( FirstValue )指定為字 符串字面量,而如果將屬性名重構為其他名稱,則很可能會忘記修改字串字面量。幸運的話, 工具和測試會幫助我們找到錯誤,但這仍然很醜陋。
在C# 5中,大部分程式碼仍然相同,但可在輔助方法中使用 CallerMemberName ,讓編譯器來 填充屬性名,如程式碼清單16-6所示。
1 class NewPropertyNotifier : INotifyPropertyChanged 2 { 3 public event PropertyChangedEventHandler PropertyChanged; 4 5 private int firstValue; 6 public int FirstValue 7 { 8 get { return firstValue; } 9 set 10 { 11 if (value != firstValue) 12 { 13 firstValue = value; 14 NotifyPropertyChanged(); 15 } 16 } 17 } 18 19 // Other properties with the same pattern 20 21 private void NotifyPropertyChanged([CallerMemberName] string propertyName = null) 22 { 23 PropertyChangedEventHandler handler = PropertyChanged; 24 if (handler != null) 25 { 26 handler(this, new PropertyChangedEventArgs(propertyName)); 27 } 28 } 29 }
此處只展示了發生變化的程式碼,就這麼簡單。現在如改變屬性的名稱,編譯器則可用新名稱 進行替代。這並不是驚天動地的大改進,但卻非常不錯。
16.2.4 在非.NET 4.5 環境下使用呼叫者資訊特性
與擴充套件方法一樣,呼叫者資訊特性也只是請求編譯器在編譯過程中進行程式碼的轉換。該類特性並沒有使用我們無法提供的資訊,只是在使用時需格外小心。跟擴充套件方法一樣,我們也可以在早期.NET版本中使用它們,只需自己宣告這些特性即可,這就如同從MSDN中複製宣告一樣簡單。這些特性本身不包含任何引數,所以在類宣告中無須提供其他內容,但仍然要放在 System.Runtime.CompilerServices 名稱空間中。
C#編譯器將按處理.NET 4.5中真正的呼叫者資訊特性那樣來處理使用者提供的特性。這麼做的 缺點是,用.NET 4.5編譯同樣的程式碼時會產生錯誤。此時只需移除手動建立的特性,以避免編譯 器產生混淆即可。
如果使用的是.NET 4、Silverlight 4/5或Windows Phone 7.5,還可使用 Microsoft.Bcl Nuget 包。包內提供了這些特性,以及其他期待中的有用型別。
這就是有關C# 5的全部內容。