精:c#語法特性總結

精:c#語法特性總結

c# 10已與.net 6、vs2022一起發布,本文按照.net的發布順序,根據微軟官方文檔整理c#中一些有趣的語法特性。

最后更新 2021/11/19 下午5:38
louzixl
预计阅读 21 分钟
分类
.NET
标签
.NET C#

C# 10 已与 .NET 6、VS2022 一起发布,本文按照.NET 的发布顺序,根据微软官方文档整理 C#中一些有趣的语法特性。

注:基于不同.NET 平台创建的项目,默认支持的 C#版本是不一样的。下面介绍的语法特性,会说明引入 C#的版本,在使用过程中,需要注意使用 C#的版本是否支持对应的特性。C#语言版本控制,可参考官方文档

匿名函數

匿名函數是 c# 2 推出的功能,顧名思義,匿名函數只有方法體,沒有名稱。匿名函數使用 delegate 創建,可轉換為委託。匿名函數不需要指定返回值類型,它會根據 return 語句自動判斷返回值類型。

註:c# 3 後推出了 lambda 表達式,使用 lambda 可以以更簡潔的方式創建匿名函數,應儘量使用 lambda 來創建匿名函數。與 lambda 不同的是,使用 delegate 創建匿名函數可以省略參數列表,可將其轉換為具有任何參數列表的委託類型。

// 使用delegate关键字创建,无需指定返回值,可转换为委托,可省略参数列表(与lambda不同)
Func<int, bool> func = delegate { return true; };

自動屬性

從 c# 3 開始,當屬性訪問器中不需要其他邏輯時,可以使用自動屬性,以更簡潔的方式聲明屬性。編譯時,編譯器會為其創建一個僅可以通過 get、set 訪問器訪問的私有、匿名欄位。使用 vs 開發時,可以通過 snippet 代碼片段 prop+2 次 tab 快速生成自動屬性。

// 属性老写法
private string _name;
public string Name
{
    get { return _name; }
    set { _name = value; }
}
// 自动属性
public string Name { get; set; }

另外,在 c# 6 以後,可以初始化自動屬性:

public string Name { get; set; } = "Louzi";

匿名類型

匿名類型是 c# 3 後推出的功能,它無需顯示定義類型,將一組只讀屬性封裝到單個對象中。編譯器會自動推斷匿名類型的每個屬性的類型,並生成類型名稱。從 clr 的角度看,匿名類型與其他引用類型沒什麼區別,匿名類型直接派生自 object。如果兩個或多個匿名對象指定了順序、名稱、類型相同的屬性,編譯器會把它們視為相同類型的實例。在創建匿名類型時,如果不指定成員名稱,編譯器會把用於初始化屬性的名稱作為屬性名稱。

匿名類型多用於 linq 查詢的 select 查詢表達式。匿名類型使用 new 與初始化列表創建:

// 使用new与初始化列表创建匿名类型
var person = new { Name = "Louzi", Age = 18 };
Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
// 用于LINQ
var productQuery =
    from prod in products
    select new { prod.Color, prod.Price };
foreach (var v in productQuery)
{
    Console.WriteLine("Color={0}, Price={1}", v.Color, v.Price);
}

LINQ

c# 3 推出了殺手鐧功能,查詢表達式,即語言集成查詢(linq)。查詢表達式以查詢語法表示查詢,由一組類似 sql 的語法編寫的子句組成。

查詢表達式必須以 from 子句開頭,必須以 select 或 group 子句結尾。在第一個 from 子句與最後一個 select 或 group 子句之間,可以包含:where、orderby、join、let、其他 from 子句等。

可以為 sql 資料庫、xml 文檔、ado.net 數據集及實現了 ienumerable 或 ienumerable 接口的集合對象進行 linq 查詢。

完整的查詢包括創建數據源、定義查詢表達式、執行查詢。查詢表達式變量是存儲查詢而不是查詢結果,只有在循環訪問查詢變量後,才會執行查詢。

可使用查詢語法表示的任何查詢都可以使用方法表示,建議使用更易讀的查詢語法。有些查詢操作(如 count 或 max)沒有等效的查詢表達式子句,必須使用方法調用。可以結合使用方法調用和查詢語法。

关于 LINQ 的详细文档,参见微软官方文档

// Data source.
int[] scores = { 90, 71, 82, 93, 75, 82 };
// Query Expression.
IEnumerable<int> scoreQuery = //query variable
    from score in scores //required
    where score > 80 // optional
    orderby score descending // optional
    select score; //must end with select or group
// Execute the query to produce the results
foreach (int testScore in scoreQuery)
{
    Console.WriteLine(testScore);
}

