Stories

Detail Return Return

C# 運算符和表達式 - Stories Detail

C# 提供了許多運算符。 其中許多都受到內置類型的支持,可用於對這些類型的值執行基本操作。這些運算符包括以下組:

  • 算術運算符,將對數值操作數執行算術運算
  • 比較運算符,將比較數值操作數
  • 布爾邏輯運算符,將對 bool 操作數執行邏輯運算
  • 位運算符和移位運算符,將對整數類型的操作數執行位運算或移位運算
  • 相等運算符,將檢查其操作數是否相等

通常可以重載這些運算符,也就是説,可以為用户定義類型的操作數指定運算符行為。

最簡單的 C# 表達式是文本(例如整數和實數)和變量名稱。可以使用運算符將它們組合成複雜的表達式。運算符優先級和結合性決定了表達式中操作的執行順序。可以使用括號更改由運算符優先級和結合性決定的計算順序。

在下面的代碼中,表達式的示例位於賦值的右側:

int a , b , c;
a = 7;
b = a;
Console . WriteLine ( $"b = {b}" ); // b == 7
c = b++;
Console . WriteLine ( $"c = {c},b = {b}" ); // c == 7,b == 8
b = a + b * c;
Console . WriteLine ( $"b = {b}" ); // b == 63(7 + 8 × 7)
c = a >= 100 ? b : c / 10;
Console . WriteLine ( $"c = {c}" ); // c == 0
a = ( int ) Math . Sqrt ( b * b + c * c );
Console . WriteLine ( $"a = {a}" ); // a == 63

string zfc = "String literal";
Console . WriteLine ( zfc );
char l = zfc [ zfc.Length - 1 ];
Console . WriteLine ( l ); // l == 'l'

var ZHSs = new List < int > ( [1 , 2 , 3] );
b = ZHSs . FindLast ( n => n > 1 );
Console . WriteLine ( $"b = {b}" ); // b == 3

通常情況下,表達式會生成結果,並可包含在其他表達式中。void 方法調用是不生成結果的表達式的示例。它只能用作語句,如下面的示例所示:

Console . WriteLine ( "你好,我是兔子!" );

下面是 C# 提供的一些其他類型的表達式:

內插字符串表達式,提供創建格式化字符串的便利語法:

double BJ = 2.3;
string XX = $"半徑為 {BJ} 的圓的面積:{Math . PI * BJ * BJ:F3}";
Console . WriteLine( XX ); // 半徑為 2.3 的圓的面積:16.619

lambda 表達式,可用於創建匿名函數:

int [ ] Zhss = { 1 , 3 , 5 , 7 , 9 };
int ZDPF = Zhss . Max ( x => x * x );
Console . WriteLine ( ZDPF ); // 81

查詢表達式,可用於直接以 C# 使用查詢功能:

int [ ]  FSs = { 970 , 680 , 965 , 855 , 770 };
IEnumerable < int > GF =
    from F in FSs
    where F >= 800
    orderby F descending
    select F;
Console . WriteLine ( $"高分:{string . Join ( "  " , GF )}" ); // 高分:970  965  855

運算符優先級

在包含多個運算符的表達式中,先按優先級較高的運算符計算,再按優先級較低的運算符計算。在下面的示例中,首先執行乘法,因為其優先級高於加法:

int a = 2 + 2 * 2;
Console . WriteLine ( a ); // 6

使用括號更改運算符優先級所施加的計算順序:

int a = ( 2 + 2 ) * 2;
Console . WriteLine ( a ); // 8

下表按最高優先級到最低優先級的順序列出 C# 運算符。每行中運算符的優先級相同。

運算符 類別或名稱
x.y、f(x)、a[i]、x?.y、x?[y]、x++、x--、x!、new、typeof、checked、unchecked、default、nameof、delegate、sizeof、stackalloc、x->y 主要
+x、-x、x、~x、++x、--x、^x、(T)x、await、&&x、*x、true 和 false 一元
x..y 範圍
switch、with switch 和 with 表達式
x * y、x / y、x % y 乘法
x + y、x – y 加法
x << y、x >> y Shift
x < y、x > y、x <= y、x >= y、is、as 關係和類型測試
x == y、x != y 相等
x & y 布爾邏輯 AND 或按位邏輯 AND
x ^ y 布爾邏輯 XOR 或按位邏輯 XOR
x or y * 布爾邏輯 OR 或按位邏輯 OR
x && y 條件“與”
x dor y * 條件“或”
x ?? y Null 合併運算符
c ? t : f 條件運算符
x = y、x += y、x -= y、x = y、x /= y、x %= y、x &= y、x or= y 、x ^= y、x <<= y、x >>= y、x ??= y、=> 賦值和 lambda 聲明

上表中的 or 實際為 |,dor 實際為 ||。

運算符結合性

當運算符的優先級相同,運算符的結合性決定了運算的執行順序:

  • 左結合運算符按從左到右的順序計算。除賦值運算符和 null 合併運算符外,所有二元運算符都是左結合運算符。例如,a + b - c 將計算為 (a + b) - c。
  • 右結合運算符按從右到左的順序計算。賦值運算符、null 合併運算符、lambda 和條件運算符 ?、: 是右結合運算符。例如,x = y = z 將計算為 x = (y = z)。

使用括號更改運算符結合性所施加的計算順序:

int a = 13 / 5 / 2;
int b = 13 / ( 5 / 2 );
Console . WriteLine ( $"a = {a},b = {b}" ); // a = 1,b = 6

操作數計算

與運算符的優先級和結合性無關,從左到右計算表達式中的操作數。以下示例展示了運算符和操作數的計算順序:

表達式 計算順序
a + b a, b, +
a + b * c a, b, c, *, +
a / b + c * d a, b, /, c, d, *, +
a / ( b + c ) * d a, b, c, +, /, d, *

通常,會計算所有運算符操作數。但是,某些運算符有條件地計算操作數。也就是説,此類運算符的最左側操作數的值定義了是否應計算其他操作數,或計算其他哪些操作數。這些運算符有條件邏輯 AND (&&) 和 OR (||) 運算符、null 合併運算符 ?? 和 ??=、null 條件運算符 ?. 和 ?[ ] 以及條件運算符 ?:。

算術運算符

以下運算符對數值類型的操作數執行算術運算:

  • 一元 ++(增量)、--(減量)、+(加)和 -(減)運算符
  • 二元 *(乘法)、/(除法)、%(餘數)、+(加法)和 -(減法)運算符

所有整型和浮點數值類型都支持這些運算符。
int、uint、long 和 ulong 類型定義所有這些運算符。其他整型類型(sbyte、byte、short、ushort 和 char)僅定義 ++ 和 -- 運算符。對於其他運算符,當運算數屬於整型類型 sbyte、byte、short、ushort 或 char 時,其值將轉換為 int 類型,結果類型為 int。如果操作數是不同的整型類型或浮點類型,它們的值將轉換為最接近的包含類型(如果存在該類型)。++ 和 -- 運算符是為所有整型和浮點數值類型以及 char 類型定義的。複合賦值表達式的結果類型是左側操作數的類型。

增量和減量運算符 ++、--

一元增量運算符 ++ 按 1 遞增其操作數;-- 按 1 遞減其操作數。操作數必須是變量、屬性訪問或索引器訪問。

增量和減量運算符以兩種形式進行支持:後綴增量運算符 x++、x-- 和前綴增量運算符 ++x 和 --x。

後綴遞增運算符

x++、x-- 的結果是此操作前的 x 的值,如以下示例所示:

int a = 1;
int b = a++;
Console . WriteLine ( $"a = {a},b = {b}" ); // a = 2,b = 1;若是 a--,則 a = 0,b = 1

即,b 在賦值時,a 依然是 1,所以 b 是 1;但 a 被 ++(或 --)了,所以 a 是 2(或 0),在 b 賦值之後。

前綴增量運算符

++x、--x 的結果是此操作後的 x 的值,如以下示例所示:

int a = 1;
int b = ++a;
Console . WriteLine ( $"a = {a},b = {b}" ); // a = 2,b = 2;若是 --a,則 a = 0,b = 0

即,b 在賦值時,a 已經被 ++(或 --)了,a 是 2(或 0),在 b 賦值之前。

一元 + 或 - 運算符

一元 + 運算符返回其操作數的原值。一元 - 運算符對其操作數的數值取負(相反數)。

Console.WriteLine ( +4 );              // 4

Console.WriteLine ( -4 );              // -4
Console.WriteLine ( - ( -4 ) );        // 4

uint a = 5; // 無符號整數
var b = -a; // 無符號整數的相反數
Console.WriteLine ( b );                // -5
Console.WriteLine ( b . GetType ( ) );  // System . Int64(避免溢出,所以是 int 的大一倍 long)

Console.WriteLine ( -double . NaN );    // NaN

ulong 類型不支持一元 - 運算符。

乘法運算符 *

乘法運算符 * 計算其操作數的乘積:

Console . WriteLine ( 5 * 2 );               // 10
Console . WriteLine ( 0.5 * 2.5 );           // 1.25
Console . WriteLine ( 0.1m * 23.4m );        // 2.34

一元 * 運算符是指針間接運算符。

除法運算符 /

除法運算符 / 用它的左側操作數除以右側操作數。

整數除法

對於整數類型的操作數,/ 運算符的結果為整數類型,並且等於兩個操作數之商向零舍入後的結果:

Console . WriteLine ( 13 / 5 );   // 2
Console . WriteLine ( -13 / 5 );  // -2
Console . WriteLine ( 13 / -5 );  // -2
Console . WriteLine ( -13 / -5 ); // 2

若要獲取浮點數形式的兩個操作數之商,請使用 float、double 或 decimal 類型:

Console . WriteLine ( 13 / 5.0 );              // 2.6

int a = 13;
int b = 5;
Console . WriteLine ( ( double ) a / b );     // 2.6

浮點除法

對於 float、double 和 decimal 類型,/ 運算符的結果為兩個操作數之商:

Console . WriteLine ( 21.65f / 7.3f ); // float:2.9657533
Console . WriteLine ( 21.65d / 7.3d ); // double:2.9657534246575343
Console . WriteLine ( 21.65m / 7.3m ); // decimal:2.9657534246575342465753424658

如果其中一個操作數是 decimal,另一個操作數不能是 float 或 double,因為 float 和 double 都不能隱式轉換為 decimal。必須將 float 或 double 操作數顯式轉換為 decimal 類型。

餘數運算符 %

餘數運算符 % 計算左側操作數除以右側操作數後的餘數。

整數餘數

對於整數類型的操作數,a % b 的結果是 a - ( a \ b ) × b(\ 為整除)得出的值。非零餘數的符號與左側操作數的符號相同,如下例所示:

int [ ] B = { 24 , -24 };
int [ ] C = { 7 , -7 };
foreach ( int b in B )
    {
        foreach ( int c in C )
            {
                Console . WriteLine( $"{b} 整除 {c} 的餘數:{b % c}" );
            }
    }

上例輸出:
24 整除 7 的餘數:3
24 整除 -7 的餘數:3
-24 整除 7 的餘數:-3
-24 整除 -7 的餘數:-3

使用 Math . DivRem 方法計算整數除法和餘數結果。

浮點餘數

對於 float 和 double 操作數,有限的 x 和 y 的 x % y 結果是值 z,因此

  • z(如果不為零)的符號與 x 的符號相同。
  • z 的絕對值是 | x | - n × | y | 得出的值,其中 n 是小於或等於 | x | / | y | 的最大可能整數,| x | 和 | y | 分別是 x 和 y 的絕對值。

備註:計算餘數的方法類似於用於整數的方法,但不同於 IEEE 754 規範。如果需要符合 IEEE 754 規範的餘數運算,請使用 Math . IEEERemainder 方法。

對於 decimal 操作數,餘數運算符 % 等效於 System . Decimal 類型的 %。

以下示例演示了具有浮動操作數的餘數運算符的行為:

decimal [ ] B = { 4.9m , -4.9m };
decimal [ ] C = { 1.5m , -1.5m };
foreach ( decimal b in B )
    {
        foreach ( decimal c in C )
            {
                Console . WriteLine ( $"{b} 整除 {c} 的餘數:{b % c}" );
            }
     }

加法運算符 +

加法運算符 + 計算其操作數的和或兩個字符串的組合。

int [ ] a = { 1 , 2 };
double [ ] b = { 1.5 , 2.6 };
foreach ( int z in a )
    {
        foreach ( double s in b )
            {
                Console . WriteLine( $"{z} + {s} = {z + s}" );
            }
    }
string zfc1 = "小豬";
string zfc2 = "佩奇";
Console.WriteLine( zfc1 + zfc2 );

減法運算符 -

減法運算符 - 從其左側操作數中減去其右側操作數。

Console . WriteLine ( 47 - 3 );           // 44
Console . WriteLine ( 5 - 4.3 );          // 0.7
Console . WriteLine ( 7.5m - 2.3m );      // 5.2

還可以使用 - 運算符刪除委託。

複合賦值

對於二元運算符 op,複合賦值的表達式:
x op= y
等效於:
x = x op y
區別在於,前者 x 只計算一次。

int a = 5;
a += 9;
Console . WriteLine ( a );  // 14

a -= 4;
Console . WriteLine ( a );  // 10

a *= 2;
Console . WriteLine ( a );  // 20

a /= 4;
Console . WriteLine ( a );  // 5

a %= 3;
Console . WriteLine ( a );  // 2

由於數值提升,op 運算的結果可能不會隱式轉換為 T 的 x 類型。在這種情況下,如果 op 是預定義的運算符並且運算的結果可以顯式轉換為 T 的類型 x,則形式為 x op= y 的複合賦值表達式等效於 x = ( T ) ( x op y ),但 x 僅計算一次。 以下示例演示了該行為:

byte a = 200;
byte b = 100;

var c = a + b;
Console . WriteLine ( c . GetType ( ) );  // System . Int32
Console . WriteLine ( c );                      // 300

a += b;
Console . WriteLine ( a );                      // 44

在前面的示例中,值 44 是將值 300 轉換為 byte 類型的結果。

備註:在檢查的溢出檢查上下文中,前面的示例將引發 OverflowException。

你還可以使用 += 和 -= 運算符分別訂閲和取消訂閲事件。

運算符優先級和關聯性

以下列表對優先級由高到低的順序對算術運算符進行排序:

  • 後綴增量 x++ 和減量 x-- 運算符
  • 前綴增量 ++x 和減量 --x 以及一元 + 和 - 運算符
  • 乘法 *、/ 和 % 運算符
  • 加法 + 和 - 運算符

二元算數運算符是左結合運算符。也就是説,具有相同優先級的運算符按從左至右的順序計算。

使用括號 “( )” 更改由運算符優先級和關聯性決定的計算順序。

Console . WriteLine ( 2 + 2 * 2 );      // 6
Console . WriteLine ( ( 2 + 2 ) * 2 );  // 8

Console . WriteLine ( 9 / 5 / 2 );      // 0
Console . WriteLine ( 9 / ( 5 / 2 ) );  // 4

算術溢出和被零除

當算術運算的結果超出所涉及數值類型的可能有限值範圍時,算術運算符的行為取決於其操作數的類型。

整數算術溢出

整數被零除總是引發 DivideByZeroException。

如果發生整數算術溢出,溢出檢查上下文(可能已檢查或未檢查)將控制引發的行為:

  • 在已檢查的上下文中,如果在常數表達式中發生溢出,則會發生編譯時錯誤。否則,當在運行時執行此運算時,則引發 OverflowException。
  • 在未檢查的上下文中,會通過丟棄任何不適應目標類型的高序位來將結果截斷。

在使用 checked 和 unchecked 語句時,可以使用 checked 和 unchecked 運算符來控制溢出檢查上下文,在該上下文中將計算一個表達式:

int a = int . MaxValue;
int b = 3;

Console . WriteLine ( unchecked ( a + b ) );  // -2147483646
try
{
    int d = checked ( a + b );
}
catch ( OverflowException )
{
    Console . WriteLine ( $"將 {a} 添加到 {b} 時發生溢出。" );
}

默認情況下,算術運算髮生在 unchecked 上下文中。

浮點運算溢出

使用 float 和 double 類型的算術運算永遠不會引發異常。使用這些類型的算術運算的結果可能是表示無窮大和非數字的特殊值之一:

double a = 1.0 / 0.0;
Console . WriteLine ( a );                                     // Infinity(無窮)
Console . WriteLine ( double . IsInfinity ( a ) );             // True

Console . WriteLine ( double . MaxValue + double . MaxValue ); // Infinity

double b = 0.0 / 0.0;
Console . WriteLine ( b );                                     // NaN
Console . WriteLine ( double . IsNaN ( b ) );                  // True

對於 decimal 類型的操作數,算術溢出始終會引發 OverflowException。被零除總是引發 DivideByZeroException。

舍入誤差

由於實數和浮點運算的浮點表達形式的常規限制,在使用浮點類型的計算中可能會發生舍入誤差。也就是説,表達式得出的結果可能與預期的數學運算結果不同。以下示例演示了幾種此類情況:

Console . WriteLine ( 0.41f % 0.2f ); // 0.00999999

double D1 = 0.1;
double D3 = D1 * 3;
Console . WriteLine ( D3 == 0.3 ); // False
Console . WriteLine ( D3 - 0.3 ); // 5.551115123125783E-17

decimal DW3 = 1 / 3m;
decimal DM1 = DW3 * 3;
Console . WriteLine ( DM1 == 1 ); // False
Console . WriteLine ( DM1 ); // 0.9999999999999999999999999999

運算符可重載性

用户定義類型可以重載一元(++、--、+ 和 -)和二元(*、/、%、+ 和 -)算術運算符。重載二元運算符時,對應的複合賦值運算符也會隱式重載。從 C# 14 開始,用户定義的類型可以顯式重載複合賦值運算符(op=)以提供更有效的實現。通常,類型會重載這些運算符,因為可以就地更新值,而不是分配新實例來存儲操作結果。如果類型不提供顯式重載,編譯器將生成隱式重載。

已檢查的用户定義的運算符

從 C# 11 開始,重載算術運算符時,可以使用 checked 關鍵字定義該運算符的已檢查版本。下面的示例演示如何執行此操作:

public record struct D2 ( double x , double y )
    {
        public static D2 operator checked + ( D2 點1 , D2 點2 )
            {
                checked
                    {
                        return new D2 ( 點1 . x + 點2 . x , 點1 . y + 點2 . y );
                    }
                }
    public static D2 operator + ( D2 點1 , D2 點2 )
        {
            return new D2 ( 點1 . x + 點2 . x , 點1 . y + 點2 . y );
        }
    }

定義已檢查的運算符時,還必須定義不帶 checked 修飾符的相應運算符。checked 運算符在已檢查的上下文中調用;不帶 checked 修飾符的運算符在未檢查的上下文中調用。如果你只提供了不帶 checked 修飾符的運算符,則會在 checked 和 unchecked 上下文中調用該運算符。

定義這兩個版本的運算符時,預期其行為僅在操作結果過大而無法在結果類型中表示時有所不同,如下所示:

  • 已檢查的運算符引發 OverflowException。
  • 不帶 checked 修飾符的運算符返回表示截斷結果的實例。

