C# 二十年語法變遷之 C# 5 和 C# 6參考

語言: CN / TW / HK

C# 二十年語法變遷之 C# 5 和 C# 6參考

https://benbowen.blog/post/two_decades_of_csharp_ii/

自從 C# 於 2000 年推出以來,該語言的規模已經大大增加,我不確定任何人是否有可能在任何時候都對每一種語言特性都有深入的瞭解。因此,我想寫一系列快速參考文章,總結自 C# 2.0 以來所有主要的新語言特性。我不會詳細介紹它們中的任何一個,但我希望這個系列可以作為我自己(希望你也是!)的參考,我可以不時回過頭來記住我使用的工具工具箱裏有。:)

開始之前的一個小提示:我將跳過一些更基本的東西(例如 C# 2.0 引入了泛型,但它們的使用範圍如此廣泛,以至於它們不值得包括在內);而且我還可以將一些功能“粘合”在一起,以使其更簡潔。本系列並不打算成為該語言的權威或歷史記錄。相反,它更像是可能派上用場的重要語言功能的“備忘單”。

C# 5.0

Async/await

此功能是 C# 的支柱,對行業產生了如此大的影響,以至於它已進入其他主流語言。有無數的教程和書籍深入到這個特性;但這篇文章只是作為一個快速參考指南,所以我只在這裏總結一下。

Async/await 允許方法使用異步但以同步方式編寫:

// async keyword tells the compiler we're writing an async method

// Task<int> is a 'Future'/Promise that will eventually yield a value of type int

async Task<int> GetUserAgeFromDatabase(string username) {

// await keyword tells the compiler to convert this method in to a state machine at this point

// Method will return the Task<int> immediately at this point (assuming GetUserDetails() does not complete immediately and synchronously)

// A continuation for the remainder of the method will be scheduled either on this thread (via captured context) or on task pool thread (by default) to be executed once GetUserDetails()'s Task has completed

var userDetails = await _databaseAccessLayer.GetUserDetails(username);



// Once we're here, we're executing the continuation

return userDetails.Age;

}

Caller Info 屬性

此功能涉及可應用於可選方法參數的三個屬性。然後編譯器將填寫詳細信息;這些主要用於記錄:

static void Log(string message, [CallerMemberName] string callerMemberName = null, [CallerFilePath] string callerFilePath = null, [CallerLineNumber] int callerLineNumber) {

Console.WriteLine($"{message} (called from {callerMemberName} on line {callerLineNumber} in file {callerFilePath})");

}



static void Test() {

Log("My message"); // Will print something like "My message (called from Test() on line 15 in file C:\...\Example.cs)"

}

• “Caller Info 屬性”

C# 6.0

靜態導入Static Imports

此功能允許在不使用類名的情況下在類上使用靜態方法:

using static System.Console;



static void Test() {

WriteLine("hello"); // No Console. prefix required

}

• “靜態導入”

異常過濾器

異常過濾器僅在滿足某些參數時才允許捕獲異常:

static void Test() {

try {

SomeOperation();

}

catch (Exception e) when (e.InnerException is OperationCanceledException oce) {

Console.WriteLine($"Operation was cancelled: {oce}");

}

}

• “異常過濾器”

不可變的自動屬性

此功能允許從自動屬性中省略設置器以使其不可變:

class MyClass {

public string Name { get; }



public MyClass(string name) {

Name = name; // Can be initialized in constructor

}

}

• “Immutable Auto-Properties” 名稱不能從構造函數以外的任何地方設置(或內聯,請參閲下一個功能)。

自動屬性初始化器

這允許在其聲明點為內聯屬性設置初始值:

class MyClass {

public string Name { get; } = "Ben";

public int Age { get; set; } = 30;

}

• “自動屬性初始化程序”

表達體成員

此功能允許將某些函數體編寫為單行表達式:

class MyClass {

// Age is a read-only property; the code to the right of the '=>' is evaluated every time the property is invoked and the result of the expression is returned

public int Age => (int) (DateTime.Now - new DateTime(1990, 01, 19)).TotalYears;



// PrintAge is a method, the code to the right of the '=>' is executed when the function is invoked

public void PrintAge() => Console.WriteLine(Age);

}

• “表達式主體成員” 在 C# 7.0 中添加了一些進一步的支持:

class MyClass {

int _age;



// Property getter and setter

public int Age {

get => _age;

set => _age = value >= 0 ?? value : throw new ArgumentOutOfRangeException(nameof(value));



// Constructor

public MyClass(int age) => Age = age;



// Finalizer

~MyClass() => ResourceManager.NotifyFinalizedMyClass(this);

}

• “更多表達主體的成員”

空傳播(“Elvis”運算符)

如果該對象不為空,則此運算符允許您訪問該對象的成員;或者簡單地將所有值摺疊為 null 否則:

static void PrintUserName(UserDetails? user) {

Console.WriteLine($"Name: {user?.Name ?? "No name"}, Age: {user?.Age.ToString() ?? "No age"}");

}



static void Test() {

PrintUserName(new UserDetails("Ben", 30)); // Prints "Name: Ben, Age: 30"

PrintUserName(null); // Prints "Name: No name, Age: No age"

}

• “空條件運算符” 當將多個屬性/方法/字段調用鏈接在一起時(即var x = a?.B?.C()?.D),如果鏈中的任何單個元素為 null,則整個表達式將返回 null。

字符串插值(和格式化字符串)

到目前為止,這是我已經在各種示例中使用的功能。字符串插值允許以更自然的方式將變量嵌入到字符串中:

static void Test() {

var name = "Ben";

Console.WriteLine($"My name is {name}"); // The $ sign before the opening quotemark indicates this is an interpolated string

}

• “基本字符串插值” 可以通過格式後綴指定值轉換為字符串的方式。以下示例顯示了在將浮點值轉換為字符串時指定小數位數的一種方法:

static void Test() {

var percentageComplete = 12.345d;

Console.WriteLine($"Percentage complete: {percentageComplete:F0}%"); // Prints "Percentage complete: 12%"

Console.WriteLine($"Percentage complete: {percentageComplete:F2}%"); // Prints "Percentage complete: 12.34%"

}

• “格式化字符串插值” 也可以指定對齊方式;用於打印 ASCII 表:

static void Test() {

var names = new[] { "Ben", "Javier", "Chris" };

var favoriteFoods = new[] { "Ramen", "Something Vegetarian", "No idea" };



for (var i = 0; i < 3; ++i) {

Console.WriteLine($"Name: {names[i],10} | Food: {favoriteFoods[i]}"); // Notice the ,10 that right-aligns names to a 10-column width

}

}



/* Prints:

* Name: Ben | Food: Ramen

* Name: Javier | Food: Something Vegetarian

* Name: Chris | Food: No idea

*/











static void Test() {

var names = new[] { "Ben", "Javier", "Chris" };

var favoriteFoods = new[] { "Ramen", "Something Vegetarian", "No idea" };



for (var i = 0; i < 3; ++i) {

Console.WriteLine($"Name: {names[i],-10} | Food: {favoriteFoods[i]}"); // Notice the ,-10 that left-aligns names to a 10-column width

}

}



/* Prints:

* Name: Ben | Food: Ramen

* Name: Javier | Food: Something Vegetarian

* Name: Chris | Food: No idea

*/

• “對齊字符串插值” 對象的默認格式使用線程本地文化作為格式提供程序。有時這不是我們想要的。因此,我們可以通過顯式創建FormattableString然後將其轉換為字符串來手動指定格式提供程序:

static void Test() {

var percentageComplete = 12.345d;

FormattableString str = $"Percentage complete: {percentageComplete:F2}%";



Console.WriteLine(str.ToString(CultureInfo.GetCultureInfo("de-DE"))); // Prints "Percentage complete: 12,35%" (German-style number formatting)

}

• “FormattableString”

"nameof" 運算符

這個小功能允許您將代碼中的標記名稱轉換為字符串。它很有用,因為它避免了在重命名這些類型時手動寫出成員/類型名稱的問題:

class User {

public string Name { get; }



public User(string name) {

if (name == null) throw new ArgumentNullException(nameof(name)); // If we rename name later this will not compile (which is good)

Name = name;

}

}

• “nameof”

關聯集合的替代初始化語法

這是一個小功能。它允許使用更簡潔的語法來初始化關聯集合(即主要是字典)。以下兩個初始化是相同的:

class User {

static void Test() {

var oldWay = new Dictionary<int, string> {

{ 1, "One" },

{ 2, "Two" },

{ 3, "Three" },

{ 4, "Four" }

};



var newWay = new Dictionary<int, string> {

[1] = "One",

[2] = "Two",

[3] = "Three",

[4] = "Four"

};

}

}

•“舊字典初始化與新字典初始化” 字典鍵和值可以是任何類型。

集合初始化器的擴展“添加”方法

假設您正在使用的庫中定義了一個集合類型(必須實現IEnumerable<> );但是添加元素的方法沒有命名為Add(T item)。這是集合初始化器工作的要求。

下面是一個使用名為UserDatabase的虛構類型的示例,該類型從虛構的第三方庫中實現IEnumerable  :

static void Test() {

// Won't compile

// Doesn't work becuase UserDatabase calls its add method AddUser(), so we have to use the second approach below

var database = new UserDatabase {

new User("Ben", 30),

new User("Seb", 27),

new User("Rob", 33)

};



// Will compile but less pretty

var database = new UserDatabase();

database.AddUser(new User("Ben", 30));

database.AddUser(new User("Seb", 27));

database.AddUser(new User("Rob", 33));

}

• “嘗試使用集合初始化程序時沒有添加方法” 在這種情況下,從 C# 6.0 開始,我們可以指定Add(T item)擴展方法來啟用集合初始值設定項:

static class UserDatabaseExtensions {

public static void Add(this UserDatabase @this, User u) => @this.AddUser(u);

}



// ...



static void Test() {

// Hooray, this works now!

var database = new UserDatabase {

new User("Ben", 30),

new User("Seb", 27),

new User("Rob", 33)

};

}

• “嘗試使用集合初始化程序時添加擴展方法”