1. 程式人生 > 其它 >C#語法特性總結

C#語法特性總結

C#語法特性總結

轉載自:C#語法特性總結 - louzi - 部落格園 (cnblogs.com)

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 };