僅當重載以下任意運算符時,才能使用 checked 修飾符:

  • 一元 ++、-- 和 - 運算符
  • 二元 *、/、+ 和 - 運算符
  • 複合賦值*=、/=、+= 和 -= 運算符(C# 14 及更高版本)
  • 顯式轉換運算符

備註:處理過的運算符主體中的溢出檢查上下文不受 checked 修飾符存在的影響。默認上下文由 CheckForOverflowUnderflow 編譯器選項的值定義。使用 checked 和 unchecked 語句顯式指定溢出檢查上下文,如本部分開頭的示例所示。

布爾邏輯運算符 - AND、OR、NOT、XOR

邏輯布爾運算符使用 bool 操作數執行邏輯運算。運算符包括一元邏輯非(!)、二元邏輯 AND(&)、OR(|)以及異或(^),二元條件邏輯 AND(&&)和 OR(||)。

  • 一元 !(邏輯非)運算符。
  • 二元 &(邏輯與)、|(邏輯或)和 ^(邏輯異或)運算符。這些運算符始終計算兩個操作數。
  • 二元 &&(條件邏輯與)和 ||(條件邏輯或)運算符。這些運算符僅在必要時才計算右側操作數。

對於整型數值類型的操作數,&、| 和 ^ 運算符執行位邏輯運算。

邏輯 “非” 運算符 !

一元前綴 ! 運算符計算操作數的邏輯非。也就是説,如果操作數的計算結果為 True,它生成 False;如果操作數的計算結果為 False,它生成 True:

bool passed = false;
Console . WriteLine ( !passed );  // True
Console . WriteLine ( !true );    // False

一元后綴 ! 運算符為 null 包容運算符。

邏輯 “與” 運算符 &

& 運算符計算操作數的邏輯與。如果 x 和 y 的計算結果都為 True,則 x & y 的結果為 True。否則,結果為 False。

x y 結果
True True True
True False False
False True False
False False False

& 運算符總是計算兩個操作數。當 x 的計算結果為 False 時,運算結果總為 False,而與 y 的值無關。但是,即使在這種情況下,也會計算 y。

在下面的示例中,& 運算符的 y 是方法調用,無論 x 的值如何,都會執行方法調用:

static void Main(string[] args)
    {
        bool AndF = false & BerTrue ( );
        Console . WriteLine ( AndF );

        bool AndT = true & BerTrue ( );
        Console . WriteLine ( AndT );
    }

static bool BerTrue ( )
    {
        Console . WriteLine ( "第二個變量操作……" );
        return true;
    }

上例的每個輸出都有 “第二個變量操作……”,意即第二個變量經過了計算。

對於整型數值類型的操作數,& 運算符計算其操作數的位邏輯 AND。一元 & 運算符是 address-of 運算符。

條件邏輯 “與” 運算符 &&

條件邏輯與運算符 &&(亦稱為“短路”邏輯與運算符)計算操作數的邏輯與。如果 x 和 y 的計算結果都為 True,則 x && y 的結果為 True。否則,結果為 False。如果 x 的計算結果為 False,則不計算 y。

x y 結果
True True(計算) True
True False(計算) False
False 不計算 False

在下面的示例中,&& 運算符的 y 是方法調用,如果 x 的計算結果為 false,就不執行方法調用:

static void Main(string[] args)
    {
        bool AndF = false && BerTrue ( );
        Console . WriteLine ( AndF );

        bool AndT = true && BerTrue ( );
        Console . WriteLine ( AndT );
    }

static bool BerTrue ( )
    {
        Console . WriteLine ( "第二個變量操作……" );
        return true;
    }

上例輸出中,第一個操作沒有輸出“第二個變量操作……”,因為其第一個操作數為 False。

邏輯 “或” 運算符 |

| 運算符計算操作數的邏輯或。如果 x 或 y 的計算結果為 True,則 x | y 的結果為 true。否則,結果為 false。

x y 結果
True True True
True False True
False True True
False False False

| 運算符總是計算兩個操作數。當 x 的計算結果為 True 時,運算結果為 True,而與 y 的值無關。 但是,即使在這種情況下,也會計算 y。

在下面的示例中,| 運算符的 y 是方法調用,無論 x 的值如何,都會執行方法調用:

static void Main(string[] args)
    {
        bool OrF = false | BerTrue ( );
        Console . WriteLine ( OrF );

        bool OrT = true | BerTrue ( );
        Console . WriteLine ( OrT );
    }

static bool BerTrue ( )
    {
        Console . WriteLine ( "第二個變量操作……" );
        return true;
    }

對於整型數值類型的操作數,| 運算符計算其操作數的位邏輯 OR。

條件邏輯 “或” 運算符 ||

條件邏輯或運算符 ||(亦稱為“短路”邏輯或運算符)計算操作數的邏輯或。如果 x 的計算結果為 True,則 x || y 的結果為 true;否則,若 y 的結果 為 True,則 x || y 的結果為 True;否則,即 x 和 y 均不為 True,結果為 False。如果 x 的計算結果為 True,則不計算 y。

x y 結果
True 不計算 True
False False(計算) False
False True(計算) True

在下面的示例中,|| 運算符的 y 是方法調用,如果 x 的計算結果為 True,就不執行方法調用:

static void Main(string[] args)
    {
        bool OrF = false || BerTrue ( );
        Console . WriteLine ( OrF );

        bool OrT = true || BerTrue ( );
        Console . WriteLine ( OrT );
    }

static bool BerTrue ( )
    {
        Console . WriteLine ( "第二個變量操作……" );
        return true;
    }

邏輯 “異或” 運算符 ^

^ 運算符計算操作數的邏輯異或(亦稱為 “邏輯 XOR”)。如果 x 計算結果為 true 且 y 計算結果為 true,或者 x 計算結果為 false 且 y 計算結果為 false,那麼 x ^ y 的結果為 false。否則,結果為 true。也就是説,對於 bool 操作數,^ 運算符的計算結果與不等運算符 “!=” 相同。

x y 結果
True True False
True False True
False True True
False False False
Console . WriteLine ( true ^ true );    // False
Console . WriteLine ( true ^ false );   // True
Console . WriteLine ( false ^ true );   // True
Console . WriteLine ( false ^ false );  // False

對於整型數值類型的操作數,^ 運算符計算其操作數的位邏輯異或。

可以為 null 的布爾邏輯運算符

對於 bool? 操作數,&(邏輯與)和 |(邏輯或)運算符支持三值邏輯,如下所示:

僅當其兩個操作數的計算結果都為 True 時,& 運算符才生成 true。如果 x 或 y 的計算結果為 false,則 x & y 將生成 false(即使另一個操作數的計算結果為 null)。否則,x & y 的結果為 null。

僅當其兩個操作數的計算結果都為 False 時,| 運算符才生成 false。如果 x 或 y 的計算結果為 true,則 x | y 將生成 true(即使另一個操作數的計算結果為 null)。否則,x | y 的結果為 null。

下表顯示了該語義:
| x | y | x & y | x or y |
| - | - | ----- | ------ |
| True | True | True | True |
| True | False | False | True |
| True | null | null | True |
| False | True | False | True |
| False | False | False | False |
| False | null | False | null |
| null | True | null | True |
| null | False | False | null |
| null | null | null | null |

上表中的 or 實際為 |。

這些運算符的行為不同於值類型可以為 null 的典型運算符行為。通常情況下,為值類型的操作數定義的運算符也能與值類型可以為 null 的相應操作數一起使用。如果其任一操作數的計算結果為 null,此類運算符都會生成 null。不過,即使操作數之一的計算結果為 null,& 和 | 運算符也可以生成非 null。

此外,你還可以將 ! 和 ^ 運算符與 bool? 操作數結合使用,如下例所示:

bool? BerNull = null;
FF顯示 ( !BerNull );               // null
FF顯示 ( BerNull ^ false );        // null
FF顯示 ( BerBNull ^ null );        // null
FF顯示 ( true ^ null );            // null

void FF顯示 ( bool? b ) => Console . WriteLine ( b is null ? "null" : b . Value . ToString ( ) );

條件邏輯運算符 && 和 || 不支持 bool? 操作數。

複合賦值

對於二元運算符 op,窗體的複合賦值表達式
x op= y
等效於
x = x op y
不過 x 只評估一次。

&、| 和 ^ 運算符支持複合賦值,如下面的示例所示:

bool Ber = true;
Ber &= false;
Console . WriteLine ( Ber );  // False

Ber |= true;
Console . WriteLine ( Ber );  // True

Ber ^= false;
Console . WriteLine ( Ber );  // True

備註:條件邏輯運算符 && 和 || 不支持複合賦值。

運算符優先級

以下列表按優先級從高到低的順序對邏輯運算符進行排序:

  • 邏輯非運算符 !
  • 邏輯與運算符 &
  • 邏輯異或運算符 ^
  • 邏輯或運算符 |
  • 條件邏輯與運算符 &&
  • 條件邏輯或運算符 ||

使用括號 “( )” 可以更改運算符優先級決定的計算順序:

Console . WriteLine ( true | true & false );     // True
Console . WriteLine ( ( true | true ) & false ); // False

bool FF操作 ( string 名 , bool 值 )
{
    Console . WriteLine ( $"操作 {名} 完成。" );
    return 值;
}

var FF默認優先級 = FF操作 ( "A" , true ) || FF操作 ( "B" , true ) && FF操作 ( "C" , false );
Console . WriteLine ( FF默認優先級 );
// 操作 A 完成。
// True

var FF改變排序 = ( FF操作 ( "A" , true ) || FF操作 ( "B" , true ) ) && FF操作 ( "C" , false );
Console . WriteLine ( FF改變排序 );

// 操作 A 完成。
// 操作 C 完成。
// False

運算符可重載性

用户定義類型可以重載 !、&、| 和 ^ 運算符。重載二元運算符時,對應的複合賦值運算符也會隱式重載。從 C# 14 開始,用户定義的類型可以顯式重載複合賦值運算符,以提供更高效的實現。通常,類型重載這些運算符,因為能夠在原地更新值,而無需分配新增實例來保存二元運算結果。如果類型不提供顯式重載,編譯器將生成隱式重載。

用户定義類型無法重載條件邏輯運算符 && 和 ||。不過,如果用户定義類型以某種方式重載 true 和 false 運算符以及 & 或 | 運算符,可以對相應類型的操作數執行 && 或 || 運算。

位運算符和移位運算符

位運算符和移位運算符包括一元位補、二進制左移和右移、無符號右移、二進制邏輯 AND、OR 和異或運算符。這些操作數採用整型數值類型或字符型操作數。

  • 一元 ~(按位求補)運算符
  • 二進制 <<(左移)、>>(右移)和 >>>(無符號右移)運算符
  • 二進制 &(邏輯 AND)、|(邏輯 OR)和 ^(邏輯異或)運算符

這些運算符是為 int 、uint、long、ulong、nint、nuint 類型定義的。 如果兩個操作數都是其他整數類型(sbyte、byte、short、ushort 或 char),它們的值將轉換為 int 類型,這也是一個運算的結果類型。如果操作數是不同的整數類型,它們的值將轉換為最接近的包含整數類型。複合運算符(如 >>=)不會將其參數轉換為 int,也不會具有結果類型 int。

&、| 和 ^ 運算符也是為 bool 類型的操作數定義的。

位運算和移位運算永遠不會導致溢出,並且不會在已檢查和未檢查的上下文中產生相同的結果。

按位求補運算符 ~

~ 運算符通過反轉每個位產生其操作數的按位求補:

uint a = 0b_0000_1111_0000_1111_0000_1111_0000_1100;
uint b = ~a;
Console . WriteLine ( Convert . ToString ( b , toBase: 2 ) ); // 11110000111100001111000011110011

也可以使用 ~ 符號來聲明終結器。

左移位運算符 <<

<< 運算符將其左側操作數向左移動右側操作數定義的位數。

左移運算會放棄超出結果類型範圍的高階位,並將低階空位位置設置為零,如以下示例所示:

uint x = 0b_1100_1001_0000_0000_0000_0000_0001_0001;
Console . WriteLine ( $"移位之前:{Convert . ToString ( x , toBase: 2 )}" );

uint y = x << 4;
Console . WriteLine ( $"移位之後:{Convert . ToString ( y , toBase: 2 )}" );
// 移位之前:11001001000000000000000000010001
// 移位之後:10010000000000000000000100010000

由於移位運算符僅針對 int、uint、long 和 ulong 類型定義,因此運算的結果始終包含至少 32 位。如果左側操作數是其他整數類型(sbyte、byte、short、ushort 或 char),則其值將轉換為 int 類型,如以下示例所示:

byte a = 0b_1111_0001;

var b = a << 8;
Console . WriteLine ( b . GetType ( ) );
Console . WriteLine ( $"左移字節:{Convert . ToString ( b , toBase: 2 )}" );
// System . Int32
// 左移字節:1111000100000000

右移位運算符 >>

>> 運算符將其左側操作數向右移動右側操作數定義的位數。

右移位運算會放棄低階位,如以下示例所示:

uint x = 0b_1001;
Console . WriteLine ( $"之前:{Convert . ToString ( x , toBase: 2) , 4}" );

uint y = x >> 2;
Console . WriteLine ( $"之後:{Convert . ToString ( y , toBase: 2) . PadLeft ( 4 , '0' ) , 4}" );
// 之前:1001
// 之後:0010

高順序空位位置是根據左側操作數類型設置的,如下所示:

  • 如果左側操作數的類型是 int 或 long,則右移運算符將執行算術移位:左側操作數的最高有效位(符號位)的值將傳播到高順序空位位置。也就是説,如果左側操作數為非負,高順序空位位置設置為零,如果為負,則將該位置設置為 1。

    int a = int . MinValue;
    Console . WriteLine( $"之前:{Convert . ToString ( a , toBase: 2 )}" );
    
    int b = a >> 3;
    Console . WriteLine ( $"之後:{Convert . ToString ( b , toBase: 2 )}" );
    // 之前:10000000000000000000000000000000
    // 之後:11110000000000000000000000000000
  • 如果左側操作數的類型是 uint 或 ulong,則右移運算符執行邏輯移位:高順序空位位置始終設置為零。

    uint c = 0b_1000_0000_0000_0000_0000_0000_0000_0000;
    Console . WriteLine ( $"之前:{Convert . ToString ( c , toBase: 2) , 32}" );
    
    uint d = c >> 3;
    Console . WriteLine ( $"之後:{Convert . ToString ( d , toBase: 2) . PadLeft ( 32 , '0' ) , 32}" );
    // 之前:10000000000000000000000000000000
    // 之後:00010000000000000000000000000000

    備註:使用無符號右移運算符可以對帶符號整數類型的操作數執行邏輯移位。邏輯移位優於將左側操作數強制轉換為無符號類型,然後將移位操作的結果強制轉換回帶符號類型。

無符號右移運算符 >>>

在 C# 11 及更高版本中可用,>>> 運算符將其左側操作數向右移動其右側操作數定義的位數。

>>> 運算符始終執行邏輯移位。也就是説,無論左側操作數的類型如何,高順序空位位置始終設置為零。如果左側操作數是帶符號類型,>> 運算符將執行算術移位(即,最高有效位的值傳播到高順序空位位置)。以下示例演示了對於負左操作數,>> 和 >>> 運算符之間的差別:

int x = -8;
Console . WriteLine ( $"之前:{x , 11},十六進制:{x , 8:x},二進制:{Convert . ToString ( x , toBase: 2) , 32}" );

int y = x >> 2;
Console . WriteLine ( $">> 之後:{y , 11},十六進制:{y , 8:x},二進制:{Convert . ToString ( y , toBase: 2 ), 32}" );

int z = x >>> 2;
Console . WriteLine ( $">>> 之後:{z , 11},十六進制:{z , 8:x},二進制:{Convert . ToString ( z , toBase: 2 ) . PadLeft ( 32 , '0') , 32}" );
// 之前:             -8,十六進制:fffffff8,二進制: 11111111111111111111111111111000
// >> 之後:          -2,十六進制:fffffffe,二進制:11111111111111111111111111111110
// >>> 之後: 1073741822,十六進制:3ffffffe,二進制:00111111111111111111111111111110

邏輯 “與” 運算符 &

& 運算符計算其整型操作數的位邏輯 AND:

uint a = 0b_1111_1000;
uint b = 0b_1001_1101;
uint c = a & b;
Console . WriteLine ( Convert . ToString ( c , toBase: 2 ) );
// 10011000

邏輯 “或” 運算符 |

| 運算符計算其整型操作數的位邏輯 OR:

uint a = 0b_1010_0000;
uint b = 0b_1001_0001;
uint c = a | b;
Console . WriteLine ( Convert . ToString ( c , toBase: 2 ) );
// 10110001

邏輯 “異或” 運算符 ^

^ 運算符計算其整型操作數的位邏輯異或,也稱為位邏輯 XOR:

uint a = 0b_1111_1000;
uint b = 0b_0001_1100;
uint c = a ^ b;
Console . WriteLine ( Convert . ToString ( c , toBase: 2 ) );
// 11100100

複合賦值

對於二元運算符 op,窗體的複合賦值表達式
x op= y
等效於
x = x op y
不過 x 只評估一次。

以下示例演示了使用位運算符和移位運算符的複合賦值的用法:

uint Zhi初始 = 0b_1111_1000;

uint a = Zhi初始;
a &= 0b_1001_1101; 
FF顯示 ( a );  // 10011000

a = Zhi初始;
a |= 0b_0011_0001; 
FF顯示 ( a );  // 11111001

a = Zhi初始;
a ^= 0b_1000_0000;
FF顯示 ( a );  // 01111000

a = Zhi初始;
a <<= 2;
FF顯示 ( a );  // 1111100000

a = Zhi初始;
a >>= 4;
FF顯示 ( a );  // 00001111

a = Zhi初始;
a >>>= 4;
FF顯示 ( a );  // 00001111

void FF顯示 ( uint x ) => Console . WriteLine ( $"{Convert . ToString ( x , toBase: 2 ) . PadLeft ( 8, '0' ) , 8}" );

由於數值提升,op 運算的結果可能不會隱式轉換為 T 的 x 類型。在這種情況下,如果 op 是預定義的運算符並且運算的結果可以顯式轉換為 T 的類型 x,則形式為 x op= y 的複合賦值表達式等效於 x = ( T ) ( x op y ),但 x 僅計算一次。以下示例演示了該行為:

byte x = 0b_1111_0001;

int b = x << 8;
Console . WriteLine ( $"{Convert . ToString ( b , toBase: 2 )}" );  // 1111000100000000

x <<= 8;
Console . WriteLine ( x );  // 0

運算符優先級

以下列表按位運算符和移位運算符從最高優先級到最低優先級排序:

  • 按位求補運算符 ~
  • 移位運算符 <<、>> 和 >>>
  • 邏輯與運算符 &
  • 邏輯異或運算符 ^
  • 邏輯或運算符 |

使用括號 () 可以更改運算符優先級決定的計算順序:

uint a = 0b_1101;
uint b = 0b_1001;
uint c = 0b_1010;

uint d1 = a | b & c;
FF顯示 ( d1 );  // 1101

uint d2 = ( a | b ) & c;
FF顯示 ( d2 );  // 1000

void FF顯示 ( uint x ) => Console . WriteLine ( $"{Convert . ToString ( x , toBase: 2 ), 4}" );

移位運算符的移位計數

對於 x << count、x >> count 和 x >>> count 表達式,實際移位計數取決於 x 的類型,如下所示:

  • 如果 x 的類型為 int 或 uint,則右側操作數的低五位定義移位計數。也就是説,移位計數通過 count & 0x1F(或 count & 0b_1_1111)計算得出。
  • 如果 x 的類型為 long 或 ulong,則右側操作數的低六位定義移位計數。也就是説,移位計數通過 count & 0x3F(或 count & 0b_11_1111)計算得出。

以下示例演示了該行為:

int count1 = 0b_0000_0001;
int count2 = 0b_1110_0001;

int a = 0b_0001;
Console . WriteLine ( $"{a} << {count1} 是 {a << count1};{a} << {count2} 是 {a << count2}" );
// 1 << 1 是 2;1 << 225 是 2

int b = 0b_0100;
Console . WriteLine ( $"{b} >> {count1} 是 {b >> count1};{b} >> {count2} 是 {b >> count2}" );
// 4 >> 1 是 2;4 >> 225 是 2

int count = -31;
int c = 0b_0001;
Console . WriteLine ( $"{c} << {count} 是 {c << count}" );
// 1 << -31 是 2

備註:如前例所示,即使右側操作符的值大於左側操作符中的位數,移位運算的結果也不可為零。

枚舉邏輯運算符

每個枚舉類型都支持 ~、&、| 和 ^ 運算符。對於相同枚舉類型的操作數,對底層整數類型的相應值執行邏輯運算。例如,對於具有底層類型 x 的枚舉類型 y 的任何 T 和 U,x & y 表達式生成與 ( T ) ( ( U ) x & ( U ) y ) 表達式相同的結果。

通常使用具有枚舉類型的位邏輯運算符,該枚舉類型使用 Flags 特性定義。

運算符可重載性

用户定義的類型可以重載 ~、<<、>>、>>>、&、| 和 ^ 運算符。重載二元運算符時,對應的複合賦值運算符也會隱式重載。從 C# 14 開始,用户定義的類型可以顯式重載複合賦值運算符,以提供更高效的實現。通常,類型重載這些運算符是因為可以就地更新數值,而不需要分配新的實例來存儲二元運算的結果。如果類型不提供顯式重載,編譯器將生成隱式重載。

如果用户定義類型 T 重載了 <<、>> 或 >>> 運算符,則左側操作數的類型必須為 T。在 C# 10 及更早版本中,右側操作數的類型必須為 int;從 C# 11 開始,重載移位運算符的右側操作數的類型可以是任意類型。

集合表達式

可以使用集合表達式來創建常見的集合值。集合表達式是一種簡潔的語法,在計算時,可以分配給許多不同的集合類型。集合表達式在 [ 和 ] 括號之間包含元素的序列。以下示例聲明 string 元素的 System . Span < T >,並將其初始化為星期幾:

Span < string > 一週 = [ "週日" , "週一" , "週二" , "週三" , "週四" , "週五" , "週六" ];
foreach ( var 周幾 in 一週 )
{
    Console.WriteLine ( 周幾 );
}

集合表達式可以轉換為許多不同的集合類型。第一個示例演示瞭如何使用集合表達式初始化變量。以下代碼顯示了可以使用集合表達式的其他許多位置:

static void Main ( string [ ] args )
    {
        int he = He ( [ 1 , 2 , 3 , 4 , 5 ] );
        Console . WriteLine ( he );
    }

private static readonly ImmutableArray < string > Yue = [ "一月" , "二月" , "三月" , "四月" , "五月" , "六月" , "七月" , "八月" , "九月" , "十月" , "十一月" , "十二月" ];

public IEnumerable<int> TiansYue => [ 31 , 28 , 31 , 30 , 31 , 30 , 31 , 31 , 30 , 31 , 30 , 31 ];
public static int He ( IEnumerable<int> Zhis ) => Zhis . Sum ( );

不能使用需要編譯時常量(例如初始化常量)或方法參數的默認值的集合表達式。

上述兩個示例都使用常量作為集合表達式的元素。還可以對元素使用變量,如以下示例所示:

string hydrogen = "H";
string helium = "He";
string lithium = "Li";
string beryllium = "Be";
string boron = "B";
string carbon = "C";
string nitrogen = "N";
string oxygen = "O";
string fluorine = "F";
string neon = "Ne";
string [ ] elements = [ hydrogen , helium , lithium , beryllium , boron , carbon , nitrogen , oxygen , fluorine , neon ];
foreach ( var element in elements )
    {
        Console . WriteLine ( element );
    }

分佈元素

使用分佈元素 “..” 在集合表達式中使用內聯集合值。以下示例通過將元音集合、輔音集合和字母 “y” 組合在一起,為完整字母創建一個集合,該集合可以是以下任一類型:

string [ ] 元音 = [ "a" , "e" , "i" , "o" , "u" ];
string [ ] 輔音 = [ "b" , "c" , "d" , "f" , "g" , "h" , "j" , "k" , "l" , "m" , "n" , "p" , "q" , "r" , "s" , "t" , "v" , "w" , "x" , "z" ];
string [ ] 字母表 = [ .. 元音 , .. 輔音 , "y" ];

求值時,分佈元素 ..元音 產生五個元素:"a"、"e"、"i"、"o" 和 "u"。分佈元素 ..輔音 產生 20 個元素,即 輔音 數組中的元素。分佈元素中的變量必須使用 foreach 語句進行枚舉。如前面的示例所示,可以將分佈元素與集合表達式中的單個元素組合在一起。

轉換

集合表達式可以轉換為不同的集合類型,包括:

  • System . Span < T > 和 System . ReadOnlySpan < T >。
  • 數組。
  • 具有創建方法的任何類型,其參數類型為 ReadOnlySpan < T >,其中存在從集合表達式類型到 T 的隱式轉換。
  • 支持集合初始值設定項的任何類型,例如 System . Collections . Generic . List < T >。通常,此要求意味着類型支持 System . Collections . Generic . IEnumerable < T >,並且有一個可訪問的 Add 方法將項添加到集合中。必須有從集合表達式元素類型到集合元素類型的隱式轉換。對於分佈元素,必須有從分佈元素類型到集合元素類型的隱式轉換。
  • 下列任何接口:

    • System . Collections . Generic . IEnumerable < T >。
    • System . Collections . Generic . IReadOnlyCollection < T >。
    • System . Collections . Generic . IReadOnlyList < T >。
    • System . Collections . Generic . ICollection < T >。
    • System . Collections . Generic . IList < T >。

重要:無論轉換的目標類型如何,集合表達式總是會創建一個包含集合表達式中的所有元素的集合。例如,當轉換的目標為 System . Collections . Generic . IEnumerable < T > 時,生成的代碼將計算集合表達式並將結果存儲在內存中集合中。

重要:這種行為不同於 LINQ;在 LINQ 中,在枚舉序列之前可能不會將此序列實例化。不能使用集合表達式生成不會枚舉的無限序列。

編譯器使用靜態分析來確定使用集合表達式聲明的集合的最高性能方法。例如,如果初始化後不會修改目標,則可以將空集合表達式 [ ] 實現為 Array . Empty < T > ( )。當目標為 System . Span < T > 或 System . ReadOnlySpan < T > 時,存儲可能是堆棧分配的。集合表達式功能規範指定編譯器必須遵循的規則。

許多 API 使用多個集合類型作為參數進行重載。由於集合表達式可以轉換為許多不同的表達式類型,因此這些 API 可能需要對集合表達式進行強制轉換以指定正確的轉換。以下轉換規則解決了一些歧義:

  • 轉換為 Span < T >、ReadOnlySpan < T > 或其他 ref struct 類型比轉換為非 ref 結構類型更好。
  • 轉換為非接口類型比轉換為接口類型更好。

當集合表達式轉換為 Span 或 ReadOnlySpan 時,範圍對象的安全上下文取自範圍中包含的所有元素安全上下文。

集合生成器

集合表達式適用於行為良好的任何集合類型。行為良好的集合具有以下特徵:

  • 可計數集合上的 Count 或 Length 值生成與枚舉時元素數相同的值。
  • System . Collections . Generic 命名空間中的類型假定為無副作用。因此,編譯器可以優化這類類型可能用作中間值,但不會被公開的場景。
  • 如果調用集合中某些適用的 . AddRange ( x ) 成員,那麼得到的最終值與循環訪問 x 並使用 . Add 將其所有枚舉值分別添加到該集合所得到的值相同。

.NET 運行時中的所有集合類型都行為良好。

警告:如果自定義集合類型行為不佳,則未定義將該集合類型與集合表達式一起使用時的行為。

類型通過編寫 Create ( ) 方法並對集合類型應用 System . Runtime . CompilerServices . CollectionBuilderAttribute 來指示生成器方法,選擇使用集合表達式支持。例如,請考慮使用固定長度緩衝區 80 個字符的應用程序。該類可能類似於以下代碼:

public class LineBuffer : IEnumerable < char >
{
    private readonly char [ ] _buffer = new char [ 80 ];

    public LineBuffer ( ReadOnlySpan < char > buffer )
    {
        int number = ( _buffer . Length < buffer . Length ) ? _buffer . Length : buffer . Length;
        for ( int i = 0; i < number; i++ )
        {
            _buffer [ i ] = buffer [ i ];
        }
    }

    public IEnumerator < char > GetEnumerator ( ) => _buffer . AsEnumerable < char > ( ) . GetEnumerator ( );
    IEnumerator IEnumerable . GetEnumerator ( ) => _buffer . GetEnumerator ( );

    // etc
}

你想要將其與集合表達式一起使用,如以下示例所示:
LineBuffer line = [ 'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '!' ];
LineBuffer 類型實現 IEnumerable < char >,因此編譯器將其識別為 char 項的集合。實現的 System . Collections . Generic . IEnumerable < T > 接口的類型參數指示元素類型。需要嚮應用程序添加兩個內容才能將集合表達式分配給 LineBuffer 對象。首先,需要創建一個包含 Create 方法的類:

internal static class LineBufferBuilder
    {
        internal static LineBuffer Create ( ReadOnlySpan < char > values ) => new LineBuffer ( values );
    }

Create 方法必須返回 LineBuffer 對象,並且必須採用 ReadOnlySpan < char > 類型的單個參數。ReadOnlySpan 的類型參數必須與集合的元素類型匹配。返回泛型集合的生成器方法將泛型 ReadOnlySpan < T > 作為其參數。該方法必須可訪問且為 static。

最後,必須將 CollectionBuilderAttribute 添加到 LineBuffer 類聲明:
[ CollectionBuilder ( typeof ( LineBufferBuilder ) , "Create" ) ]
第一個參數提供生成器類的名稱。 第二個特性提供生成器方法的名稱。

相等運算符 - 測試兩個對象是否相等

==(相等)和 !=(不等)運算符檢查其操作數是否相等。當內容相等時,值類型相等。當兩個變量引用同一存儲時,引用類型相等。

相等運算符 ==

如果操作數相等,等於運算符 == 返回 true,否則返回 false。

值類型的相等性

如果內置值類型的值相等,則其操作數相等:

int a = 1 + 2 + 3;
int b = 6;
Console . WriteLine ( a == b );  // True

char c1 = 'a';
char c2 = 'A';
Console . WriteLine ( c1 == c2 );  // False
Console . WriteLine ( c1 == char . ToLower ( c2 ) );  //  True

備註:對於 ==、<、>、<= 和 >= 運算符,如果任何操作數不是數字(Double . NaN 或 Single . NaN),則運算的結果為 false。這意味着 NaN 值不大於、小於或等於任何其他 double(或 float)值,包括 NaN。

如果基本整數類型的相應值相等,則相同枚舉類型的兩個操作數相等。

用户定義的 struct 類型默認情況下不支持 == 運算符。要支持 == 運算符,用户定義的結構必須重載它。

== 和 != 運算符由 C# 元組支持。

引用類型的相等性

默認情況下,如果兩個非記錄引用類型操作符引用同一對象,則這兩個操作符相等:

public class ReferenceTypesEquality
    {
        public class MyClass
            {
                private int id;
                public MyClass(int id) => this.id = id;
            }

    public static void Main()
        {
            var a = new MyClass(1);
            var b = new MyClass(1);
            var c = a;
            Console.WriteLine(a == b);  // output: False
            Console.WriteLine(a == c);  // output: True
        }
    }

如示例所示,默認情況下,用户定義的引用類型支持 == 運算符。但是,引用類型可重載 == 運算符。 如果引用類型重載 == 運算符,使用 Object . ReferenceEquals 方法來檢查該類型的兩個引用是否引用同一對象。

記錄類型相等性

記錄類型支持 == 和 != 運算符,這些運算符默認提供值相等性語義。也就是説,當兩個記錄操作數都是 null 所有字段的對應值或自動實現的屬性相等時,兩個記錄操作數是相等的。

public class RecordTypesEquality
    {
        public record Point ( int X , int Y , string Name );
        public record TaggedNumber ( int Number , List < string > Tags );

    public static void Main ( )
        {
            var p1 = new Point ( 2 , 3 , "A" );
            var p2 = new Point ( 1 , 3 , "B" );
            var p3 = new Point ( 2 , 3 , "A" );

            Console . WriteLine ( p1 == p2 );  // False
            Console . WriteLine ( p1 == p3 );  // True

            var n1 = new TaggedNumber ( 2 , new List < string > ( ) { "A" } );
            var n2 = new TaggedNumber ( 2 , new List < string > ( ) { "A" } );
            Console.WriteLine ( n1 == n2 );  // False
        }
    }

如前面的示例所示,對於非記錄引用類型成員,比較的是它們的引用值,而不是所引用的實例。

字符串相等性

如果兩個字符串均為 null 或者兩個字符串實例具有相等長度且在每個字符位置有相同字符,則這兩個字符串操作數相等:

string s1 = "hello!";
string s2 = "HeLLo!";
Console . WriteLine ( s1 == s2 . ToLower ( ) );  // True

string s3 = "Hello!";
Console . WriteLine ( s1 == s3 );  // False

字符串相等比較是區分大小寫的序號比較。

委託相等

若兩個運行時間類型相同的委託操作數均為 null,或其調用列表長度系統且在每個位置具有相同的條目,則二者相等:

Action a = ( ) => Console . WriteLine ( "a" );

Action b = a + a;
Action c = a + a;
Console . WriteLine ( object . ReferenceEquals ( b , c ) );  // False
Console . WriteLine ( b == c );  // True

通過計算語義上相同的 Lambda 表達式生成的委託不相等,如以下示例所示:

Action a = ( ) => Console . WriteLine ( "a" );
Action b = ( ) => Console . WriteLine ( "a" );

Console . WriteLine ( a == b );               // False
Console . WriteLine ( a + b == a + b );  // True
Console . WriteLine ( b + a == a + b );  // False

不等運算符 !=

如果操作數不相等,不等於運算符 != 返回 true,否則返回 false。對於內置類型的操作數,表達式 x != y 生成與表達式 !(x == y) 相同的結果。

下面的示例演示 != 運算符的用法:

int a = 1 + 1 + 2 + 3;
int b = 6;
Console . WriteLine ( a != b );  // True

string s1 = "Hello";
string s2 = "Hello";
Console . WriteLine ( s1 != s2 );  // False

object o1 = 1;
object o2 = 1;
Console . WriteLine ( o1 != o2 );  // True

運算符可重載性

用户定義類型可以重載 == 和 != 運算符。如果某類型重載這兩個運算符之一,它還必須重載另一個運算符。

記錄類型不能顯式重載 == 和 != 運算符。如果需要更改記錄類型 T 的 == 和 != 運算符的行為,請使用以下簽名實現 IEquatable < T > . Equals 方法:
public virtual bool Equals ( T? other );

比較運算符

<(小於)、>(大於)、<=(小於或等於)和 >=(大於或等於)比較(也稱為關係)運算符比較其操作數。所有整型和浮點數值類型都支持這些運算符。
備註:對於 ==、<、>、<= 和 >= 運算符,如果任何操作數不是數字(Double . NaN 或 Single . NaN),則運算的結果為 false。這意味着 NaN 值不大於、小於或等於任何其他 double(或 float)值,包括 NaN。

char 類型也支持比較運算符。在使用 char 操作數時,將比較對應的字符代碼。

枚舉類型也支持比較運算符。 對於相同枚舉類型的操作數,基礎整數類型的相應值會進行比較。

== 和 != 運算符檢查其操作數是否相等。

小於運算符 <

如果左側操作數小於右側操作數,< 運算符返回 true,否則返回 false:

Console . WriteLine ( 7.0 < 5.1 );   // False
Console . WriteLine ( 5.1 < 5.1 );   // False
Console . WriteLine ( 0.0 < 5.1 );   // True

Console . WriteLine ( double . NaN < 5.1);   // False
Console . WriteLine ( double . NaN >= 5.1 );  // False

大於運算符 >

如果左側操作數大於右側操作數,> 運算符返回 true,否則返回 false:

Console . WriteLine ( 7.0 > 5.1 );   // True
Console . WriteLine ( 5.1 > 5.1 );   // False
Console . WriteLine ( 0.0 > 5.1 );   // False

Console . WriteLine ( double . NaN > 5.1 );   // False
Console . WriteLine ( double . NaN <= 5.1 );  // False

小於等於運算符 <=

如果左側操作數小於或等於右側操作數,<= 運算符返回 true,否則返回 false:

Console . WriteLine ( 7.0 <= 5.1 );   // False
Console . WriteLine ( 5.1 <= 5.1 );   // True
Console . WriteLine ( 0.0 <= 5.1 );   // True

Console . WriteLine ( double . NaN > 5.1);   // False
Console . WriteLine ( double . NaN <= 5.1 );  // False

大於等於運算符 >=

如果左側操作數大於或等於右側操作數,> 運算符返回 true,否則返回 false:

Console . WriteLine ( 7.0 >= 5.1 );   // True
Console . WriteLine ( 5.1 >= 5.1 );   // True
Console . WriteLine ( 0.0 >= 5.1 );   // False

Console . WriteLine ( double . NaN < 5.1 );   // False
Console . WriteLine ( double . NaN >= 5.1 );  // False

運算符可重載性

用户定義類型可以重載 <、>、<= 和 >= 運算符。

如果某類型重載 < 或 > 運算符之一,它必須同時重載 < 和 >。如果某類型重載 <= 或 >= 運算符之一,它必須同時重載 <= 和 >=。

成員訪問運算符和表達式 - 點(.)、索引器([ ])和調用運算符

使用多個運算符和表達式來訪問類型成員。成員訪問運算符包括成員訪問(.)、數組元素或索引器訪問([ ])、從端索引(^)、範圍(..)、null 條件運算符(?. 和 ?[ ])和方法調用(( ))。這些運算符包括空條件成員訪問(?.)和索引器訪問(?[ ])運算符。

  • .(成員訪問):訪問命名空間或類型的成員
  • [ ](數組元素或索引器訪問):用於訪問數組元素或類型索引器
  • ?. 和 ?[ ](null 條件運算符):僅當操作數為非 null 時才用於執行成員或元素訪問運算
  • ( )(調用):用於調用被訪問的方法或調用委託
  • ^(從末尾開始索引):指示元素位置來自序列的末尾
  • ..(範圍):指定可用於獲取一系列序列元素的索引範圍

成員訪問表達式 .

可以使用 . 標記來訪問命名空間或類型的成員,如以下示例所示:

  • 使用 . 來訪問命名空間中的嵌套命名空間,如以下 using 指令所示:
    using System . Collections . Generic;
  • 使用 . 構成 限定名稱 以訪問命名空間中的類型,如下面的代碼所示:
    System . Collections . Generic . IEnumerable < int > numbers = [ 1 , 2 , 3 ]; 使用 using 指令來使用可選的限定名稱。
  • 使用 . 訪問類型成員(static 和非 static),如下面的代碼所示:

    List<double> 常數s =
      [
          Math.PI,
          Math.E
      ];
    Console . WriteLine ( $"顯示 {常數s . Count} 個值:" );
    Console . WriteLine ( string . Join ( "," , 常數s ) );
    // 顯示 2 個值:
    // 3.14159265358979,2.71828182845905

    還可以使用 . 訪問擴展方法。

索引器運算符 [ ]

方括號 [ ] 通常用於數組、索引器或指針元素訪問。從 C# 12 開始,[ ] 包含一個集合表達式。

數組訪問

下面的示例演示如何訪問數組元素:

int [ ] fib = new int [ 10 ];
fib [ 0 ] = fib [ 1 ] = 1;
for ( int i = 2; i < fib . Length; i++ )
    {
        fib [ i ] = fib [ i - 1 ] + fib [ i - 2 ];
    }
Console . WriteLine ( fib [ fib . Length - 1 ] );  // 55

double[,] matrix = new double[2,2];
matrix[0,0] = 1.0;
matrix[0,1] = 2.0;
matrix[1,0] = matrix[1,1] = 3.0;
var determinant = matrix[0,0] * matrix[1,1] - matrix[1,0] * matrix[0,1];
Console.WriteLine(determinant);  // output: -3

如果數組索引超出數組相應維度的邊界,將引發 IndexOutOfRangeException。

如上述示例所示,在聲明數組類型或實例化數組實例時,還會使用方括號。

索引器訪問

下面的示例使用 .NET Dictionary < TKey ,TValue > 類型來演示索引器訪問:

var dict = new Dictionary < string , double > ( );
dict [ "one" ] = 1;
dict [ "pi" ] = Math . PI;
Console . WriteLine ( dict [ "one" ] + dict [ "pi" ] );  // 4.14159265358979

使用索引器,可通過類似於編制數組索引的方式對用户定義類型的實例編制索引。與必須是整數的數組索引不同,可以將索引器參數聲明為任何類型。

[ ] 的其他用法

方括號還用於指定屬性:

[ System . Diagnostics . Conditional ( "DEBUG" ) ]
void TraceMethod ( ) { }

此外,方括號可用於指定用於模式匹配或測試的列表模式。
arr is ( [ 1 , 2 , .. ] ) //Specifies that an array starts with ( 1 , 2 )

Null 條件運算符 ?. 和 ?[ ]

僅當操作數的計算結果為非 NULL 時,NULL 條件運算符才對其操作數應用成員訪問(?.)或元素訪問(?[ ])操作;否則,它會返回 null。 換句話説:

  • 如果 a 的計算結果為 null,則 a? . x 或 a? [ x ] 的結果為 null。
  • 如果 a 的計算結果為非 null,則 a? . x 或 a? [ x ] 的結果將分別與 a . x 或 a [ x ] 的結果相同。

備註:如果 a . x 或 a [ x ] 引發異常,則 a? . x 或 a? [ x ] 將對非 null a 引發相同的異常。例如,如果是一個非 null 數組實例且位於的邊界之外,那麼將會拋出 IndexOutOfRangeException 異常。

NULL 條件運算符采用最小化求值策略。也就是説,如果條件成員或元素訪問操作鏈中的一個操作返回null,則鏈的其餘部分不會執行。在以下示例中,如果 B 的計算結果為 A,則不會計算 null;如果 C 或 A 的計算結果為 B,則不會計算 null:

A? . B? . Do ( C );
A? . B? [ C ];

如果 A 可以為 null,而如果 A 不為 null,B 和 C 將不為 null,你只需要對 A 應用 null 條件運算符。
a? . B . C ( );
在上述示例中,如果 B 為 null,則不會計算 C ( ),也不會調用 A。但是,如果鏈接的成員訪問被中斷,例如被 ( A? . B ) . C ( ) 中的括號中斷,則不會發生短路。

以下示例演示了 ? . 和 ? [ ] 運算符的用法:

double H ( List < double [ ] >? S集合 , int Zhs集合和索引 )
    {
        return S集合? [ Zhs集合和索引 ]? . Sum ( ) ?? double . NaN;
    }

var H1 = H ( null , 0 );
Console . WriteLine ( H1 );  // NaN

List< double [ ]? > SJD集合 =
    [
        [ 1.0 , 2.0 , 3.0 ],
        null
    ];

var H2 = H ( SJD集合 , 0 );
Console . WriteLine ( H2 );  // 6

var H3 = H ( SJD集合 , 1 );
Console . WriteLine ( H3 );  // NaN
static void Main ( string [ ] args )
    {
        Ren? NR = null;
        NR? . 姓名 . 全名 ( ); // 無輸出:由於短路,Write( )不會被調用。
        try
            {
                ( NR?.姓名 ) . 全名 ( );
            }
        catch ( NullReferenceException )
            {
                Console . WriteLine ( "NullReferenceException" );
            }
    }

public class Ren
    {
        public required QuanMing 姓名
            {
                get; set;
            }
    }

public class QuanMing
    {
        public required string Xing
            {
                get; set;
            }

        public required string Ming
            {
                get; set;
            }

        public void 全名 ( ) => Console . WriteLine ( $"{Xing}{Ming}" );
    }

前面的第一個例子也使用了空值合併操作符 “??” 指定一個替代表達式,以便在空條件操作的結果為空時進行計算。

如果 a . x 或 a [ x ] 是非空值類型 T,則 a? . X 或 a? [ x ] 是對應的可空值類型 T?。如果需要類型為 T 的表達式,可以使用空值合併操作符 “??” 轉換為空條件表達式,如下面的例子所示:

static void Main ( string [ ] args )
    {
        Console . WriteLine ( ZHS獲取前兩個元素的和或默認 ( null ) );
        Console . WriteLine ( ZHS獲取前兩個元素的和或默認 ( [  ] ) );
        Console . WriteLine ( ZHS獲取前兩個元素的和或默認 ( [ 3 , 4 , 5 ] ) );
    }

static int ZHS獲取前兩個元素的和或默認 ( int [ ]? Zhss )
    {
        if ( ( Zhss? . Length ?? 0 ) < 2 )
            {
                return 0;
            }
        return Zhss [ 0 ] + Zhss [ 1 ];
    }

在前面的示例中,如果不使用 “??” 運算符,則在 Zhss? . Length < 2 為 false 時,Zhss 的計算結果為 null。

備註:運算符 ?. 計算其左側作數不超過一次,以確保在驗證為非 null 後無法將其更改為 null。

從 C# 14 開始,允許對引用類型使用 null 條件訪問表達式(“?.” 和 “?[ ]”)分配。例如,請參閲以下方法:

小周? . 姓 = "周";
messages? [ 5 ] = "五";

前面的示例顯示了對可能為 null 的引用類型的屬性和索引元素的賦值。該賦值的一個重要行為是,只有當已知 = 左側表達式為非空時,才會對 = 右側的表達式進行求值。例如,在以下代碼中,僅當 Zhss 數組不為 null 時,才會調用 FF生成下一個索引 函數。 如果 Zhss 數組為 null,FF生成下一個索引 函數則不調用:

if ( Zhss is not null )
    {
        Zhss [ 2 ] = FF生成下一個索引 ( );
    }

除賦值外,還允許使用任何形式的 複合賦值,例如 += 或 -=。但是,不允許遞增(++)和遞減(--)。

此增強功能不會將 null 條件表達式分類為變量。它不能 ref 分配,也不能將其分配給 ref 變量,也不能作為 ref 或 out 參數傳遞給方法。

線程安全的委託調用

使用 ?. 運算符來檢查委託是否非 null 並以線程安全的方式調用它(例如引發事件時),如下面的代碼所示:
PropertyChanged ?. Invoke ( … )
該代碼等同於以下代碼:

var handler = this . PropertyChanged;
if ( handler != null )
    {
        handler ( … );
    }

前面的示例是一種線程安全方法,可確保只調用非 null handler。由於委託實例是不可變的,因此,任何線程都不能更改 handler 本地變量所引用的對象。具體而言,如果另一個線程執行的代碼從 PropertyChanged 事件中取消訂閲,並且 PropertyChanged 在調用 null 之前變為 handler,則 handler 引用的對象不受影響。

調用表達式 ( )

使用括號 ( ) 調用方法或調用委託。

以下代碼演示瞭如何在使用或不使用參數的情況下調用方法,以及調用委託:

Action < int > FF顯示 = s => Console . WriteLine ( s );

List < int > Zhss =
[
    10,
    17
];
FF顯示 ( Zhss . Count );   // 2

Zhss . Clear ( );
FF顯示 ( Zhss . Count );   // 0

在調用帶運算符的 new 時,還可以使用括號。

( ) 的其他用法

此外可以使用括號來調整表達式中計算操作的順序。

強制轉換表達式,其執行顯式類型轉換,也可以使用括號。

從末尾運算符 ^ 開始索引

索引和範圍操作符可以用於可計數的類型。countable 類型有一個 int 屬性 Count 或 Length,還有一個可訪問的 get 訪問器。集合表達式也依賴於可數類型。

備註:單維數組可計數。多維數組不是。 “^” 和 “..”(range)運算符不能用於多維數組。

^ 運算符指示序列末尾的元素位置。對於長度為 length 的序列,^n 指向從序列起始偏移 length - n 的元素。例如,^1 指向序列的最後一個元素,^length 指向序列的第一個元素。

int [ ] Zhss = [ 0 , 10 , 20 , 30 , 40 ];
int MoWei = Zhss [ ^1 ];
Console . WriteLine ( MoWei );  // 40

List < string > Zfcs行 = [ "一", "二", "三", "四" ];
string DS第二個 = Zfcs行 [ ^2 ];
Console . WriteLine ( DS第二個 );  // 三

string Zfc二十 = "Twenty";
Index ZS第一個 = ^Zfc二十 . Length;
char Cha1 = word [ ZS第一個 ];
Console . WriteLine ( Cha1 );  // T

如前面的示例所示,表達式 ^e 是 System . Index 類型。 在表達式 ^e 中,e 的結果必須能夠隱式轉換為 int。

還可以將 ^ 運算符與 range 運算符一起使用,以創建一個索引範圍。

從 C# 13 開始,可在對象初始值設定項中使用結束運算符中的索引。

範圍運算符 ..

.. 運算符指定索引範圍的開始和結束作為其操作數。左側操作數是範圍的包含性開頭。右側操作數是範圍的包含性末尾。任一操作數都可以是序列開頭或末尾的索引,如以下示例所示:
如前面的示例所示,表達式 a..b 是 System . Range 類型。 在表達式 a..b 中,a 和 b 的結果必須能隱式轉換為 Int32 或 Index。

重要:當值為負數時,從 int 隱式轉換為 Index 會引發 ArgumentOutOfRangeException。

通過省略 .. 運算符的任何操作數,可以獲得一個開放區間:

  • a .. 與 a .. ^0 等效
  • .. b 與 0 .. b 等效
  • .. 與 0 .. ^0 等效
int [ ] Zhss = [ 0 , 10 , 20 , 30 , 40 , 50 ];
int Z掉落量 = Zhss . Length / 2;

int [ ] Zhss右一半 = Zhss [ Z掉落量 .. ];
FF顯示 ( Zhss右一半 );  // 30 40 50

int [ ] Zhss左一半 = Zhss [ .. ^Z掉落量 ];
FF顯示 ( Zhss左一半 );  // 0 10 20

int [ ] Zhss全部 = Zhss [ .. ];
FF顯示 ( Zhss全部 );  // 0 10 20 30 40 50

void FF顯示 < T > ( IEnumerable < T > YS ) => Console . WriteLine ( string . Join ( " ", YS ) );

下表顯示了表達集合範圍的各種方法:

範圍運算符表達式 説明 -- (項目 、項目)不包含項目本身;【項目、項目】 包含項目本身
.. 或 0 .. ^0 集合中的所有值
.. end 或 0 .. end 【起始,結尾)
start .. 或 start .. ^0 【起始,結尾】
start .. end 【起始,結尾)
^start .. 或 ^start .. ^0 【起始,倒計數結尾】
.. ^end (start,end】(從 end 倒計數)
start..^end 【start,end(從 end 倒計數))
^start..^end 【start,end)(都從 end 倒計數)
int [ ] Zhss0到9 = [ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ];

