C# 5.0隨着VisualStudio 2012一起正式發布了,讓我們來看看C#5.0中增加了哪些功能。
1. 異步編程
在.Net 4.5中,通過async和await兩個關鍵字,引入了一種新的基於任務的異步編程模型(TAP)。在這種方式下,可以通過類似同步方式編寫異步代碼,極大簡化了異步編程模型。如下式一個簡單的實例:
static async void DownloadStringAsync2(Uri uri) { var webClient = new WebClient(); var result = await webClient.DownloadStringTaskAsync(uri); Console.WriteLine(result); }
而之前的方式是這樣的:
static void DownloadStringAsync(Uri uri) { var webClient = new WebClient(); webClient.DownloadStringCompleted += (s, e) => { Console.WriteLine(e.Result); }; webClient.DownloadStringAsync(uri); }
也許前面這個例子不足以體現async和await帶來的優越性,下面這個例子就明顯多了:
1 public void CopyToAsyncTheHardWay(Stream source, Stream destination) 2 { 3 byte[] buffer = new byte[0x1000]; 4 Action<IAsyncResult> readWriteLoop = null; 5 readWriteLoop = iar => 6 { 7 for (bool isRead = (iar == null); ; isRead = !isRead) 8 { 9 switch (isRead) 10 { 11 case true: 12 iar = source.BeginRead(buffer, 0, buffer.Length, 13 readResult => 14 { 15 if (readResult.CompletedSynchronously) return; 16 readWriteLoop(readResult); 17 }, null); 18 if (!iar.CompletedSynchronously) return; 19 break; 20 case false: 21 int numRead = source.EndRead(iar); 22 if (numRead == 0) 23 { 24 return; 25 } 26 iar = destination.BeginWrite(buffer, 0, numRead, 27 writeResult => 28 { 29 if (writeResult.CompletedSynchronously) return; 30 destination.EndWrite(writeResult); 31 readWriteLoop(null); 32 }, null); 33 if (!iar.CompletedSynchronously) return; 34 destination.EndWrite(iar); 35 break; 36 } 37 } 38 }; 39 readWriteLoop(null); 40 } 41 42 public async Task CopyToAsync(Stream source, Stream destination) 43 { 44 byte[] buffer = new byte[0x1000]; 45 int numRead; 46 while ((numRead = await source.ReadAsync(buffer, 0, buffer.Length)) != 0) 47 { 48 await destination.WriteAsync(buffer, 0, numRead); 49 } 50 }
關於基於任務的異步編程模型需要介紹的地方還比較多,不是一兩句能說完的,有空的話后面再專門寫篇文章來詳細介紹下。另外也可參看微軟的官方網站:Visual Studio Asynchronous Programming,其官方文檔Task-Based Asynchronous Pattern Overview介紹的非常詳細, VisualStudio中自帶的CSharp Language Specification中也有一些說明。
2. 調用方信息
很多時候,我們需要在運行過程中記錄一些調測的日志信息,如下所示:
public void DoProcessing() { TraceMessage("Something happened."); }
為了調測方便,除了事件信息外,我們往往還需要知道發生該事件的代碼位置以及調用棧信息。在C++中,我們可以通過定義一個宏,然后再宏中通過__FILE__和__LINE__來獲取當前代碼的位置,但C#並不支持宏,往往只能通過StackTrace來實現這一功能,但StackTrace卻有不是很靠譜,常常獲取不了我們所要的結果。
針對這個問題,在.Net 4.5中引入了三個Attribute:CallerMemberName、CallerFilePath和CallerLineNumber。在編譯器的配合下,分別可以獲取到調用函數(准確講應該是成員)名稱,調用文件及調用行號。上面的TraceMessage函數可以實現如下:
public void TraceMessage(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0) { Trace.WriteLine("message: " + message); Trace.WriteLine("member name: " + memberName); Trace.WriteLine("source file path: " + sourceFilePath); Trace.WriteLine("source line number: " + sourceLineNumber); }
另外,在構造函數,析構函數、屬性等特殊的地方調用CallerMemberName屬性所標記的函數時,獲取的值有所不同,其取值如下表所示:
| 調用的地方 |
CallerMemberName獲取的結果 |
| 方法、屬性或事件 |
方法,屬性或事件的名稱 |
| 構造函數 |
字符串 ".ctor" |
| 靜態構造函數 |
字符串 ".cctor" |
| 析構函數 |
該字符串 "Finalize" |
| 用戶定義的運算符或轉換 |
生成的名稱成員,例如, "op_Addition"。 |
| 特性構造函數 |
特性所應用的成員的名稱 |
例如,對於在屬性中調用CallerMemberName所標記的函數即可獲取屬性名稱,通過這種方式可以簡化 INotifyPropertyChanged 接口的實現。關於調用方信息更詳細的資料,請參看MSDN:http://msdn.microsoft.com/zh-cn/library/hh534540.aspx。
方法調用信息
這是一個被寫在Writting Enterprisey Code上的完整風格指南,但是其中我最喜歡的是迷人對你調用過的所有函數的日志記錄:
Function AddTwoNumbers(a As Integer, b As Integer) As Integer Logger.Trace("ArithmeticHelpers", "AddTwoNumbers", "Entering AddTwoNumbers") Dim result = OracleHelpers.ExecInteger("SELECT " & a & " + " & b) Logger.Trace("ArithmeticHelpers", "AddTwoNumbers", "Calling PrintPurchaseOrders") PrintPurchaseOrders() ' IFT 12.11.96: don't know why this is needed but shipping module crashes if it is removed Logger.Trace("ArithmeticHelpers", "AddTwoNumbers", "Returned from PrintPurchaseOrders") Logger.Trace("ArithmeticHelpers", "AddTwoNumbers", "Exiting AddTwoNumbers") Return result End Function
即使這段代碼用企業級標准編寫得有效而清晰,而使用C# 5可以使它更加高效和清晰。C# 4推薦使用可選的參數,這意味着方法調用者可以不用考慮參數,編譯器將會用默認值填充。
public void WonderMethod(int a = 123, string b = "hello") { ... } WonderMethod(456); // compiles to WonderMethod(456, "hello") WonderMethod(); // compiles to WonderMethod(123, "hello")
有了C# 5,你可以將一個特殊屬性放置在可選參數上,編譯器將會使用調用方法的信息填充變量而不是使用某個常量。這意味着我們能夠實現Logger.Trace,來自動收集它是從哪里調用的信息:
public static void Trace(string message, [CallerFilePath] string sourceFile = "", [CallerMemberName] string memberName = "") { string msg = String.Format("{0}: {1}.{2}: {3}", DateTime.Now.ToString("yyyy-mm-dd HH:MM:ss.fff"), // Lurking 'minutes'/'months' bug introduced during .NET port in 2003 and has not been noticed because nobody ever looks at the log files because they contain too much useless detail Path.GetFileNameWithoutExtension(sourceFile), memberName, message); LoggingInfrastructure.Log(msg); }
現在,如果調用LogTrace("some message"),編譯器將會不會用空字符串填充而是使用文件以及調用所發生的成員:
// In file Validation.cs public void ValidateDatabase() { Log.Trace("Entering method"); // compiles to Log.Trace("Entering method", "Validation.cs", "ValidateDatabase") Log.Trace("Exiting method"); }
請注意這些你為參數設置的屬性必須是可選的,如果不是可選的,C#編譯器將需要調用代碼主動提供,並且提供的值必須覆蓋默認值。
另一個你怎樣使用這個的例子便是實現INotifyPropertyChanged,不需要逐字的匹配字符串,表達式:
public class ViewModelBase : INotifyPropertyChanged { protected void Set<T>(ref T field, T value, [CallerMemberName] string propertyName = "") { if (!Object.Equals(field, value)) { field = value; OnPropertyChanged(propertyName); } } // usual INPC boilerplate } public class Widget : ViewModelBase { private int _sprocketSize; public int SprocketSize { get { return _sprocketSize; } set { Set(ref _sprocketSize, value); } // Compiler fills in "SprocketSize" as propertyName } }
很值得的是,你也可以使用[CallerLineNumber]得到調用代碼的行號,這個也許對診斷方法有用,但是如果你真的需要它,這也許是這段調用代碼太過“企業化”的跡象。
在lambdas中使用循環變量
技術上,這個對長期存在的困擾和煎熬的修正,但是使得C#增加了可用性,所以我將會提及它。
自從C# 3以來,編寫匿名函數比命名的更見快捷和容易了,匿名函數被廣泛地使用魚LINQ,但是他們也在其他情況下被使用,如你想要在不需要授權的巨大層級類和接口以及可見函數中快速的擁有參數化行為。匿名函數的一個重要特性就是你可以從本地環境中捕獲變量,以下是一個示例:
public static IEnumerable<int> GetGreaterThan(IEnumerable<int> source, int n) { return source.Where(i => i > n); }
看這里,i=>i>n是一個捕獲了n值的匿名函數。例如如果n是17,那么該函數便是 i=>i>17
在C#之前的版本中,如果你編寫了一個循環,你不能在lambda中使用循環變量。事實上,它比想象中更糟。你可以在lambda中使用循環變量,但是它將給你一個錯誤的結果。它會使用循環退出時的玄幻變量值,不是唄捕獲時的值。
例如,下面是一個返回“加法器”集合的函數:
public static List<Func<int, int>> GetAdders(params int[] addends) { var funcs = new List<Func<int, int>>(); foreach (int addend in addends) { funcs.Add(i => i + addend); } return funcs; }
var adders = GetAdders(1, 2, 3, 4, 5); foreach (var adder in adders) { Console.WriteLine(adder(10)); }
很明顯這大錯特錯!在返回的集合中的每個函數都在捕獲5作為加數后結束。這是因為為他們結束在循環變量,加數,然后最終的循環變量值為5。
要想在C# 3和4中使用這些,你需要記住將循環變量拷貝至一個局部變量中,然后用你的lambda覆蓋局部變量:
foreach (var addend_ in addends) { var addend = addend_; // DON'T GO NEAR THE LOOP VARIABLE funcs.Add(i => i + addend) }
由於這些函數是被局部變量覆蓋而不是用循環變量,這些值現在被保存,你便能獲得真確的值。
以此種方式並不是一種模糊的邊緣情況,我在我的項目中碰到過很多次。有一個來自某個項目中的更加現實的例子便是構建一個用來過濾的函數,這個函數是構建自被用戶指定的約束對象集合。該代碼循環處理約束對象,並構建代表子句的函數列表(如 Name Equals "BOB" 變成 r =>r["Name"]=="BOB"),然后將這些函數混合至一個最終的過濾器中,該過濾器運行這所有的子句然后檢查他們是不是為真。我第一次運行沒有成功因為每隔子句函數以相同的約束對象覆蓋--集合中的最后一個。
在C# 5中,這些修復以及你可以覆蓋的循環變量能使你獲得你期望的結果。
via:mindscapehq.com , OSChina原創編譯
