只讀的自動屬性
通過宣告只有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 =https://www.cnblogs.com/xclw/p/ 0b_0101_0101; int hexValue = https://www.cnblogs.com/xclw/p/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() => $"({X}, {Y}) is {Distance} from the origin"; }
通常ToString()方法不會也不應該修改狀態,所以你可以通過給它加上一個 readonly 修飾符來表明這一點,代碼如下:
public readonly override string ToString() => $"({X}, {Y}) is {Distance} from the origin";
由于ToString()方法中用到了 Distance屬性,而Distance并非只讀的,所以當編譯時會收到如下警告:
warning CS8656: Call to non-readonly member 'Point.Distance.get' from a 'readonly' member results in an implicit copy of 'this'
要想消除這個警告,可以給DIstance添加 readonly 修飾符
public readonly double Distance => Math.Sqrt(X * X + Y * Y);
由于X和Y屬性的getter是自動實作的,編譯器默認它們是readonly的,所以不會給出警告,
帶有 readonly的成員并非一定不能修改狀態, 說白了它只起到對程式員的提示作用,沒有強制作用,以下代碼仍然能編譯通過:
public readonly void Translate(int xOffset, int yOffset) { X += xOffset; Y += yOffset; }
默認介面方法
你可以在介面定義中給成員添加一個默認實作,如果在實作類中沒有重寫該成員,則實作類繼承了這個默認實作,此時該成員并非公共可見的成員,考慮如下代碼:
public interface IControl { void Paint() => Console.WriteLine("Default Paint method"); } public class SampleClass : IControl { // Paint() is inherited from IControl. }
在上面的代碼中,SampleClass默認繼承了IConrol的Paint()方法,但不會向外顯露,即你不能通過SampleClass.Paint()來訪問,你需要先把SampleClass轉成IControl再訪問,代碼如下:
var sample = new SampleClass(); //sample.Paint();// "Paint" isn't accessible. var control = sample as IControl; control.Paint();
模式匹配
模式匹配是對現有 is 和 switch 陳述句的擴展和增強,它包括檢驗值和提取值兩部分功能,
假設我們有如下圖形類, Square(正方形), Circle(圓形), Rectangle(矩形), Triangle(三角形):
public class Square { public double Side { get; } public Square(double side) { Side = side; } } public class Circle { public double Radius { get; } public Circle(double radius) { Radius = radius; } } public struct Rectangle { public double Length { get; } public double Height { get; } public Rectangle(double length, double height) { Length = length; Height = height; } } public class Triangle { public double Base { get; } public double Height { get; } public Triangle(double @base, double height) { Base = @base; Height = height; } }
對于這些圖形,我們寫一個方法用來計算它們的面積,傳統的寫法如下:
public static double ComputeArea(object shape) { if (shape is Square) { var s = (Square)shape; return s.Side * s.Side; } else if (shape is Circle) { var c = (Circle)shape; return c.Radius * c.Radius * Math.PI; } // elided throw new ArgumentException( message: "shape is not a recognized shape", paramName: nameof(shape)); }
現在對 is運算式進行一下擴展,使它不僅能用于檢查,并且如果檢查通過的話,隨即賦值給一個變數,這樣一來,我們的代碼就會變得非常簡單,如下:
public static double ComputeAreaModernIs(object shape) { if (shape is Square s) return s.Side * s.Side; else if (shape is Circle c) return c.Radius * c.Radius * Math.PI; else if (shape is Rectangle r) return r.Height * r.Length; // elided throw new ArgumentException( message: "shape is not a recognized shape", paramName: nameof(shape)); }
在這個更新后的版本中,is運算式不僅檢查變數的型別,還賦值給一個新的擁有合適的型別的變數,另外,這個版本中還包含了 Rectangel 型別,它是一個struct, 也就是說 is運算式不僅能作用于參考型別,還能作用于值型別,上面這種模式匹配稱為型別模式,
語法如下:
expr is type varname
如果expr是type型別或者其派生類,則把expr轉成type型別并賦值給變數varname.
常量模式
string aaa="abc"; if(aaa.Length is 3) { //當長度為3時的處理邏輯 } if(aaa is null) { //為null時的邏輯 }
從上述代碼可以看出is還能判斷是否為null,
var模式
語法如下:
expr is var varname
var模式總是成功的,上面的代碼主要是為了把expr賦值給變數varname,考慮如下代碼
int[] testSet = { 100271, 234335, 342439, 999683 }; var primes = testSet.Where(n => Factor(n).ToList() is var factors && factors.Count == 2 && factors.Contains(1) && factors.Contains(n));
上述代碼中的變數s, c, r遵循如下規則:
- 只有所在的if條件滿足時才會被賦值
- 只有在相應的if分支中可用,在別的地方不可見
上述代碼中的if可以用switch陳述句替換,如下所示
public static double ComputeAreaModernSwitch(object shape) { switch (shape) { case Square s: return s.Side * s.Side; case Circle c: return c.Radius * c.Radius * Math.PI; case Rectangle r: return r.Height * r.Length; default: throw new ArgumentException( message: "shape is not a recognized shape", paramName: nameof(shape)); } }
這跟傳統的switch陳述句有所不同,傳統的swich陳述句,case后面只能跟常量,所以也限制了swich只能用于檢測數值型和string型的變數,而新的語法中switch后面不再限制型別,并且case運算式也不再限制為常量,這意味著之前只有一個case會匹配成功,現在會出現多個case都匹配的情況,這樣一來,各case的順序不同,程式的運行結果也就不同,
接下再說swich運算式,跟switch陳述句不同,switch陳述句是一段代碼塊,而switch運算式是一個運算式,嚴格來說它表示為一個值,把上面的代碼改用swich運算式來寫,代碼如下
public static double ComputeAreaModernSwitch(object shape) => shape switch { Square s => s.Side * s.Side, Circle c => c.Radius * c.Radius * Math.PI, Rectangle r => r.Height * r.Length, _ => throw new ArgumentException( message: "shape is not a recognized shape", paramName: nameof(shape)) };
這跟swich陳述句不同的地方有:
- 變數出現在switch前面,從這個順序上一眼就能看出這個是switch陳述句,還是switch運算式
- case 和 :(冒號) 被 => 代替,更加簡潔和直觀
- default 被 _(忽略符) 代替
- 每個case的body都是一個運算式,而不是陳述句
接下來的例子中一般會寫出兩種寫法,以做比較,
在case運算式中使用when陳述句
當正方形邊長為0時,其面積為0;當矩形任一邊長為0時,其面積為0;當圓形的半徑為0時,其面積為0;當三角形的底或者高為0時,其面積為0;為了檢測這些情況,我們需要進行額外的條件判斷,代碼如下:
//switch陳述句 public static double ComputeArea_Version4(object shape) { switch (shape) { case Square s when s.Side == 0: case Circle c when c.Radius == 0: case Triangle t when t.Base == 0 || t.Height == 0: case Rectangle r when r.Length == 0 || r.Height == 0: return 0; case Square s: return s.Side * s.Side; case Circle c: return c.Radius * c.Radius * Math.PI; case Triangle t: return t.Base * t.Height / 2; case Rectangle r: return r.Length * r.Height; case null: throw new ArgumentNullException(paramName: nameof(shape), message: "Shape must not be null"); default: throw new ArgumentException( message: "shape is not a recognized shape", paramName: nameof(shape)); } }
//switch運算式 public static double ComputeArea_Version4(object shape) => shape switch { Square s when s.Side == 0 => 0, Circle c when c.Radius == 0 => 0, Triangle t when t.Base == 0 || t.Height == 0 => 0, Rectangle r when r.Length == 0 || r.Height == 0 => 0, Square s => s.Side * s.Side, Circle c => c.Radius * c.Radius * Math.PI, Triangle t => t.Base * t.Height / 2, Rectangle r => r.Length * r.Height, null => throw new ArgumentNullException(paramName: nameof(shape), message: "Shape must not be null"), _ => throw new ArgumentException( message=> "shape is not a recognized shape", paramName=> nameof(shape)) }
在case運算式中使用when陳述句,可以進行額外的條件判斷,
在case運算式中使用var
使用var情況下,編譯器會根據switch前的變數推斷出型別,該型別是編譯時的型別而不是運行時的真正型別,即如果有介面實體或者繼承關系,var后面的變數不會是形參的實際型別,
遞回模式匹配
所謂遞回模式匹配是指一個運算式可以作為另一個運算式的輸出,如此反復,可以無限級嵌套,
swich運算式相當于是一個值,它可以作為另一個運算式的輸出,當然也可以作為另一個switch運算式的輸出,所以可以遞回使用
考慮如下場景:中國的地方省、市、縣三級,都實作IArea介面,要求給出一個IArea,回傳其所在省的名稱,其中如果是市的話,要考慮直轄市和地級市,代碼如下:
public interface IArea { string Name { get; } IArea Parent { get; } } public class County: IArea { } public class City: IArea { } public class Province: IArea { } public string GetProvinceName(IArea area) => area switch { Province p => p.Name, City c => c switch { var c when c.Parent == null => c.Name,//直轄市 var c => c.Parent.Name, }, County ct => ct switch { var ct when ct.Parent.Parent ==null => ct.Parent.Name,//直轄市下面的縣 var ct => ct.Parent.Parent.Name } };
這段代碼只是為了演示遞回模式,不是解決該問題的最優寫法,
屬性模式匹配
就是對被檢測物件的某些屬性做匹配,比如一個電商網站要根據客戶所在地區實作不同的稅率,直接上代碼,很好理解
public static decimal ComputeSalesTax(Address location, decimal salePrice) => location switch { { State: "WA" } => salePrice * 0.06M, { State: "MN" } => salePrice * 0.75M, { State: "MI" } => salePrice * 0.05M, // other cases removed for brevity... _ => 0M };
元組(Tuple)模式
有些演算法需要多個輸入引數以進行檢測,此時可能使用一個tuple作為switch表達的檢測物件,如下代碼,顯示剪刀、石頭、布游戲,輸入兩個的出的什么,根據輸入輸出結果
public static string RockPaperScissors(string first, string second) => (first, second) switch { ("rock", "paper") => "rock is covered by paper. Paper wins.", ("rock", "scissors") => "rock breaks scissors. Rock wins.", ("paper", "rock") => "paper covers rock. Paper wins.", ("paper", "scissors") => "paper is cut by scissors. Scissors wins.", ("scissors", "rock") => "scissors is broken by rock. Rock wins.", ("scissors", "paper") => "scissors cuts paper. Scissors wins.", (_, _) => "tie" };
位置模式
有些型別帶有解構(Deconstruct)方法,該方法可以把屬性解構到多個變數中,基于這一特性,可以把利用位置模式可以對物件的多個屬性應用匹配模式,
比如下面的Point類中含有Deconstruct方法,可以把它的 X 和 Y 屬性分解到變數中,
public class Point { public int X { get; } public int Y { get; } public Point(int x, int y) => (X, Y) = (x, y); public void Deconstruct(out int x, out int y) => (x, y) = (X, Y); }
下面的列舉表示坐標系統中的不同區域
public enum Quadrant { Unknown, Origin, One, Two, Three, Four, OnBorder }
下面這個方法使用位置模式提取x, y 的值,并用when陳述句確定某個點在坐標系中所處的區域
static Quadrant GetQuadrant(Point point) => point switch { (0, 0) => Quadrant.Origin, var (x, y) when x > 0 && y > 0 => Quadrant.One, var (x, y) when x < 0 && y > 0 => Quadrant.Two, var (x, y) when x < 0 && y < 0 => Quadrant.Three, var (x, y) when x > 0 && y < 0 => Quadrant.Four, var (_, _) => Quadrant.OnBorder, _ => Quadrant.Unknown };
using宣告
using宣告是為了宣告一個變數,在超出其作用域時,將其銷毀(dispose),如下面的代碼:
static int WriteLinesToFile(IEnumerable<string> lines) { using var file = new System.IO.StreamWriter("WriteLines2.txt"); // Notice how we declare skippedLines after the using statement. int skippedLines = 0; foreach (string line in lines) { if (!line.Contains("Second")) { file.WriteLine(line); } else { skippedLines++; } } // Notice how skippedLines is in scope here. return skippedLines; // file is disposed here }
之前的語法需要用到大括號,當遇到結束大括號時,物件被銷毀,代碼如下:
static int WriteLinesToFile(IEnumerable<string> lines) { // We must declare the variable outside of the using block // so that it is in scope to be returned. int skippedLines = 0; using (var file = new System.IO.StreamWriter("WriteLines2.txt")) { foreach (string line in lines) { if (!line.Contains("Second")) { file.WriteLine(line); } else { skippedLines++; } } } // file is disposed here return skippedLines; }
寫法比以前相對簡潔了,另外當該方法中有多個需要即時銷毀的物件時,你不需要使用using嵌套的寫法,考慮如下代碼:
static void WriteLinesToFile(IEnumerable<string> lines) { using (var file1 = new System.IO.StreamWriter("WriteLines1.txt")) { using (var file2 = new System.IO.StreamWriter("WriteLines1.txt")) { foreach (string line in lines) { if (!line.Contains("Second")) { file1.WriteLine(line); } else { file2.WriteLine(line); } } }// file2 is disposed here } // file1 is disposed here // // some other statements // }
使用新語法代碼如下:
static void WriteLinesToFile(IEnumerable<string> lines) { using var file1 = new System.IO.StreamWriter("WriteLines1.txt"); using (var file2 = new System.IO.StreamWriter("WriteLines1.txt"); foreach (string line in lines) { if (!line.Contains("Second")) { file1.WriteLine(line); } else { file2.WriteLine(line); } } // // some other statements // // file2 is disposed here // file1 is disposed here }
ref區域化和回傳值
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/50921.html
標籤:C#
上一篇:devExpress GridControl 焦點行覆寫行原來的背景顏色?
下一篇:C# winform滾動字幕效果