FF顯示 ( Zhss0到9 , .. ); // 0,1,2,3,4,5,6,7,8,9
FF顯示 ( Zhss0到9 , ..3 ); // 0,1,2
FF顯示 ( Zhss0到9 , 2.. ); // 2,3,4,5,6,7,8,9
FF顯示 ( Zhss0到9 , 3..5 ); // 3,4
FF顯示 ( Zhss0到9 , ^2.. ); // 8,9
FF顯示 ( Zhss0到9 , ..^3 ); // 0,1,2,3,4,5,6
FF顯示 ( Zhss0到9 , 3..^4 ); // 3,4,5
FF顯示 ( Zhss0到9 , ^4..^2 ); // 6,7

void FF顯示 ( int [ ] 值 , Range 範圍 ) => Console . WriteLine ( $"{範圍}:\t{string . Join ( "," , 值 [ 範圍 ] )}" );

上例輸出:
0..^0: 0,1,2,3,4,5,6,7,8,9
0..3: 0,1,2
2..^0: 2,3,4,5,6,7,8,9
3..5: 3,4
^2..^0: 8,9
0..^3: 0,1,2,3,4,5,6
3..^4: 3,4,5
^4..^2: 6,7

.. 詞元還用作集合表達式中的擴展元素。

運算符可重載性

.、()、^ 和 .. 運算符無法進行重載。[ ] 運算符也被視為非可重載運算符。使用索引器以支持對用户定義的類型編制索引。

類型測試運算符和強制轉換表達式 - is、as、typeof 和強制轉換

這些運算符和表達式執行類型檢查或類型轉換。is 運算符檢查表達式結果的運行時類型是否與給定類型兼容。as 運算符將表達式顯式轉換為給定類型(如果其運行時類型與該類型兼容)。強制轉換表達式執行到目標類型的顯式轉換。typeof 運算符用於獲取某個類型的 System . Type 實例。

is 運算符

is 運算符檢查表達式結果的運行時類型是否與給定類型兼容。is 運算符還會對照某個模式測試表達式結果。

具有類型測試 is 運算符的表達式具有以下形式
E is T
其中 E 是返回一個值的表達式,T 是類型或類型參數的名稱。E 不得為匿名方法或 Lambda 表達式。

如果表達式結果為非 null 並且滿足以下任一條件,則 is 運算符將返回 true:

  • 表達式結果的運行時類型為 T。
  • 表達式結果的運行時類型派生自類型 T、實現接口 T,或者存在從其到 T 的另一種隱式引用轉換。
  • 表達式結果的運行時類型是基礎類型為 T 且 Nullable < T > . HasValue 為 true 的可為空值類型。
  • 存在從表達式結果的運行時類型到類型 T 的裝箱或取消裝箱轉換。

is 運算符不會考慮用户定義的轉換。

以下示例演示,如果表達式結果的運行時類型派生自給定類型,即類型之間存在引用轉換,is 運算符將返回 true:

public class Ji { }

public class PS : Ji { }

public static class Is操作
    {
        public static void Main ( )
            {
                object J = new Base ( );
                Console . WriteLine ( J is Ji );    // True
                Console . WriteLine ( J is PS );  // False

                object P = new PS ( );
                Console . WriteLine ( P is Ji );   // True
                Console . WriteLine ( P is PS ); // True
            }
    }

