只讀的自動屬性 通過聲明只有get訪問器的自動屬性,實現該屬性只讀 public string FirstName { get; } public string LastName { get; } 自動只讀屬性在能在構造函數中賦值,任何其他地方的賦值都會報編譯錯誤。 自動屬性初始化器 在聲明自動屬性 ...
只讀的自動屬性
通過聲明只有get訪問器的自動屬性,實現該屬性只讀
public string FirstName { get; } public string LastName { get; }
自動只讀屬性在能在構造函數中賦值,任何其他地方的賦值都會報編譯錯誤。
自動屬性初始化器
在聲明自動屬性時,還可以給它指定一個初始值。初始值作為整個聲明的一部分。
public ICollection<double> Grades { get; } = new List<double>();
字元串插入
允許你在字元串中嵌入表達式。字元串以$開頭,把要嵌入的表達式在相應的位置用{和}包起來。
public string FullName => $"{FirstName} {LastName}";
你還可以對錶達式進行格式化
public string GetGradePointPercentage() => $"Name: {LastName}, {FirstName}. G.P.A: {Grades.Average():F2}";
異常過濾器
public static async Task<string> MakeRequest() { WebRequestHandler webRequestHandler = new WebRequestHandler(); webRequestHandler.AllowAutoRedirect = false; using (HttpClient client = new HttpClient(webRequestHandler)) { var stringTask = client.GetStringAsync("https://docs.microsoft.com/en-us/dotnet/about/"); try { var responseText = await stringTask; return responseText; } catch (System.Net.Http.HttpRequestException e) when (e.Message.Contains("301")) { return "Site Moved"; } } }
nameof表達式
獲取變數、屬性或者成員欄位的名稱
if (IsNullOrWhiteSpace(lastName)) throw new ArgumentException(message: "Cannot be blank", paramName: nameof(lastName));
await應用於catch和finally代碼塊
這個就不多說了,很簡單,看代碼吧
public static async Task<string> MakeRequestAndLogFailures() { await logMethodEntrance(); var client = new System.Net.Http.HttpClient(); var streamTask = client.GetStringAsync("https://localHost:10000"); try { var responseText = await streamTask; return responseText; } catch (System.Net.Http.HttpRequestException e) when (e.Message.Contains("301")) { await logError("Recovered from redirect", e); return "Site Moved"; } finally { await logMethodExit(); client.Dispose(); } }
通過索引器初始化集合
索引初始化器使得對集合元素的初始化與通過索引訪問保持一致。之前對Dictionary的初始化使用大括弧的方式,如下:
private Dictionary<int, string> messages = new Dictionary<int, string> { { 404, "Page not Found"}, { 302, "Page moved, but left a forwarding address."}, { 500, "The web server can't come out to play today."} };
現在你可以通過類似索引訪問的方式進行初始化,上面的代碼可以改為:
private Dictionary<int, string> webErrors = new Dictionary<int, string> { [404] = "Page not Found", [302] = "Page moved, but left a forwarding address.", [500] = "The web server can't come out to play today." };
out變數聲明
前要調用一個還有out參數的方法前,你需要先聲明一個變數並賦一個初始值,然後才能調用這個方法
int result=0; if (int.TryParse(input, out result)) Console.WriteLine(result); else Console.WriteLine("Could not parse input");
現在可以在調用方法的同時聲明out變數
if (int.TryParse(input, out int result)) Console.WriteLine(result); else Console.WriteLine("Could not parse input");
同時這種方式還支持隱式類型,你可以用var代理實際的參數類型
if (int.TryParse(input, out var answer)) Console.WriteLine(answer); else Console.WriteLine("Could not parse input");
加強型元組(Tuple)
在7.0之前,要使用元組必須通過new Tuple<T1, T2....>()這種方式,並且元組中的各元素只能通過屬性名Item1, Item2...的方式訪問,費力且可讀性不強。
現在你可以通過如下方式聲明元組,給元組賦值,且給元組中的每個屬性指定一個名稱
(string Alpha, string Beta) namedLetters = ("a", "b"); Console.WriteLine($"{namedLetters.Alpha}, {namedLetters.Beta}");
元組namedLetters包含兩個欄位,Alpha和Beta。欄位名只在編譯時有效,在運行時又會變成Item1, Item2...的形式。所以在反射時不要用這些名字。
你還可以在賦值時,在右側指定欄位的名字,查看下麵的代碼
var alphabetStart = (Alpha: "a", Beta: "b"); Console.WriteLine($"{alphabetStart.Alpha}, {alphabetStart.Beta}");
此外,編譯器還可以從變數中推斷出欄位的名稱,例如下麵的代碼
int count = 5; string label = "Colors used in the map"; var pair = (count: count, label: label); //上面一行,可以換個寫法,欄位名自動從變數名中推斷出來了 var pair = (count, label);
你還可以對從方法返回的元組進行拆包操作,為元組中的每個成員聲明獨立的變數,以提取其中的成員。這個操作稱為解構。查看如下代碼
(int max, int min) = Range(numbers); Console.WriteLine(max); Console.WriteLine(min);
你可以為任意.NET類型提供類似的解構操作。為這個類提供一個Deconstruct方法,此方法需要一組out參數,每個要提取的屬性對應一個out參數。
public class User { public User(string fullName) { var arr = fullName.Split(' '); (FirstName, LastName) = (arr[0], arr[1]); } public string FirstName { get; } public string LastName { get; } public void Deconstruct(out string firstName, out string lastName) => (firstName, lastName) = (this.FirstName, this.LastName); }
通過把User賦值給一個元組,就可以提取各個欄位了
var user = new User("Rock Wang"); (string first, string last) = user; Console.WriteLine($"First Name is: {first}, Last Name is: {last}");
捨棄物
經常會遇到這樣的情況:在解構元組或者調用有out參數的方法時,有些變數的值你根本不關心,或者在後續的代碼也不打算用到它,但你還是必須定義一個變數來接收它的值。C#引入了捨棄物的概念來處理這種情況。
捨棄物是一個名稱為_(下劃線)的只讀變數,你可以把所有想捨棄的值賦值給同一個捨棄物變數,捨棄物變數造價於一個未賦值的變數。捨棄物變數只能在給它賦值的語句中使用,在其它地方不能使用。
捨棄物可以使用在以下場景中:
- 對元組或者用戶定義的類型進行解構操作時
using System; using System.Collections.Generic; public class Example { public static void Main() { var (_, _, _, pop1, _, pop2) = QueryCityDataForYears("New York City", 1960, 2010); Console.WriteLine($"Population change, 1960 to 2010: {pop2 - pop1:N0}"); } private static (string, double, int, int, int, int) QueryCityDataForYears(string name, int year1, int year2) { int population1 = 0, population2 = 0; double area = 0; if (name == "New York City") { area = 468.48; if (year1 == 1960) { population1 = 7781984; } if (year2 == 2010) { population2 = 8175133; } return (name, area, year1, population1, year2, population2); } return ("", 0, 0, 0, 0, 0); } } // The example displays the following output: // Population change, 1960 to 2010: 393,149
- 調用帶有out參數的方法時
using System; public class Example { public static void Main() { string[] dateStrings = {"05/01/2018 14:57:32.8", "2018-05-01 14:57:32.8", "2018-05-01T14:57:32.8375298-04:00", "5/01/2018", "5/01/2018 14:57:32.80 -07:00", "1 May 2018 2:57:32.8 PM", "16-05-2018 1:00:32 PM", "Fri, 15 May 2018 20:10:57 GMT" }; foreach (string dateString in dateStrings) { if (DateTime.TryParse(dateString, out _)) Console.WriteLine($"'{dateString}': valid"); else Console.WriteLine($"'{dateString}': invalid"); } } } // The example displays output like the following: // '05/01/2018 14:57:32.8': valid // '2018-05-01 14:57:32.8': valid // '2018-05-01T14:57:32.8375298-04:00': valid // '5/01/2018': valid // '5/01/2018 14:57:32.80 -07:00': valid // '1 May 2018 2:57:32.8 PM': valid // '16-05-2018 1:00:32 PM': invalid // 'Fri, 15 May 2018 20:10:57 GMT': invalid
- 在進行帶有is和switch語句的模式匹配時(模式匹配下麵會講到)
using System; using System.Globalization; public class Example { public static void Main() { object[] objects = { CultureInfo.CurrentCulture, CultureInfo.CurrentCulture.DateTimeFormat, CultureInfo.CurrentCulture.NumberFormat, new ArgumentException(), null }; foreach (var obj in objects) ProvidesFormatInfo(obj); } private static void ProvidesFormatInfo(object obj) { switch (obj) { case IFormatProvider fmt: Console.WriteLine($"{fmt} object"); break; case null: Console.Write("A null object reference: "); Console.WriteLine("Its use could result in a NullReferenceException"); break; case object _: Console.WriteLine("Some object type without format information"); break; } } } // The example displays the following output: // en-US object // System.Globalization.DateTimeFormatInfo object // System.Globalization.NumberFormatInfo object // Some object type without format information // A null object reference: Its use could result in a NullReferenceException
- 在任何你想忽略一個變數的時,它可以作為一個標識符使用
using System; using System.Threading.Tasks; public class Example { public static async Task Main(string[] args) { await ExecuteAsyncMethods(); } private static async Task ExecuteAsyncMethods() { Console.WriteLine("About to launch a task..."); _ = Task.Run(() => { var iterations = 0; for (int ctr = 0; ctr < int.MaxValue; ctr++) iterations++; Console.WriteLine("Completed looping operation..."); throw new InvalidOperationException(); }); await Task.Delay(5000); Console.WriteLine("Exiting after 5 second delay"); } } // The example displays output like the following: // About to launch a task... // Completed looping operation... // Exiting after 5 second delay
ref局部化和返回值
此特性允許你對一個在別的地方定義的變數進行引用,並可以把它以引用的形式返回給調用者。下麵的例子用來操作一個矩陣,找到一個具有某一特征的位置上的元素,並返回這個元素的引用。
public static ref int Find(int[,] matrix, Func<int, bool> predicate) { for (int i = 0; i < matrix.GetLength(0); i++) for (int j = 0; j < matrix.GetLength(1); j++) if (predicate(matrix[i, j])) return ref matrix[i, j]; throw new InvalidOperationException("Not found"); }
你可以把返回值聲明成ref並修改保存在原矩陣中的值。
ref var item = ref MatrixSearch.Find(matrix, (val) => val == 42); Console.WriteLine(item); item = 24; Console.WriteLine(matrix[4, 2]);
為了防止誤用,C#要求在使用ref局部化和返回值時,需要遵守以下規則:
- 定義方法時,必須在方法簽名和所有的return語句上都要加上ref關鍵字
- ref返回值可以賦值給一個值變數,可以賦值給引用變數
- 不能把一個普通方法的返回值賦值一個ref的局部變數,像 ref int i = sequence.Count() 這樣的語句是不允許的。
- 要返回的ref變數,作用域不能小於方法本身。如果是方法的局部變數,方法執行完畢後,其作用域也消失了,這樣的變數是不能被ref返回的
- 不能在非同步(async)方法中使用
幾個提升性能的代碼改進
當以引用的方式操作一些值類型時,可用如下幾種方式,起到減少記憶體分配,提升性能的目的。
- 給參數加上 in 修飾符。in 是對現有的 ref 和 out的補充。它指明該參數以引用方式傳遞,但在方法內它的值不會被修改。在給方法傳遞值類型參數量,如果沒有指定out, ref和in中的任意一種修飾符,那該值在記憶體中會被覆制一份。這三種修飾符指明參數值以引用方式傳遞,從而避免被覆制。當傳遞的參數類型是比較大的結構(通過批大於IntPtr.Size)時,對性能的提升比較明顯;對於一些小的值類型,其作用並不明顯,甚至會降低性能,比如sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool, enum等。這些修飾行有各自的作用,分別如下:
- out: 在方法內必須修改參數的值
- ref: 在方法內可能會修改參數的值
- in: 在方法內不能修改參數的值
- 對ref返回值(參見特性ref局部化和返回值),如果你不想調用方修改返回的值,可以在返回時加上ref readonly,同時調用者也要用ref readonly變數來接收返回值,所以之前的代碼可以修改如下:
public static ref readonly int Find(int[,] matrix, Func<int, bool> predicate) { for (int i = 0; i < matrix.GetLength(0); i++) for (int j = 0; j < matrix.GetLength(1); j++) if (predicate(matrix[i, j])) return ref matrix[i, j]; throw new InvalidOperationException("Not found"); }
ref readonly var item = ref MatrixSearch.Find(matrix, (val) => val == 42); Console.WriteLine(item); item = 24; Console.WriteLine(matrix[4, 2]);
- 聲明結構體時加上readonly修飾符,用來指明該struct是不可修改的,並且應當以in參數的形式傳給方法
非顯式命名參數
命名參數是指給方法傳參時可以以“參數名:參數值”的形式傳參而不用管該參數在方法簽名中的位置。
static void PrintOrderDetails(string sellerName, int orderNum, string productName) { if (string.IsNullOrWhiteSpace(sellerName)) { throw new ArgumentException(message: "Seller name cannot be null or empty.", paramName: nameof(sellerName)); } Console.WriteLine($"Seller: {sellerName}, Order #: {orderNum}, Product: {productName}"); }
PrintOrderDetails(orderNum: 31, productName: "Red Mug", sellerName: "Gift Shop"); PrintOrderDetails(productName: "Red Mug", sellerName: "Gift Shop", orderNum: 31);
如上面的調用,是對同一方法的調用,而非重載方法,可見參數位置可以不按方法簽名中的位置。如果某一參數出現的位置同它在方法簽名中的位置相同,則可以省略參數名,只傳參數值。如下所示:
PrintOrderDetails(sellerName: "Gift Shop", 31, productName: "Red Mug");
上面的例子中orderNum在正確的位置上,只傳參數值就可以了,不用指定參數名;但如果參數沒有出現豐正確的位置上,就必須指定參數名,下麵的語句編譯器會拋出異常
// This generates CS1738: Named argument specifications must appear after all fixed arguments have been specified. PrintOrderDetails(productName: "Red Mug", 31, "Gift Shop");
表達式體成員
有些函數或者屬性只有一條語句,它可能只是一個表達式,這時可以用表達式體成員來代替
// 在構造器中使用 public ExpressionMembersExample(string label) => this.Label = label; // 在終結器中使用 ~ExpressionMembersExample() => Console.Error.WriteLine("Finalized!"); private string label; // 在get, set存取器中使用 public string Label { get => label; set => this.label = value ?? "Default label"; } //在方法中使用 public override string ToString() => $"{LastName}, {FirstName}"; //在只讀屬性中使用 public string FullName => $"{FirstName} {LastName}";
throw表達式
在7.0之前,throw只能作為語句使用。這使得在一些場景下不支持拋出異常,這些場景包括:
- 條件操作符。如下麵的例子,如果傳入的參數是一個空的string數組,則會拋出異常,如果在7.0之前,你需要用到 if / else語句,現在不需要了
private static void DisplayFirstNumber(string[] args) { string arg = args.Length >= 1 ? args[0] : throw new ArgumentException("You must supply an argument"); if (Int64.TryParse(arg, out var number)) Console.WriteLine($"You entered {number:F0}"); else Console.WriteLine($"{arg} is not a number."); }
- 在空接合操作符中。在下麵的例子中,throw表達式跟空接合操作符一起使用。在給Name屬性賦值時,如果傳入的value是null, 則拋出異常
public string Name { get => name; set => name = value ?? throw new ArgumentNullException(paramName: nameof(value), message: "Name cannot be null"); }
- 在lambda表達式或者具有表達式體的方法中
DateTime ToDateTime(IFormatProvider provider) => throw new InvalidCastException("Conversion to a DateTime is not supported.");
數值寫法的改進
數值常量常常容易寫錯或者讀錯。c#引入了更易讀的寫法
public const int Sixteen = 0b0001_0000; public const int ThirtyTwo = 0b0010_0000; public const int SixtyFour = 0b0100_0000; public const int OneHundredTwentyEight = 0b1000_0000;
開頭的 0b 表示這是一個二進位數,_(下劃線) 表示數字分隔符。分隔符可以出現在這個常量的任意位置,只要能幫助你閱讀就行。比如在寫十進位數時,可以寫成下麵的形式
public const long BillionsAndBillions = 100_000_000_000;
分隔符還可以用於 decimal, float, double類型
public const double AvogadroConstant = 6.022_140_857_747_474e23; public const decimal GoldenRatio = 1.618_033_988_749_894_848_204_586_834_365_638_117_720_309_179M;
從7.2開始,二進位和十六進位的數組還可以 _ 開頭
int binaryValue = 0b_0101_0101; int hexValue = 0x_ffee_eeff;
private protected訪問修飾符
private protected指明一個成員只能被包含類(相對內部類而言)或者在同一程式集下的派生類訪問
註:protected internal指明一個成員只能被派生類或者在同一程式集內的其他類訪問
條件的ref表達式
現在條件表達式可以返回一個ref的結果了,如下:
ref var r = ref (arr != null ? ref arr[0] : ref otherArr[0]);
非同步Main方法
async main 方法使你能夠在Main方法中使用 await。之前你可能需要這麼寫:
static int Main() { return DoAsyncWork().GetAwaiter().GetResult(); }
現在你可以這麼寫:
static async Task<int> Main() { // This could also be replaced with the body // DoAsyncWork, including its await expressions: return await DoAsyncWork(); }
如果你的程式不需要返回任何退出碼,你可以讓Main方法返回一個Task:
static async Task Main() { await SomeAsyncMethod(); }
default字面的表達式
default字面的表達式是對defalut值表達式的改進,用於給變數賦一個預設值。之前你是這麼寫的:
Func<string, bool> whereClause = default(Func<string, bool>);
現在你可以省略右邊的類型
Func<string, bool> whereClause = default;
using static
using static語句允許你把一個類中的靜態方法導出進來,在當前文件中可以直接使用它的靜態方法,而不用帶上類名
using static System.Math //舊寫法 System.Math.Abs(1, 2, 3); //新寫法 Abs(1, 2, 3);
空條件操作符(null-conditional operator)
空條件操作符使判空更加容易和流暢。把成員訪問操作符 . 換成 ?.
var first = person?.FirstName;
在上述代碼中,如果person為null,則把null賦值給first,並不會拋出NullReferenceException;否則,把person.FirstName賦值給first。你還可以把空條件操作符應用於只讀的自動屬性
通過聲明只有get訪問器的自動屬性,實現該屬性只讀
public string FirstName { get; } public string LastName { get; }
自動只讀屬性在能在構造函數中賦值,任何其他地方的賦值都會報編譯錯誤。
自動屬性初始化器
在聲明自動屬性時,還可以給它指定一個初始值。初始值作為整個聲明的一部分。
public ICollection<double> Grades { get; } = new List<double>();
局部函數(Local functions)
有些方法只在一個地方被調用,這想方法通常很小且功能單一,沒有很複雜的邏輯。局部函數允許你在一個方法內部聲明另一個方法。局部函數使得別人一眼就能看出這個方法只在聲明它的方法內使用到。代碼如下:
int M() { int y; AddOne(); return y; void AddOne() => y += 1; }
上面的代碼中, AddOne就是一個局部函數,它的作用是給y加1。有時候你可能希望這些局部函數更“獨立”一些,不希望它們直接使用上下文中的變數,這時你可以把局部函數聲明成靜態方法,如果你在靜態方法中使用了上下文中的變數,編譯器會報錯CS8421。如下代碼所示:
int M() { int y; AddOne(); return y; static void AddOne() => y += 1; }
這時你的代碼要做相應的修改
int M() { int y; y=AddOne(y); return y; static intAddOne(int toAdd) =>{ toAdd += 1; return toAdd;} }
序列的下標和範圍
在通過下標取序列的元素時,如果在下麵前加上 ^ 表示從末尾開始計數。操作符 .. 兩邊的數表示開始下標和結束下標,假設有如下數組
var words = new string[] { // index from start index from end "The", // 0 ^9 "quick", // 1 ^8 "brown", // 2 ^7 "fox", // 3 ^6 "jumped", // 4 ^5 "over", // 5 ^4 "the", // 6 ^3 "lazy", // 7 ^2 "dog" // 8 ^1 }; // 9 (or words.Length) ^0
你可以通過 ^1下標來取最後一個元素(註意:^0相當於words.Length,會拋出異常)
Console.WriteLine($"The last word is {words[^1]}"); // writes "dog"
下麵的代碼會取出一個包含"quick", "brown"和"fox"的子集,分別對應words[1], words[2], words[3]這3個元素,words[4]不包括
var quickBrownFox = words[1..4];
下麵的代碼會取出"lazy"和"dog"的子集體,分別對應words[^2]和words[^1]。wrods[^0]不包括。
var lazyDog = words[^2..^0];
空聯合賦值
先回憶一下空聯合操作符 ??
它表示如果操作符左邊不為null,則返回它。否則,返回操作符右邊的計算結果
int? a = null; int b = a ?? -1; Console.WriteLine(b); // output: -1
空聯合賦值:當??左邊為null時,把右邊的計算結果賦值給左邊
List<int> numbers = null; int? a = null; (numbers ??= new List<int>()).Add(5); Console.WriteLine(string.Join(" ", numbers)); // output: 5 numbers.Add(a ??= 0); Console.WriteLine(string.Join(" ", numbers)); // output: 5 0 Console.WriteLine(a); // output: 0
內插值替換的string的增強
$@現在等價於@$
var text1 = $@"{a}_{b}_{c}"; var text2 = @$"{a}_{b}_{c}";
只讀成員(Readonly Members)
可以把 readonly 修飾符應用於struct的成員上,這表明該成員不會修改狀態。相對於在 struct 上應用readonly顯示更加精細化。
考慮正面這個可變結構體:
public struct Point { public double X { get; set; } public double Y { get; set; } public double Distance => Math.Sqrt(X * X + Y * Y); public override string ToString() => $