Lambda

c# 3 推出了很多強大的功能,如自動屬性、擴展方法、隱式類型、linq,以及 lambda 表達式。

創建 lambda 表達式,需要在 => 左側指定輸入參數(空括號指定零個參數,一個參數可以省略括號),右側指定表達式或語句塊(通常兩三條語句)。任何 lambda 表達式都可以轉換為委託類型,表達式 lambda 語句還可以轉換為表達式樹(語句 lambda 不可以)。

匿名函數可以省略參數列表,lambda 中不使用的參數可以使用棄元指定(c# 9)。

使用 async 和 await,可以創建包含異步處理的 lambda 表達式和語句(c# 5)。

從 c# 10 開始,當編譯器無法推斷返回類型時,可以在參數前面指定 lambda 表達式的返回類型,此時參數必須加括號。

// Lambda转换为委托
Func<int, int> square = x => x * x;
// Lambda转换为表达式树
System.Linq.Expressions.Expression<Func<int, int>> e = x => x * x;
// 使用弃元指定不使用的参数
Func<int, int, int> constant = (_, _) => 42;
// 异步Lambda
var lambdaAsync = async () => await JustDelayAsync();
Console.WriteLine($"main thread id: {Thread.CurrentThread.ManagedThreadId}");
lambdaAsync();
static async Task JustDelayAsync()
{
    await Task.Delay(1000);
    Console.WriteLine($"JustDelayAsync thread id: {Thread.CurrentThread.ManagedThreadId}");
}
// 指定返回类型,不指定返回类型会报错
var choose = object (bool b) => b ? 1 : "two";

擴展方法

擴展方法也是 c# 3 推出的功能,它能夠向現有類型添加方法,且無需修改原始類型。擴展方法是一種靜態方法,不過是通過實例對象語法進行調用,它的第一個參數指定方法操作的類型,用 this 修飾。編譯器在編譯為 il 時會轉換為靜態方法的調用。

如果類型中具有與擴展方法相同名稱和簽名的方法,則編譯器會選擇類型中的方法。編譯器進行方法調用時,會先在該類型的的實例方法中尋找,找不到再去搜索該類型的擴展方法。

最常見的擴展方法是 linq,它將查詢功能添加到現有的 system.collections.ienumerable 和 system.collections.generic.ienumerable 類型中。

為 struct 添加擴展方法時,由於是值傳遞,只能對 struct 對象的複本進行更改。從 c# 7.2 開始,可以為第一個參數添加 ref 修飾以進行引用傳遞,這樣就可以對 struct 對象本身進行修改了。

static class MyExtensions
{
    public static void OutputStringExtension(this string s) => Console.WriteLine($"output: {s}");
    public static void OutputPointExtension(this Point p)
    {
        p.X = 10;
        p.Y = 10;
        Console.WriteLine($"output: ({p.X}, {p.Y})");
    }
    public static void OutputPointWithRefExtension(ref this Point p)
    {
        p.X = 20;
        p.Y = 20;
        Console.WriteLine($"output: ({p.X}, {p.Y})");
    }
}
// class扩展方法
"Louzi".OutputStringExtension();
// struct扩展方法
Point p = new Point(5, 5);
p.OutputPointExtension(); // output: (10, 10)
Console.WriteLine($"original point: ({p.X}, {p.Y})");  // output: (5, 5)
p.OutputPointWithRefExtension();  // output: (20, 20)
Console.WriteLine($"original point: ({p.X}, {p.Y})");  // output: (20, 20)

隱式類型(var)

從 c# 3 開始,在方法範圍內可以聲明隱式類型變量(var)。隱式類型為強類型,由編譯器決定類型。

var 常用於調用構造函數創建對象實例時,從 c# 9 開始,這種場景也可以使用確定類型的 new 表達式:

// 隐式类型
var s = new List<int>();
// new表达式
List<int> ss = new();

註:當返回匿名類型時,只能使用 var。

對象、集合初始化列表

從 c# 3 開始,可以在單條語句中實例化對象或集合併執行成員分配。

使用對象初始化列表,可以在創建對象時向對象的任何可訪問欄位或屬性分配值,可以指定構造函數參數或忽略參數以及括號。

public class Person
{
    // 自动属性
    public int Age { get; set; }
    public string Name { get; set; }
    public Person() { }
    public Person(string name)
    {
        Name = name;
    }
}
var p1 = new Person { Age = 18, Name = "Louzi" };
var p2 = new Person("Sherilyn") { Age = 18 };

從 c# 6 開始,對象初始化列表不僅可以初始化可訪問欄位和屬性,還可以設置索引器。

public class MyIntArray
{
    public int CurrentIndex { get; set; }
    public int[] data = new int[3];
    public int this[int index]
    {
        get => data[index];
        set => data[index] = value;
    }
}
var myArray = new MyIntArray { [0] = 1, [1] = 3, [2] = 5, CurrentIndex = 0 };

集合初始化列表可以指定一個或多個初始值:

var persons = new List<Person>
{
    new Person { Age = 18, Name = "Louzi" },
    new Person { Age = 18, Name = "Sherilyn" }
};

內置泛型委託

net framework 3.5/4.0,分別提供了內置的 action 和 func 泛型委託類型。void 返回類型的委託可以使用 action 類型,action 的變體最多有 16 個參數。有返回值類型的委託可以使用 func 類型,func 類型的變體最多同樣 16 個參數,返回類型為 func 聲明中的最後一個類型參數。

Action<int> actionInstance = ActionInstance;
Func<int, string> funcInstance = FuncInstance;
static void ActionInstance(int n) => Console.WriteLine($"input: {n}");
static string FuncInstance(int n) => $"param: {n}";

dynamic

c# 4 主要的功能就是引入了 dynamic 關鍵字。dynamic 類型在變量使用及其成員引用時會繞過編譯時類型檢查,在運行時再進行解析。這便實現了與動態類型語言(如 javascript)類似的構造。

dynamic dyn = 1;
Console.WriteLine(dyn.GetType()); // output: System.Int32
dyn = dyn + 3; // 如果dyn是object类型,此句则会报错

命名參數與可選參數

c# 4 引入了命名參數和可選參數。命名參數可為形參指定實參,方式是指定匹配的實參與形參,這時無需匹配參數列表中的位置。可選參數通過指定參數默認值,可以省略實參。可選參數需位於參數列表末尾,如果為一系列可選參數中的任意一個提供了實參,則必須為該參數前面的所有可選參數提供實參。

也可以使用 optionalattribute 特性聲明可選參數,此時無需為形參提供默認值。

// 命名参数与可选参数
PrintPerson(age: 18, name: "Louzi");
// static void PrintPerson(string name, int age, [Optional, DefaultParameterValue("男")] string sex)
static void PrintPerson(string name, int age, string sex = "男") =>
    Console.WriteLine($"name: {name}, age: {age}, sex: {sex}");

靜態導入

c# 6 中推出了靜態導入功能,使用 using static 指令導入類型,可以無需指定類型名稱即可訪問其靜態成員和嵌套類型,這樣避免了重複輸入類型名稱導致的晦澀代碼。

using static System.Console;
WriteLine("Hello CSharp");

異常篩選器(when)

從 c# 6 開始,when 可用於 catch 語句中,用來指定為執行特定異常處理程式必須為 true 的條件表達式,當表達式為 false 時,則不會執行異常處理。

public static async Task<string> MakeRequest()
{
    var client = new HttpClient();
    var streamTask = client.GetStringAsync("https://localHost:10000");
    try
    {
        var responseText = await streamTask;
        return responseText;
    }
    catch (HttpRequestException e) when (e.Message.Contains("301"))
    {
        return "Site Moved";
    }
    catch (HttpRequestException e) when (e.Message.Contains("404"))
    {
        return "Page Not Found";
    }
    catch (HttpRequestException e)
    {
        return e.Message;
    }
}

自動屬性初始化表達式

c# 6 開始,可以為自動屬性指定初始化值以使用類型默認值以外的值:

public class DefaultValueOfProperty
{
    public string MyProperty { get; set; } = "Property";
}

表達式體

從 c# 6 起,支持方法、運算符和只讀屬性的表達式體定義,自 c# 7.0 起,支持構造函數、終結器、屬性、索引器的表達式體定義。

static void NewLine() => Console.WriteLine();

null 條件運算符

c# 6 起,推出了 null 條件運算符,僅當操作數的計算結果為非 null 時,null 條件運算符才會將成員訪問?.或元素訪問? []運算應用於其操作數;否則,將返回 null。

// null条件表达式
public class ConditionalNull
{
    event EventHandler AEvent;
    public void RaiseAEvent() => AEvent?.Invoke(this, EventArgs.Empty);
}

內插字符串

從 c# 6 開始,可以使用$在字符串中插入表達式,使代碼可讀性更高也降低了字符串拼接出錯的概率。如果在內插字符串中包含大括號,需使用兩個大括號(”{{“或””}}”)。如果內插表達式需使用條件運算符,需要將其放在括號內。從c# 8起,可以使用$@”…”或@\(”…”形式的內插逐字字符串,在此之前的版本,必須使用\)@”…”形式。

Console.WriteLine($"{name} is {age} year{(age == 1 ? "" : "s")} old.");

nameof

c# 6 提供了 nameof 表達式,nameof 可生成變量、類型或成員名稱(非完全限定)作為字符串常量。

public string Name
{
    get => name;
    set => name = value ?? throw new ArgumentNullException(nameof(value), $"{nameof(Name)} cannot be null");
}

out 改進

c# 7.0 中對 out 語法進行了改進,可以直接在方法調用的參數列表中聲明 out 變量,無需再單獨編寫一條聲明語句:

void Function(out int arg) { ... }
// 未改进前
int n;
Function(out n);
// 改进后
Function(out int n);

元組

c# 7.0 中引入了對元組的語言支持(之前版本也有元組但效率低下),可以使用元組表示包含多個數據的簡單結構,無需再專門寫一個 class 或 struct。元組是值類型的,是包含多個公共欄位以表示數據成員的輕量級數據結構,無法為其定義方法。c# 7.3 後元組支持==與!=。

// 方式一,使用元组字段的默认名称:Item1、Item2、Item3等
(string, string) unnamedLetters = ("a", "b");
Console.WriteLine($"{unnamedLetters.Item1}, {unnamedLetters.Item2}");
// 方式二
(string Alpha, string Beta) namedLetters = ("a", "b");
Console.WriteLine($"{namedLetters.Alpha}, {namedLetters.Beta}");
// 方式三
var alphabetStart = (Alpha: "a", Beta: "b");
Console.WriteLine($"{alphabetStart.Alpha}, {alphabetStart.Beta}");
// 方式四,C# 7.1开始支持自动推断变量名称
int count = 5;
string label = "Colors used in the map";
var pair = (count, label); // 元组元素名为"count"和"label"

當某方法返回元組時,如需提取元組成員,可通過為元組的每個值聲明單獨的變量來實現,稱為解構元組。使用元組作為方法返回類型,可以替代定義 out 方法參數。

// 解构元组
var (first, last) = Range(numbers);
Console.WriteLine($"{first} to {last}");
(int max, int min) = Range(numbers);
Console.WriteLine($"{min} to {max}");

棄元

從 c# 7.0 開始支持棄元,棄元是占位符變量,相當於未賦值的變量,表示不想使用該變量,使用下劃線_表示棄元變量。如下列舉了一些棄元的使用場景:

// 场景一:丢弃元组值
(_, _, area) = city.GetCityInformation(cityName);
// 场景二:从C# 9开始,可以丢弃Lambda表达式中的参数
Func<int, int, int> constant = (_, _) => 42;
// 场景三,丢弃out参数
DiscardsOut(out _);
static void DiscardsOut(out string s)
{
    s = "nothing";
    Console.WriteLine($"input is {s}");
}

模式匹配

c# 7.0 添加了模式匹配功能,之後每個主要 c#版本都擴展了模式匹配功能。模式匹配用來測試表達式是否具有某些特徵,is 表達式、switch 語句和 switch 表達式均支持模式匹配,可使用 when 關鍵字來指定模式的其他規則。

模式匹配目前包含這些類型:聲明模式、類型模式、常量模式、關係模式、邏輯模式、屬性模式、位置模式、var 模式、棄元模式,詳細內容可參考官方文檔。

is 模式表達式改進了 is 運算符功能,可在一條指令分配結果:

// is模式匹配
if (input is int count) do somthing... ;
// 老写法
if (input is int)
{
    int count = (int)input;
    do somthing... ;
}
// is模式进行空检查
string? message = "This is not the null string";
if (message is not null) Console.WriteLine(message);

default 文本表達式

默認值表達式生成類型的默認值,之前版本僅支持 default 運算符,c# 7.1 後增強了 default 表達式的功能,當編譯器可以推斷表達式類型時,可以使用 default 生成類型的默認值。

// 新写法
Func<string, bool> whereClause = default;
// 老写法
Func<string, bool> whereClause = default(Func<string, bool>);

switch 表達式

從 c# 8 開始,可以使用 switch 表達式。switch 表達式相較於 switch 語句的改進之處在於:

  • 變量在 switch 關鍵字之前;
  • 使用=>替換 case :結構;
  • 使用棄元_替換 default 運算符;
  • 使用表達式替換語句。
public enum Level
{
    One,
    Two,
    Three
}
public static int LevelToScore(Level level) => level switch
{
    Level.One   => 1,
    Level.Two   => 5,
    Level.Three => 10,
    _ => throw new ArgumentOutOfRangeException(nameof(level), $"Not expected level value: {level}"),
};

using 聲明

c# 8 添加了 using 聲明功能,它指示編譯器聲明的變量應在代碼塊的末尾進行處理。using 聲明相比傳統的 using 語句代碼更簡潔,這兩種寫法都會使編譯器在代碼塊末尾調用 dispose()。

static void WriteLinesToFile(IEnumerable<string> lines)
{
    using var file = new System.IO.StreamWriter("WriteLines.txt");
    do somthing... ;
    return;
    // file is disposed here
}

索引和範圍

c# 8 中添加了索引和範圍功能,為訪問序列中的單個元素或範圍提供了簡潔的語法。該語法依賴兩個新類型與兩個新運算符:

  • system.index 表示一個序列索引;
  • system.range 表示序列的子範圍;
  • 末尾運算符^,使用該運算符加數字,指定倒數第幾個;
  • 範圍運算符..,指定範圍的開始和末尾。

範圍運算符包括此範圍的開始,但不包括此範圍的末尾。

var words = new string[]
{               // 正常索引             索引对应的末尾运算符
    "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 (words.Length)    ^0
Console.WriteLine($"The last word is {words[^1]}"); // dog
var allWords = words[..]; // 包含所有值,等同于words[0..^0].
var firstPhrase = words[..4]; // 开始到words[4],不包含words[4]
var lastPhrase = words[6..]; // words[6]到末尾
// 声明范围变量
Range phrase = 1..4;
var text = words[phrase];

??與??=

??合併運算符:c# 6 後可用,如果左操作數的值不為 null,則??返回該值;否則,它會計算右操作數並返回其結果。如果左操作數的計算結果為非 null,則不會計算其右操作數。

??=合併賦值運算符:c# 8 後可用,僅在左側操作數的求值結果為 null 時,才將右操作數的值賦值給左操作數。否則,不會計算其右操作數。??=運算符的左操作數必須是變量、屬性或索引器元素。

// ??合并运算符
Console.WriteLine($"name is {OutputName(null)}");
static string OutputName(string name) => name ?? "some one";
// 使用??=赋值运算符
variable ??= expression;
// 老写法
if (variable is null)
{
    variable = expression;
}

頂級語句

c# 9 推出了頂級語句,它從應用程式中刪除了不必要的流程,應用程式中只有一個文件可使用頂級語句。頂級語句使主程式更易讀,減少了不必要的模式:命名空間、class program 和 static void main()。

使用 vs 創建命令行項目,選擇.net 5 及以上版本,就會使用頂級語句。

// 使用VS2022创建.NET 6.0平台的命令行程序默认生成的内容
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");

global using

c# 10 添加了 global using 指令,當關鍵字 global 出現在 using 指令之前時,該 using 適用於整個項目,這樣可以減少每個文件 using 指令的行數。global using 指令可以出現在任何原始碼文件的開頭,但需添加在非全局 using 之前。

global 修飾符可以與 static 修飾符一起使用,也可以應用於 using 別名指令。在這兩種情況下,指令的作用域都是當前編譯中的所有文件。

global using System;
global using static System.Console; // 全局静态导入
global using Env = System.Environment; // 全局别名

文件範圍的命名空間

c# 10 引入了文件範圍的命名空間,可將命名空間包含為語句,後加分號且無需添加大括號。一個代碼文件通常只包含一個命名空間,這樣簡化了代碼且消除了一層嵌套。文件範圍的命名空間不能聲明嵌套的命名空間或第二個文件範圍的命名空間,且它必須在聲明任何類型之前,該文件內的所有類型都屬於該命名空間。

using System;
namespace SampleFileScopedNamespace;
class SampleClass { }
interface ISampleInterface { }
struct SampleStruct { }
enum SampleEnum { a, b }
delegate void SampleDelegate(int i);

with 表達式

c# 9 開始引入了 with 表達式,它使用修改的特定屬性和欄位生成其操作對象的複本,未修改的值將保留與原對象相同的值。對於引用類型成員,在複製操作數時僅複製對該成員實例的引用,with 表達式生成的複本和原對象都具有對同一引用類型實例的訪問權限。

在 c# 9 中,with 表達式的左操作數必須為 record 類型,c# 10 進行了改進,with 表達式的左操作數也可以是 struct 類型。

public record NamedPoint(string Name, int X, int Y);
var p1 = new NamedPoint("A", 0, 0);
var p2 = p1 with { Name = "B", X = 5 };
Keep Exploring

延伸阅读

更多文章
同分类 / 同标签 2026/2/7

aot使用經驗總結

從項目創建伊始,就應養成良好的習慣,即只要添加了新功能或使用了較新的語法,就及時進行 aot 發布測試。

继续阅读