以下示例演示,is 運算符將考慮裝箱和取消裝箱轉換,但不會考慮數值轉換:

int i = 27;
Console . WriteLine ( i is System . IFormattable );  // True

object iBoxed = i;
Console . WriteLine ( iBoxed is int );     // True
Console . WriteLine ( iBoxed is long );  // False

有模式匹配的類型測試

is 運算符還會對照某個模式測試表達式結果。下面的示例演示如何使用聲明模式來檢查表達式的運行時類型:

int i = 23;
object iBoxed = i;
int? jNullable = 7;
if ( iBoxed is int a && jNullable is int b )
    {
        Console . WriteLine ( a + b );  // 30
    }

as 運算符

as 運算符將表達式結果顯式轉換為給定的引用或可以為 null 值的類型。如果無法進行轉換,則 as 運算符返回 null。與強制轉換表達式 不同,as 運算符永遠不會引發異常。

形式如下的表達式
E as T
其中,E 為返回值的表達式,T 為類型或類型參數的名稱,生成相同的結果,
E is T ? ( T ) ( E ) : ( T ) null
不同的是 E 只計算一次。

as 運算符僅考慮引用、可以為 null、box 和 unbox 轉換。不能使用 as 運算符執行用户定義的轉換。為此,請使用強制轉換表達式。

下面的示例演示 as 運算符的用法:

IEnumerable < int > Zhss = [ 10 , 20 , 30 ];
IList < int >? SuoYin = Zhss as IList<int >;
if ( SuoYin != null )
    {
        Console . WriteLine ( SuoYin [ 0 ] + SuoYin [ SuoYin . Count - 1 ] ); // 40
    }

備註:如之前的示例所示,你需要將 as 表達式的結果與 null 進行比較,以檢查轉換是否成功。可以使用 is 運算符測試轉換是否成功,如果成功,則將其結果分配給新變量。

強制轉換表達式

形式為 ( T ) E 的強制轉換表達式將表達式 E 的結果顯式轉換為類型 T。如果不存在從類型 E 到類型 T 的顯式轉換,則發生編譯時錯誤。在運行時,顯式轉換可能不會成功,強制轉換表達式可能會引發異常。

下面的示例演示顯式數值和引用轉換:

double x = 1234.7;
int a = ( int ) x;
Console . WriteLine ( a );   // 1234

int [ ] ints = [ 10 , 20 , 30 ];
IEnumerable < int > numbers = ints;
IList < int > list = ( IList < int > ) numbers;
Console . WriteLine ( list . Count );  // 3
Console . WriteLine ( list [ 1 ] );  // 20

( ) 的其他用法

你還可以使用括號調用方法或調用委託。

括號的其他用法是調整表達式中計算操作的順序。

typeof 運算符

typeof 運算符用於獲取某個類型的 System.Type 實例。typeof 運算符的實參必須是類型或類型形參的名稱,如以下示例所示:

void PrintType < T > ( ) => Console . WriteLine ( typeof ( T ) );

Console . WriteLine ( typeof ( List < string > ) );
PrintType < int > ( );
PrintType < System . Int32 > ( );
PrintType < Dictionary < int , char > > ( );
// System . Collections . Generic . List`1 [ System . String ]
// System . Int32
// System . Int32
// System . Collections . Generic . Dictionary`2 [ System . Int32 , System . Char ]

參數不能是需要元數據註釋的類型。示例包括以下類型:

  • dynamic
  • string?(或任何可為 null 的引用類型)

這些類型不會直接在元數據中表示出來。這些類型包括描述基礎類型的屬性。在這兩種情況下,都可以使用基礎類型。可以使用 object 來代替 dynamic。可以使用 string 來代替 string?。

你還可以使用具有未綁定泛型類型的 typeof 運算符。未綁定泛型類型的名稱必須包含適當數量的逗號,且此數量小於類型參數的數量。以下示例演示了具有未綁定泛型類型的 typeof 運算符的用法:

Console . WriteLine ( typeof ( Dictionary < , > ) );
// System . Collections . Generic . Dictionary`2 [ TKey , TValue ]

表達式不能為 typeof 運算符的參數。若要獲取表達式結果的運行時類型的 System . Type 實例,請使用 Object . GetType 方法。

使用 typeof 運算符進行類型測試

使用 typeof 運算符來檢查表達式結果的運行時類型是否與給定的類型完全匹配。以下示例演示了使用 typeof 運算符和 is 運算符執行的類型檢查之間的差異:

public class DongWu { }

public class ChangJingLu : DongWu { }

public static class TypeOf例子
    {
        public static void Main()
            {
                object b = new ChangJingLu ( );
                Console . WriteLine ( b is DongWu );  // True
                Console . WriteLine ( b . GetType ( ) == typeof ( DongWu ) );  // False

                Console . WriteLine ( b is ChangJingLu );  // True
                Console . WriteLine ( b . GetType ( ) == typeof ( ChangJingLu ) );  // True
    }
}

用户定義的顯式和隱式轉換運算符

用户定義類型可以定義從或到另一個類型的自定義隱式或顯式轉換。隱式轉換無需調用特殊語法,並且可以在各種情況(如在賦值和方法調用中)下發生。預定義的 C# 隱式轉換始終成功,且永遠不會引發異常。用户定義隱式轉換也應如此。如果自定義轉換可能會引發異常或丟失信息,請將其定義為顯式轉換。

is 和 as 運算符不考慮使用用户定義轉換。強制轉換表達式用於調用用户定義顯式轉換。

operator 和 implicit 或 explicit 關鍵字分別用於定義隱式轉換或顯式轉換。定義轉換的類型必須是該轉換的源類型或目標類型。可用兩種類型中的任何一種類型來定義兩種用户定義類型之間的轉換。

下面的示例展示如何定義隱式轉換和顯式轉換:

public readonly struct 數位
    {
        private readonly byte Wei;

        public 數位 ( byte 位 )
            {
                if ( 位 > 9 )
                    {
                        throw new ArgumentOutOfRangeException ( nameof ( 位 ), "位不能大於 9。" );
                    }
                this . Wei = 位;
            }

        public static implicit operator byte ( 數位 W ) => W . Wei;
        public static explicit operator Digit ( byte Z ) => new Digit ( Z );

        public override string ToString ( ) => $"{Wei}";
    }

public static class UserDefinedConversions
    {
        public static void Main()
            {
                var W = new Digit ( 7 );

                byte ZJ = W;
                Console . WriteLine ( ZJ);  // 7

                數位 W = ( 數位 ) ZJ;
                Console . WriteLine ( W );  // 7
            }
    }

從 C# 11 開始,可以定義 checked 顯式轉換運算符。

operator 關鍵字也可用於重載預定義的 C# 運算符。

指針相關運算符 - 獲取變量的地址、取消引用存儲位置和訪問內存位置

使用指針運算符可以獲取變量的地址(&)、取消引用指針(*)、比較指針值以及添加或減去指針和整數。

使用以下運算符來使用指針:

  • 一元 & (address-of) 運算符:用於獲取變量的地址
  • 一元 *(指針間接)運算符:用於獲取指針指向的變量
  • ->(成員訪問)和 [ ](元素訪問)運算符
  • 算術運算符 +、-、++ 和 --
  • 比較運算符 ==、!=、<、>、<= 和 >=

備註:任何帶指針的運算都需要使用 unsafe 上下文。必須使用 AllowUnsafeBlocks 編譯器選項來編譯包含不安全塊的代碼。

Address-of 運算符 &

一元 & 運算符返回其操作數的地址:

unsafe
    {
        int Zhs = 27;
        int* Zhs指針 = &Zhs;
        Console . WriteLine ( $"變量的值:{Zhs},變量的地址:{( long ) Zhs指針:X}" );
    }

上例輸出(地址是可變的):
變量的值:27,變量的地址:FB1957E62C

& 運算符的操作數必須是固定變量。固定變量是駐留在不受垃圾回收器操作影響的存儲位置的變量。在上述示例中,局部變量 number 是固定變量,因為它駐留在堆棧上。駐留在可能受垃圾回收器影響的存儲位置的變量(如重定位)稱為可移動變量,例如對象字段和數組元素是可移動變量。如果使用 fixed 語句“固定”,則可以獲取可移動變量的地址。獲取的地址僅在 fixed 語句塊中有效。以下示例顯示如何使用 fixed 語句和 & 運算符:

unsafe
    {
        byte [ ] ZJs = { 1 , 2 , 3 , 4 , 5 };
        fixed ( byte* ZhiZhen1 = &ZJs [ 0 ] )
            {
                Console . WriteLine ( ( long )  ZhiZhen1:X );
            }
    }

你也無法獲取常量或值的地址。

二進制 & 運算符計算其布爾操作數的邏輯 AND 或其整型操作數的位邏輯 AND。

指針間接運算符 *

一元指針間接運算符 獲取其操作數指向的變量。它也稱為取消引用運算符。 運算符的操作數必須是指針類型。

unsafe
    {
        char a = 'a';
        char* ZhiZhena = &a;
        Console . WriteLine ( $"`a` 變量的值:{a}" );
        Console . WriteLine ( $"`a` 變量的地址:{( long ) ZhiZhena:X}" );

        *ZhiZhena = 'Z';
        Console . WriteLine ( $"更新的變量:{a}" );
    }

上例輸出(地址會有變化):
a 變量的值:a
a 變量的地址:87F877ED3C
更新的變量:Z

不能將 運算符應用於類型 void 的表達式。

二進制 * 運算符計算其數值操作數的乘積。

指針成員訪問運算符 ->

-> 運算符將指針間接和成員訪問合併。也就是説,如果 x 是類型為 T* 的指針且 y 是類型 T 的可訪問成員,則形式的表達式
x -> y
等效於
( *x ) . y

static unsafe void Main ( string [ ] args )
    {
        D2 d;
        D2* dy = &d;
        dy->x = 3;
        dy->y = 4;
        Console . WriteLine ( dy ->ToString ( ) );
    }

public struct D2
    {
        public int x;
        public int y;
        public override string ToString ( )
            {
                return $"{x},{y}";
            }
    }

不能將 -> 運算符應用於類型 void* 的表達式。

指針元素訪問運算符 [ ]

對於指針類型的表達式 p,p [ n ] 形式的指針元素訪問計算方式為 * ( p + n ),其中 n 必須是可隱式轉換為 int、uint、long 或 ulong 的類型。

以下示例演示如何使用指針和 [ ] 運算符訪問數組元素:

unsafe
    {
        char* ZF指針 = stackalloc char [ 123 ];
        for ( int i = 0 ; i < 123 ; i++ )
            {
                ZF指針 [ i ] = ( char ) i;
            }

        Console . WriteLine ( "大寫字母:" );
        for ( int i = 65 ; i < 91 ; i++ )
            {
                Console . Write( ZF指針 [ i ] );
            }
    }

在前面的示例中,stackalloc 表達式在堆棧上分配內存塊。

備註:指針元素訪問運算符不檢查越界錯誤。

不能將 [ ] 用於帶有類型為 void* 的表達式的指針元素訪問。

還可以將 [ ] 運算符用於數組元素或索引器訪問。

指針算術運算符

可以使用指針執行以下算術運算:

  • 向指針增加或從指針中減少一個整數值
  • 減少兩個指針
  • 增量或減量指針

不能使用類型為 void* 的指針執行這些操作。

向指針增加或從指針中減少整數值

對於類型為 T* 的指針 p 和可隱式轉換為 int、uint、long 或 ulong 的類型的表達式 n,加法和減法定義如下:

  • p + n 和 n + p 表達式都生成 T 類型的指針,該指針是將 n sizeof ( T ) 添加到 p 給出的地址得到的。
  • p - n 表達式生成類型為 T 的指針,該指針是從 p 給出的地址中減去 n sizeof ( T ) 得到的。

sizeof 運算符獲取類型的大小(以字節為單位)。

以下示例演示了 + 運算符與指針的用法:

unsafe
    {
        const int JiShu = 3;
        int [ ] Zhss = new int [ JiShu ] { 10 , 20 , 30 };
        fixed ( int* ZhiZhen1 = &Zhss [ 0 ] )
            {
                int* ZhiZhenM = ZhiZhen1 + ( JiShu - 1 );
                Console . WriteLine ( $"值 {*ZhiZhen1} 在地址:{( long ) ZhiZhen1}" );
                Console . WriteLine ( $"值 {*ZhiZhenM} 在地址:{( long ) ZhiZhenM}" );
            }
    }

上例輸出(地址會有變化):
值 10 在地址:1880219399264
值 30 在地址:1880219399272

指針減法

對於類型為 T* 的兩個指針 p1 和 p2,表達式 p1 - p2 生成 p1 和 p2 給出的地址之間的差除以 sizeof(T) 的值。結果的類型為 long。即 p1 - p2 計算公式為 ( ( long ) ( p1 ) - ( long )( p2 ) ) / sizeof ( T )。

以下示例演示了指針減法:

unsafe
    {
        int* Zhss = stackalloc int[ ] { 0 , 1 , 2 , 3 , 4 , 5 };
        int* Z1 = &Zhss [ 1 ];
        int* Z2 = &Zhss [ 5 ];
        Console . WriteLine ( Z2 - Z1 );  // 4
    }

指針增量和減量

++ 增量運算符將 1 添加到其指針操作數。-- 減量運算符從其指針操作數中減去 1。

兩種運算符都支持兩種形式:後綴(p++ 和 p--)和前綴(++p 和 --p)。p++ 和 p-- 的結果是該運算之前 p 的值。 ++p 和 --p 的結果是該運算之後 p 的值。

以下示例演示了後綴和前綴增量運算符的行為:

unsafe
    {
        int* Zhss = stackalloc int[] { 0, 1, 2 };
        int* Z1 = &Zhss[0];
        int* Z2 = Z1;
        Console . WriteLine ( $"操作前: Z1 - {( long ) Z1:X},Z2 - {( long ) Z2:X}" );
        Console . WriteLine ( $"後綴 ++ Z1:{( long ) ( Z1++ ):X}" );
        Console . WriteLine ( $"前綴 ++ Z2:{( long ) ( ++Z2 ):X}" );
        Console . WriteLine ( $"操作後:Z1 - {( long ) Z1:X},Z2 - {( long ) Z2:X}" );
    }

指針比較運算符

可以使用 ==、!=、<、>、<= 和 >= 運算符來比較任何指針類型(包括 void*)的操作數。這些運算符將兩個操作數給出的地址進行比較,就好像它們是無符號整數一樣。

運算符優先級

以下列表按優先級從高到低的順序對指針相關運算符進行排序:

  • 後綴增量 x++ 和減量 x-- 運算符以及 -> 和 [ ] 運算符
  • 前綴增量 ++x 和減量 --x 運算符以及 & 和 * 運算符
  • 加法 + 和 - 運算符
  • 比較 <、>、<= 和 >= 運算符
  • 等式 == 和 != 運算符

使用括號 ( ) 更改運算符優先級所施加的計算順序。

運算符可重載性

用户定義的類型不能重載與指針相關的運算符 &、*、-> 和 [ ]。

賦值運算符

賦值運算符 = 將其右操作數的值賦給變量、屬性或由其左操作數給出的索引器元素。賦值表達式的結果是分配給左操作數的值。右操作數類型必須與左操作數類型相同,或可隱式轉換為左操作數的類型。

賦值運算符 = 為右聯運算符,即形式的表達式
a = b = c
等效於
a = ( b = c )
以下示例演示使用局部變量、屬性和索引器元素作為其左操作數的賦值運算符的用法:

List < double > SJDs = [ 1.0 , 2.0 , 3.0 ];

Console . WriteLine ( SJDs . Capacity ); // 4
SJDs . Capacity = 100;
Console . WriteLine ( SJDs . Capacity ); // 100

int YS新的第一個;
double YS原始的第一個 = SJDs [ 0 ];
YS新的第一個 = 5;
Zhss [ 0 ] = YS新的第一個;
Console . WriteLine ( YS原始的第一個 );
Console . WriteLine ( Zhss [ 0 ] );
// 1
// 5

賦值操作的左操作數接收右操作數的值。當操作數是值類型時,賦值操作將複製右側操作數的內容。當操作數為引用類型時,賦值操作會複製對對象的引用。

此操作叫做賦值:賦予值。

從 C# 14 開始,賦值的左側可以包含 null 條件成員表達式,例如 ?. 或 ?[ ]。 如果左側為空,則不會計算右側表達式。

ref 賦值

如果 Ref 賦值 = ref,則其左側操作數成為右側操作數的別名,如以下示例所示:

void FF顯示 ( double [ ] s ) => Console . WriteLine ( string . Join ( " " , s ) );

double [ ] SJDs = { 0.0 , 0.0 , 0.0 };
FF顯示 ( SJDs ); // 0  0  0

ref double YS數組 = ref SJDs[0];
YS數組 = 3.0;
FF顯示 ( SJDs ); // 3  0  0

YS數組 = ref SJDs [ SJDs . Length - 1 ];
YS數組 = 5.0;
FF顯示 ( SJDs ); // 3  0  5

在前面的示例中,局部引用變量 YS數組 初始化為第一個數組元素的別名。然後,向其重新賦值 ref,以引用最後一個數組元素。因為它是別名,所以使用普通賦值運算符 = 更新其值時,也會更新相應的數組元素。

ref 賦值的左側操作數可以是局部引用變量、ref 字段和 ref、out 或 in 方法參數。兩個操作數的類型必須相同。

ref 賦值意味着引用變量具有其他引用對象。它不再引用其先前的引用對象。對 ref = 參數使用 ref 意味着該參數不再引用其參數。在 ref 重新賦值之後修改對象狀態的任何操作都會對新項目進行這些修改。例如,考慮使用以下方法:

static void Main (  string [ ] args )
    {
        string z = "123";
        FF變換 ( ref z );
        Console . WriteLine ( z );
    }

public static void FF變換 ( scoped ref string ZFC )
    {
        string ZFC本地 = "我的天啊";
        Console . WriteLine ( ZFC本地 );

        ZFC = ref ZFC本地;
        ZFC = "我的媽啊";
        Console . WriteLine ( ZFC );
    }

以下用法表明,在進行方法調用之後對參數 z 的賦值不可見,因為在修改字符串之前,已對 z 進行了 ref 重新賦值以引用 ZFC本地。上例輸出:
我的天啊
我的媽啊
123

複合賦值

對於二元運算符 op,窗體的複合賦值表達式
x op= y
等效於
x = x op y
不過 x 只評估一次。
算術、布爾邏輯以及邏輯位和移位運算符都支持複合賦值。

Null 合併賦值

只有在左操作數計算為 null 時,才能使用 ??= 合併賦值運算符將其右操作數的值分配給左操作數。

運算符可重載性

用户定義類型不能重載賦值運算符。但是,用户定義類型可以定義到其他類型的隱式轉換。這樣,可以將用户定義類型的值分配給其他類型的變量、屬性或索引器元素。

如果用户定義類型重載二進制運算符 op,則 op= 運算符(如果存在)也隱式重載。從 C# 14 開始,用户定義的類型可以顯式重載複合賦值運算符(op=)以提供更有效的實現。通常,類型重載這些運算符,因為能夠在原地更新值,而無需分配新增實例來保存二元運算結果。如果類型不提供顯式重載,編譯器將生成隱式重載。

Lambda 表達式和匿名函數

使用 Lambda 表達式來創建匿名函數。使用 lambda 聲明運算符 => 從其主體中分離 lambda 參數列表。Lambda 表達式可採用以下任意一種形式:

  • 以表達式為主體的 lambda 表達式:
    ( 輸入參數 ) => 表達式
  • 語句塊作為其主體的 lambda 語句:
    ( 輸入參數 ) => { < 語句塊 > }
    若要創建 Lambda 表達式,需要在 Lambda 運算符左側指定輸入參數(如果有),然後在另一側輸入表達式或語句塊。

任何 Lambda 表達式都可以轉換為委託類型。其參數的類型和返回值定義了 Lambda 表達式可轉換成的委託類型。如果 lambda 表達式不返回值,則可以將其轉換為 Action 委託類型之一;否則,可將其轉換為 Func 委託類型之一。例如,有 2 個參數且不返回值的 Lambda 表達式可轉換為 Action < T1 ,T2 > 委託。有 1 個參數且不返回值的 Lambda 表達式可轉換為 Func < T , TResult > 委託。以下示例中,Lambda 表達式 x => x * x(指定名為 x 的參數並返回 x 平方值)將分配給委託類型的變量:

Func < double , double > FF乘方 = x => x * x;
Console . WriteLine ( FF乘方 ( 5  ) );

表達式 lambda 還可以轉換為表達式樹類型,如下面的示例所示:

System . Linq . Expressions . Expression < Func < double , double > > e = x => x * x;
Console . WriteLine ( e );

您可以在任何需要委託類型或表達式樹實例的代碼中使用 lambda 表達式。一個示例是 Task . Run ( Action ) 方法的參數,用於傳遞應在後台執行的代碼。用 C# 編寫 LINQ 時,還可以使用 lambda 表達式,如下例所示:

int [ ] Zhss = [ 2 , 3 , 4 , 5 , 6 , 7 ];
var Zhss平方 = Zhss . Select ( x => x * x );
Console . WriteLine ( string . Join ( "  ", Zhss平方 ) );

如果使用基於方法的語法在 System . Linq . Enumerable 類中(例如,在 LINQ to Objects 和 LINQ to XML 中)調用 Enumerable . Select 方法,則參數為委託類型 System . Func < T , TResult >。如果在 System . Linq . Queryable 類中(例如,在 LINQ to SQL 中)調用 Queryable . Select 方法,則參數類型為表達式樹類型 Expression < Func < TSource , TResult > >。在這兩種情況下,都可以使用相同的 Lambda 表達式來指定參數值。儘管通過 Lambda 創建的對象實際具有不同的類型,但其使得 2 個 Select 調用看起來類似。

表達式 lambda

表達式位於 => 運算符右側的 lambda 表達式稱為 “表達式 lambda”。表達式 lambda 會返回表達式的結果,並採用以下基本形式:
( 輸入參數 ) => 表達式
表達式 lambda 的主體可以包含方法調用。不過,若要創建在 .NET 公共語言運行時(CLR)的上下文之外(如在 SQL Server 中)計算的表達式樹,則不得在 Lambda 表達式中使用方法調用。在 .NET 公共語言運行時(CLR)上下文之外,方法沒有任何意義。

語句 lambda

語句 lambda 與表達式 lambda 類似,只是語句括在大括號中:
( 輸入參數 ) => { 語句塊 }
語句 lambda 的主體可以包含任意數量的語句;但是,實際上通常不會多於兩個或三個。

Action < string > XM = XingMing =>
    {
        Console .WriteLine ( $"你好,{XingMing}" );
    };
XM ( "小豬豬" );

不能使用語句 Lambda 創建表達式樹。

lambda 表達式的輸入參數

將 lambda 表達式的輸入參數括在括號中。使用空括號指定零個輸入參數:
Action KH = ( ) => Console . WriteLine ( );
如果 lambda 表達式只有一個輸入參數,則括號是可選的:
Func < double , double > LiF = x => x * x * x;
兩個以上的輸入參數使用逗號加以分隔:
Func < int , int , bool > Ber相等 = ( x , y ) => x == y;
有時,編譯器無法推斷輸入參數的類型。可以顯式指定類型,如下面的示例所示:
Func < int , string , bool > Ber太長 = ( int x , string zfc ) => zfc . Length > x;
輸入參數類型必須全部為顯式或全部為隱式;否則,便會生成 CS0748 編譯器錯誤。

可以使用忽略來指定 lambda 表達式中不使用的兩個或多個輸入參數。
Func < int , int , int > con42 = ( _ , _ ) => 42;
使用 Lambda 表達式提供事件處理程序時,Lambda 棄元參數可能很有用。

備註:為了向後兼容,如果只有一個輸入參數命名為 \_,則在 lambda 表達式中,\_ 將被視為該參數的名稱(忽略轉義字符 \)。

從 C# 12 開始,可以為 Lambda 表達式上的參數提供默認值。默認參數值的語法和限制與方法和局部函數相同。以下示例聲明一個具有默認參數的 lambda 表達式,然後使用默認值調用它一次,使用兩個顯式參數調用它一次:

var Jia1或其他 = ( int 源 , int 加數 = 1 ) => 源 + 加數;
Console . WriteLine ( Jia1或其他 ( 5 ) ); // 6
Console . WriteLine ( Jia1或其他 ( 5 , 2 ) ); // 7

