發現很多.net 程序員水平一直停留在c#3.0階段,現在來整理下c#5.0/6.0/7.0新語法新特性。
人生需要不斷充電,不斷去get新技能而不是固步自封,對於我自己而言,雖不盲目追求新技術,但每當有新技術出現時也會去了解一下,因為我一直認為,存在 就有它的價值所在。
C# 3.5
擴展方法
擴展方法所在的類和擴展方法必須是靜態的 並且擴展方法第一個參數是要擴展的類名 並在this,
拓展方法這個真的很實用,我在我的所有項目中都封裝了一套值轉換的拓展方法,比如
public static string ConvertToString(this object s) { if (s == null) { return ""; } else { return Convert.ToString(s); } } public static Int32 ConvertToInt32(this string s) { int i = 0; if (s == null) { return 0; } else { int.TryParse(s, out i); } return i; }
比如我封裝了2個拓展方法ConvertToString和ConvertToInt32,在我需要把字符串轉int的時候,傳統的做法可能是 int.Parse("123") 又或者是Convert.ToInt32("123") 但是無論是哪種方法,都會引起異常,而當我封裝了拓展方法以后 我可以直接用 "123".ConvertToInt32() 則可以進行轉換,如果是"a".ConvertToInt32() 則會返回0,而不是向原有一樣去做try...catch
C# 5.0
異步文件 I/O
在.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帶來的優越性,下面這個例子就明顯多了:
public void CopyToAsyncTheHardWay(Stream source, Stream destination) { byte[] buffer = new byte[0x1000]; Action<IAsyncResult> readWriteLoop = null; readWriteLoop = iar => { for (bool isRead = (iar == null); ; isRead = !isRead) { switch (isRead) { case true: iar = source.BeginRead(buffer, 0, buffer.Length, readResult => { if (readResult.CompletedSynchronously) return; readWriteLoop(readResult); }, null); if (!iar.CompletedSynchronously) return; break; case false: int numRead = source.EndRead(iar); if (numRead == 0) { return; } iar = destination.BeginWrite(buffer, 0, numRead, writeResult => { if (writeResult.CompletedSynchronously) return; destination.EndWrite(writeResult); readWriteLoop(null); }, null); if (!iar.CompletedSynchronously) return; destination.EndWrite(iar); break; } } }; readWriteLoop(null); } public async Task CopyToAsync(Stream source, Stream destination) { byte[] buffer = new byte[0x1000]; int numRead; while ((numRead = await source.ReadAsync(buffer, 0, buffer.Length)) != 0) { await destination.WriteAsync(buffer, 0, numRead); } }
在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; }
C# 6.0
1. 自動的屬性初始化器Auto Property initialzier
之前的方式
初始化一個自動屬性Auto Property的唯一方式,就是去實現一個明確的構造器,在里面對屬性值進行設置.
public class AutoPropertyBeforeCsharp6 { private string _postTitle = string.Empty; public AutoPropertyBeforeCsharp6() { //assign initial values PostID = 1; PostName = "Post 1"; } public long PostID { get; set; } public string PostName { get; set; } public string PostTitle { get { return _postTitle; } protected set { _postTitle = value; } } }
有了這個特性之后的方式
使用 C# 6 自動實現的帶有初始值的屬性可以不用編寫構造器就能被初始化. 我們可以用下面的代碼簡化上面的示例:
public class AutoPropertyInCsharp6 { public long PostID { get; } = 1; public string PostName { get; } = "Post 1"; public string PostTitle { get; protected set; } = string.Empty; }
2. 主構造器
我們使用構造器主要是來初始化里面的值.(接受參數值並將這些參數值賦值給實體屬性).
之前的方式
public class PrimaryConstructorsBeforeCSharp6 { public PrimaryConstructorsBeforeCSharp6(long postId, string postName, string postTitle) { PostID = postId; PostName = postName; PostTitle = postTitle; } public long PostID { get; set; } public string PostName { get; set; } public string PostTitle { get; set; } }
有了這個特性之后的方式
public class PrimaryConstructorsInCSharp6(long postId, string postName, string postTitle) { public long PostID { get; } = postId; public string PostName { get; } = postName; public string PostTitle { get; } = postTitle; }
3. 字典初始化器
之前的方式
編寫一個字典初始化器的老辦法如下
public class DictionaryInitializerBeforeCSharp6 { public Dictionary<string, string> _users = new Dictionary<string, string>() { {"users", "Venkat Baggu Blog" }, {"Features", "Whats new in C# 6" } }; }
有了這個特性之后的方式
我們可以像數組里使用方括號的方式那樣定義一個字典初始化器
public class DictionaryInitializerInCSharp6 { public Dictionary<string, string> _users { get; } = new Dictionary<string, string>() { ["users"] = "Venkat Baggu Blog", ["Features"] = "Whats new in C# 6" }; }
4. 聲明表達式
之前的方式
public class DeclarationExpressionsBeforeCShapr6() { public static int CheckUserExist(string userId) { //Example 1 int id; if (!int.TryParse(userId, out id)) { return id; } return id; } public static string GetUserRole(long userId) { ////Example 2 var user = _userRepository.Users.FindById(x => x.UserID == userId); if (user!=null) { // work with address ... return user.City; } } }
有了這個特性之后的方式
在 C# 6 中你可以在表達式的中間聲明一個本地變量. 使用聲明表達式我們還可以在if表達式和各種循環表達式中聲明變量
public class DeclarationExpressionsInCShapr6() { public static int CheckUserExist(string userId) { if (!int.TryParse(userId, out var id)) { return id; } return 0; } public static string GetUserRole(long userId) { ////Example 2 if ((var user = _userRepository.Users.FindById(x => x.UserID == userId) != null) { // work with address ... return user.City; } } }
5. 靜態的 Using
之前的方式
對於你的靜態成員而言,沒必要為了調用一個方法而去弄一個對象實例. 你會使用下面的語法
TypeName.MethodName public class StaticUsingBeforeCSharp6 { public void TestMethod() { Console.WriteLine("Static Using Before C# 6"); } }
之后的方式
在 C# 6 中,你不用類名就能使用 靜態成員 . 你可以在命名空間中引入靜態類.
如果你看了下面這個實例,就會看到我們將靜態的Console類移動到了命名空間中
using System.Console; namespace newfeatureincsharp6 { public class StaticUsingInCSharp6 { public void TestMethod() { WriteLine("Static Using Before C# 6"); } } }
看到這里各位小伙伴有木有驚呆了!!沒錯,你可以引用靜態類了。
6. catch塊里面的await
C# 6 之前catch和finally塊中是不能用 await 關鍵詞的. 在 C# 6 中,我們終於可以再這兩個地方使用await了.
try { //Do something } catch (Exception) { await Logger.Error("exception logging") }
如果不知道await是干嘛的看上面的異步I/O
7. 異常過濾器
異常過濾器可以讓你在catch塊執行之前先進行一個 if 條件判斷.
看看這個發生了一個異常的示例,現在我們想要先判斷里面的Exception是否為null,然后再執行catch塊
//示例 1 try { //Some code } catch (Exception ex) if (ex.InnerException == null) { //Do work } //Before C# 6 we write the above code as follows //示例 1 try { //Some code } catch (Exception ex) { if(ex.InnerException != null) { //Do work; } }
8. 用於檢查NULL值的條件訪問操作符?.
看看這個實例,我們基於UserID是否不為null這個條件判斷來提取一個UserRanking.
之前的方式
var userRank = "No Rank"; if(UserID != null) { userRank = Rank; } //or var userRank = UserID != null ? Rank : "No Rank"
有了這個特性之后方式
var userRank = UserID?.Rank ?? "No Rank";
C# 7.0
輸出變量
在當前的 C# 中,使用輸出參數並不像我們想的那樣方便。在你調用一個無輸出參數的方法之前,首先必須聲明一個變量並傳遞給它。如果你沒有初始化這些變量,你就無法使用 var 來聲明它們,除非先指定完整的類型:
public void PrintCoordinates(Point p) { int x, y; // have to "predeclare" p.GetCoordinates(out x, out y); WriteLine($"({x}, {y})"); }
在 C#7.0 中,我們正在增加輸出變量和聲明一個作為能夠被傳遞的輸出實參的變量的能力:
public void PrintCoordinates(Point p) { p.GetCoordinates(out int x, out int y); WriteLine($"({x}, {y})"); }
注意,變量是在封閉塊的范圍內,所以后續也可以使用它們。大多數類型的聲明不建立自己的范圍,因此在他們中聲明的變量通常會被引入到封閉范圍。
Note:在 Preview 4 中,適用范圍規則更為嚴格:輸出變量的作用域是聲明它們的語句,因此直到下個版本發布時,上面的示例才會起作用。
由於輸出變量直接被聲明為實參傳遞給輸出形參,編譯器通常會告訴他們應該是的類型(除非有沖突過載),所以使用 var 來代替聲明它們的方式是比較好的:
p.GetCoordinates(out var x, out var y);
模式匹配
C# 7.0 引入了模式概念。抽象地講,模式是句法元素,能用來測試一個數據是否具有某種“形”,並在被應用時,從值中提取有效信息。
C#7.0 中的模式示例:
- C 形式的常量模式(C是C#中的常量表達式),可以測試輸入是否等於C
- T X 形式的類型模式(T是一種類型、X是一個標識符),可以測試輸入是否是T類型,如果是,會將輸入值提取成T類型的新變量X
- Var x 形式的 Var 模式(x是一個標識符),它總是匹配的,並簡單地將輸入值以它原本的類型存入一個新變量X中。
這僅僅是個開始 - 模式是一種新型的 C# 中的語言元素。未來,我們希望增加更多的模式到 C# 中。
在 C#7.0,我們正在加強兩個現有的具有模式的語言結構:
- is 表達式現在具有一種右手側的模式,而不僅僅是一種類型
- switch 語句中的 case 語句現在可以使用匹配模式,不只是常數值
在 C#的未來版本中,我們可能會增加更多的被用到的模式。
具有模式的 IS 表達式
下面是使用 is 表達式的示例,其中利用了常量模式和類型模式:
public void PrintStars(object o) { if (o is null) return; // constant pattern "null" if (!(o is int i)) return; // type pattern "int i" WriteLine(new string('*', i)); }
正如你們看到,模式變量(模式引入的變量)和早前描述的輸出變量比較類似,它們可以在表達式中間聲明,並在最近的范圍內使用。就像輸出變量一樣,模式變量是可變的。
注:就像輸出變量一樣,嚴格范圍規則適用於Preview 4。
模式和 Try方法可以很好地協同:
if (o is int i || (o is string s && int.TryParse(s, out i)) { /* use i */ }
具有模式的 Switch 語句
我們正在歸納 Switch 語句:
- 可以設定任何類型的 Switch 語句(不只是原始類型)
- 模式可以用在 case 語句中
- Case 語句可以有特殊的條件
下面是一個簡單的例子:
switch(shape) { case Circle c: WriteLine($"circle with radius {c.Radius}"); break; case Rectangle s when (s.Length == s.Height): WriteLine($"{s.Length} x {s.Height} square"); break; case Rectangle r: WriteLine($"{r.Length} x {r.Height} rectangle"); break; default: WriteLine("<unknown shape>"); break; case null: throw new ArgumentNullException(nameof(shape)); }
c#7.0 switch支持不同類型的一起switch了。
關於新擴展的 switch 語句,有幾點需要注意:
- Case 語句的順序現在變得重要:就像 catch 語句一樣,case 語句的范圍現在可以相交,第一個匹配上的會被選中。此外,就像 catch 語句一樣,編譯器通過去除明顯不會進入的 case 來幫助你。在此之前,你甚至不需要告訴判斷的順序,所以這並不是一個使用 case 語句的巨大的改變。
- 默認的語句還是最后被判斷:盡管 null 的 case 語句在最后語句之前出現,它也會在默認語句被選中之前被測試。這是與現有 Switch 語義兼容的。然而,好的做法通常會將默認語句放到最后。
- Switch 不會到最后的 null 語句:這是因為當前 IS 表達式的例子具有類型匹配,不會匹配到 null。這保證了空值不會不小心被任何的類型模式匹配上的情況;你必須更明確如何處理它們(或放棄它而使用默認語句)。
通過一個 case 引入模式變量:標簽僅在相應的 Switch 范圍內。
元組
這是一個從方法中返回多個值的常見模式。目前可選用的選項並非是最佳的:
- 輸出參數:使用起來比較笨拙(即使有上述的改進),他們在使用異步方法是不起作用的。
- System.Tuple<...> 返回類型:冗余使用和請求一個元組對象的分配。
- 方法的定制傳輸類型:對於類型,具有大量的代碼開銷,其目的只是暫時將一些值組合起來。
- 通過動態返回類型返回匿名類型:很高的性能開銷,沒有靜態類型檢查。
在這點要做到更好,C#7.0 增加的元組類型和元組文字:
(string, string, string) LookupName(long id) // tuple return type { ... // retrieve first, middle and last from data storage return (first, middle, last); // tuple literal }
這個方法可以有效地返回三個字符串,以元素的形式包含在一個元組值里。
這種方法的調用將會收到一個元組,並且可以單獨地訪問其中的元素:
var names = LookupName(id); WriteLine($"found {names.Item1} {names.Item3}.");
Item1 等是元組元素的默認名稱,也可以被一直使用。但他們不具有描述性,所以你可以選擇添加更好的:
(string first, string middle, string last) LookupName(long id) // tuple elements have names
現在元組的接收者有多個具有描述性的名稱可用:
var names = LookupName(id); WriteLine($"found {names.first} {names.last}.");
你也可以直接在元組文字指定元素名稱:
return (first: first, middle: middle, last: last); // named tuple elements in a literal
一般可以給元組類型分配一些彼此無關的名稱:只要各個元素是可分配的,元組類型就可以自如地轉換為其他的元組類型。也有一些限制,特別是對元組文字,即常見的和告警錯誤,如不慎交換元素名稱的情況下,就會出現錯誤。
Note:這些限制尚未在 Preview 4 中實現。
元組是值類型的,它們的元素是公開的,可變的。他們有值相等,如果所有的元素都是成對相等的(並且具有相同的哈希值),那么這兩個元組也是相等的(並且具有相同的哈希值)。
這使得在需要返回多個值的情況下,元組會非常有用。舉例來說,如果你需要多個 key 值的字典,使用元組作為你的 key 值,一切會非常順利。如果你需要在每個位置都具有多個值的列表,使用元組進行列表搜索,會工作的很好。
解構
消耗元組的另一種方法是將解構它們。一個解構聲明是一個將元組(或其他值)分割成部分並單獨分配到新變量的語法:
(string first, string middle, string last) = LookupName(id1); // deconstructing declaration WriteLine($"found {first} {last}.");
在解構聲明中,您可以使用 var 來聲明單獨的變量:
(var first, var middle, var last) = LookupName(id1); // var inside
或者將一個單獨的 var 作為一個縮寫放入圓括號外面:
var (first, middle, last) = LookupName(id1); // var outside
你也可以使用解構任務來解構成現有的變量
(first, middle, last) = LookupName(id2); // deconstructing assignment
解構不只是應用於元組。任何的類型都可以被解構,只要它具有(實例或擴展)的解構方法:
public void Deconstruct(out T1 x1, ..., out Tn xn) { ... }
輸出參數構成了解構結果中的值。
(為什么它使用了參數,而不是返回一個元組?這是為了讓你針對不同的值擁有多個重載)。
class Point { public int X { get; } public int Y { get; } public Point(int x, int y) { X = x; Y = y; } public void Deconstruct(out int x, out int y) { x = X; y = Y; } } (var myX, var myY) = GetPoint(); // calls Deconstruct(out myX, out myY);
這是一種常見的模式,以一種對稱的方式包含了構建和解構。
對於輸出變量,我們計划在解構中加入通配符,來化簡你不關心的變量:
(var myX, *) = GetPoint(); // I only care about myX
局部函數
有時候,一個輔助函數可以在一個獨立函數內部起作用。現在,你可以以一個局部函數的方式在其它函數內部聲明這樣的函數:
public int Fibonacci(int x) { if (x < 0) throw new ArgumentException("Less negativity please!", nameof(x)); return Fib(x).current; (int current, int previous) Fib(int i) { if (i == 0) return (1, 0); var (p, pp) = Fib(i - 1); return (p + pp, p); } }
內的參數和局部變量在局部函數的內部是可用的,就如同它們在 lambda 表達式中一樣。
舉一個例子,迭代的方法實現通常需要一個非迭代的封裝方法,以便在調用時檢查實參。(迭代器本身不啟動運行,直到 MoveNext 被調用)。局部函數非常適合這樣的場景:
public IEnumerable<T> Filter<T>(IEnumerable<T> source, Func<T, bool> filter) { if (source == null) throw new ArgumentNullException(nameof(source)); if (filter == null) throw new ArgumentNullException(nameof(filter)); return Iterator(); IEnumerable<T> Iterator() { foreach (var element in source) { if (filter(element)) { yield return element; } } } }
文字改進
C#7.0 允許 _ 出現,作為數字分隔號:
var d = 123_456; var x = 0xAB_CD_EF;
你可以將 _ 放入任意的數字之間,以提高可讀性,它們對值沒有影響。
此外,C#7.0 引入了二進制文字,這樣你就可以指定二進制模式而不用去了解十六進制。
var b = 0b1010_1011_1100_1101_1110_1111;
引用返回和局部引用
就像在 C# 中通過引用來傳遞參數(使用引用修改器),你現在也可以通過引用來返回參數,同樣也可以以局部變量的方式存儲參數。
public ref int Find(int number, int[] numbers) { for (int i = 0; i < numbers.Length; i++) { if (numbers[i] == number) { return ref numbers[i]; // return the storage location, not the value } } throw new IndexOutOfRangeException($"{nameof(number)} not found"); } int[] array = { 1, 15, -39, 0, 7, 14, -12 }; ref int place = ref Find(7, array); // aliases 7's place in the array place = 9; // replaces 7 with 9 in the array WriteLine(array[4]); // prints 9
這是繞過占位符進入大數據結構的好方法。例如,一個游戲也許會將它的數據保存在大型預分配的陣列結構中(為了避免垃圾回收機制暫停)。方法可以將直接引用返回成一個結構,通過它的調用者可以讀取和修改它。
也有一些限制,以確保安全:
- 你只能返回“安全返回”的引用:一個是傳遞給你的引用,一個是指向對象中的引用。
- 本地引用會被初始化成一個本地存儲,並且不能指向另一個存儲。
異步返回類型
到現在為止,C# 的異步方法必須返回 void,Task 或 Task<T>。C#7.0 允許其它類型以這種能從一個方法中返回的方式被定義,因為它們可以以異步方法被返回的方式來定義其它類型。
例如我們計划建立一個 ValueTask<T>
結
構類型的數據。建立它是為了防止異步運行的結果在等待時已可用的情境下,對 Task<T> 進行分配。對於許多實例中設計緩沖的異步場景,這可以大大減少分配的數量並顯著地提升性能。
Note:異步返回類型尚未在 Preview 4 中提供。
更多的 expression bodied 成員:
expression bodied 的方法和屬性是對 C# 6.0 的巨大提升。C# 7.0 為 expression bodied 事件列表增加了訪問器,結構器和終結器。
class Person { private static ConcurrentDictionary<int, string> names = new ConcurrentDictionary<int, string>(); private int id = GetId(); public Person(string name) => names.TryAdd(id, name); // constructors ~Person() => names.TryRemove(id, out *); // destructors public string Name { get => names[id]; // getters set => names[id] = value; // setters } }
Throw 表達式
在表達式中間拋出一個異常是很容易的:只需為自己的代碼調用一個方法!但在 C#7.0 中,我們允許在任意地方拋出一個表達式:
class Person { public string Name { get; } public Person(string name) => Name = name ?? throw new ArgumentNullException(name); public string GetFirstName() { var parts = Name.Split(" "); return (parts.Length > 0) ? parts[0] : throw new InvalidOperationException("No name!"); } public string GetLastName() => throw new NotImplementedException(); }
好了,新特性比較實用的部分整理到這里。希望大家還是要跟上時代的步伐,不斷進步,不斷去學習!!!!