還可以使用 params 數組或集合作為參數聲明 Lambda 表達式:

var He = ( params IEnumerable < int > Zhis ) =>
    {
        int he = 0;
        foreach ( var z in Zhis )
            {
                he += z;
            }
        return he;
    };

var k = He ( );
Console . WriteLine ( k ); // 0

var ShuLie = new [ ] { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 };
var ZongHe = He ( ShuLie );
Console . WriteLine ( ZongHe ); // 55

在這些更新過程中,將具有默認參數的方法組分配給 lambda 表達式時,該 lambda 表達式也具有相同的默認參數。還可以將具有 params 集合參數的方法組分配給 Lambda 表達式。

使用默認參數或 params 集合作為參數的 Lambda 表達式沒有與 Func<> 或 Action<> 類型對應的自然類型。但是,可以定義包含默認參數值的委託類型:

delegate int Jia1或其他委託 ( int 源 , int 加數 = 1 );
delegate int He委託 ( params int [ ] Zhis );
delegate int He集合委託 ( params IEnumerable < int > Zhis );

或者,可以將隱式類型化變量與 var 聲明一起使用來定義委託類型 編譯器合成正確的委託類型。

異步 lambda

通過使用 async 和 await 關鍵字,你可以輕鬆創建包含異步處理的 lambda 表達式和語句。例如,下面的 Windows 窗體示例中有一個事件處理程序,它調用並等待一個異步方法,FF異步。

public partial class CHT : Form
    {
        public CHT ( )
            {
                InitializeComponent ( );
                Ann . Click += Ann_Click;
            }

        private async void Ann_Click ( object sender , EventArgs e )
            {
                await FF異步 ( );
                WBK . Text += "\r\n控件返回的單擊事件處理程序。\n";
            }

        private async Task FF異步 ( )
            {
                // 下面這行代碼模擬了一個返回任務的異步過程
                await Task . Delay ( 1000 );
            }
    }

你可以使用異步 lambda 添加同一事件處理程序。若要添加此處理程序,請在 lambda 參數列表前添加 async 修飾符,如下面的示例所示:

public partial class CHT : Form
    {
        public CHT ( )
            {
                InitializeComponent();
                Ann . Click += async ( sender , e ) =>
                    {
                        await FF異步 ( );
                        Wbk . Text += "\r\n控件返回的單擊事件處理程序。\n";
                    };
            }
    }

        private async Task FF異步 ( )
            {
                // 下面這行代碼模擬了一個返回任務的異步過程
                await Task . Delay ( 1000 );
            }
    }

lambda 表達式和元組

C# 語言提供對元組的內置支持。可以提供一個元組作為 Lambda 表達式的參數,同時 Lambda 表達式也可以返回元組。在某些情況下,C# 編譯器使用類型推理來確定元組組件的類型。

可通過用括號括住用逗號分隔的組件列表來定義元組。下面的示例使用包含三個組件的元組,將一系列數字傳遞給 lambda 表達式,此表達式將每個值翻倍,然後返回包含乘法運算結果的元組(內含三個組件)。

Func < ( int , int , int ) , ( int , int , int ) > JiaBei = Y => ( 2 * Y . Item1 , 2 * Y . Item2 , 2 * Y . Item3 );
var Zhis = ( 2 , 3 , 4 );
 var ZhisBei = JiaBei ( Zhis );
Console . WriteLine ( $"{Zhis} 加倍後:{ZhisBei}" ); //(2, 3, 4) 加倍後:(4, 6, 8)

通常,元組字段命名為 Item1、Item2 等等。但是,可以使用命名組件定義元組字段,如以下示例所示。

Func < ( int Z1 , int Z2 , int Z3 ) , ( int , int , int ) > JiaBei = Y => ( 2 * Y . Z1 , 4 * Y . Z2 , 8 * Y . Z3 );
var Zhis = ( 2 , 3 , 4 );
var ZhisBei = JiaBei ( Zhis );
Console . WriteLine ( $"{Zhis} 加倍後:{ZhisBei}" ); //(2, 3, 4) 加倍後:(4, 12, 32)

含標準查詢運算符的 lambda

在其他實現中,LINQ to Objects 有一個輸入參數,其類型是泛型委託 Func < TResult > 系列中的一種。這些委託使用類型參數來定義輸入參數的數量和類型,以及委託的返回類型。Func 委託對於封裝用户定義的表達式很有用,這些表達式將應用於一組源數據中的每個元素。例如,假設為 Func < T , T結果 > 委託類型:
public delegate T結果 Func < in T , out T結果 > ( T arg )
可以將委託實例化為 Func < int, bool > 實例,其中 int 是輸入參數,bool 是返回值。返回值始終在最後一個類型參數中指定。例如,Func < int , string , bool > 定義包含兩個輸入參數(int 和 string)且返回類型為 bool 的委託。以下 Func 委託在調用時返回一個布爾值,該布爾值指示輸入參數是否等於 5。

Func < int , bool > DengY5 = x => x == 5;
bool Ber = DengY5 ( 4 );
Console . WriteLine ( Ber );   // False

參數類型為 Expression < TDelegate > 時,也可以提供 Lambda 表達式,例如在 Queryable 類型內定義的標準查詢運算符中提供。指定 Expression < TDelegate > 參數時,lambda 編譯為表達式樹。

下面的示例使用 Count 標準查詢運算符:

int [ ] Zhss = [ 5 , 4 , 1 , 3 , 9 , 8 , 6 , 7 , 2 , 0 ];
int JiShus = Zhss . Count ( n => n % 2 == 1 );
Array . Sort ( Zhss );
Console . WriteLine ( $"有 {JiShus} 個奇數:{string . Join ( "  " , Zhss )}" ); // 有 5 個奇數:0  1  2  3  4  5  6  7  8  9

編譯器可以推斷輸入參數的類型,或者你也可以顯式指定該類型。此特定的 lambda 表達式計算那些整數(n),這些整數被 2 除後餘數為 1。

下面的示例生成一個序列,其中包含 numbers 數組中位於 9 之前的所有元素,因為這是序列中第一個不符合條件的數字:

int [ ] Zhss = [ 5 , 4 , 1 , 3 , 9 , 8 , 6 , 7 , 2 , 0 ];
var XY6從頭開始 = Zhss . TakeWhile ( n => n < 6 );
Console . WriteLine ( $"從頭開始有 {XY6從頭開始 . Count ( )} 個數小於 6(大於等於 6 為止):{string . Join ( "  " , XY6從頭開始 )}" ); // 從頭開始有 4 個數小於 6(大於等於 6 為止):5  4  1  3

以下示例通過將輸入參數括在括號中來指定多個輸入參數。 此方法返回 numbers 數組中的所有元素,直至發現值小於其在數組中的序號位置的數字為止:

int [ ] Zhss = [ 5 , 4 , 1 , 3 , 9 , 8 , 6 , 7 , 2 , 0 ];
var DaDY索引從頭開始 = Zhss . TakeWhile ( (n , sy ) => n >= sy );
Console . WriteLine ( $"從頭開始有 {DaDY索引從頭開始 . Count ( )} 個數大於等於其索引(小於其索引為止):{string . Join ( "  " , DaDY索引從頭開始 )}" ); // 從頭開始有 2 個數大於等於其索引(小於其索引為止):5  4

不能直接在查詢表達式中使用 lambda 表達式,但你可以在查詢表達式的方法調用中使用它們,如以下示例所示:

var Zhs集合 = new List < int [ ] >
    {
        new [ ] { 1 , 2 , 3 , 4 , 5 },
        new [ ] { 0 , 0 , 0 },
        new [ ] { 9 , 8 },
        new [ ] { 1 , 0 , 1 , 0 , 1 , 0 , 1 , 0 }
    };

var JH多正數 =
    from JH in Zhs集合
    where JH . Count ( n => n > 0 ) > 3
    select JH ;

foreach ( var JH in JH多正數 )
    {
        Console . WriteLine ( string . Join ( "  " , JH ) );
    }
// 1  2  3  4  5
// 1  0  1  0  1  0  1  0

Lambda 表達式中的類型推理

編寫 lambda 時,通常不必為輸入參數指定類型,因為編譯器可以根據 lambda 主體、參數類型以及 C# 語言規範中描述的其他因素來推斷類型。對於大多數標準查詢運算符,第一個輸入是源序列中的元素類型。如果要查詢 IEnumerable < KH >,則輸入變量將被推斷為 KH 對象,這意味着你可以訪問其方法和屬性:
KH . Where ( k => k . City == "吉隆坡");
lambda 類型推理的一般規則如下:

  • Lambda 包含的參數數量必須與委託類型包含的參數數量相同。
  • Lambda 中的每個輸入參數必須都能夠隱式轉換為其對應的委託參數。
  • Lambda 的返回值(如果有)必須能夠隱式轉換為委託的返回類型。

Lambda 表達式的自然類型

Lambda 表達式本身沒有類型,因為通用類型系統沒有“Lambda 表達式”這一固有概念。不過,有時以非正式的方式談論 Lambda 表達式的“類型”會很方便。該非正式“類型”是指委託類型或 Lambda 表達式所轉換到的 Expression 類型。

lambda 表達式可以具有自然類型。編譯器不會強制你為 Lambda 表達式聲明委託類型(例如 Func < ... > 或 Action < ... >),而是根據 Lambda 表達式推斷委託類型。例如,請考慮以下聲明:
var ZH = (string s) => int . Parse ( s );
編譯器可以將 ZH 推斷為 Func < string , int >。編譯器選擇可用的 Func 或 Action 委託(如果存在合適的委託)。否則,它將合成委託類型。例如,如果 Lambda 表達式具有 ref 參數,則合成委託類型。如果 Lambda 表達式具有自然類型,可將其分配給不太顯式的類型,例如 System . Object 或 System . Delegate:

object ZH = (string s) => int . Parse ( s );   // Func < string , int >
Delegate ZH = (string s) => int . Parse ( s ); // Func < string , int >

只有一個重載的方法組(即沒有參數列表的方法名稱)具有自然類型:

var DQ = Console . Read; // 只有一個重載;Func < int > 推斷出
var XR = Console . Write; // 錯誤:多個重載,無法選擇

如果將 lambda 表達式分配給 System . Linq . Expressions . LambdaExpression 或 System . Linq . Expressions . Expression,並且 lambda 具有自然委託類型,則表達式的自然類型為 System . Linq . Expressions . Expression < TDelegate >,自然委託類型作為類型參數的參數使用。

Lambda表達式 ZH表達式 = ( string s ) => int . Parse ( s ); // 表達式< Func < string , int > >
Expression ZH表達式 = ( string s ) => int . Parse ( s ); // 表達式 < Func < string , int > >

並非所有 Lambda 表達式都有自然類型。請考慮以下聲明:
var ZH = s => int . Parse ( s ); // 錯誤:lambda 表達式中的類型信息不夠
編譯器無法推斷 s 的參數類型。如果編譯器無法推斷自然類型,必須聲明類型:
Func < string , int > ZH = s => int . Parse ( s );

顯式返回類型

通常,Lambda 表達式的返回類型是顯而易見的並且是推斷出來的。對於某些表達式,它不起作用:
var XZ = ( bool b ) => b ? 1 : "二"; // 錯誤:無法推斷返回類型
可以在輸入參數前面指定 Lambda 表達式的返回類型。指定顯式返回類型時,必須將輸入參數括起來:
var XZ = object ( bool b ) => b ? 1 : "二"; // Func < bool , object >

特性

可以將屬性添加到 Lambda 表達式及其參數。下面的示例演示瞭如何將屬性添加到 Lambda 表達式:
Func < string? , int? > ZH = [ProvidesNullCheck] ( s ) => ( s is not null ) ? int . Parse ( s ) : null;
還可以將屬性添加到輸入參數或返回值,如下面的示例所示:

var concat = ( [ DisallowNull ] string a , [ DisallowNull ] string b ) => a + b;
var inc = [ return : NotNullIfNotNull ( nameof ( s ) ) ] ( int? s ) => s . HasValue ? s++ : null;

如前面的示例所示,將屬性添加到 Lambda 表達式或其參數時,必須將輸入參數括起來。

重要:Lambda 表達式通過基礎委託類型調用。這與方法和本地函數不同。委託的 Invoke 方法不會檢查 Lambda 表達式的屬性。調用 Lambda 表達式時,對屬性不產生任何影響。Lambda 表達式的屬性對於代碼分析很有用,可以通過反射發現。此決定的一個後果是 System . Diagnostics . ConditionalAttribute 不能應用於 Lambda 表達式。

捕獲 lambda 表達式中的外部變量和變量範圍

lambda 可以引用外部變量。 外部變量是在定義 lambda 表達式的方法中或包含 lambda 表達式的類型中的範圍內變量。 以這種方式捕獲的變量將進行存儲以備在 lambda 表達式中使用,即使在其他情況下,這些變量將超出範圍並進行垃圾回收。 必須明確地分配外部變量,然後才能在 lambda 表達式中使用該變量。 下面的示例演示這些規則:

static void Main (  string [ ] args )
    {
        var YX = new Lei變量保存遊戲 ( );

        int ZShuRu = 5;
        YX . YX ( ZShuRu );

        int ZJ = 10;
        bool B = YX . FF是否等於保存的本地變量 ! ( ZJ );
        Console . WriteLine ( $"保存的局部變量等於 {ZJ}:{B}" );

        int Z3 = 3;
        YX . CZ更新保存本地變量 ! ( Z3 );

        bool B3 = YX . FF是否等於保存的本地變量 ( Z3 );
        Console . WriteLine ( $"另一個 lambda 函數觀察到保存的變量新值:{B3}" );
    }

public class Lei變量保存遊戲
    {
        internal Action < int >? CZ更新保存本地變量;
        internal Func < int , bool >? FF是否等於保存的本地變量;

        public void YX ( int 輸入 )
            {
                int j = 0;
                CZ更新保存本地變量 = x =>
                    {
                        j = x;
                        bool B = j > 輸入;
                        Console . WriteLine ( $"{j} 大於 {輸入}:{B}" );
                    };

                FF是否等於保存的本地變量 = x => x == j;
                Console . WriteLine ( $"在調用 lambda 函數之前,局部變量的值為 {j}" );
                CZ更新保存本地變量 ( 10 );
                Console . WriteLine ( $"在調用 lambda 函數之後,局部變量的值變為 {j}" );
        }
    }

下列規則適用於 lambda 表達式中的變量範圍:

  • 捕獲的變量將不會被作為垃圾回收,直至引用變量的委託符合垃圾回收的條件。
  • 在封閉方法中看不到 Lambda 表達式內引入的變量。
  • Lambda 表達式無法從封閉方法中直接捕獲 in、ref 或 out 參數。
  • lambda 表達式中的 return 語句不會導致封閉方法返回。
  • lambda 表達式不能包含 goto、break 或 continue 語句,如果這些跳轉語句的目標在 lambda 表達式塊之外。同樣,如果目標在塊內部,在 lambda 表達式塊外部使用跳轉語句也是錯誤的。

可以將 static 修飾符應用於 Lambda 表達式,來防止 Lambda 無意中捕獲本地變量或實例狀態:
Func<double, double> square = static x => x * x;
靜態 lambda 無法從封閉範圍中捕獲本地變量或實例狀態,但可以引用靜態成員和常量定義。

模式匹配 - is 和 switch 表達式以及運算符 and, or以及 not 模式

使用表達式、switch 語句和 is 將輸入表達式與任意數量的特徵匹配。C# 支持多種模式,包括聲明、類型、常量、關係、屬性、列表、var 和棄元。可以使用布爾邏輯關鍵字 and、or 和 not 組合模式。

以下 C# 表達式和語句支持模式匹配:

  • is 表達式
  • switch 語句
  • switch 表達式

在這些構造中,可將輸入表達式與以下任一模式進行匹配:

  • 聲明模式:用於檢查表達式的運行時類型,如果匹配成功,則將表達式結果分配給聲明的變量。
  • 類型模式:用於檢查表達式的運行時類型。
  • 常量模式:測試表達式結果是否等於指定的常量。
  • 關係模式:用於將表達式結果與指定常量進行比較。
  • 邏輯模式:測試表達式是否與模式的邏輯組合匹配。
  • 屬性模式:測試表達式的屬性或字段是否與嵌套模式匹配。
  • 位置模式:用於解構表達式結果並測試結果值是否與嵌套模式匹配。
  • var 模式:用於匹配任何表達式並將其結果分配給聲明的變量。
  • 棄元模式:用於匹配任何表達式。
  • 列表模式:測試元素序列是否與相應的嵌套模式匹配。在 C# 11 中引入。

邏輯、屬性、位置和列表模式都是遞歸模式。也就是説,它們可包含嵌套模式。

聲明和類型模式

使用聲明和類型模式檢查表達式的運行時類型是否與給定類型兼容。藉助聲明模式,還可聲明新的局部變量。當聲明模式與表達式匹配時,將為該變量分配轉換後的表達式結果,如以下示例所示:

object NiHao = "Hello, World!";
if ( NiHao is string nh )
    {
        Console . WriteLine ( nh . ToLower ( ) ); // hello, world!
    }

類型為 T 的聲明模式在表達式結果為非 null 且滿足以下任一條件時與表達式匹配:

  • 表達式結果的運行時類型為 T。
  • 表達式結果的運行時類型派生自類型 T,實現接口 T,或者存在從其到 的另一種 T。下面的示例演示滿足此條件時的兩種案例:

    var Zhs10 = new int [ ] { 10, 20, 30 };
    Console . WriteLine ( FF獲取源標籤 ( Zhs10 ) );  // 1
    
    var ZMs = new List < char > { 'a', 'b', 'c', 'd' };
    Console . WriteLine ( FF獲取源標籤 ( ZMs ) );  // 2
    
    static int FF獲取源標籤 < T > ( IEnumerable < T > 源 ) => 源 switch
      {
          Array array => 1,
          ICollection < T > collection => 2,
          _ => 3,
      };

    在上述示例中,在第一次調用 FF獲取源標籤 方法時,第一種模式與參數值匹配,因為參數的運行時類型 int[ ] 派生自 Array 類型。在第二次調用 FF獲取源標籤 方法時,參數的運行時類型 List < T > 並非派生自 Array 類型,但卻實現 ICollection < T > 接口。

  • 表達式結果的運行時類型是具有基礎類型的 T。
  • 存在從表達式結果的運行時類型到類型 T 的裝箱或轉換。

下面的示例演示最後兩個條件:

int? xNullable = 7;
int y = 23;
object yBoxed = y;
if ( xNullable is int a && yBoxed is int b )
    {
        Console . WriteLine ( a + b );  // 30
    }

如果只想檢查表達式類型,可使用棄元 _ 代替變量名,如以下示例所示:

public abstract class Lei交通工具 { }
public class Lei轎車 : Lei交通工具 { }
public class Lei卡車 : Lei交通工具 { }

public static class Lei交通費計算器
    {
        public static decimal dec交通費 ( this Lei交通工具 Che ) => Che switch
            {
                Lei轎車 _ => 2.00m,
                Lei卡車 _ => 7.50m,
                null => throw new ArgumentNullException ( nameof ( Che ) ) ,
                _ => throw new ArgumentException ( "未知交通工具" , nameof ( 交通工具 ) ),
            };
    }

可對此使用類型模式,如以下示例所示:

public static decimal dec交通費 ( this Lei交通工具 Che ) => Che switch
    {
        Lei轎車 => 2.00m,
        Lei卡車 => 7.50m,
        null => throw new ArgumentNullException ( nameof ( Che ) ) ,
        _ => throw new ArgumentException ( "未知交通工具" , nameof ( 交通工具 ) ),
    };

與聲明模式一樣,當表達式結果為非 null 並且其運行時類型滿足上述任何條件時,類型模式與表達式匹配。

若要檢查非 null,可使用否定 null 常量模式,如以下示例所示:

if ( input is not null )
    {
        // ……
    }

常量模式

可使用常量模式來測試表達式結果是否等於指定的常量,如以下示例所示:

public static decimal FF獲取組團票價 ( int 團員數 ) => 團員數 switch
    {
        1 => 12.0m,
        2 => 20.0m,
        3 => 27.0m,
        4 => 32.0m,
        0 => 0.0m,
        _ => throw new ArgumentException($"不支持的團員數:{團員數}" , nameof ( 團員數 ) ),
    };

在常量模式中,可使用任何常量表達式,例如:

  • integer 或 floating-point 數值文本
  • 字符型
  • 字符串字面量。
  • 布爾值 true 或 false
  • enum 值
  • 聲明常量字段或本地的名稱
  • null

表達式類型必須可轉換為常量類型,但有一個例外:類型為 Span < char > 或 ReadOnlySpan < char > 的表達式可以在 C# 11 及更高版本中針對常量字符串進行匹配。

常量模式用於檢查 null,如以下示例所示:

if ( X is null )
    {
        return;
    }

編譯器保證在計算表達式 X is null 時,不會調用用户重載的相等運算符 ==。

可使用否定 null 常量模式來檢查非 null,如以下示例所示:

if ( X is not null )
    {
        // ……
    }

關係模式

請使用關係模式將表達式結果與常量進行比較,如以下示例所示:

Console . WriteLine ( FF分類 ( 13 ) );  // 太高了
Console . WriteLine ( FF分類 ( double . NaN ) );  // 未知
Console . WriteLine ( FF分類 ( 2.4 ) );  // 正好

static string FF分類 ( double 温度 ) => 温度 switch
    {
        < -4.0 => "太低了",
        > 10.0 => "太高了",
        double . NaN => "未知",
        _ => "正好",
    };

在關係模式中,可使用關係運算符 <、>、<= 或 >= 中的任何一個。關係模式的右側部分必須是常數表達式。常數表達式可以是 integer、floating-point、char 或 enum 類型。

要檢查表達式結果是否在某個範圍內,請將其與合取 and 模式匹配,如以下示例所示:

Console . WriteLine ( FF獲取四季 ( new DateTime ( 2021 , 3 , 14 ) ) );  // 春
Console . WriteLine ( FF獲取四季 ( new DateTime ( 2021 , 7 , 19 ) ) );  // 夏
Console . WriteLine ( FF獲取四季 ( new DateTime ( 2021 , 2 , 17 ) ) );  // 冬

static string FF獲取四季 ( DateTime 日期 ) => 日期 . Month switch
    {
        >= 3 and < 6 => "春",
        >= 6 and < 9 => "夏",
        >= 9 and < 12 => "秋",
        12 or (>= 1 and < 3) => "冬",
        _ => throw new ArgumentOutOfRangeException ( nameof ( 日期 ) , $"日期具有出乎意料的月份:{日期 . Month}。"),
    };

如果表達式結果為 null 或未能通過可為 null 或 unbox 轉換轉換為常量類型,則關係模式與表達式不匹配。

邏輯模式

請使用 not、and 和 or 模式連結符來創建以下邏輯模式:

否定操作是指當否定模式與表達式不匹配時,仍將該否定模式視為與該表達式匹配的一種模式。以下示例展示瞭如何對常量空值模式進行否定處理,以檢查表達式是否非空:

if ( input is not null )
    {
        // ……
    }

當兩個模式都與表達式匹配時,所使用的連接符和模式會與該表達式匹配(合取模式)。以下示例展示瞭如何結合關係模式來檢查某個值是否處於特定範圍內:

Console . WriteLine ( FF分類 ( 13 ) );  // 高
Console . WriteLine ( FF分類 ( -100 ) );  // 太低了
Console . WriteLine ( FF分類 ( 5.7 ) );  // 正好

static string FF分類 ( double 温度 ) => 温度 switch
    {
        < -40.0 => "太低了",
        >= -40.0 and < 0.0 => "低",
        >= 0.0 and < 10.0 => "正好",
        >= 10.0 and < 20.0 => "高",
        >= 20.0 => "太高了",
        double . NaN => "未知",
    };

析取模式在任一模式與表達式匹配時與表達式匹配,如以下示例所示:

Console . WriteLine ( FF獲取四季 ( new DateTime ( 2021 , 1 , 19 ) ) );  // 冬
Console . WriteLine ( FF獲取四季 ( new DateTime ( 2021 , 10 , 9 ) ) );  // 秋
Console . WriteLine ( FF獲取四季 ( new DateTime ( 2021 , 5 , 11 ) ) );  // 春

static string FF獲取四季 ( DateTime 日期 ) => 日期 . Month switch
    {
        3 or 4 or 5 => "春",
        6 or 7 or 8 => "夏",
        9 or 10 or 11 => "秋",
        12 or 1 or 2 => "冬",
        _ => throw new ArgumentOutOfRangeException ( nameof ( 日期 ), $"日期具有出乎意料的月份:{日期 . Month}。"),
    };

如前面的示例所示,可在模式中重複使用模式連結符。

檢查的優先級和順序

模式組合器根據表達式的綁定順序進行排序,如下所示:

  • not
  • and
  • or

“not” 模式首先綁定其操作數。“and” 模式在任何 “not” 模式表達式綁定之後進行綁定。而 “or” 模式則在所有 “not” 和 “and” 模式都綁定到操作數之後進行綁定。以下示例試圖匹配所有不是小寫字母(a ~ z)的字符。但它存在錯誤,因為 “not” 模式的綁定順序在 “and” 模式之前:

// 錯誤的模式:`not` 的綁定優先於 `and`
static bool FF不是小寫字符 ( char z ) => z is not >= 'a' and <= 'z';

默認綁定表示將分析前面的示例,如以下示例所示:

// 此方法中默認的無括號綁定方式如下所示。`not` 的綁定優先於 `and`
static bool FF不是小寫字符默認綁定 ( char z ) => z is ( ( not >= 'a' ) and <= 'z' );

若要修復此問題,必須指定要 not 綁定到 >= 'a' and <= 'z' 表達式:

// 正確的模式:限定 `and` 在 `not` 之前
static bool FF不是小寫字符默認綁定正確模式 ( char z ) => z is not ( >= 'a' and <= 'z' );

添加括號變得更加重要,因為模式變得更加複雜。一般情況下,使用括號來闡明其他開發人員的模式,如以下示例所示:
static bool FF是字母 ( char c ) => c is ( >= 'a' and <= 'z' ) or ( >= 'A' and <= 'Z' );
備註:檢查具有相同綁定順序的模式的順序是未定義的。在運行時,可以先檢查多個 or 模式和多個 and 模式的右側嵌套模式。

屬性模式

可以使用屬性模式將表達式的屬性或字段與嵌套模式進行匹配,如以下示例所示:
static bool FF是否會議日期 ( DateTime 日期 ) => date is { Year : 2020 , Month : 5 , Day : 19 or 20 or 21 };
當表達式結果為非 NULL 且每個嵌套模式都與表達式結果的相應屬性或字段匹配時,屬性模式將與表達式匹配。

還可將運行時類型檢查和變量聲明添加到屬性模式,如以下示例所示:

internal static readonly char [ ] charArray = new [ ] { '1' , '2' , '3' , '4' , '5' , '6' , '7' };
internal static readonly char [ ] charArray0 = new [ ] { 'a' , 'b' , 'c' };

Console . WriteLine ( FF獲取五個字符 ( "Hello, world!" ) ); // Hello
Console . WriteLine ( FF獲取五個字符 ( "Hi!" ) ); // Hi!
Console . WriteLine ( FF獲取五個字符 ( new [ ] { '1' , '2' , '3' , '4' , '5' , '6' , '7' } ) ); // 12345
Console . WriteLine ( FF獲取五個字符 ( new [ ] { 'a' , 'b' , 'c' } ) ); // abc

static string FF獲取五個字符 ( object 輸入 ) => 輸入 switch
    {
        string { Length: >= 5 } z => z . Substring ( 0 , 5 ),
        string z => z,

        ICollection<char> { Count: >= 5 } FH => new string ( FH . Take ( 5 ) . ToArray ( ) ),
        ICollection<char> FH => new string ( FH . ToArray ( ) ),

        null => throw new ArgumentNullException ( nameof ( 輸入 ) ),
        _ => throw new ArgumentException ( "不支持的輸入格式。" ),
    };

屬性模式是一種遞歸模式。也就是説,可以將任何模式用作嵌套模式。使用屬性模式將部分數據與嵌套模式進行匹配,如以下示例所示:

public record D2 ( int X , int Y );
public record XD ( D2 起點 , D2 終點 );

static bool Ber端點是否在x軸 ( XD 線段 ) =>
    線段 is { 起點 : { Y : 0 } } or { 終點 : { Y : 0 } };

上一示例使用 or 模式結合法和記錄類型。

可以在屬性模式中引用嵌套屬性或字段。該功能稱為“擴展屬性模式”。例如,可將上述示例中的方法重構為以下等效代碼:

static bool Ber端點是否在x軸 ( XD 線段 ) =>
    線段 is { 起點 . Y : 0 } or { 終點 . Y : 0 };

提示:可以使用簡化屬性模式(IDE0170)樣式規則,通過建議使用擴展屬性模式的位置來提高代碼的可讀性。

位置模式

可使用位置模式來解構表達式結果並將結果值與相應的嵌套模式匹配,如以下示例所示:

public readonly struct D2
    {
        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);
    }

static string FF分類 ( D2 點 ) => 點 switch
    {
        ( 0 , 0 ) => "原點",
        ( 1 , 0 ) => "X 正軸基點",
        ( 0 , 1 ) => "Y 正軸基點",
        _ => "只是一個點",
    };

在前面的示例中,表達式的類型包含 Deconstruct 方法,該方法用於解構表達式結果。

重要:位置模式中成員的順序必須與 Deconstruct 方法中的參數順序匹配。這是因為為位置模式生成的代碼調用 Deconstruct 方法。

還可將元組類型的表達式與位置模式進行匹配。這樣,就可將多個輸入與各種模式進行匹配,如以下示例所示:

static decimal FF獲取團體票價格折扣 ( int 團體大小 , DateTime 日期 )
    => ( 團體大小 , 日期 . DayOfWeek ) switch
        {
            ( <= 0 , _ ) => throw new ArgumentException("團體大小必須是正數。"),
            ( _ , DayOfWeek . Saturday or DayOfWeek . Sunday ) => 0.0m,
            ( >= 5 and < 10 , DayOfWeek . Monday ) => 20.0m,
            ( >= 10 , DayOfWeek.Monday ) => 30.0m,
            ( >= 5 and < 10 , _ ) => 12.0m,
            ( >= 10 , _ ) => 15.0m,
            _ => 0.0m,
        };

上一示例使用關係和邏輯模式。

可在位置模式中使用元組元素的名稱和 Deconstruct 參數,如以下示例所示:

var Zhss = new List < int > { 1 , 2 , 3 , 4 };
if ( FF和與計數 ( Zhss ) is ( 和: double He , 計數: int Ge ) )
    {
        Console . WriteLine ( $"【{string . Join ( " " , Zhss ) }】 的和是 {He},平均值是:{He / Ge}" );  // 【1 2 3 4】 的和是 10,平均值是: 2.5
    }

static ( double 和 , int 計數 ) FF和與計數 ( IEnumerable < int > Zhss )
    {
        int he = 0;
        int ge = 0;
        foreach ( int z in Zhss )
            {
                he += z;
                ge++;
            }
        return ( he , ge );
    }

還可通過以下任一方式擴展位置模式:

添加運行時類型檢查和變量聲明,如以下示例所示:

public record D2 ( int X , int Y );
public record D3 ( int X , int Y , int Z );

static string FF如果座標均為正值輸出 ( object 點 ) => 點 switch
    {
        D2 ( > 0 , > 0 ) d => d . ToString ( ),
        D3 ( > 0 , > 0 , > 0 ) d => d . ToString ( ),
        _ => string . Empty,
    };

前面的示例使用隱式提供的 Deconstruct 方法。

在位置模式中使用屬性模式,如以下示例所示:

public record D加權 ( int X , int Y )
    {
        public double 權 { get; set; }
    }

static bool FF在範圍內 ( D加權 權點 ) => 權點 is ( >= 0 , >= 0 ) { 權: >= 0.0 };

結合前面的兩種用法,如以下示例所示:

if ( ShuRu is D加權 ( > 0 , > 0 ) { 權: > 0.0 } d )
    {
        // ……
    }

位置模式是一種遞歸模式。也就是説,可以將任何模式用作嵌套模式。

var 模式

您可以使用 var 模式匹配任何表達式(包括 null),並將結果賦值給一個新的局部變量,如下例所示:

static bool FF在範圍內 ( int ID , int Z絕對值限制 ) =>
    FF模擬數據獲取 ( ID ) is var JGs
    && JGs . Min ( ) >= -Z絕對值限制
    && JGs . Max ( ) <= Z絕對值限制;

static int [ ] FF模擬數據獲取 ( int ID )
    {
        Random SJS = new ( );
        return [ .. Enumerable
            . Range ( 0 , 5 )
            . Select ( z => SJS . Next ( -10 , 11 ) ) ];
    }

需要布爾表達式中的臨時變量來保存中間計算的結果時,var 模式很有用。當需要在 var 表達式或語句的 when 大小寫臨界子句中執行更多檢查時,也可使用 switch 模式,如以下示例所示:

public record D2 ( int X , int Y );

static D2 FF變換 ( D2 D ) => D switch
    {
        var ( x , y ) when x < y => new Point ( -x , y ),
        var ( x , y ) when x > y => new Point ( x , -y ),
        var ( x , y ) => new Point ( x , y ),
    };

static void FF測試變換 ( )
    {
        Console . WriteLine ( FF變換 ( new D2 ( 1 , 2 ) ) ); // D2 { X = -1 , Y = 2 }
        Console . WriteLine ( FF變換 ( new D2 ( 5 , 2 ) ) ); // D2 { X = 5, Y = -2 }
    }

在前面的示例中,模式 var (x , y) 等效於位置模式 ( var x , var y )。

在 var 模式中,聲明變量的類型是與該模式匹配的表達式的編譯時類型。

棄元模式

可使用棄元模式來匹配任何表達式,包括 _,如以下示例所示 null:

Console . WriteLine ( FF獲取折扣百分比 ( DayOfWeek . Friday ) ); // 5.0
Console . WriteLine ( FF獲取折扣百分比 ( null ) ); // 0.0
Console . WriteLine ( GetDiscountInPercent ( ( DayOfWeek ) 10 ) ); // 0.0

static decimal FF獲取折扣百分比 ( DayOfWeek? Zhou ) => Zhou switch
    {
        DayOfWeek . Monday => 0.5m,
        DayOfWeek . Tuesday => 12.5m,
        DayOfWeek . Wednesday => 7.5m,
        DayOfWeek . Thursday => 12.5m,
        DayOfWeek . Friday => 5.0m,
        DayOfWeek . Saturday => 2.5m,
        DayOfWeek . Sunday => 2.0m,
        _ => 0.0m,
    };

在前面的示例中,棄元模式用於處理 null 以及沒有相應的 DayOfWeek 枚舉成員的任何整數值。這可保證示例中的 switch 表達式可處理所有可能的輸入值。如果沒有在 switch 表達式中使用棄元模式,並且該表達式的任何模式均與輸入不匹配,則運行時會引發異常。如果 switch 表達式未處理所有可能的輸入值,則編譯器會生成警告。

棄元模式不能是 is 表達式或 switch 語句中的模式。在這些案例中,要匹配任何表達式,請使用帶有棄元的 var。棄元模式可以是表達式 switch 中的模式。

帶括號模式

可在任何模式兩邊加上括號。通常,這樣做是為了強調或更改邏輯模式中的優先級,如以下示例所示:

if ( Shr is not ( float or double ) )
    {
        return;
    }

列表模式

從 C# 11 開始,可以將數組或列表與模式的序列進行匹配,如以下示例所示:

int [ ] numbers = { 1 , 2 , 3 };

Console . WriteLine ( numbers is [ 1 , 2 , 3 ] ); // True
Console . WriteLine ( numbers is [ 1 , 2 , 4 ] ); // False
Console . WriteLine ( numbers is [ 1 , 2 , 3 , 4 ] ); // False
Console . WriteLine ( numbers is [ 0 or 1 , <= 2 , >= 3 ] ); // True

如前面的示例所示,當每個嵌套模式與輸入序列的相應元素匹配時,都會匹配列表模式。可使用列表模式中的任何模式。若要匹配任何元素,請使用棄元模式,或者,如果還想捕獲元素,請使用 var 模式,如以下示例所示:

List<int> numbers = new ( ) { 1 , 2 , 3 };

if ( numbers is [ var first , _ , _ ] )
    {
        Console . WriteLine ( $"三個項目中的第一個元素是:{first}。" );
    }
// 三個項目中的第一個元素是: 1。

前面的示例將整個輸入序列與列表模式匹配。若要僅匹配輸入序列開頭或 | 和結尾的元素,請使用切片模式 ..,如以下示例所示:

Console . WriteLine ( new [ ] { 1 , 2 , 3 , 4 , 5 } is [ > 0 , > 0 , ..]); // True
Console . WriteLine ( new [ ] { 1 , 1 } is [ _ , _ , .. ] ); // True
Console . WriteLine ( new [ ] { 0 , 1 , 2 , 3 , 4 } is [ > 0 , > 0 , .. ] ); // False
Console . WriteLine ( new [ ] { 1 } is [ 1 , 2 , .. ] ); // False

Console . WriteLine ( new [ ] { 1 , 2 , 3 , 4 } is [ .. , > 0 , > 0 ] ); // True
Console . WriteLine ( new [ ] { 2 , 4 } is [ .. , > 0 , 2 , 4 ] ); // False
Console . WriteLine ( new [ ] { 2 , 4 } is [ .. , 2 , 4 ] ); // True

Console . WriteLine ( new [ ] { 1 , 2 , 3 , 4 } is [ >= 0 , .. , 2 or 4 ] ); // True
Console . WriteLine ( new [ ] { 1, 0, 0, 1 } is [1 , 0 , .. , 0 , 1 ] ); // True
Console . WriteLine ( new [ ] { 1 , 0 , 1 } is [ 1 , 0 , .. , 0 , 1 ] ); // False

切片模式匹配零個或多個元素。最多可在列表模式中使用一個切片模式。切片模式只能顯示在列表模式中。

還可以在切片模式中嵌套子模式,如以下示例所示:

void FF信息匹配 ( string 信息 )
    {
        var 結果 = 信息 is [ 'a' or 'A' , .. var s , 'a' or 'A' ]
        ? $"信息 {信息} 匹配;內部部分是:{s}。"
        : $"信息 {信息} 不匹配。";
        Console . WriteLine ( 結果 );
    }

FF信息匹配 ( "aBBA" ); // 信息 aBBA 匹配;內部部分是:BB。
FF信息匹配 ( "apron" ); // 信息 apron 不匹配。

void FF驗證 ( int [ ] Zhss )
    {
        var 結果 = Zhss is [ < 0 , .. { Length: 2 or 4 } , > 0 ] ? "有效" : "無效";
        Console . WriteLine ( 結果 );
    }

FF驗證 ( new [ ] { -1 , 0 , 1 } ); // 無效
FF驗證 ( new [ ] { -1 , 0 , 0 , 1 } ); // 有效

加法運算符 “+” 和 “+=”

內置 整型 和 浮點 數值類型、字符串 類型和 委託 類型都支持 + 和 += 運算符。

字符串拼接

當一個或兩個操作數屬於 字符串 類型時, + 運算符將連接其操作數的字符串表示形式(null 為空字符串):

Console . WriteLine ( "Forgot" + "white space" ); //Forgotwhite space
Console . WriteLine ( "可能是最古老的恆量:" + Math . PI); // 可能是最古老的恆量:3.14159265358979
Console . WriteLine ( null + "Nothing to add."); // Nothing to add.

字符串內插提供了一種更方便地設置字符串格式的方法:

Console . WriteLine ($"可能是最古老的恆量:{Math . PI:F2}"); // 可能是最古老的恆量:3.14

當用於佔位符的所有表達式也是常量字符串時,可以使用字符串內插來初始化常量字符串。

從 C# 11 開始, + 運算符對 UTF-8 文本字符串執行字符串串聯。此運算符連接兩個 ReadOnlySpan < byte > 對象。

委託組合

對於同一 委託 類型的操作數, + 運算符將返回一個新的委託實例,該實例在調用時調用左側操作數,然後調用右側操作數。如果任一作數為 null 操作數,則 + 運算符返回另一個操作數的值(也可能是 null)。以下示例演示如何將委託與 + 運算符組合:

Action a = ( ) => Console . Write ( "a" );
Action b = ( ) => Console . Write ( "b" );
Action ab = a + b;
ab ( ); // ab

若要執行委託刪除,請使用 - 運算符。

加法賦值運算符 +=

使用 += 運算符的表達式,例如

x += y

等效於:

x = x + y

不過 x 只評估一次。

以下示例演示了 += 運算符的用法:

int i = 5;
i += 9;
Console . WriteLine ( i ); // 14

string GuShi = "起始。";
GuShi += "結束。";
Console . WriteLine ( GuShi ); // 起始。結束。

Action 輸出a = ( ) => Console . Write ( "a" );
輸出a ( ); // a

Console . WriteLine ( );
輸出a += ( ) => Console . Write ( "b" );
輸出a ( ); // ab

還可以使用 += 運算符指定在訂閲事件時要添加的事件處理程序方法。

運算符可重載性

用户定義的類型可以重載 + 運算符。重載 + 二元運算符時,+= 運算符也會隱式重載。從 C# 14 開始,用户定義的類型可以顯式重載 += 運算符以提供更高效的實現。通常,類型重載 += 運算符,因為可以就地更新值,而不是分配新實例來保存添加的結果。如果類型不提供顯式重載,編譯器將生成隱式重載。

減法運算符 “-” 和 “-=”

內置 整型 和 浮點 數值類型、字符串 類型和 委託 類型都支持 - 和 -= 運算符。

委託刪除

對於委託類型相同的操作數,- 運算符返回如下計算的委託實例:

  • 如果兩個作數都是非 null 的,而右側作數的調用列表是左側作數調用列表的正確連續子列表,則該作的結果是一個新的調用列表,通過從左側作數的調用列表中刪除右側作數的條目來獲取。如果右側操作數的列表與左側操作數列表中的多個連續子列表匹配,則僅刪除最右側的匹配子列表。如果刪除結果為空列表,則結果為 null。

    Action a = ( ) => Console . Write( "a" );
    Action b = ( ) => Console . Write( "b" );
    
    var abbaab = a + b + b + a + a + b;
    abbaab ( ); // abbaab
    Console . WriteLine ( );
    
    var ab = a + b;
    var abba = abbaab - ab;
    abba ( ); // abba
    Console . WriteLine ( );
    
    var Kong = abbaab - abbaab;
    Console . WriteLine ( Kong is null ); // True
  • 如果右側操作數的調用列表不是左側操作數調用列表的正確連續子列表,則操作的結果是左側操作數。例如,刪除不屬於多播委託的委託不會產生任何影響,結果是多播委託保持不變。

    Action a = ( ) => Console . Write ( "a" );
    Action b = ( ) => Console . Write ( "b" );
    
    var abbaab = a + b + b + a + a + b;
    var aba = a + b + a;
    
    var Yi = abbaab - aba;
    Yi ( ); // abbaab
    Console . WriteLine ( );
    Console . WriteLine ( object . ReferenceEquals ( abbaab , Yi ) ); // True
    
    Action a2 = ( ) => Console . Write ( "a" );
    var XiuGai = aba - a;
    XiuGai ( ); // ab
    Console . WriteLine ( );
    var WeiXiuGai = aba - a2;
    WeiXiuGai ( ); // aba
    Console . WriteLine ( );
    Console . WriteLine ( object . ReferenceEquals ( aba , WeiXiuGai ) ); // True

    前面的示例還演示了在刪除委託期間對委託實例進行比較。例如,通過計算相同的 Lambda 表達式生成的委託不相等。

  • 如果左側作數為 null,則運算的結果為 null。如果右側操作數為 null,則結果為左側操作數。

    Action a = ( ) => Console.Write ( "a" );
    
    var nothing = null - a;
    Console . WriteLine ( nothing is null ); // True
    
    var Yi = a - null;
    a ( ); // a
    Console . WriteLine ( );
    Console . WriteLine ( object . ReferenceEquals ( Yi , a ) ); // True

    要合併委託,請使用 + 運算符。

    減法賦值運算符 -=

    使用 -= 運算符的表達式,例如

    x -= y
    
    等效於:
    
    x = x - y

    不過 x 只評估一次。

以下示例演示了 += 運算符的用法:

int i = 5;
i -= 9;
Console . WriteLine ( i ); // -4

Action 輸出a = ( ) => Console . Write ( "a" );
Action 輸出b = ( ) => Console . Write ( "b" );
輸出a ( ); // a
輸出b ( ); // b

Console . WriteLine ( );
Action 輸出ab = 輸出a + 輸出b;
輸出ab ( ); // ab
Console . WriteLine ( );
輸出ab -= 輸出b;
輸出ab ( ); // a

還可以使用 -= 運算符指定在取消訂閲事件時要刪除的事件處理程序方法。

運算符可重載性

用户定義的類型可以重載 - 運算符。重載 - 二元運算符時,-= 運算符也會隱式重載。從 C# 14 開始,用户定義的類型可以顯式重載 -= 運算符以提供更高效的實現。通常,類型重載 -= 運算符,因為可以就地更新值,而不是分配新實例來保存添加的結果。如果類型不提供顯式重載,編譯器將生成隱式重載。

三元條件運算符 ?……:……

條件運算符(?……:……)也稱為三元條件運算符,用於計算布爾表達式,並根據布爾表達式的計算結果為 true 還是 false 來返回兩個表達式中的一個結果,如以下示例所示:

string FF獲取適宜度 ( double 温度 ) => 温度 < 20.0 ? "冷。" : "很好!";

Console . WriteLine ( FF獲取適宜度 ( 15 ) ); // 冷。
Console . WriteLine ( FF獲取適宜度 ( 27 ) ); // 很好!

如上述示例所示,條件運算符的語法如下所示:
條件 ? 成立 : 不成立
條件 表達式的計算結果必須為 true 或 false。若 條件 的計算結果為 true,將計算 成立,其結果成為運算結果。若 條件 的計算結果為 false,將計算 不成立,其結果成為運算結果。只會計算 成立 或 不成立。條件表達式是目標類型的。也就是説,如果條件表達式的目標類型是已知的,則 成立 和 不成立 的類型必須可隱式轉換為目標類型,如以下示例所示:

var SJS = new Random ( );
var TJ = SJS . NextDouble ( ) > 0.5;

int? x = TJ ? 12 : null;

IEnumerable < int > xs = x is null ? new List<int>( ) { 0 , 1 } : new int [ ] { 2 , 3 };

如果條件表達式的目標類型未知(例如使用 var 關鍵字時)或 成立 和 不成立 的類型必須相同,或者必須存在從一種類型到另一種類型的隱式轉換:

var SJS = new Random ( );
var TJ = rand . NextDouble ( ) > 0.5;

var x = TJ ? 12 : ( int? ) null;

條件運算符為右聯運算符,即形式的表達式
a ? b : c ? d : e
計算結果為
a ? b : (c ? d : e)
提示:可以使用以下助記鍵設備記住條件運算符的計算方式:條件成立嗎? ? 是 : 否

ref 條件表達式

條件 ref 表達式可有條件地返回變量引用,如以下示例所示:

int [ ] xiaoshu = { 1 , 2 , 3 , 4 , 5 };
int [ ] dashu = { 10 , 20 , 30 , 40 , 50 };

int SY = 7;
ref int refZhi = ref ( ( SY < 5 ) ? ref xiaoshu [ SY ] : ref dashu [ SY - 5 ] );
refZhi = 0;

SY = 2;
( ( SY < 5 ) ? ref xiaoshu [ SY ] : ref dashu [ SY - 5 ] ) = 100;

Console . WriteLine ( string . Join ( " " , xiaoshu ) ); // 1  2  100  4  5
Console . WriteLine ( string . Join ( " " , dashu ) ); // 10  20  0  40  50

可以 ref 分配條件 ref 表達式的結果,將其用作引用返回,或將其作為 ref、out、in 或 ref readonly 方法參數傳遞。還可以分配條件 ref 表達式的結果,如前面的示例所示。

ref 條件表達式的語法如下所示:
條件 ? ref 成立 : ref 不成立
條件 ref 表達式與條件運算符相似,僅計算兩個表達式其中之一:成立 或 不成立。

在 ref 條件表達式中,成立 和 不成立 的類型必須相同。 ref 條件表達式不由目標確定類型。

條件運算符和 if 語句

需要根據條件計算值時,使用條件運算符而不是 if 語句可以使代碼更簡潔。下面的示例演示了將整數歸類為負數或非負數的兩種方法:

int ShuRu = new Random ( ) . Next ( -5 , 5 );

string zfc分類;
if ( ShuRu >= 0 )
    {
        zfc分類 = "非負數";
    }
else
    {
        zfc分類 = "負數";
    }

zfc分類 = ( ShuRu >= 0 ) ? "非負數" : "負數";

運算符可重載性

用户定義類型不能重載條件運算符。

null 包容運算符 !

一元后綴 ! 運算符是 null 包容運算符或 null 抑制運算符。在已啓用的可為 null 的註釋上下文中,使用 null 包容運算符來取消上述表達式的所有可為 null 警告。一元前綴 ! 運算符是邏輯非運算符。null 包容運算符在運行時不起作用。它僅通過更改表達式的 null 狀態來影響編譯器的靜態流分析。在運行時,表達式 x! 的計算結果為基礎表達式 x 的結果。

示例

null 包容運算符的一個用例是測試參數驗證邏輯。例如,請考慮以下類:

#nullable enable
        public class Ren
            {
                public Ren ( string XingMing ) => 姓名 = XingMing ?? throw new ArgumentNullException ( nameof ( XingMing ) );
                public string 姓名
                    {
                        get;
                    }
            }

使用測試框架,可以在構造函數中為驗證邏輯創建以下測試:

try
    {
        Ren CSNull姓名 = new ( null ! );
        Console . WriteLine ( CSNull姓名 );
    }
catch ( Exception e )
    {
        Console . WriteLine ( e . Message ); } // Value cannot be null. (Parameter 'XingMing')
    }

如果不使用 null 包容運算符,編譯器將為前面的代碼生成以下警告:警告 CS8625:無法將 null 字面量轉換為非 null 的引用類型。通過使用 null 包容運算符,可以告知編譯器傳遞 null 是預期行為,不應發出警告。

如果你明確知道某個表達式不能為 null,但編譯器無法識別它,也可以使用 null 包容運算符。在下面的示例中,如果 IsValid 方法返回 true,則其參數不是 null,可以放心取消對它的引用:

static void Main (  string [ ] args )
    {
        Ren ? ren = new ( "小梅" );
        if ( Ber是否有效 ( ren ) )
            {
                Console . WriteLine ( ren ! . 姓名 );
            }
    }

public static bool Ber是否有效 ( Ren? ren ) => ren is not null && ren . 姓名 is not null;

如果沒有 null 包容運算符,編譯器將為 ren . 姓名 代碼生成以下警告:警告 CS8602:對可能為 null 的引用進行解引用操作。

如果可以修改 IsValid 方法,則可使用 NotNullWhen 屬性告知編譯器,當方法返回 IsValid 時,null 方法的參數不能是 true:
public static bool Ber是否有效 ( [ NotNullWhen ( true ) ] Ren? ren ) => ren is not null && ren . 姓名 is not null;
在上述示例中,您無需使用空值忽略運算符,因為編譯器已具備足夠的信息來確定在 if 語句內部,變量 p 不能為 null。

null 合併運算符 ?? 和 ??=

如果左操作數不是 null,則 ?? 合併運算符返回其左操作數的值;否則,它將計算右側作數並返回其結果。如果左側操作數的計算結果為非 null,則 ?? 運算符不會計算其右側操作數。僅當左操作數的計算結果為 null 時,??= 合併賦值運算符才會將其右操作數的值賦值給其左操作數。如果左側操作數的計算結果為非 null,則 ??= 運算符不會計算其右側操作數。

List<int>? Zhss = null;
int? a = null;

Console . WriteLine ( ( Zhss is null ) ); // true
// 如果 “Zhss” 為 null,則對其進行初始化。然後,將 5 加到 “Zhss” 上
( Zhss ??= new List<int> ( ) ) . Add ( 5 );
Console . WriteLine ( string . Join ( " " , Zhss ) ); // 5
Console . WriteLine ( ( Zhss is null ) ); // false

Console . WriteLine ( ( a is null ) ); // true
Console . WriteLine ( ( a ?? 3 ) ); // 3 由於 a 仍然為空值
// 如果變量 a 為 null,則將 0 賦值給 a,並將 a 添加到列表中
Zhss . Add ( a ??= 0 );
Console . WriteLine ( ( a is null ) ); // false
Console . WriteLine ( string . Join ( " " , Zhss ) ); // 5 0
Console . WriteLine ( a ); // 0

運算符的 ??= 左側作數必須是變量、屬性或索引器元素。

?? 和 ??= 運算符的左操作數的類型必須是可以為 null 的值類型。具體而言,可以將 null 合併運算符與不受約束的類型參數一起使用:

private static void FF顯示<T> ( T a , T backup )
    {
        Console . WriteLine ( a ?? backup );
    }

null 合併運算符是右結合運算符。也就是説,是 form 的表達式

a ?? b ?? c
d ??= e ??= f

會像這樣求值

a ?? (b ?? c)
d ??= (e ??= f)

示例

?? 和 ??= 運算符在以下情境中非常有用:

  • 在帶有 null-conditional 運算符 ? . ? [ ] 的表達式中,可以使用 ?? 運算符提供一個備選表達式,以便在 null-conditional 運算的結果為 null 時進行計算。

    double FF數的和 ( List<double [ ]> Ji數 , int SY數集的索引 )
      {
          return Ji數? [ SY數集的索引 ]? . Sum ( ) ?? double . NaN;
      }
    
    var sum = FF數的和 ( null , 0 );
    Console . WriteLine ( sum ); // NaN
  • 當您處理可為 null 值類型並需要提供一個基礎值類型的數據時,請使用 ?? 運算符來指定在可為 null 值類型值為 null 時應提供的值:

    int? a = null;
    int b = a ?? -1;
    Console . WriteLine ( b ); // -1

    如果可以為 null 的類型的值為 Nullable < T > . GetValueOrDefault ( ) 時要使用的值應為基礎值類型的默認值,請使用 null 方法。

可以使用 throw 表達式作為運算符的 ?? 右側操作數,使參數檢查代碼更加簡潔:

public string 名
    {
        get => ming;
        set => ming = value ?? throw new ArgumentNullException ( nameof ( value ) , "名不能是 null");
    }

前面的示例還演示如何使用表達式主體成員定義屬性。

可以使用 ??= 運算符替換 form 代碼

if ( 變量 is null )
    {
        變量 = 表達式;
    }

替換為以下代碼:

變量 ??= 表達式;

運算符可重載性

運算符 ??、??= 不能重載。

lambda 表達式運算符 =>

=> 令牌支持兩種形式:作為 lambda 運算符、作為成員名稱的分隔符和表達式主體定義中的成員實現。

lambda 運算符

在 lambda 表達式中,lambda 運算符 => 將左側的輸入參數與右側的 lambda 主體分開。

以下示例使用帶有方法語法的 LINQ 功能來演示 lambda 表達式的用法:

string [ ] dancis = { "bot", "apple", "apricot" };
int Z最小長度 = dancis
  . Where ( d => d . StartsWith ( "a" ) )
  . Min ( d => d . Length );
Console . WriteLine ( Z最小長度 ); // 5(anpple 的長度)

int [ ] Zhss = { 4 , 7 , 10 };
int Z乘積 = Zhss . Aggregate ( 1 , ( 過渡 , 下一個 ) => 過渡 * 下一個 );
Console . WriteLine ( Z乘積 ); // 280

lambda 表達式的輸入參數在編譯時是強類型。當編譯器可以推斷輸入參數的類型時,如前面的示例所示,可以省略類型聲明。如果需要指定輸入參數的類型,則必須對每個參數執行類型聲明,如以下示例所示:

int [ ] Zhss = { 4 , 7 , 10 };
int Z乘積 = Zhss . Aggregate ( 1 , ( int 過渡 , int 下一個 ) => 過渡 * 下一個 );
Console . WriteLine ( Z乘積 ); // 280

以下示例顯示如何在沒有輸入參數的情況下定義 lambda 表達式:

Func<string> zfc歡迎 = ( ) => "Hello, World!";
Console . WriteLine ( greet ( ) );

表達式主體定義

表達式主體定義具有下列常規語法:
成員 => 表達式;
其中 表達式 是有效的表達式。表達式 的返回類型必須可隱式轉換為成員的返回類型。 如果成員:

  • 具有 void 返回類型或
  • 是一個:

    • 構造函數
    • 終結器
    • 屬性或索引器 set 訪問器

表達式 必須是語句表達式。由於表達式的結果被丟棄,該表達式的返回類型可以是任何類型。

以下示例演示了用於 Ren . ToString 方法的表達式主體定義:
public override string ToString ( ) => $"{姓} {名}" . Trim ( );
它是以下方法定義的簡寫版:

public override string ToString ( )
    {
        return $"{姓} {名}" . Trim ( );
    }

可以為方法、運算符、只讀屬性、構造函數、終結器以及屬性和索引器訪問器創建表達式正文定義。

運算符可重載性

不能重載 => 運算符。

命名空間別名運算符 ::

使用命名空間別名限定符 “::” 訪問已設置別名的命名空間的成員。只能使用兩個標識符之間的 :: 限定符。左側標識符可以是命名空間別名、外部別名或 global 別名之一。例如:

  • 用 using 別名指令創建的命名空間別名:

    using forwinforms = System . Drawing;
    using forwpf = System . Windows;
    
    public class Converters
      {
          public static forwpf::Point Convert ( forwinforms::Point DForm ) => new forwpf::Point ( DForm . X , DForm . Y );
      }
  • 外部別名。
  • global 別名,該別名是全局命名空間別名。全局命名空間是包含未在命名空間中聲明的命名空間和類型的命名空間。與 :: 限定符一起使用時,global 別名始終引用全局命名空間,即使存在用户定義的 global 命名空間別名也是如此。
    以下示例使用 global 別名訪問 .NET System 命名空間,該命名空間是全局命名空間的成員。如果沒有 global 別名,則將訪問用户定義的 System 命名空間(該命名空間是 MyCompany . MyProduct 命名空間的成員):

    namespace MyCompany . MyProduct . System
      {
          class Program
              {
                  static void Main ( ) => global::System . Console . WriteLine ( "使用全局別名");
              }
    
          class Console
              {
                  string zfc建議 => "考慮給這個類重新命名";
              }
      }

    備註:僅當 global 關鍵字是 :: 限定符的左側標識符時,該關鍵字才是全局命名空間別名。

此外,還可以使用 . 令牌來訪問設置了別名的命名空間的成員。不過,. 令牌還可用於訪問類型成員。:: 限定符確保其左側標識符始終引用命名空間別名,即使存在同名的類型或命名空間也是如此。

異步等待任務完成運算符 await

await 運算符暫停對其所屬的 async 方法的求值,直到其操作數表示的異步操作完成。異步操作完成後,await 運算符將返回操作的結果(如果有)。當 await 運算符應用到表示已完成操作的操作數時,它將立即返回操作的結果,而不會暫停其所屬的方法。await 運算符不會阻止計算異步方法的線程。當 await 運算符暫停其所屬的異步方法時,控件將返回到方法的調用方。

在下面的示例中,HttpClient . GetByteArrayAsync 方法返回 Task < byte [ ] > 實例,該實例表示在完成時生成字節數組的異步操作。在操作完成之前,await 運算符將暫停 DownloadDocsMainPageAsync 方法。當 DownloadDocsMainPageAsync 暫停時,控件將返回到 Main 方法,該方法是 DownloadDocsMainPageAsync 的調用方。Main 方法將執行,直至它需要 DownloadDocsMainPageAsync 方法執行的異步操作的結果。當 GetByteArrayAsync 獲取所有字節時,將計算 DownloadDocsMainPageAsync 方法的其餘部分。之後,將計算 Main 方法的其餘部分。

public class Await操作
    {
        public static async Task Main ( )
        {
            Task<int> XZ = FF下載文檔主頁面 ( );
            Console . WriteLine ( $"{nameof ( Main )}:下載進行中。" );

            int Z加載的字節 = await FF下載中;
            Console . WriteLine ( $"{nameof ( Main )}:下載完畢 {Z加載的字節} 字節。" );
        }

    private static async Task<int> FF下載文檔主頁面 ( )
        {
            Console . WriteLine ( $"{nameof ( FF下載文檔主頁面 )}:關於開始下載。" );

            var HC = new HttpClient ( );
            byte [ ] XZ內容 = await client . GetByteArrayAsync ( "https://learn.microsoft.com/en-us/" );

            Console . WriteLine ( $"{nameof ( FF下載文檔主頁面 )}:下載完成。" );
            return XZ內容 . Length;
        }
}
// Output similar to:
// FF下載文檔主頁面:關於開始下載。
// Main: 下載進行中。
// FF下載文檔主頁面:下載完成。
// Main: 下載完畢 27700 字節。

必須提供 await 表達式的操作數,才能在任務完成時進行通知。通常,任務完成時(無論成功還是失敗)都會調用委託。

前一個示例使用異步 Main 方法。

備註:利用 async 和 await 的異步編程遵循基於任務的異步模式。

只能在通過 async 關鍵字修改的方法、lambda 表達式或匿名方法中使用 await 運算符。在異步方法中,不能在同步函數的本地主體、lock 語句塊內以及不安全的上下文中使用 await 運算符。

await 運算符的操作數通常是以下其中一個 .NET 類型:Task、Task < TResult >、ValueTask 或 ValueTask < TResult >。但是,任何可等待表達式都可以是 await 運算符的操作數。

如果表達式 t 的類型為 Task < TResult > 或 ValueTask < TResult >,則表達式 await t 的類型為 TResult。如果 t 的類型為 Task 或 ValueTask,則 await t 的類型為 void。在這兩種情況下,如果 t 引發異常,則 await t 將重新引發異常。

異步流和可釋放對象

可使用 await foreach 語句來使用異步數據流。

可使用 await using 語句來處理異步可釋放對象,即其類型可實現 IAsyncDisposable 接口的對象。

Main 方法中的 await 運算符

作為應用程序入口點的 Main 方法可以返回 Task 或 Task < int >,使其成為異步的,以便在其主體中使用 await 運算符。在較早的 C# 版本中,為了確保 Main 方法等待異步操作完成,可以檢索由相應的異步方法返回的 Task < TResult > 實例的 Task < TResult > . Result 屬性值。對於不生成值的異步操作,可以調用 Task . Wait 方法。

默認值表達式:default

default value 表達式生成類型的默認值。有兩種類型的 default value 表達式:default 運算符調用和 default 文本。

你還可以將 default 關鍵字用作 switch 語句中的默認用例標籤。

default 運算符

default 運算符的實參必須是類型或類型形參的名稱,如以下示例所示:

Console . WriteLine ( default ( int ) ); // 0
Console . WriteLine ( default ( object ) is null ); // True

void FF顯示默認值<T> ( )
    {
        var val = default ( T );
        Console . WriteLine ( $"{typeof(T)} 的默認值是:{( val == null ? "null" : val . ToString ( ) )}。" );
    }

FF顯示默認值<int?> ( ); // System . Nullable`1 [ System . Int32 ] 的默認值是:null
FF顯示默認值<System . Numerics . Complex> ( ); // System . Numerics . Complex 的默認值是:( 0 , 0 )
FF顯示默認值<System . Collections . Generic . List<int>> ( ); // System . Collections . Generic . List`1 [ System.Int32 ]

default 文本

當編譯器可以推斷表達式類型時,可以使用 default 文本生成類型的默認值。default 文本表達式生成與 default ( T ) 表達式(其中,T 是推斷的類型)相同的值。可以在以下任一情況下使用 default 文本:

  • 對變量進行賦值或初始化時。
  • 在聲明可選方法參數的默認值時。
  • 在方法調用中提供參數值時。
  • 在 return 語句中或作為表達式主體成員中的表達式時。

下面的示例演示 default 文本的用法:

T [ ] FF初始化數組<T> ( int 長度 , T initialValue = default )
    {
        if ( length < 0 )
        {
            throw new ArgumentOutOfRangeException ( nameof ( 長度 ) , "數組長度必須為非負數。" );
        }

    var SZ = new T [ 長度 ];
    for ( var i = 0; i < 長度; i++ )
        {
            array [ i ] = FF初始化數組;
        }
    return SZ;
    }

void FF顯示<T> ( T [ ] Zhis ) => Console . WriteLine ( $"【 {string . Join ( "," , Zhis )} 】" );

FF顯示 ( FF初始化數組<int> ( 3 ) ); // [ 0 , 0 , 0 ]
FF顯示 ( FF初始化數組<bool> ( 4 , default ) ); // [ False , False , False , False ]

System . Numerics . Complex FS充值 = default;
FF顯示 ( FF初始化數組 ( 3 , FS充值 ) ); // [ ( 0 , 0 ) , ( 0 , 0 ) , ( 0 , 0 ) ]

提示:使用 .NET 樣式規則 IDE0034 來指定代碼庫中使用 default 文本的首選項。

委託運算符 delegate

delegate 運算符創建一個可以轉換為委託類型的匿名方法。匿名方法可以轉換為 System . Action 和 System . Func < TResult > 等類型,用作許多方法的參數。

Func<int, int, int> He = delegate ( int a , int b ) { return a + b; };
Console . WriteLine ( He ( 3 , 4 ) ); // 7

備註:lambda 表達式提供了一種更簡潔和富有表現力的方式來創建匿名函數。使用 => 運算符構造 Lambda 表達式:

Func<int, int, int> He = ( a , b ) => a + b;
Console . WriteLine ( He ( 3 , 4 ) ); // 7

使用 delegate 運算符時,可以省略參數列表。如果這樣做,可以將創建的匿名方法轉換為具有任何參數列表的委託類型,如以下示例所示:

Action greet = delegate { Console . WriteLine ( "Hello!" ); };
greet ( ); // Hello!

Action<int , double> introduce = delegate { Console . WriteLine("This is world!"); };
introduce ( 42 , 2.7 ); // This is world!

這是 lambda 表達式不支持的匿名方法的唯一功能。在所有其他情況下,lambda 表達式是編寫內聯代碼的首選方法。可以使用棄元指定該方法未使用的兩個或更多個匿名方法輸入參數:

Func<int, int, int> constant = delegate ( int _ , int _ ) { return 42; };
Console . WriteLine ( constant ( 3 , 4 ) );  // 42

為實現向後兼容性,如果只有一個參數名為 ,則將 視為匿名方法中該參數的名稱。

可以在匿名方法的聲明中使用 static 修飾符:

Func<int, int, int> He = static delegate ( int a , int b ) { return a + b; };
Console . WriteLine ( He ( 10 , 4 ) ); // 14

靜態匿名方法無法從封閉範圍捕獲局部變量或實例狀態。

還可以使用 delegate 關鍵字聲明委託類型。

從 C# 11 開始,編譯器可以緩存從方法組轉換創建的委託對象。請考慮以下方法:
static void StaticFunction() { }
將方法組分配給委託時,編譯器將緩存委託:
Action a = StaticFunction;
在 C# 11 之前,需要使用 lambda 表達式來重複使用單個委託對象:
Action a = ( ) => StaticFunction();

is 運算符

is 運算符檢查表達式的結果是否與給定的類型相匹配。還可使用 is 運算符將表達式與模式相匹配,如下例所示:
static bool Ber十月的第一個週五 ( DateTime 日期 ) => 日期 is { Month: 10 , Day: <=7 , DayOfWeek: DayOfWeek . Friday };
在前面的示例中,is 運算符將表達式與關係模式和帶有嵌套常量的屬性模式相匹配。

is 運算符在以下應用場景中很有用:

檢查表達式的運行時類型,如下例所示:

int i = 34;
object iBoxed = i;
int? jNullable = 42;
if ( iBoxed is int a && jNullable is int b )
    {
        Console . WriteLine ( a + b ); // 76
    }

前面的示例演示聲明模式的用法。

檢查是否為 null,如下例所示:

if ( ShuRu is null )
    {
        return;
    }

將表達式與 null 匹配時,編譯器保證不會調用用户重載的 == 或 != 運算符。

可使用否定模式執行非 null 檢查,如下例所示:

if ( result is not null )
    {
        Console . WriteLine ( result . ToString ( ) );
    }

從 C# 11 開始,可以使用列表模式來匹配列表或數組的元素。以下代碼檢查數組中處於預期位置的整數值:

int [ ] Kong = [ ];
int [ ] Yi = [ 1 ];
int [ ] Ji = [ 1 , 3 , 5 ];
int [ ] Ou = [ 2 , 4 , 6 ];
int [ ] FBNQ = [ 1 , 1 , 2 , 3 , 5 ];

Console . WriteLine ( Ji is [ 1 , _ , 2 , .. ] ); // false
Console . WriteLine ( FBNQ is [ 1 , _ , 2 , .. ] ); // true
Console . WriteLine ( FBNQ is [ _ , 1 , 2 , 3 , .. ] ); // true
Console . WriteLine ( FBNQ is [ .. , 1 , 2 , 3 , _ ] ); // true
Console . WriteLine ( Ou is [ 2 , _ , 6 ] ); // true
Console . WriteLine ( Ou is [ 2 , .. , 6 ] ); // true
Console . WriteLine ( Ji is [ .. , 3 , 5 ] ); // true
Console . WriteLine ( Ou is [ .. , 3 , 5 ] ); // false
Console . WriteLine ( FBNQ is [ .. , 3 , 5 ] ); // true

nameof 表達式

nameof 表達式可生成變量、類型或成員的名稱作為字符串常量。nameof 表達式在編譯時進行求值,在運行時無效。當操作數是類型或命名空間時,生成的名稱不是完全限定的。以下示例顯示了 nameof 表達式的用法:

Console . WriteLine ( nameof ( System . Collections . Generic ) ); // Generic
Console . WriteLine ( nameof ( List<int> ) ); // List
Console . WriteLine ( nameof ( List<int> . Count ) ); // Count
Console . WriteLine ( nameof ( List<int> . Add ) ); // Add

List<int> Zhss = new List<int> ( ) { 1 , 2 , 3 };
Console . WriteLine ( nameof ( Zhss ) ); // Zhss
Console . WriteLine ( nameof ( Zhss . Count ) ); // Count
Console . WriteLine ( nameof ( Zhss . Add ) ); // Add

可以使用 nameof 表達式使參數檢查代碼更易於維護:

public string 姓名
    {
    get => XM;
    set => XM = value ?? throw new ArgumentNullException ( nameof ( value ) , $"{nameof ( 姓名 )} 不能是 null" );
    }

從 C# 11 開始,可以在方法或其參數的屬性 中使用具有方法參數的 nameof 表達式。以下代碼演示如何對方法、本地函數和 lambda 表達式的參數執行該操作:

[ ParameterString ( nameof ( msg ) ) ]
public static void Method ( string msg )
    {
        [ ParameterString ( nameof ( T ) ) ]
        void LocalFunction<T> ( T param ) { }

        var lambdaExpression = ( [ ParameterString ( nameof ( aNumber ) ) ] int aNumber ) => aNumber . ToString ( );
    }

在使用可為 null 的分析屬性或 CallerArgumentExpression 屬性時,帶有參數的 nameof 表達式很有用。

當操作數是逐字標識符時, “@” 該字符不是名稱的一部分,如以下示例所示:

var @new = 5;
Console.WriteLine ( nameof ( @new ) ); // new

創建類型的新實例 new

new 運算符創建類型的新實例。new 關鍵字還可用作成員聲明修飾符或泛型類型約束。

構造函數調用

要創建類型的新實例,通常使用 new 運算符調用該類型的某個構造函數:

var CD = new Dictionary < string , int > ( );
CD [ "1" ] = 10;
CD [ "2" ] = 20;
CD [ "3" ] = 30;

Console . WriteLine ( string . Join (";" , CD . Select ( DC => $"{DC . Key}:{DC . Value}" ) ) );
// 1:10;2:20;3:30

可以使用帶有 new 運算符的對象或集合初始值設定項實例化和初始化一個語句中的對象,如下例所示:

var CD = new Dictionary < string , int >
    {
        [ "1" ] = 10,
        [ "2" ] = 20,
        [ "3" ] = 30
    };

Console . WriteLine ( string . Join ( ";" , CD . Select ( DC => $"{DC . Key}:{DC . Value}" ) ) );
// 1:10;2:20;3:30

目標類型化 new

構造調用表達式由目標確定類型。也就是説,如果已知表達式的目標類型,則可以省略類型名稱,如下面的示例所示:

List<int> xs = new ( );
List<int> ys = new ( capacity: 10_000 );
List<int> zs = new ( ) { Capacity = 20_000 };

Dictionary<int , List<int>> lookup = new ( )
    {
        [ 1 ] = new ( ) { 1 , 2 , 3 },
        [ 2 ] = new ( ) { 5 , 8 , 3 },
        [ 5 ] = new ( ) { 1 , 0 , 4 }
    };

如前面的示例所示,在由目標確定類型的 new 表達式中始終使用括號。

如果 new 表達式的目標類型未知(例如使用 var 關鍵字時),必須指定類型名稱。

數組創建

還可以使用 new 運算符創建數組實例,如下例所示:

var Zhss = new int [ 3 ];
Zhss [ 0 ] = 10;
Zhss [ 1 ] = 20;
Zhss [ 2 ] = 30;
Console . WriteLine ( string . Join ( "," , Zhss ) );
// 10,20,30

使用數組初始化語法創建數組實例,並在一個語句中使用元素填充該實例。以下示例顯示可以執行該操作的各種方法:

var a = new int [ 3 ] { 10 , 20 , 30 };
var b = new int [ ] { 10 , 20 , 30 };
var c = new [ ] { 10 , 20 , 30 };
Console . WriteLine ( c . GetType ( ) ); // System . Int32 [ ]

匿名類型的實例化

要創建匿名類型的實例,請使用 new 運算符和對象初始值設定項語法:

var example = new { 見面 = "Hello" , 名 = "World" };
Console . WriteLine ( $"{example . 見面},{example . 名}!");
// Hello,World!

類型實例的析構

無需銷燬此前創建的類型實例。引用和值類型的實例將自動銷燬。包含值類型的上下文銷燬後,值類型的實例隨之銷燬。在引用類型的最後一次引用被刪除後,垃圾回收器會在某個非指定的時間銷燬其實例。

對於包含非託管資源的類型實例(例如,文件句柄),建議採用確定性的清理來確保儘快釋放其包含的資源。

運算符可重載性

用户定義的類型可以重載 new 運算符。

sizeof 運算符 - 確定給定類型的內存需求

sizeof 運算符返回給定類型的變量佔用的字節數。在安全代碼中,sizeof 運算符的參數必須是 非託管類型 的名稱,或是被 約束 為非託管類型的類型參數。非託管類型包括所有數值類型、枚舉類型和元組和結構類型,其中所有成員都是非託管類型。

下表中顯示的表達式在編譯時計算為相應的常量值,不需要不安全的上下文:

表達式 常量值
sizeof(sbyte) 1
sizeof(byte) 1
sizeof(short) 2
sizeof(ushort) 2
sizeof(int) 4
sizeof(uint) 4
sizeof(long) 8
sizeof(ulong) 8
sizeof(char) 2
sizeof(float) 4
sizeof(double) 8
sizeof(decimal) 16
sizeof(bool) 1

在不安全的代碼中,sizeof 的參數可以包括指針類型和託管類型,包括無約束的類型參數。示例包括 object 和 string。

以下示例演示了 sizeof 運算符的用法:

public struct D2
    {
        public D2 ( byte 標籤 , double x , double y ) => ( BQ , X , Y ) = ( 標籤 , x , y );

        public byte 標籤 { get; }
        public double X { get; }
        public double Y { get; }
    }

public class LeiSizeOf操作
    {
        public static void Main ( )
        {
            Console . WriteLine ( sizeof ( byte ) ); // 1
            Console . WriteLine ( sizeof ( double ) ); // 8

            FF顯示空間<D2> ( ); // D2 的佔用空間:24
            FF顯示空間<decimal> ( ); // System . Decimal 的佔用空間:16

            unsafe
                {
                    Console . WriteLine ( sizeof ( D2* ) ); // 8
                }
        }

    static unsafe void FF顯示空間<T> ( ) where T : unmanaged
        {
            Console . WriteLine ( $"{typeof ( T )} 的佔用空間:{sizeof ( T )}");
        }
    }

sizeof 運算符返回託管內存中公共語言運行時分配的字節數。對於 結構 類型,該值包括任何填充,如前面的示例所示。sizeof 運算符的結果可能與 Marshal . SizeOf 方法的結果不同,該方法返回非託管內存中類型的大小。

在不安全的代碼中,當參數為託管類型時,sizeof 運算符返回引用的大小,而不是為該類型的實例分配的字節數。

重要:sizeof 返回的值可能與 Marshal . SizeOf ( Object ) 的結果不同,後者返回非託管內存中類型的大小。

stackalloc 表達式

stackalloc 表達式在堆棧上分配內存塊。當該方法返回時,將自動丟棄在方法執行期間創建的已分配堆棧內存塊。不能顯式釋放使用 stackalloc 分配的內存。堆棧中分配的內存塊不受垃圾回收的影響,也不必通過 fixed 語句固定。

可以將 stackalloc 表達式的結果分配給以下任一類型的變量:

  • System . Span < T > 或 System . ReadOnlySpan < T >,如以下示例所示:

    int length = 3;
    Span<int> Zhss = stackalloc int [ length ];
    for ( var i = 0; i < length; i++ )
      {
          numbers [ i ] = i;
      }

    將堆棧中分配的內存塊分配給 ReadOnlySpan < T > 或 Span < T > 變量時,不必使用 unsafe 上下文。
    使用這些類型時,可以在 stackalloc 表達式中使用 條件 或 賦值 表達式,如以下示例所示:

    int CD = 1000;
    Span<byte> buffer = CD <= 1024 ? stackalloc byte [ CD ] : new byte [ CD ];

    只要允許使用 stackalloc 或 Span < T > 變量,就可以在其他表達式中使用 ReadOnlySpan < T > 表達式或集合表達式,如下例所示:

    Span<int> Zhss = stackalloc [ ] { 1 , 2 , 3 , 4 , 5 , 6 };
    var SY = Zhss . IndexOfAny ( stackalloc [ ] { 2 , 4 , 6 , 8 } );
    Console . WriteLine ( SY ); // 1
    
    Span<int> Zhss2 = [ 1 , 2 , 3 , 4 , 5 , 6 ];
    var SY2 = Zhss2 . IndexOfAny ( [ 2 , 4 , 6 , 8 ] );
    Console . WriteLine ( SY2 ); // 1

    備註:建議儘可能使用 Span < T > 或 ReadOnlySpan < T > 類型來處理堆棧中分配的內存。

  • 指針類型,如以下示例所示:

    unsafe
      {
          int CD = 3;
          int* Zhss = stackalloc int [ CD ];
          for ( var i = 0; i < CD; i++ )
              {
                  Zhss [ i ] = i;
              }
      }

    如前面的示例所示,在使用指針類型時必須使用 unsafe 上下文。
    對於指針類型,只能在局部變量聲明中使用 stackalloc 表達式來初始化變量。

堆棧上可用的內存量存在限制。如果在堆棧上分配過多的內存,會引發 StackOverflowException。為避免這種情況,請遵循以下規則:

  • 限制使用 stackalloc 分配的內存量。例如,如果預期的緩衝區大小低於特定限制,則在堆棧上分配內存;否則,使用所需長度的數組,如以下代碼所示:

    const int MaxStackLimit = 1024;
    Span<byte> buffer = inputLength <= MaxStackLimit ? stackalloc byte[MaxStackLimit] : new byte[inputLength];

    備註:由於堆棧上可用的內存量取決於執行代碼的環境,因此在定義實際限值時請持保守態度。

  • 避免在循環內使用 stackalloc。在循環外分配內存塊,然後在循環內重用它。

新分配的內存的內容未定義。應使用 stackalloc 初始值設定項初始化它,或者使用方法(如 Span < T > . Clear 使用前)。

重要:不初始化由 stackalloc 該運算符分配的 new 內存是一個重要區別。使用運算符分配的 new 內存將初始化為 0 位模式。

可以使用數組初始值設定項語法來定義新分配的內存的內容。下面的示例演示執行此操作的各種方法:

Span<int> Yi = stackalloc int [ 3 ] { 1 , 2 , 3 };
Span<int> Er = stackalloc int [ ] { 1 , 2 , 3 };
ReadOnlySpan<int> San = stackalloc [ ] { 1 , 2 , 3 };

// 使用隊列表達式:
Span<int> Si = [ 1 , 2 , 3 ];
ReadOnlySpan<int> Wu = [ 1 , 2 , 3 ];

在表達式 stackalloc T [ E ] 中,T 必須是非託管類型,並且 E 的計算結果必須為非負 int 值。使用集合表達式語法來初始化範圍時,如果編譯器沒有違反 ref 安全性,編譯器可能會對範圍使用堆棧分配的存儲。

安全性

使用 stackalloc 會自動啓用公共語言運行時(CLR)中的緩衝區溢出檢測功能。如果檢測到緩衝區溢出,則將盡快終止進程,以便將執行惡意代碼的可能性降到最低。

switch 表達式 - 使用 switch 關鍵字的模式匹配表達式

可以使用 switch 表達式,根據與輸入表達式匹配的模式,對候選表達式列表中的單個表達式進行求值。

下面的示例演示了一個 switch 表達式,該表達式將在線地圖中表示視覺方向的 enum 中的值轉換為相應的基本方位:

public static class SwitchExample
    {
        public enum 方向
            {
                上,
                下,
                右,
                左
            }

        public enum 目標
            {
                北,
                南,
                東,
                西
            }

        public static 目標 FF轉目標 ( 方向 F ) => F switch
            {
                方向 . 上 => 目標 . 北,
                方向 . 右 => 目標 . 東,
                方向 . 下 => 目標 . 南,
                方向 . 左 => 目標 . 西,
                _ => throw new ArgumentOutOfRangeException ( nameof ( F ) , $"不支持的方向值:{F}" ),
            };

        public static void Main ( )
            {
                var FX = 方向 . 右;
                Console . WriteLine ( $"地圖視圖方向:{FX}" );
                Console . WriteLine ( $"方向定則為:{FF轉目標 ( FX )}" );
                // 地圖視圖方向:右
                // 方向定則為:東
            }
    }

上述示例展示了 switch 表達式的基本元素:

  • 後跟 switch 關鍵字的表達式。在上述示例中,這是 方向 方法參數。
  • switch expression arm,用逗號分隔。每個 switch expression arm 都包含一個模式、一個可選的 case guard、=> 標記和一個表達式 。

在上述示例中,switch 表達式使用以下模式:

  • 常數模式:用於處理 方向 枚舉的定義值。
  • 棄元模式:用於處理沒有相應的 方向 枚舉成員的任何整數值(例如 方向 10)。這會使 switch 表達式詳盡。

switch 表達式的結果是第一個 switch expression arm 的表達式的值,該 switch expression arm 的模式與範圍表達式匹配,並且它的 case guard(如果存在)求值為 true。switch expression arm 按文本順序求值。

如果無法選擇較低的 switch expression arm,編譯器會發出錯誤,因為較高的 switch expression arm 匹配其所有值。

Case guard

模式或許表現力不夠,無法指定用於計算 arm 的表達式的條件。在這種情況下,可以使用 case guard。case guard 是一個附加條件,必須與匹配模式同時滿足。case guard 必須是布爾表達式。 可以在模式後面的 when 關鍵字之後指定一個 case guard,如以下示例所示:

public readonly struct D2
    {
        public D2 ( int x , int y ) => ( X , Y ) = ( x , y );

        public int X { get; }
        public int Y { get; }
    }

static Point FF變形 ( D2 d ) => d switch
    {
        { X: 0 , Y: 0 }                    => new D2 ( 0 , 0 ),
        { X: var x , Y: var y } when x < y => new D2 ( x + y , y ),
        { X: var x , Y: var y } when x > y => new D2 ( x - y , y ),
        { X: var x , Y: var y }            => new D2 ( 2 * x , 2 * y ),
    };

上述示例使用帶有嵌套 var 模式的屬性模式。

非詳盡的 switch 表達式

如果 switch 表達式的模式均未捕獲輸入值,則運行時將引發異常。在 .NET Core 3.0 及更高版本中,異常是 System . Runtime . CompilerServices . SwitchExpressionException。在 .NET Framework 中,異常是 InvalidOperationException。大多數情況下,如果 switch 表達式未處理所有可能的輸入值,則編譯器會生成警告。 如果未處理所有可能的輸入,列表模式不會生成警告。

提示:為了保證 switch 表達式處理所有可能的輸入值,請為 switch expression arm 提供棄元模式。

true 和 false 運算符 - 將對象視為布爾值

true 運算符返回 bool 值 true 來指示其操作數一定為 true,而 false 運算符則返回 bool 值 true 來指示其操作數一定為 false。

請注意,同時實現 true 和 false 運算符的類型必須遵循以下語義:

  • “此對象是否為 true?” 解析為運算符 true。如果對象為 true,運算符 true 將返回 true。應答為 “是,此對象為 true”。
  • “此對象是否為 false?” 解析為運算符 false。如果對象為 false,運算符 false 將返回 true。應答為 “是,此對象為 false”。

無法確保 true 和 false 運算符互補。也就是説,true 和 false 運算符可能同時針對同一個操作數返回 bool 值 false。 如果某類型定義這兩個運算符之一,則其還必須定義另一個運算符。
提示:如需支持三值邏輯(例如,在使用支持三值布爾類型的數據庫時),請使用 bool? 類型。C# 提供 & 和 | 運算符,它們通過 bool? 操作數支持三值邏輯。

布爾表達式

包含已定義 true 運算符的類型可以是 if、do、while 和 for 語句以及條件運算符 ?: 中控制條件表達式的結果的類型。

用户定義的條件邏輯運算符

如果包含已定義 true 和 false 運算符的類型以某種方式重載邏輯 OR 運算符 | 或邏輯 AND 運算符 &,可以對相應類型的操作數分別執行條件邏輯 OR 運算符 || 或條件邏輯 AND 運算符 && 運算。

示例

下面的示例演示了定義 true 和 false 運算符的類型。此外,該類型還重載了邏輯 AND 運算符 &,因此,也可以對相應類型的操作數計算運算符 && 重載了。

static void Main (  string [ ] args )
    {
        啓動信號 FS準備 = FF獲取燃料信號 ( ) && FF獲取導航狀態 ( );
        Console . WriteLine ( FS準備 ? "準備發射!" : "等一下……" );
    }

public struct 啓動信號
    {
        private int XHs;
        private 啓動信號 ( int 信號 )
            {
                XHs = 信號;
            }
        public static readonly 啓動信號 綠 = new 啓動信號 ( 0 );
        public static readonly 啓動信號 黃 = new 啓動信號 ( 1 );
        public static readonly 啓動信號 紅 = new 啓動信號 ( 2 );

        public static bool operator == ( 啓動信號 x , 啓動信號 y ) => x . XHs == y . XHs;
        public static bool operator != ( 啓動信號 x , 啓動信號 y ) => x . XHs != y . XHs;
        public static bool operator true ( 啓動信號 x ) => x == 綠 || x == 黃;
        public static bool operator false ( 啓動信號 x ) => x == 紅;

        public static 啓動信號 operator & ( 啓動信號 x , 啓動信號 y )
            {
                if ( x == 紅 || y == 紅 || ( x == 黃 && y == 黃 ) )
                    {
                        return 紅;
                    }

                if ( x == 黃 || y == 黃 )
                    {
                        return 黃;
                    }
                return 綠;
            }
        public override readonly bool Equals ( object? obj )
            {
                return obj is 啓動信號 other && this == other;
            }

        public override readonly int GetHashCode ( ) => XHs . GetHashCode ( );
    }

static 啓動信號 FF獲取燃料信號 ( )
    {
        Console . WriteLine ( "獲取燃料狀態……" );
        return 啓動信號 . 紅;
    }

static 啓動信號 FF獲取導航狀態 ( )
    {
        Console . WriteLine ( "獲取導航狀態……" );
        return 啓動信號 . 黃;
    }

請注意 && 運算符的短路行為。當 FF獲取燃料信號 方法返回 啓動信號 . 紅 時,&& 不會進行計算運算符的右側操作數。這是因為 啓動信號 . 紅 一定為 false。然後,邏輯 AND 運算符的結果不依賴右側操作數的值。示例的輸出如下所示:
獲取燃料狀態……
等一下……

with 表達式 - 非破壞性突變創建具有修改屬性的新對象

with 表達式使用修改的特定屬性和字段生成其操作數的副本。使用 對象初始化器 語法來指定要修改的成員以及它們的新值:

D2名 Dy = new ( "原點" , 0 , 0 );
Console . WriteLine ( $"{nameof ( Dy )}:{Dy}" ); // Dy:D2名 { 名 = 原點, X = 0, Y = 0 }

var D50 = Dy with
    {
        X = 5 ,
        名 = "50點"
    };
Console . WriteLine ( $"{nameof ( D50 )}:{D50}" ); // D04:D2名 { 名 = 50點, X = 5, Y = 0 }

var D04 = Dy with
    {
        Y = 4 ,
        名 = "04點"
    };
Console . WriteLine ( $"{nameof ( D04 )}:{D04}" ); // D04:D2名 { 名 = 04點, X = 0, Y = 4 }
Console . WriteLine ( $"{nameof ( Dy )}:{Dy}" ); // Dy:D2名 { 名 = 原點, X = 0, Y = 0 }

var PGs = new
    {
        項目 = "蘋果",
        單價 = 1.95m,
    };
Console . WriteLine ( $"原價:{PGs}" ); // 原價:{ 項目 = 蘋果, 單價 = 1.95 }
var PGs八折 = PGs with
    {
        單價 = 1.56m
    };
Console . WriteLine ( $"八折後:{PGs八折}" ); // 八折後:{ 項目 = 蘋果, 單價 = 1.56 }

with 表達式的左側操作數可以是一個記錄類型。with 表達式的左側操作數也可以為結構類型或匿名類型。

with 表達式的結果與表達式的操作數具有相同的運行時類型,如以下示例所示:

public record D2 ( int X , int Y );
public record D2名 ( string 名 , int X , int Y ) : D2 ( X , Y );

public static void Main ( )
    {
        D2 d1 = new D2名 ( "A" , 0 , 0 );
        D2 d2 = d1 with { X = 5 , Y = 3 };
        Console . WriteLine ( d2 is D2名 ); // True
        Console . WriteLine ( d2 ); // D2名 { X = 5, Y = 3, 名 = A }
    }

對於引用類型成員,在複製操作數時僅複製對成員實例的引用。複製和原始操作數都有權訪問同一引用類型實例。以下示例演示了該行為:

public record JL標籤數( int 數 , List<string> 標籤 )
    {
        public string FF打印標籤 ( ) => string . Join ( "," , 標籤 );
    }

public static void Main ( )
    {
        var Yuan = new JL標籤數 ( 1 , new List<string> { "A" , "B" } );

        var FuBen = Yuan with { 數 = 2 };
        Console . WriteLine ( $"{nameof ( FuBen )} 的標籤:{FuBen . FF打印標籤 ( )}" );
        // FuBen 的標籤:A,B

        Yuan . 標籤 . Add ( "C" );
        Console . WriteLine ( $"{nameof ( FuBen )} 的標籤:{FuBen . FF打印標籤 ( )}" );
        // FuBen 的標籤:A,B,C
    }

自定義複製語義

任何記錄類類型都具有複製構造函數。複製構造函數是一個包含記錄類型的單個參數的構造函數。它將參數的狀態複製到新的記錄實例。在計算 with 表達式時,將調用複製構造函數,以基於原始記錄實例化新記錄實例。之後,新實例會根據指定的修改進行更新。默認情況下,複製構造函數是隱式的,即編譯器生成的。如果需要自定義記錄複製語義,請顯式聲明具有所需行為的複製構造函數。以下示例使用顯式複製構造函數更新前面的示例。新的複製方式是在複製記錄時,複製列表項而不是列表引用。

public class UserDefinedCopyConstructorExample
{
    public record TaggedNumber(int Number, List<string> Tags)
    {
        protected TaggedNumber(TaggedNumber original)
        {
            Number = original.Number;
            Tags = new List<string>(original.Tags);
        }

        public string PrintTags() => string.Join(", ", Tags);
    }

    public static void Main()
    {
        var original = new TaggedNumber(1, new List<string> { "A", "B" });

        var copy = original with { Number = 2 };
        Console.WriteLine($"Tags of {nameof(copy)}: {copy.PrintTags()}");
        // output: Tags of copy: A, B

        original.Tags.Add("C");
        Console.WriteLine($"Tags of {nameof(copy)}: {copy.PrintTags()}");
        // output: Tags of copy: A, B
    }
}

不能自定義結構類型的複製語義。

析構表達式 - 從元組或其他用户定義的類型中提取字段的屬性

析構表達式從對象的實例中提取數據字段。每個離散數據元素將寫入不同的變量,如以下示例所示:

var YZ = (X: 1 , Y: 2);
var (x, y) = YZ;

Console . WriteLine ( x ); // 1
Console . WriteLine ( y ); // 2

前面的代碼片段創建了一個元組,它有兩個整數值 X 和 Y。第二個語句將解構元組,並將元組元素存儲在離散變量 x 和 y 中。

元組析構

所有元組類型都支持析構表達式。元組析構提取所有元組的元素。如果只想使用某些元組元素,請對未使用的元組成員使用棄元,如以下示例所示:

var YZ = (X: 0 , Y: 1 , B標籤: "原始的");
var ( x , _ , _ ) = YZ;

在前面的示例中,將丟棄 Y 和 B標籤 成員。可以在同一解構表達式中指定多個棄元。可以對元組的所有成員使用丟棄。以下示例是合法的,但沒有用:
var ( _ , _ , _ ) = YZ2;

記錄析構

具有主構造函數的記錄類型支持對位置參數的析構。編譯器合成 Deconstruct 方法,該方法提取從主構造函數中的位置參數合成的屬性。編譯器合成的 Deconstruction 方法不會提取在記錄類型中聲明為屬性的屬性。

以下代碼中顯示的 record 聲明兩個位置屬性 面積 和 地址,以及另一個屬性 經紀人提示:

public record JL房產 ( double 面積 , string 地址 )
    {
        public required string 經紀人提示 { get; set; }
    }

析構 House 對象時,所有的 地址 屬性(並且只有 地址 屬性)被析構,如以下示例所示:

var FC = new JL房產 ( 160 , "123 興學街" )
    {
        經紀人提示 = """
        這是一套非常適合初學者的住宅,其中有一個獨立的房間,非常適合作為家庭辦公室使用。
        """
    };

var ( MJ , DZ ) = FC;
Console . WriteLine ( MJ ); // 160
Console . WriteLine ( address ); // 123 興學街
Console . WriteLine ( FC . 經紀人提示 );

可以使用此行為來指定記錄類型的哪些屬性是編譯器合成 Deconstruct 方法的一部分。

聲明 Deconstruct 方法

可以向所聲明的任何類、結構或接口添加析構支持。可以在類型中聲明一個或多個 Deconstruct 方法,或聲明為該類型的擴展方法。析構表達式調用方法 void Deconstruct ( out var p1 , …… , out var pn )。Deconstruct 方法可以是實例方法或擴展方法。Deconstruct 方法中每個參數的類型必須與析構表達式中相應參數的類型匹配。析構表達式將每個參數的值分配給 Deconstruct 方法中相應 out 參數的值。如果多個 Deconstruct 方法與析構表達式匹配,編譯器會報告歧義錯誤。

以下代碼聲明具有兩個 Deconstruct 方法的 D3 結構:

public struct D3
    {
        public int X { get; set; }
        public int Y { get; set; }
        public int Z { get; set; }

        public void Deconstruct ( out int x , out int y , out int z )
            {
                x = X;
                y = Y;
                z = Z;
            }

        public void Deconstruct ( out int x , out int y )
            {
                x = X;
                y = Y;
            }
    }

第一種方法支持提取所有三個軸值的析構表達式:X、Y 和 Z。第二種方法僅支持析構平面值:X 和 Y。第一種方法的 參數數量 為 3;第二種方法的 參數數量 為 2。

上一部分介紹了 record 類型的編譯器合成 Deconstruct 方法,該方法帶有一個主構造函數。可以在記錄類型中聲明更多 Deconstruct 方法。這些方法可以添加其他屬性、刪除一些默認屬性或兩者。還可以聲明與編譯器合成簽名匹配的 Deconstruct。如果聲明這樣的 Deconstruct 方法,編譯器不會合成一個。

只要編譯器可以確定解構表達式的唯一 Deconstruct 方法,就可以允許多個 Deconstruct 方法。通常,同一類型的多個 Deconstruct 方法具有不同數量的參數。還可以創建多個因參數類型而異的 Deconstruct 方法。但是,在許多情況下,過多的 Deconstruct 方法可能會導致歧義錯誤和誤導性結果。

Add a new Comments

Some HTML is okay.