共用方式為


12 個表達式

12.1 一般

表達式是運算子和操作數序列。 這個子句會定義語法、操作數和運算子的評估順序,以及表達式的意義。

12.2 運算式分類

12.2.1 一般

運算式的結果會分類為下列其中一項:

  • 一個值。 每個值都有相關聯的類型。
  • 變數。 除非另有指定,否則變數會明確類型化,而且具有關聯類型,也就是變數的宣告類型。 隱含型別變數沒有相關聯的型別。
  • 空字面量。 具有此分類的表達式可以隱含地轉換成參考類型或可為 Null 的實值類型。
  • 匿名函式。 具有此分類的表達式可以隱含轉換成相容的委派類型或表達式樹狀結構類型。
  • 一個元組。 每個元組都有固定數目的元素,每個元素都有運算式和選用的元素名稱。
  • 屬性存取。 每個屬性存取都有相關聯的類型,也就是 屬性的類型。 此外,屬性存取可能會有相關聯的實例表達式。 叫用實例屬性存取的存取子時,評估實例表達式的結果會變成以 this 表示的實例(#.12.8.14)。
  • 使用索引器進行存取。 每個索引器存取都有相關聯的類型,也就是索引器的項目類型。 此外,索引器存取具有相關聯的實例表達式和相關聯的自變數清單。 當叫用索引器的存取子時,評估實例表達式的結果會轉變為由 this 代表的實例(§12.8.14),而評估引數清單的結果會轉變為調用的參數清單。
  • 無。 當表達式是傳回型別為 void的方法調用時,就會發生這種情況。 分類為 nothing 的運算式只有在 statement_expression 的上下文中才有效(§13.7)或作為 lambda_expression 的主體(§12.19)。

對於作為較大表達式的子表達式出現的表達式,在已指出的限制下,結果也可以分類為以下之一:

  • 命名空間。 具有此分類的表達式只能顯示為 member_access 的左側(.12.8.7)。 在任何其他情境中,分類為命名空間的表達式會導致編譯時錯誤。
  • 型別。 具有此分類的表達式只能出現在 member_access 的左邊(§12.8.7)。 在任何其他情境下,被分類為類型的表達式會導致編譯時錯誤。
  • 方法群組,是由成員查詢所產生的一組重載方法(§12.5)。 方法群組可能會有相關聯的實例表達式和相關聯的類型自變數清單。 當實例方法被調用時,評估實例表達式的結果會成為由 this 代表的實例(§12.8.14)。 允許在 invocation_expression§12.8.10)或 delegate_creation_expression§12.8.17.6)中使用方法群組,並且可以隱式轉換成相容的委派類型(§10.8)。 在任何其他情況下,分類為方法群組的表達式會產生編譯時期的錯誤。
  • 事件存取。 每個事件存取都有相關聯的類型,也就是事件的型別。 此外,事件存取可能會有相關聯的實例表達式。 事件存取可能會顯示為 +=-= 運算子的左操作數(\12.21.5)。 在任何其他情境中,分類為事件存取的表達式會導致編譯時錯誤。 當實例事件存取的存取子被叫用時,評估實例表達式的結果將成為由 this 表示的實例(§12.8.14)。
  • throw 表達式可以在表達式中用於多種情境來拋出例外狀況。 擲出運算式可能會透過隱含轉換轉為任何類型。

屬性存取或索引器存取一律會藉由執行 get 存取子或 set 存取子的調用,重新分類為值。 特定的存取子是由屬性或索引器存取的上下文所決定:如果存取是作為指派的目標,則會叫用 set 存取子來指派新的值(§12.21.2)。 否則,將調用 get 存取子來獲取當前的值(§12.2.2)。

實例存取子 是實例上的屬性存取、實例上的事件存取或索引器存取。

12.2.2 表達式的值

大部分牽涉到表達式的建構最終都需要表達式來表示 。 在這種情況下,如果實際表達式代表命名空間、類型、方法群組或什麼都不是,就會發生編譯時錯誤。 不過,如果表達式表示屬性存取、索引器存取或變數,則會隱含取代屬性、索引器或變數的值:

  • 變數的值只是變數目前儲存在變數所識別儲存位置的值。 在取得變數值之前,變數必須被賦予明確的初值(§9.4),否則會發生編譯時期錯誤。
  • 叫用 屬性的 get 存取子,即可取得屬性存取表達式的值。 如果屬性沒有 get 存取子,就會發生編譯時期錯誤。 否則,會執行函式成員調用 (\12.6.6),而調用的結果會變成屬性存取表達式的值。
  • 透過呼叫索引器的 get 存取子,可以取得索引器存取表達式的值。 如果索引器沒有 get 存取子,就會發生編譯時期錯誤。 否則,會使用與索引器存取表達式相關聯的自變數清單執行函式成員調用(•12.6.6),而調用的結果會成為索引器存取表達式的值。
  • 元組表達式的值是藉由將隱含的元組轉換(§10.2.13)套用至元組表達式的類型來取得。 取得一個沒有類型的元組運算式的值是一個錯誤。

12.3 靜態和動態系結

12.3.1 一般

綁定 是根據表達式的類型或值(參數、操作數、接收者)來決定操作所涉及的內容的過程。 例如,方法呼叫的系結是根據接收者和自變數的類型來決定。 運算子的系結是根據其操作數的類型來決定。

在 C# 中,作業的系結通常是在編譯階段根據其子表達式的編譯時間類型來決定。 同樣地,如果表達式包含錯誤,則會在編譯時期偵測並報告錯誤。 這種方法稱為靜態系結

不過,如果表達式是 動態表達式(亦即,具有類型 dynamic),這表示任何參與的系結都應該以其運行時間類型為基礎,而不是它在編譯時期擁有的類型。 因此,這類作業的系結會延後,直到程式執行過程中需要執行該作業時。 這稱為 動態系結

當作業以動態方式系結時,在編譯時幾乎不會或完全不會進行檢查。 相反地,如果執行期間系結失敗,錯誤會在執行期間以例外狀況回報。

C# 中的下列作業受限於系結:

  • 成員存取:e.M
  • 方法調用:e.M(e₁,...,eᵥ)
  • 委派調用:e(e₁,...,eᵥ)
  • 元素存取:e[e₁,...,eᵥ]
  • 物件建立:新增的 C(e₁,...,eᵥ)
  • 多載一元運算符:+-!(僅限邏輯否定)、~++--truefalse
  • 多載二進位運算符:+-*/%&&&|||??^<<>>==!=><>=<=
  • 指派運算符:== ref+=-=*=/=%=&=|=^=<<=>>=
  • 隱式和顯式轉換

當未涉及任何動態表達式時,C# 預設為靜態系結,這表示選取程式中會使用子表達式的編譯時間類型。 不過,當上述作業中的其中一個子表達式是動態表達式時,作業會改為動態系結。

如果方法調用是動態系結,而且任何參數,包括接收者,都是輸入參數,則為編譯時間錯誤。

12.3.2 系結時間

靜態系結會在編譯階段進行,而動態系結則會在運行時間進行。 在下列子集中,系結時間 一詞 是指編譯時間或運行時間,視系結發生的時間而定。

範例:下列說明靜態和動態系結和系結時間的概念:

object o = 5;
dynamic d = 5;
Console.WriteLine(5); // static binding to Console.WriteLine(int)
Console.WriteLine(o); // static binding to Console.WriteLine(object)
Console.WriteLine(d); // dynamic binding to Console.WriteLine(int)

前兩個呼叫會以靜態方式系結:會根據自變數的編譯時間類型來挑選 Console.WriteLine 的多載。 因此,綁定時間是 編譯時間

第三個呼叫會動態系結:會根據自變數的運行時間類型來挑選 Console.WriteLine 的多載。 這是因為自變數是動態表示式, 其編譯時間類型是動態的。 因此,第三次調用的綁定時間是 執行時間

結束範例

12.3.3 動態系結

這個子句供參考。

動態系結可讓 C# 程式與動態物件互動,也就是不符合 C# 類型系統的一般規則的物件。 動態物件可能是其他具有不同類型系統之程式設計語言的物件,或者可能是以程式設計方式設定的物件,以實作不同作業的系結語意。

動態物件實作其本身語意的機制是實作定義的。 指定的介面 – 再次實作定義 – 是由動態物件實作,以向 C# 運行時間發出訊號,指出其具有特殊語意。 因此,每當動態物件上的作業進行動態系結時,會以其自身的系結語意接管,而不是此規格中所指定的 C# 系結語意。

雖然動態系結的目的是允許與動態物件互通,但 C# 允許在所有物件上動態系結,無論它們是否為動態。 這可讓您更順暢地整合動態對象,因為其作業的結果可能不是動態物件,但在編譯階段仍屬於程式設計人員未知的類型。 此外,即使沒有涉及任何對像是動態物件,動態系結也能協助消除容易出錯的反映型程序代碼。

12.3.4 子表達式的類型

當作業以靜態方式系結時,子表達式的類型(例如接收者和自變數、索引或操作數)一律會被視為該表達式的編譯時間類型。

動態系結作業時,子表達式的類型會根據子表達式的編譯時間類型,以不同的方式決定:

  • 編譯時期類型為動態的子表達式被認為具有該表達式在執行時的實際值類型。
  • 被視為具有編譯時間類型為類型參數的子表達式,在運行時被系結至類型參數所代表的類型。
  • 否則,子表達式會被視為具有其編譯時間類型。

12.4 運算符

12.4.1 一般

表達式是從操作數和運算元建構。 表達式的運算子會指出要套用至操作數的運算。

範例:運算符範例包括 +-*/new。 操作數的範例包括常值、字段、局部變數和表達式。 結束範例

運算子有三種:

  • 一元運算子。 一元運算子接受一個操作數,並使用前置表示法(例如 –x)或後置表示法(例如 x++)。
  • 二元運算子。 二元運算符使用兩個操作數,且全都使用中置表示法(例如 x + y)。
  • 三元運算子。 只有一個三元運算子?:,它需要三個操作數,並使用中序表示法(c ? x : y)。

運算子在運算式中的評估順序取決於運算子的 優先順序結合性§12.4.2)。

表達式中的操作數會從左至右進行評估。

範例:在 F(i) + G(i++) * H(i)中,會使用 i的舊值呼叫 方法 F,然後使用 i的舊值呼叫方法 G,最後會以 i 的新值呼叫方法 H。 這與運算符優先順序無關。 結束範例

某些運算子可以 多載。 運算子多載 (•12.4.3) 允許針對一或兩個操作數屬於使用者定義類別或結構類型的作業指定使用者定義的運算子實作。

12.4.2 運算元優先順序和關聯性

當表達式包含多個運算符時,運算子 優先順序 會控制個別運算符的評估順序。

Note:例如,表達式 x + y * z 會評估為 x + (y * z),因為 * 運算符的優先順序高於二進位 + 運算符。 尾注

運算符的優先順序是由其相關聯文法產生式的定義所建立。

附註:例如,additive_expression 是由 +- 運算符分隔的 multiplicative_expression序列所組成,因此讓 +- 運算符的優先順序低於 */% 運算符。 尾注

附註:下表摘要說明優先等級從最高到最低的所有運算子:

子條款 類別 運算子
§12.8 主要 x.y x?.y f(x) a[x] a?[x] x++ x-- x! new typeof default checked unchecked delegate stackalloc
§12.9 一元 + - !x ~ ++x --x (T)x await x
§12.10 乘法 * / %
§12.10 添加劑 + -
§12.11 轉變 << >>
§12.12 關係型和類型測試 < > <= >= is as
§12.12 平等 == !=
§12.13 邏輯與運算 &
§12.13 邏輯 XOR ^
§12.13 邏輯或 \|
§12.14 條件式 AND &&
§12.14 條件式 OR \|\|
§12.15§12.16 空合併運算子和拋出表達式 ?? throw x
§12.18 有條件的 ?:
§12.21§12.19 指派和 Lambda 運算式 = = ref *= /= %= += -= <<= >>= &= ^= \|= =>

尾注

當操作數發生在具有相同優先順序的兩個運算符之間時,運算子的 關聯性 會控制執行作業的順序:

  • 除了指派運算符和 Null 聯合運算子之外,所有二元運算子都會 左關聯,這表示作業是從左到右執行。

    範例x + y + z 評估為 (x + y) + z結束範例

  • 指派運算符、null 聯合運算子和條件運算符 (?:) 是 右關聯,這表示作業是從右至左執行。

    範例x = y = z 評估為 x = (y = z)結束範例

優先順序和關聯性可以使用括號來控制。

範例x + y * z 先將 y 乘以 z,然後將結果新增至 x,但 (x + y) * z 先新增 xy,再將結果乘以 z結束範例

12.4.3 運算元多載

所有一元和二元運算子都有預先定義的實作。 此外,您可以藉由在類別和結構中包含運算符宣告(§15.10)來引入使用者定義的實作。 使用者定義的運算子實作一律優先於預先定義的運算子實作:只有當不存在任何適用的使用者定義運算子實作時,才會考慮預先定義的運算子實作,如 §12.4.4§12.4.5中所述。

可多載的一元運算符 如下:

+ - ! (僅限邏輯否定) ~ ++ -- true false

附注:雖然 truefalse 不會被明確用於表達式中(因此不會包含在 §12.4.2的優先順序數據表中),但它們在數個表達式上下文中被調用,因此被視為運算符:布爾表達式(§12.24)以及涉及條件運算子的表達式(§12.18)和條件邏輯運算符(§12.14)。 尾注

附註:空值容忍運算子(後置 !§12.8.9)是無法多載的運算子。 尾注

可多載的二進位運算子 如下:

+  -  *  /  %  &  |  ^  <<  >>  ==  !=  >  <  <=  >=

只有上述運算子可以多載。 特別是,無法多載成員存取、方法調用或 =&&||???:=>checkeduncheckednewtypeofdefaultasis 運算符。

當二元運算子多載時,對應的複合指派運算子(如果有的話)也會被隱含多載。

範例:運算子 * 的多載也是運算子 *=的多載。 這會在 §12.21中進一步說明。 結束範例

指派運算符本身 (=) 無法多載。 指派總是將值簡單儲存到變數中(§12.21.2)。

轉換作業,例如 (T)x,會藉由提供使用者定義的轉換來重載(§10.5)。

注意:使用者定義轉換不會影響 isas 運算符的行為。 尾注

元素存取,例如 a[x],不會被視為可多載運算符。 相對地,索引器支援使用者定義的索引編製(§15.9)。

在表達式中,運算符會使用運算符表示法來參考,而宣告中會使用功能表示法來參考運算符。 下表顯示一元運算符和二元運算子的運算符與功能表示法之間的關聯性。 在第一個項目中,«op» 表示任何可多載的一元前綴運算符。 在第二個項目中,«op» 代表一元後置 ++-- 運算符。 在第三個條目中,«op» 表示任何可重載的二元運算符。

附註:如需多載 ++-- 運算子的範例,請參閱 §15.10.2尾注

運算符表示法 功能表示法
«op» x operator «op»(x)
x «op» operator «op»(x)
x «op» y operator «op»(x, y)

使用者定義的運算符宣告一律至少需要其中一個參數是包含運算符宣告的類別或結構類型。

注意:因此,使用者定義運算符不可能擁有與預先定義運算符相同的簽章。 尾注

使用者定義運算符宣告無法修改運算子的語法、優先順序或關聯性。

範例/ 運算符一律為二進位運算符,一律具有 指定之優先順序層級 。12.4.2,且一律為左關聯。 結束範例

附註:雖然使用者定義運算符可以執行任何計算,但產生結果的實作並非直覺預期的結果,強烈建議您不要使用。 例如,運算子 == 的實現應該比較這兩個操作數是否相等,並傳回適當的 bool 結果。 尾注

-12.912.21 中的個別運算符描述 , 指定運算符的預先定義實作,以及套用至每個運算符的任何其他規則。 這些描述會使用 一元運算子多載解析二元運算子多載解析數值升階這些詞彙,以及在下列子章節中找到的提升運算子定義。

12.4.4 一元運算子多載解析

操作的形式為 «op» xx «op»,其中 «op» 是可多載的單元運算符,且 x 是類型為 X的表達式,處理方式如下:

  • X 所提供的適用於操作 operator «op»(x) 的候選使用者定義運算符集合,是藉由使用 第12.4.6節的規則來決定的。
  • 如果候選使用者定義運算符集合不是空的,則這會成為作業的候選運算元集合。 否則,預先定義的二進位 operator «op» 實作,包括其提升形式,會成為操作的候選運算符集合。 給定運算子的預定義實作是在運算子的描述中說明的。 列舉或委派類型所提供的預先定義運算子,只有在任一操作數的系結時間類型或基礎類型(若為可為 Null 的型別)是列舉或委派類型時,才會包含在此運算子集中。
  • \12.6.4 的多載解析規則會套用至一組候選運算符,以選取自變數清單 (x)的最佳運算符,而這個運算符會成為多載解析程序的結果。 如果多載解析無法選取單一最佳運算符,則會發生系結時間錯誤。

12.4.5 二元運算子多載解析

表單 x «op» y的作業,其中 «op» 是可多載的二進位運算符,x 是類型 X的表達式,而 y 是類型為 Y的表達式,會依下列方式處理:

  • XY 提供的用於操作 operator «op»(x, y) 的候選使用者定義運算子的集合已被確定。 此集合包含 X 所提供的候選運算符聯集,以及由 Y提供的候選運算符聯集,每個運算符都是使用 -12.4.6的規則所決定。 針對合併集,候選者將合併如下:
    • 如果 XY 可轉換身分識別,或 XY 衍生自一般基底類型,則共用候選運算符只會發生在合併集一次。
    • 如果 XY之間有身份轉換,Y 所提供的運算子 «op»Y 的傳回類型與 X 所提供的 «op»X 相同,且 «op»Y 的操作數類型會轉換成對應的操作數類型 «op»X,那麼集合中只有 «op»X 發生。
  • 如果候選使用者定義運算符集合不是空的,則這會成為作業的候選運算元集合。 否則,預先定義的 operator «op» 二元實作及其提升形式,將成為該操作的候選運算子集合。 預先定義的運算子實作會在運算子的描述中說明。 對於預先定義的列舉和委派運算元,唯一考慮的運算符是列舉或委派類型所提供的運算符,該類型是其中一個操作數的系結時間類型。
  • \12.6.4 的多載解析規則會套用至一組候選運算符,以選取自變數清單 (x, y)的最佳運算符,而這個運算符會成為多載解析程序的結果。 如果多載解析無法選取單一最佳運算符,則會發生系結時間錯誤。

12.4.6 候選用戶定義運算符

假設類型 T 和運算 operator «op»(A),其中 «op» 是可多載的運算符,而 A 是參數清單,則由 T 為運算符 «op»(A) 所提供的候選使用者定義運算符集合會依下列方式決定:

  • 判斷類型 T₀。 如果 T 是可為 Null 的實值型別,T₀ 是其基礎類型;否則,T₀ 等於 T
  • 對於 T₀ 中的所有 operator «op» 宣告,以及這類運算子的所有提升形式,如果至少有一個運算子適用於自變數清單§12.6.4.2A,則候選運算子集合由 T₀中的所有這些適用運算子組成。
  • 否則,如果 T₀object,則一組候選運算符是空的。
  • 否則,T₀ 所提供的一組候選運算符是 T₀的直接基類所提供的候選運算符集合,如果 T₀ 是類型參數,則為 T₀ 的有效基類。

12.4.7 數值升級

12.4.7.1 一般

本子條款僅供參考。

12.4.7 及其子項是綜合效應的總結:

數值升階是由自動執行預先定義一元和二進位數值運算元操作數的特定隱含轉換所組成。 數值升階不是不同的機制,而是將多載解析套用至預先定義的運算符的效果。 數值升階特別不會影響使用者定義運算子的評估,不過可以實作使用者定義的運算符來呈現類似的效果。

作為數值提升的範例,請考慮二進位 * 運算符的預先定義實作:

int operator *(int x, int y);
uint operator *(uint x, uint y);
long operator *(long x, long y);
ulong operator *(ulong x, ulong y);
float operator *(float x, float y);
double operator *(double x, double y);
decimal operator *(decimal x, decimal y);

當多載解析規則 (\12.6.4)套用至這組運算符時,效果是選取操作數類型中隱含轉換的第一個運算符。

範例:針對作業 b * s,其中 bbyte,而 sshort,多載解析會選取 operator *(int, int) 為最佳運算符。 因此,效果是 bs 會轉換成 int,而結果的類型 int。 同樣地,針對作業 i * d,而其中 iintddoubleoverload 解析過程中選擇 operator *(double, double) 作為最佳操作員。 結束範例

說明文字結尾。

12.4.7.2 一元數值升階

這個子章節具有資訊性。

針對預先定義的一元運算子 +-~ 的操作數,會發生一元數值升階。 一元數值升階只包含將類型 sbytebyteshortushortchar 轉換成類型 int的操作數。 此外,針對一元 – 運算符,一元數值升階會將類型 uint 的操作數轉換為類型 long

資訊文字結尾。

12.4.7.3 二進位數值升階

這個子條款供參考。

預先定義的 +-*/%&|^==!=><>=<= 二元運算符的操作數會發生二進位數值提升。 二進位數值升階會隱含地將這兩個操作數轉換成共同類型,如果是非關係運算符,這個類型也會成為運算結果的類型。 二進位數值升級包括依次套用以下規則,其順序如下:

  • 如果任一操作數的類型為 decimal,則另一個操作數會轉換成類型 decimal,或者如果另一個操作數的類型為 floatdouble,則會發生係結時間錯誤。
  • 否則,如果任一操作數的類型為 double,則另一個操作數會轉換成類型 double
  • 否則,如果任一操作數的類型為 float,則另一個操作數會轉換成類型 float
  • 否則,如果任一操作數的類型為 ulong,則另一個操作數會轉換成類型 ulong,或者如果另一個操作數是 type sbyteshortintlong,則會發生系結時間錯誤。
  • 否則,如果任一操作數的類型為 long,則另一個操作數會轉換成類型 long
  • 否則,如果任一操作數的類型為 uint,而另一個操作數的類型為 sbyteshortint,則兩個操作數都會轉換成類型 long
  • 否則,如果任一操作數的類型為 uint,則另一個操作數會轉換成類型 uint
  • 否則,這兩個操作數都會轉換成類型 int

Note:第一個規則不允許將 decimal 類型與 doublefloat 類型混合的任何作業。 規則遵循的事實是,decimal 類型與 doublefloat 類型之間沒有隱含轉換。 尾注

附註:另請注意,當另一個操作數是帶正負號整數型別時,操作數不可能是類型 ulong。 原因是沒有任何整數型別可以同時代表 ulong 的完整範圍和有符號整數型別的範圍。 尾注

在上述兩種情況下,轉換表達式可以用來明確地將一個操作數轉換成與另一個操作數相容的類型。

範例:在下列程式代碼中

decimal AddPercent(decimal x, double percent) =>
    x * (1.0 + percent / 100.0);

因為 decimal 不能乘以 double,所以會發生系結時間錯誤。 錯誤已被解決,方法是將第二個操作數明確轉換成 decimal,如下所示:

decimal AddPercent(decimal x, double percent) =>
    x * (decimal)(1.0 + percent / 100.0);

結束範例

資訊性文字結尾。

12.4.8 增益運算符

增益運算符 允許在不可為 Null 的實值型別上運作的預先定義和使用者定義運算子,以搭配這些類型的可為 Null 形式使用。 增益運算符是從符合特定需求的預先定義和使用者定義運算符所建構,如下所述:

  • 對於一元運算符 +++---!(邏輯否定)和 ~,如果運算元和結果型別都是不可為 Null 的實值型別,則存在運算符的提升形式。 提升形式是藉由將單一 ? 修飾符新增至運算元和結果類型來構建。 如果操作數是 null,則增益運算符會產生 null 值。 否則,增益運算符會解除包裝操作數、套用基礎運算符,並包裝結果。
  • 對於二元運算符 +-*/%&|^<<>>,如果操作數和結果型別都是不可為 Null 的實值型別,則運算符的增益形式存在。 將單一 ? 修飾詞新增至每個運算元和結果類型,即可建構提升形式。 如果一或兩個操作數為 null,則提升運算符會產生 null 值(例外情況是 bool? 類型的 &| 運算符,如 第12.13.5章所述)。 否則,提升運算符會解包操作數,應用底層運算符,並封裝結果。
  • 對於等號運算子 ==!=,如果操作數型別都是不可為 Null 的實值型別,而且如果結果型別是 bool,則存在運算子的增益形式。 提升形式是通過在每個運算元類型中添加一個 ? 修飾詞來構建的。 提昇運算子會將兩個 null 值視作相等,且 null 值與任何非null 值視作不相等。 如果兩個操作數都是非null,則提升運算符會解開操作數,並套用基礎運算符來產生 bool 結果。
  • 對於關係運算子 <><=>=,如果操作數的類型都是不可為 Null 的實值型別,並且結果型別是 bool,則該運算子具有提升形式。 提升形式是藉由將單一 ? 修飾符加入至每個操作數類型來構建。 若其中一個或兩個運算元是 null,則提升運算子會產生值 false。 否則,增益運算符會解除包裝操作數,並套用基礎運算符來產生 bool 結果。

12.5 成員查找

12.5.1 一般

成員查找指的是在一個類型的上下文中確定名稱定義的過程。 成員查詢可以作為評估表達式中的simple_name§12.8.4)或member_access§12.8.7)的一部分。 如果 simple_namemember_access 作為 invocation_expressionprimary_expression§12.8.10.2),則成員稱為 調用

如果成員是方法或事件,或者它是委派類型的常數、欄位或屬性(§20),或類型 dynamic§8.2.4),則表示成員 可被調用。

成員查閱不僅會考慮成員的名稱,也會考慮成員擁有的類型參數數目,以及成員是否可存取。 為了進行成員查閱,泛型方法和巢狀泛型型別具有各自宣告中指出的類型參數數目,而所有其他成員都有零個類型參數。

在類型 T 中,名稱 N 具有 K 型別引數的成員查找按以下方式處理:

  • 首先,會決定一組名為 N 的可存取成員:
    • 如果 T 是類型參數,則此集合是包含在 T中,指定為主要約束或次要約束(§15.2.5)之每個類型中名為 N 的可存取成員集合的聯集,以及 object中名為 N 的可存取成員集合。
    • 否則,此集合由 T中名為 N 的所有可存取成員(包括繼承的成員和 object中名為 N 的可存取成員)所組成(§7.5)。 如果 T 是建構型別,則會藉由替代型別參數來取得成員集合,如 §15.3.3中所述。 包含 override 修飾詞的成員會從集合中排除。
  • 接下來,如果 K 為零,則會移除宣告包含類型參數的所有巢狀類型。 如果 K 不是零,則會移除具有不同類型參數數目的所有成員。 當 K 為零時,不會移除具有型別參數的方法,因為型別推斷過程(§12.6.3)可能會推斷型別參數。
  • 接下來,如果叫用成員,則會從集合中移除所有不可叫用的成員。
  • 接下來,會從集合中移除其他成員隱藏的成員。 對於集合中每個成員 S.M,其中 S 是宣告成員 M 的類型,則會套用下列規則:
    • 如果 M 是常數、欄位、屬性、事件或列舉成員,則會從以 S 作為基底類型所宣告的集合中移除所有成員。
    • 如果 M 是類型宣告,則會從集合中移除在 S 基底類型中宣告的所有非型別成員,並且會移除在 S 基底類型中宣告且型別參數數目與 M 相同的所有型別宣告。
    • 如果 M 是一個方法,那麼在 S 的基類型中宣告的所有非方法成員都會從集合中刪除。
  • 接下來,類別成員所隱藏的介面成員會從集合中移除。 如果 T 是類型參數,而且 T 除了 object 之外,還有有效的基類,以及非空白的有效介面集(\15.2.5),這個步驟才有效果。 對於集合中每個成員 S.M,其中 S 是宣告成員 M 的類型,如果 Sobject以外的類別宣告,則會套用下列規則:
    • 如果 M 是常數、字段、屬性、事件、列舉成員或類型宣告,則介面宣告中的所有成員都會從集合中移除。
    • 如果 M 是方法,則會從集合中移除介面宣告中宣告的所有非方法成員,而且所有簽章與介面宣告中宣告 M 相同的方法都會從集合中移除。
  • 最後,移除隱藏成員之後,查找的結果是:
    • 如果集合是由不是方法的單一成員所組成,則此成員是查閱的結果。
    • 否則,如果集合只包含方法,則這個方法群組就是查閱的結果。
    • 否則,查找會出現模糊不清,並且會發生綁定時間錯誤。

在類型參數和介面之外的類型中查詢成員,以及在只允許單一繼承的介面中查詢成員(繼承鏈中的每個介面最多只有一個直接基底介面),查詢規則的效果僅為衍生成員會隱藏具有相同名稱或簽名的基底成員。 這類單一繼承查閱從不會有模糊不清的情況。 多重繼承介面中的成員查閱可能引發的模稜兩可情況,在 §18.4.6中有描述。

附注:此階段僅涉及一種模棱兩可的情況。 如果成員查閱結果在方法群組中,則方法群組的進一步使用可能會因為模棱兩可而失敗,例如,如 !12. 6.6.2中所述。 尾注

12.5.2 基底類型

為了方便成員查閱,T 類型會被視為具有下列基底類型:

  • 如果 Tobjectdynamic,則 T 沒有基底類型。
  • 如果 Tenum_type,則 T 的基底類型是類別類型 System.EnumSystem.ValueTypeobject
  • 如果 Tstruct_typeT 的基底類型是類別類型 System.ValueTypeobject

    附註nullable_value_typestruct_type§8.3.1)。 尾注

  • 如果 Tclass_typeT 的基底類型是 T的基類,包括類別類型 object
  • 如果 Tinterface_type,則 T 的基底類型是 T 的基底介面,而類別類型 object
  • 如果 Tarray_type,則 T 的基底類型是類別類型 System.Arrayobject
  • 如果 Tdelegate_type,則 T 的基底類型是類別類型 System.Delegateobject

12.6 函式成員

12.6.1 一般

函式成員是包含可執行語句的成員。 函式成員一律是型別的成員,不能是命名空間的成員。 C# 會定義下列函式成員類別:

  • 方法
  • 性能
  • 事件
  • 索引員
  • 使用者定義運算子
  • 實例建構函式
  • 靜態建構函式
  • 終結器

除了終結器和靜態建構函式(無法被明確呼叫)之外,函式中包含的語句會透過函式呼叫來執行。 撰寫函式成員調用的實際語法取決於特定函式成員類別。

函式成員的呼叫的引數清單(§12.6.2)提供函式成員參數的實際值或變數參考。

泛型方法的調用可能會採用型別推斷來判斷要傳遞至方法的類型自變數集。 此過程在 §12.6.3中描述。

方法、索引器、運算符和實例建構函式的調用會採用多載解析,以判斷要叫用的候選函式成員集。 此過程在 §12.6.4中描述。

在繫結時間識別出特定的函式成員後,可能透過多載解析,叫用該函式成員的實際運行過程描述於 §12.6.6

附注:下表摘要說明在涉及可明確叫用之函式成員六個類別的建構中發生的處理。 在數據表中,exyvalue 表示分類為變數或值的表達式,T 表示分類為類型的運算式,F 是方法的簡單名稱,P 是屬性的簡單名稱。

構建 描述
方法調用 F(x, y) 重載解析用來在包含的類別或結構中選出最佳方法F。 方法會使用自變數清單 (x, y)叫用。 如果方法不是 static,則實例表達式為 this
T.F(x, y) 多載解析會套用至 類別或結構中選取最佳方法 FT。 如果方法未為 static,就會發生系結時間錯誤。 方法會使用自變數清單 (x, y)叫用。
e.F(x, y) 多載解析用於選擇由 e類型所指定之類別、結構或介面中的最佳方法 F。 如果方法 static,就會發生系結時間錯誤。 使用實例表達式 e 和自變數清單 (x, y)叫用 方法。
屬性存取 P 屬性 P 的 get 存取子在其包含的類別或結構中被呼叫。 如果 P 為僅限寫入,就會發生編譯時期錯誤。 如果 P 不是 static,則實例表示式會 this
P = value 包含類別或結構中屬性 P 的 set 存取子會使用自變數清單 (value)叫用。 如果 P 是唯讀的,就會發生編譯時錯誤。 如果 P 不是 static,則實例表示式會 this
T.P 在類別或結構 T 中,屬性 P 的 get 存取子已被呼叫。 如果 P 不是 static,或 P 為僅限寫入,就會發生編譯時期錯誤。
T.P = value 類別或結構 T 中屬性 P 的 set 存取子會叫用自變數清單 (value)。 如果 P 不是 static,或者 P 是唯讀,就會發生編譯時錯誤。
e.P 在由 E 型別提供的類別、結構或介面中,由實例表達式 e叫用屬性 P 的 get 存取子。 如果 Pstatic,或 P 為僅限寫入,則會發生系結時間錯誤。
e.P = value 實例表達式 Pe 和自變數清單 (value),叫用類別、結構或介面中 E 型別所指定之屬性的 set 存取子。 如果 Pstatic,或 P 是唯讀的,則會發生綁定時間錯誤。
事件存取 E += value 在包含類別或結構中,事件 E 的 add 存取子被呼叫。 如果 E 不是 static,則實例表示式會 this
E -= value 叫用包含類別或結構中事件 E 的 remove 存取子。 如果 E 不是 static,則實例表示式會 this
T.E += value 叫用 類別或結構中事件 E 的 add 存取子 T。 如果 E 不是 static,就會發生系結時間錯誤。
T.E -= value 調用類別或結構 T 中事件 E 的 remove 存取子。 如果 E 不是 static,就會發生系結時間錯誤。
e.E += value 事件 E 的 add 存取子由 E 型別的類別、結構或介面使用實例表達式 e進行呼叫。 如果 Estatic,就會發生系結時間錯誤。
e.E -= value 類別、結構或介面中,由型別 E 指定的事件 E 的 remove 存取子被實例運算式 e叫用。 如果 Estatic,就會發生系結時間錯誤。
索引器存取 e[x, y] 重載解析將應用於類別、結構或介面中,以選擇由 e類型提供的最佳索引器。 索引器的 get 存取子會使用實例表示式 e 和自變數清單 (x, y)叫用。 如果索引器是唯寫的,就會發生系結時間錯誤。
e[x, y] = value 多載決策會應用於類別、結構或介面中,以選出由 e類型指定的最佳索引器。 索引器的 set 存取子被調用實例表達式 e 和參數列表 (x, y, value)。 如果索引器是只讀的,就會發生系結時間錯誤。
運算符調用 -x 多載決策適用於依據類型 x的類別或結構中,以選擇最佳的一元運算子。 選取的運算子會叫用自變數清單 (x)
x + y 多載解析會在類別或結構中,根據 xy類型來選擇最佳的二進位運算元。 選取的運算子會用參數清單 (x, y)被叫用。
實例建構函式呼叫 new T(x, y) 多載解析可用於在類別或結構 T中選擇最佳的實例建構函式。 實例建構函式會使用自變數清單 (x, y)叫用。

尾注

12.6.2 自變數清單

12.6.2.1 一般

每個函式成員和委派調用都包含自變數清單,它提供函式成員參數的實際值或變數參考。 指定函式成員呼叫自變數清單的語法取決於函式成員類別:

  • 針對實例建構函式、方法、索引器和委派,參數會指定為 argument_list,如下所述。 對於索引器,在調用 set 存取子時,參數列表會額外包含作為賦值運算符右操作數指定的表達式。

    附註:這個額外的參數不會用於多載解析,只在呼叫 set 存取子時使用。 尾注

  • 對於屬性,在調用 get 存取子時,參數列表是空的,而在調用 set 存取子時,參數列表則包含指定為賦值運算子右操作數的表達式。
  • 針對事件,參數清單由指定為 +=-= 運算子右操作數的表達式組成。
  • 對於使用者定義運算符,自變數清單是由一元運算符的單一操作數或二元運算符的兩個操作數所組成。

屬性的自變數(\15.7)和事件(\15.8)一律會傳遞為值參數(\15.6.2.2)。 使用者定義運算符的自變數(\15.10)一律會傳遞為值參數(\15.6.2.2)或輸入參數(\9.2.8)。 索引器的自變數(\15.9)一律會傳遞為值參數(\15.6.2.2)、輸入參數(!\9.2.8),或參數數組 (“15.6.2.4)。 這些函式成員類別不支援輸出和參考參數。

實例建構函式、方法、索引器或委派調用的參數指定為 argument_list

argument_list
    : argument (',' argument)*
    ;

argument
    : argument_name? argument_value
    ;

argument_name
    : identifier ':'
    ;

argument_value
    : expression
    | 'in' variable_reference
    | 'ref' variable_reference
    | 'out' variable_reference
    ;

argument_list 包含一或多個 自變數,並以逗號分隔。 每個參數都可以包含選擇性的 argument_name,然後是 argument_value。 具有 argument_name自變數 稱為 具名自變數,而沒有 argument_name自變數位置自變數

argument_value 可以採用下列其中一種形式:

  • 表示式,表示自變數作為值參數傳遞,或轉換成輸入參數,然後以此方式傳遞,如§12.6.4.2 所決定,並在 §12.6.2.3中所述。
  • 關鍵詞 in 後面接著 變數參考§9.5),表示該引數會傳遞作為輸入參數(§15.6.2.3.2)。 變數必須明確指派 (\9.4),然後才能傳遞為輸入參數。
  • 關鍵詞 ref 後面接著 變數引用§9.5),表示該引數將以參考參數的形式傳遞(§15.6.2.3.3)。 變數必須明確指派 (\9.4),才能傳遞為參考參數。
  • 關鍵詞 out 後面接著 variable_reference“\9.5),表示自變數會傳遞為輸出參數 (.15.6.2.3.4)。 變數在函數成員調用中傳遞為輸出參數之後,會被視為絕對指派的變數(§9.4)。

表單會分別決定引數的 參數傳遞模式輸入參考,或 輸出。 不過,如上所述,具有值傳遞模式的自變數可能會轉換成具有輸入傳遞模式的自變數。

以輸入、輸出或參考參數的形式傳遞易變欄位(§15.5.4)會引發警告,因為被叫用的方法可能不會將欄位視為易變。

12.6.2.2 對應的參數

針對引數清單中的每個引數,必須在被叫用的函式成員或委派中有對應的參數。

下列中使用的參數清單會依下列方式決定:

  • 對於類別中定義的虛擬方法和索引器,參數清單是從以接收者的靜態類型開始,然後搜尋其基類時找到的函式成員中的第一個宣告或覆寫中挑選出來的。
  • 對於部分方法,會使用定義部分方法宣告的參數清單。
  • ** 對於所有其他功能成員和委派,只有一個參數清單,並且就是所使用的那個清單。

自變數或參數的位置會定義為自變數清單或參數清單中前面的自變數或參數數目。

函式成員自變數的對應參數會建立如下:

  • 實例建構函式、方法、索引器和委派的 argument_list 中的參數:
    • 位置自變數,其中參數在參數清單中的相同位置發生,會對應至該參數,除非參數是參數陣列,而且函式成員會以其展開形式叫用。
    • 以展開形式叫用具有參數陣列的函式成員時,位置引數發生在參數陣列的位置或之後,對應至參數陣列中的一個元素。
    • 具名自變數會對應至參數清單中相同名稱的參數。
    • 對於索引器,當叫用 set 存取子時,作為賦值運算子的右運算元指定的表達式會對應至 set 存取子宣告中的隱含 value 參數。
  • 針對屬性,在叫用 get 存取子時,沒有任何引數。 叫用 set 存取子時,指定為指派運算子右操作數的表示式會對應至 set 存取子宣告的隱含值參數。
  • 針對使用者定義的一元運算元(包括轉換),單一操作數會對應至運算符宣告的單一參數。
  • 針對使用者定義的二元運算符,左操作數會對應至第一個參數,而右操作數會對應至運算符宣告的第二個參數。
  • 未命名的自變數會在位外命名自變數或對應至參數陣列的具名自變數之後,對應至無參數。

    附注:這可防止 M(c: false, valueB);叫用 void M(bool a = true, bool b = true, bool c = true);。 第一個自變數會用在位置外(自變數用於第一個位置,但名為 c 的參數位於第三個位置),因此應該命名下列自變數。 換句話說,只有當名稱和位置指出相同的對應參數時,才允許非結尾的具名參數。 結尾註解

12.6.2.3 自變數清單的運行時間評估

在函式成員調用的執行時處理期間(§12.6.6),參數清單的表達式或變數參考會按照從左到右的順序被評估,如下所示:

  • 如果是 value 自變數,如果參數的傳遞模式為 value

    • 自變數表示式會被評估,並執行對應參數類型的隱含轉換(§10.2)。 產生的值會成為函式成員調用中 value 參數的初始值。

    • 否則,參數的傳遞模式為輸入。 如果引數是變數參考,而且引數的類型與參數類型之間存在識別轉換(§10.2.2),那麼結果的儲存位置將變為函數成員調用中參數所代表的儲存位置。 否則,會使用與對應參數相同的類型來建立儲存位置。 引數表示式會進行評估,並執行對應參數類型的隱含轉換(§10.2)。 產生的值會儲存在該儲存位置內。 該儲存位置是由函式成員調用中的輸入參數表示。

      範例:假設有下列宣告和方法呼叫:

      static void M1(in int p1) { ... }
      int i = 10;
      M1(i);         // i is passed as an input argument
      M1(i + 5);     // transformed to a temporary input argument
      

      M1(i) 方法呼叫中,i 本身會當做輸入自變數傳遞,因為它分類為變數,且類型與輸入參數相同 int。 在 M1(i + 5) 方法呼叫中,會建立一個未命名的 int 變數,使用引數的值初始化,然後作為輸入引數進行傳遞。 請參閱 •12.6.4.2#12.6.4.4

      結束範例

  • 針對輸入、輸出或參考自變數,會評估變數參考,而產生的儲存位置會成為函式成員調用中 參數所代表的儲存位置。 對於輸入或參考自變數,變數應該在方法呼叫的點上明確指派。 如果變數參考是以輸出參數的形式提供,或是 reference_type的陣列元素,則會執行運行時檢查,以確保陣列的元素類型與參數的類型相同。 如果這項檢查失敗,則會拋出 System.ArrayTypeMismatchException

附註:由於陣列共變異(§17.6),因此需要這個運行時檢查。 尾注

範例:在下列程式代碼中

class Test
{
    static void F(ref object x) {...}

    static void Main()
    {
        object[] a = new object[10];
        object[] b = new string[10];
        F(ref a[0]); // Ok
        F(ref b[1]); // ArrayTypeMismatchException
    }
}

第二次叫用 F 會導致擲回 System.ArrayTypeMismatchException,因為 b 的實際項目類型是 string,而不是 object

結束範例

方法、索引器和實例建構函式可能會將其最右邊的參數宣告為參數陣列(§15.6.2.4)。 這類函式成員會根據適用的情況,以其正常形式或擴充形式來叫用(§12.6.4.2):

  • 以一般形式叫用具有參數陣列的函式成員時,為參數陣列指定的引數應該是可隱式轉換成參數陣列類型的單一表達式(第10.2節)。 在此情況下,參數陣列的行為與實值參數類似。
  • 當具有參數陣列的函式成員以其擴展形式被調用時,調用時應為參數陣列指定零個或多個位置參數,其中每個參數都是可以隱式轉換為參數陣列元素類型的表達式(§10.2)。 在此情況下,呼叫會建立一個參數陣列類型的實例,其長度對應到自變數的數目,並以給定的自變數值初始化這個陣列實例的元素,然後使用新建立的陣列實例作為實際參數。

參數列表中的表達式一律會依照文本順序被計算。

範例:因此,範例

class Test
{
    static void F(int x, int y = -1, int z = -2) =>
        Console.WriteLine($"x = {x}, y = {y}, z = {z}");

    static void Main()
    {
        int i = 0;
        F(i++, i++, i++);
        F(z: i++, x: i++);
    }
}

會產生輸出

x = 0, y = 1, z = 2
x = 4, y = -1, z = 3

結束範例

當具有參數陣列的函式成員被以至少一個展開參數的形式調用時,其調用處理相當於在展開的參數周圍插入一個具有陣列初始化表達式的陣列建立表達式(§12.8.17.5)。 當參數陣列沒有自變數時,會傳遞空陣列;未指定傳遞的參考是新配置的或現有的空陣列。

範例:給定的宣告

void F(int x, int y, params object[] args);

方法展開格式的下列調用

F(10, 20, 30, 40);
F(10, 20, 1, "hello", 3.0);

完全對應至

F(10, 20, new object[] { 30, 40 });
F(10, 20, new object[] { 1, "hello", 3.0 });

結束範例

當自變數從具有對應選擇性參數的函式成員中省略時,會隱含傳遞函式成員宣告的預設自變數。 (這可以涉及建立儲存位置,如上所述。

附注:因為這些一律是常數,因此它們的評估不會影響其餘參數的評估。 尾注

12.6.3 類型推斷

12.6.3.1 一般

在沒有指定類型自變數的情況下呼叫泛型方法時,類型推斷 進程會嘗試推斷呼叫的類型自變數。 型別推斷的存在可讓更方便的語法用於呼叫泛型方法,並允許程式設計人員避免指定多餘的類型資訊。

範例

class Chooser
{
    static Random rand = new Random();

    public static T Choose<T>(T first, T second) =>
        rand.Next(2) == 0 ? first : second;
}

class A
{
    static void M()
    {
        int i = Chooser.Choose(5, 213); // Calls Choose<int>
        string s = Chooser.Choose("apple", "banana"); // Calls Choose<string>
    }
}

透過類型推斷,型別參數 intstring 是從方法的參數中確定的。

結束範例

類型推斷會在方法調用的系結時間處理中發生(\12.8.10.2),並在調用的多載解析步驟之前進行。 在方法調用中指定特定方法群組,而且方法調用中未指定任何類型自變數時,類型推斷會套用至方法群組中的每個泛型方法。 如果類型推斷成功,則會使用推斷的類型自變數來判斷後續多載解析的自變數類型。 如果多載解析選擇泛型方法做為要叫用的方法,則會使用推斷的類型自變數作為調用的類型自變數。 如果特定方法的類型推斷失敗,該方法就不會參與多載解析。 型別推斷的失敗本身不會造成系結時間錯誤。 不過,當多載解析無法找到任何適用的方法時,通常會導致綁定时间錯誤。

如果每個提供的自變數都未對應至 方法中的一個參數(.12.6.2.2),或沒有對應自變數的非選擇性參數,則推斷會立即失敗。 否則,假設泛型方法具有下列簽章:

Tₑ M<X₁...Xᵥ>(T₁ p₁ ... Tₓ pₓ)

使用表單的方法呼叫 M(E₁ ...Eₓ) 類型推斷的工作是尋找每個類型參數的唯一類型自變數 S₁...SᵥX₁...Xᵥ,讓呼叫 M<S₁...Sᵥ>(E₁...Eₓ) 變成有效。

類型推斷的程式如下所述為演算法。 一致性編譯程式可以使用替代方法來實作,前提是在所有案例中都達到相同的結果。

在推斷每個類型參數 Xᵢ 的過程中,會 固定 至特定類型 Sᵢ,或 未固定 與一組相關聯的 界限。 每個界限都是某些類型 T。 一開始,每個類型變數 Xᵢ 都未固定一組空白界限。

類型推斷會分階段進行。 每個階段都會根據上一個階段的結果,嘗試推斷更多類型變數的類型自變數。 第一個階段會對界限進行一些初始推斷,而第二個階段會將類型變數修正為特定類型,並推斷進一步界限。 第二個階段可能必須重複多次。

附註:類型推斷也用於其他上下文,包括在方法群組的轉換(§12.6.3.14),以及尋找一組表達式的最佳通用類型(§12.6.3.15)。 尾注

12.6.3.2 第一個階段

針對每個方法自變數 Eᵢ

  • 如果 是匿名函式,明確參數類型推斷12.6.3.8) 會從
  • 否則,如果 具有類型 ,且對應的參數是值參數 (15.6.2.2),則 下限推斷%%.12.6.3.10) 會從
  • 否則為 如果 具有類型 ,且對應的參數是參考參數(\15.6.2.3.3),或輸出參數 (\15.6.2 .3.4),然後 確切推斷12.6.3.9)從
  • 否則為 如果 具有類型 ,且對應的參數是輸入參數(15.6.2.3.2),而 是輸入自變數,則 確切推斷
  • 否則,如果 具有類型 ,且對應的參數是輸入參數(\15.6.2.3.2),則 下限推斷\12.6.3.10)從
  • 否則,不會針對這個參數進行推斷。

12.6.3.3 第二階段

第二個階段會繼續進行,如下所示:

  • 所有 未固定 類型變數 相依於\12.6.3.6), 任何 都固定 .6.3.12)。
  • 如果不存在這類類型變數,則符合下列所有條件的所有 未固定 類型變數 Xᵢ 都會 固定
    • 至少有一個類型變數 Xₑ相依於Xᵢ
    • Xᵢ 有一組非空白的界限
  • 如果不存在這類類型變數,但仍 未修正 類型變數,則類型推斷會失敗。
  • 否則,如果沒有進一步 未修正 類型變數存在,類型推斷就會成功。
  • 否則為 針對具有對應參數類型 的所有自變數 ,其中 輸出類型.12.6.3.5) 包含 未修正 類型變數 ,但 輸入類型.6.3.4) 則不會, 輸出類型推斷12.6.3.7), 是從進行 。 然後重複第二個階段。

12.6.3.4 輸入類型

如果 是方法群組或隱含型別匿名函式,且 是委派類型或表達式樹狀結構類型,那麼 的所有參數類型都是輸入類型,其類型是

12.6.3.5 輸出類型

如果 是方法群組或匿名函式,且 是委派類型或表達式樹狀結構類型,則 的傳回類型是輸出類型,具有類型

12.6.3.6 相依性

未修正的 類型變數 直接相依於 具有類型 的某些自變數 ,如果具有類型 的某些自變數 發生在類型為 且類型為 輸出類型 中。

如果 Xₑ直接相依於Xᵢ,或 Xᵢ直接相依於Xᵥ,而 Xᵥ則取決於Xₑ,則 Xₑ相依於Xᵢ。 因此,“取決於”是“直接依賴”的傳遞但不具反射閉包。

12.6.3.7 輸出類型推斷

輸出類型推斷 是從 表示式 ,以下列方式 類型 T:

  • 如果 是匿名函式,且具有推斷的傳回型別 ),而 是具有傳回型別的委派類型或表達式樹狀結構類型 ,然後 下限推斷12.6.3.10)從
  • 否則,如果 是方法群組,而 是具有參數類型 且傳回型別 的委派型別或表達式樹狀結構類型,且具有型別 的重載 解析會產生具有傳回型別 的單一方法,則 下限推斷 會從
  • 否則,如果 是類型為 的表達式,則會 建立 下限推斷。
  • 否則,不會進行推斷。

12.6.3.8 明確參數類型推斷

明確參數類型推斷 是從表達式 ,以下列方式進行, 成為類型

  • 如果 是具有參數類型的明確具型別匿名函式, 是具有參數 類型的委派型別或表達式樹狀目錄類型, 則每個 確切推斷“”.12.6.3.9)會從 對應的

12.6.3.9 確切推斷

從 類型 類型 確切推斷,如下所示:

  • 如果 V 是其中一個 未固定的Xᵢ,則會將 U 新增至 Xᵢ的確切界限集。
  • 否則,將藉由檢查下列任何情況是否適用來判斷 V₁...VₑU₁...Uₑ
    • V 是陣列類型 V₁[...]U 是相同順位的陣列類型 U₁[...]
    • V 是類型 V₁?U 是類型 U₁
    • V 是建構的類型 C<V₁...Vₑ>U 是建構的類型 C<U₁...Uₑ>
      如果這些情況中的任何一個適用,則會從每個 Uᵢ 到對應的 Vᵢ進行 精確推理
  • 否則,不會進行推斷。

12.6.3.10 下限推斷

從 類型 類型 下限推斷,如下所示:

  • 如果 V 是未固定的 Xᵢ 之一,那麼會將 U 添加至 Xᵢ的下界集合。
  • 否則,如果 V 是類型 V₁?,且 U 是類型 U₁?,則會從 U₁V₁進行下限推斷。
  • 否則,會透過檢查下列任何情況是否適用來判斷 U₁...UₑV₁...Vₑ
    • V 是陣列類型 V₁[...]U 是相同順位的陣列類型 U₁[...]
    • VIEnumerable<V₁>ICollection<V₁>IReadOnlyList<V₁>>IReadOnlyCollection<V₁>IList<V₁> 之一,U 是單維數位類型 U₁[]
    • V 是建構的 classstructinterfacedelegate 類型 C<V₁...Vₑ>,並且有一個唯一的類型 C<U₁...Uₑ>,使得 U(或,如果 U 是類型 parameter,則其有效基類或其有效介面集的任何成員)與 inherits 相同,或直接或間接地從 C<U₁...Uₑ>派生或實作。
    • (「唯一性」限制表示在案例介面 C<T>{} class U: C<X>, C<Y>{}中,則從 U 推斷為 C<T> 時不會進行推斷,因為 U₁ 可以是 XY
      如果適用上述任一情況,則會從每個 Uᵢ 到對應的 Vᵢ 進行推斷,如下所示:
    • 若未知 Uᵢ 為參考類型,則會進行 精確推斷
    • 否則,如果 U 是陣列類型,則會進行 下限推斷
    • 否則,如果 VC<V₁...Vₑ>,則推斷取決於 Ci-th 類型參數:
      • 如果它是共變數,則會建立 下限推斷
      • 如果是反變數,則會進行 上限推斷
      • 如果它是不變的,則會建立 精確的推論
  • 否則,不會進行推斷。

12.6.3.11 上限推斷

從 類型 類型 上限推斷,如下所示:

  • 如果 V 是為數其中一個未固定的 Xᵢ,則將 U 加入為 Xᵢ的上界集合。
  • 否則,透過檢查下列情況是否適用來判斷 V₁...VₑU₁...Uₑ
    • U 是陣列類型 U₁[...]V 是相同階層的陣列類型 V₁[...]
    • UIEnumerable<Uₑ>ICollection<Uₑ>IReadOnlyList<Uₑ>IReadOnlyCollection<Uₑ>IList<Uₑ> 之一,V 是單維數位類型 Vₑ[]
    • U 是類型 U1?V 是類型 V1?
    • U 是由類別、結構、介面或委派類型 C<U₁...Uₑ> 構成,V 是一種 class, struct, interfacedelegate 類型,該類型能(直接或間接)轉換為 identical,從 inherits(直接或間接)轉換來,或(直接或間接)實作唯一類型 C<V₁...Vₑ>
    • (「唯一性」限制表示,指定介面 C<T>{} class V<Z>: C<X<Z>>, C<Y<Z>>{}時,則從 C<U₁> 推理至 V<Q>時不會進行推理。推理不從 U₁X<Q>Y<Q>。)
      如果適用上述任一情況,則會從每個 Uᵢ 推斷至對應的 Vᵢ,如下所示:
    • 如果 Uᵢ 不被確知為參考類型,則進行 精確推斷
    • 否則,如果 V 是陣列類型,則會建立 上限推斷
    • 否則,如果 UC<U₁...Uₑ>,則推斷取決於 Ci-th 類型參數:
      • 如果它是共變數,則會建立 上限推斷
      • 如果是反變數,則會進行 下限推斷
      • 如果它是不變的,則會進行 確切的推斷
  • 否則,不會進行推斷。

12.6.3.12 修正

具有一組界限的 未固定 類型變數 Xᵢ固定,如下所示:

  • 一組 候選型別Uₑ 最初是 Xᵢ界限中的所有型別。
  • 接著會逐一檢查每個系結 Xᵢ:針對每個 Xᵢ 的確切系結 U,從候選集中移除所有與 U 不一致的 Uₑ 類型。 對於每個下限 UXᵢXᵢ 所有類型 Uₑ不會 從候選集移除隱含轉換 U。 對於 Xᵢ 的每個上界 U,從候選集合中移除所有類型 Uₑ,這些類型是沒有隱含轉換到 U(但不是)。
  • 如果在剩餘的候選類型 Uₑ 之中存在一個可以從所有其他候選類型進行隱式轉換的唯一類型 V,則 Xᵢ 被固定為 V
  • 否則,類型推斷會失敗。

12.6.3.13 推斷傳回類型

匿名函式推斷出的傳回型別 F 在類型推斷及多載解析過程中會被使用。 推斷的返回類型只能針對已知所有參數類型的匿名函數來確定,可能是因為它們被明確指定,透過匿名函數轉換提供,或在封閉泛型方法調用中的類型推斷過程中推斷。

推斷的有效返回類型,決定如下:

  • 如果 F 的主體是具有類型的 表示式,則 F 的推斷有效傳回類型是該表示式的類型。
  • 如果 F 主體是 區塊,且區塊 return 語句中的表達式集合具有最佳常見類型 T\12.6.3.15),則推斷的有效傳回類型為 FT
  • 否則,無法針對 F推斷有效的傳回型別。

推斷的傳回類型,如下所示:

  • 如果 F 是異步的,且 F 的主體是被歸類為 nothing 的表達式(§12.2),或是沒有任何 return 語句中含有表達式的區塊,則推斷的回傳類型為 «TaskType»§15.15.1)。
  • 如果 F 是異步的,並且具有推斷的有效的傳回型別 T,則推斷的傳回型別是 «TaskType»<T>»15.15.1)。
  • 如果 F 為非異步,且具有推斷的有效傳回類型 T,則推斷的傳回類型會 T
  • 否則,無法推斷 F的傳回型別。

範例:作為涉及匿名函式的類型推斷範例,請考慮在 System.Linq.Enumerable 類別中宣告的 Select 擴充方法:

namespace System.Linq
{
    public static class Enumerable
    {
        public static IEnumerable<TResult> Select<TSource,TResult>(
            this IEnumerable<TSource> source,
            Func<TSource,TResult> selector)
        {
            foreach (TSource element in source)
            {
                yield return selector(element);
            }
        }
   }
}

假設 System.Linq 命名空間是使用 using namespace 指令匯入,並且類別 Customer 具有類型 stringName 屬性,則 Select 方法可以用來選取客戶清單的名稱:

List<Customer> customers = GetCustomerList();
IEnumerable<string> names = customers.Select(c => c.Name);

Select 的擴展方法呼叫(§12.8.10.3)是透過將呼叫重寫為靜態方法呼叫來處理:

IEnumerable<string> names = Enumerable.Select(customers, c => c.Name);

由於未明確指定類型自變數,因此會使用類型推斷來推斷類型自變數。 首先,客戶的參數與來源參數相關,推斷 TSourceCustomer。 然後,使用上述匿名函式類型推斷程式,c 會指定類型 Customer,而表達式 c.Name 與選取器參數的傳回型別有關,推斷 TResultstring。 因此,調用相當於

Sequence.Select<Customer,string>(customers, (Customer c) => c.Name)

而結果的類型為 IEnumerable<string>

下列範例示範匿名函式類型推斷如何允許泛型方法調用中的自變數之間「流動」類型資訊。 假設有下列方法和調用:

class A
{
    static Z F<X,Y,Z>(X value, Func<X,Y> f1, Func<Y,Z> f2)
    {
        return f2(f1(value));
    }

    static void M()
    {
        double hours = F("1:15:30", s => TimeSpan.Parse(s), t => t.TotalHours);
    }
}

呼叫的類型推斷如下進行:首先,引數 “1:15:30” 與 value 參數相關,推斷 X 為字串類型。 然後,第一個匿名函式的 參數 s會得到推斷的類型 string,而表達式 TimeSpan.Parse(s)f1的傳回型別有關,推斷 YSystem.TimeSpan。 最後,第二個匿名函式 t的參數會得到推斷的類型 System.TimeSpan,而表達式 t.TotalHours 與傳回型別 f2相關,推斷 Zdouble。 因此,呼叫的結果的類型為 double

結束範例

12.6.3.14 方法群組轉換的類型推斷

類似於泛型方法的呼叫,當包含泛型方法的方法群組 M 被轉換為指定的委派類型 D§10.8)時,也需要應用型別推斷。 給定的方法

Tₑ M<X₁...Xᵥ>(T₁ x₁ ... Tₑ xₑ)

將方法群組 M 指派給委派類型 D,類型推斷的任務是尋找類型參數 S₁...Sᵥ,使表達式:

M<S₁...Sᵥ>

會變成與 D兼容的(§20.2)。

不同於泛型方法呼叫的類型推斷演算法,在此情況下,只有自變數 類型,沒有自變數 表達式。 特別是沒有匿名函式,因此不需要多個推斷階段。

相反地,所有 都會視為未,而 下限推斷 會從 的每個自變數類型 的對應參數類型 。 如果沒有找到任何 Xᵢ 的界限,則類型推斷將失敗。 否則,所有 Xᵢ 都會 固定 對應 Sᵢ,這是類型推斷的結果。

12.6.3.15 尋找一組表達式的最佳常見類型

在某些情況下,必須推斷一組表達式的一般型別。 特別是透過這種方式來確定隱含型別陣列的元素類型,以及具有 區塊 主體的匿名函式的傳回型別。

一組表達式 E₁...Eᵥ 的最佳常見類型取決於如下:

  • 引進新的 未固定的 類型變數 X
  • 針對每個表達式 Ei 執行 輸出類型推斷§12.6.3.7),從中推斷到 X
  • X 固定§12.6.3.12),如果可能的話,產生的類型是最好的常見類型。
  • 否則推斷會失敗。

附註:直覺上,此推斷相當於呼叫方法 void M<X>(X x₁ ... X xᵥ),並將 Eᵢ 當做自變數,並推斷 X尾注

12.6.4 多載解析

12.6.4.1 一般

多載解析是一種系結時間機制,用於從引數清單和一組候選的函式成員中選取最適合的函式成員來調用。 在 C# 中,多載解析會針對以下不同情境選擇要叫用的函式成員:

  • invocation_expression§12.8.10)中指定的某個方法的調用。
  • object_creation_expression 中名為某實例建構函式的調用(§12.8.17.2)。
  • 透過 element_access 呼叫索引器存取子(§12.8.12)。
  • 在表達式中所參考的預先定義或使用者定義的運算符呼叫(§12.4.4§12.4.5)。

每個上下文都以自己獨特的方式定義候選函式成員集合及其引數清單。 例如,方法調用的候選專案集合不包含標示覆寫的方法(\12.5),如果衍生類別中的任何方法適用,則基類中的方法不是候選專案(\12.8.10.2)。

一旦識別出候選函式成員和自變數清單之後,在所有情況下,最佳函式成員的選取都會相同:

  • 首先,候選函式成員集合會縮減為與指定自變數清單相關的函式成員()。 如果這個縮減的集合是空的,就會發生編譯時期錯誤。
  • 然後,會找到一組適用候選函式成員中的最佳函式成員。 如果集合只包含一個函式成員,則該函式成員是最佳函式成員。 最佳函式成員是相較於所提供的引數列表,比其他函式成員更優的那一個,前提是依據 §12.6.4.3 和的規則,將每個函式成員與所有其他函式成員進行比較。 如果沒有完全比所有其他函式成員更好的函式成員,則函式成員調用模棱兩可,而且會發生系結時間錯誤。

下列子條款定義字詞 適用函式成員更好的函式成員的確切意義。

12.6.4.2 適用的函式成員

當滿足以下所有條件時,一個函式成員被稱為 適用的函式成員,以參數列表 A 為其依據:

  • A 中的每個自變數都會對應至函式成員宣告中的參數,如 “”12.6.2.2中所述,最多一個自變數對應至每個參數,而沒有自變數對應的任何參數都是選擇性參數。
  • 針對 A中的每個自變數,自變數的參數傳遞模式與對應參數的參數傳遞模式相同,以及
    • 若為值參數或參數陣列,則從參數表達式到對應參數的類型存在隱含轉換(§10.2)或
    • 對於參考或輸出參數,若有引數表達式,則該引數表達式的類型與對應參數的類型之間存在同一性轉換,或
    • 當對應的引數具有 in 修飾符時,輸入參數在引數表達式的類型(如果有的話)和對應參數的類型之間有身份轉換,或
    • 當對應的引數省略in修飾詞時,會從引數表達式到對應引數類型自動進行隱含轉換(§10.2)。

對於包含參數陣列的函式成員,如果函式成員適用於上述規則,則表示其 一般形式適用。 如果包含參數陣列的函式成員不適用於其一般形式,該函式成員可能會改為適用於其 展開的形式

  • 展開形式是透過在函式成員宣告中將參數陣列替換為該陣列元素型別的零或多個值參數,從而使引數清單中的引數數目 A 與參數總數相符。 如果 A 自變數比函數成員宣告中的固定參數數目少,則無法建構函式成員的展開形式,因此不適用。
  • 否則,若對於 A中的每個參數,下列條件之一成立,則展開形式適用:
    • 自變數的參數傳遞模式與對應參數的參數傳遞模式相同,以及
      • 對於固定值參數或由擴充建立的數值參數,從自變數表示式到對應參數類型的隱式轉換(§10.2)存在,或
      • 對於 by-reference 參數,自變數表達式的類型與對應參數的類型相同。
    • 自變數的參數傳遞模式為實值,而對應參數的參數傳遞模式為輸入,且從自變數表達式到對應參數類型的隱含轉換(§10.2)存在。

當從引數的類型隱含轉換成輸入參數的參數類型是動態隱含轉換時(§10.2.10),則結果未定義。

範例:給定下列的宣告和方法呼叫:

public static void M1(int p1) { ... }
public static void M1(in int p1) { ... }
public static void M2(in int p1) { ... }
public static void Test()
{
    int i = 10; uint ui = 34U;

    M1(in i);   // M1(in int) is applicable
    M1(in ui);  // no exact type match, so M1(in int) is not applicable
    M1(i);      // M1(int) and M1(in int) are applicable
    M1(i + 5);  // M1(int) and M1(in int) are applicable
    M1(100u);   // no implicit conversion exists, so M1(int) is not applicable

    M2(in i);   // M2(in int) is applicable
    M2(i);      // M2(in int) is applicable
    M2(i + 5);  // M2(in int) is applicable
}

結束範例

  • 只有當方法群組透過類型從 simple_namemember_access 產生時,才適用靜態方法。
  • 只有當方法組由 simple_name、透過變數或值的 member_accessbase_access所產生時,才適用實例方法。
    • 如果方法群組從 simple_name產生,則只有在 \12.8.14才允許 this 存取時,才適用實例方法。
  • 當方法群組來自 member_access,並且可以透過實例或類型取得時,如 §12.8.7.2所述,則實例和靜態方法皆適用。
  • 類型參數(無論是明確指定還是推斷)的泛型方法,若沒有全部符合其約束條件,將不可用。
  • 在方法群組轉換的上下文中,應該存在從方法傳回型別到委派傳回型別的識別轉換(§10.2.2)或隱含參考轉換(§10.2.8)。 否則,候選方法不適用。

12.6.4.3 功能較佳的函式成員

為了判斷更好的函式成員,會建構一個精簡後的引數清單 A,該清單只包含引數表達式本身,並按它們在原始引數清單中的順序排列,並排除任何 outref 引數。

每個候選函式成員的參數清單會以下列方式建構:

  • 如果函數成員僅適用於展開的形式,則會使用展開的形式。
  • 沒有對應自變數的選擇性參數會從參數清單中移除
  • 從參數清單中移除參考和輸出參數
  • 參數會重新排序,使其與引數清單中的對應引數位於相同位置。

假設自變數清單 具有一組自變數表達式 和兩個適用的函式成員 ,並使用參數類型 定義為比 更好的函式成員

  • 針對每個自變數,從 EᵥQᵥ 的隱含轉換不比從 EᵥPᵥ的隱含轉換更好。
  • 對於至少一個自變數,從 Eᵥ 轉換成 Pᵥ 比從 Eᵥ 轉換成 Qᵥ更好。

如果參數類型序列 {P₁, P₂, ..., Pᵥ}{Q₁, Q₂, ..., Qᵥ} 相等(也就是每個 Pᵢ 都有對應 Qᵢ的身分識別轉換),則會套用下列中斷系結規則,以判斷更好的函式成員。

  • 如果 Mᵢ 是非泛型方法,而且 Mₑ 是泛型方法,則 Mᵢ 優於 Mₑ
  • 否則,如果 Mᵢ 適用於其一般形式,且 Mₑ 具有參數陣列,且僅適用於其展開形式,則 Mᵢ 會優於 Mₑ
  • 否則,如果這兩種方法都有參數數位,而且只適用於其展開形式,而且如果 Mᵢ 的 params 陣列的元素比 Mₑ的 params 陣列少,則 MᵢMₑ更好。
  • 否則,如果 Mᵥ 具有比 Mₓ更明確的參數類型,則 MᵥMₓ更好。 讓 {R1, R2, ..., Rn}{S1, S2, ..., Sn} 代表 MᵥMₓ的未經驗證和未展開的參數類型。 Mᵥ的參數類型比 Mₓ的更具體,若對於每一個參數,Rx 不比 Sx不具體,且至少有一個參數,RxSx更具體。
    • 類型參數比非類型參數更不明確。
    • 遞歸地,如果至少有一個型別參數更具體,且沒有一個型別參數比另一個建構類型中對應的型別參數更不具體,則此建構型別比另一個建構型別更具體。
    • 如果兩個陣列類型的維度數目相同,且第一個陣列的元素類型比第二個的具體,那麼第一個陣列類型就是比第二個更具體。
  • 否則,如果一個成員是非提升運算符,另一個是提升運算符,則非提升的運算符比較好。
  • 如果兩個函式成員都找不到較佳,而且 Mᵥ 的所有參數都有對應的自變數,而預設自變數則至少要取代 Mₓ中的一個選擇性參數,則 MᵥMₓ更好。
  • 如果至少有一個參數 Mᵥ 使用 Mₓ 中的對應參數更好的參數傳遞選擇§12.6.4.4),而且 Mₓ 中沒有任何參數使用比 Mᵥ更好的參數傳遞選擇,MᵥMₓ更好。
  • 否則,沒有更好的函式成員。

12.6.4.4 更好的參數傳遞模式

如果兩個重載方法中的對應參數,只在參數傳遞模式上有所不同,且其中一個參數必須是值傳遞模式,則是允許的,如下所示:

public static void M1(int p1) { ... }
public static void M1(in int p1) { ... }

假設 int i = 10;,根據 .12.6.4.2,呼叫 M1(i)M1(i + 5) 會導致這兩個多載都適用。 在這種情況下,使用值參數傳遞模式的方法是 更好的參數傳遞模式選擇

附註:輸入、輸出或參考傳遞模式的自變數不需要這類選擇,因為這些自變數只符合完全相同的參數傳遞模式。 尾注

12.6.4.5 更好的運算式轉換

假設隱含轉換 C₁ 從表達式 E 轉換為類型 T₁,還有將表達式 E 轉換為類型 T₂的隱含轉換 C₂,若下列條件之一成立,C₁C₂ 更好的轉換

  • E 完全符合 T₁ET₂ 不完全相符(§12.6.4.6
  • E 完全符合或完全不符合 T₁T₂,且 T₁ 是比 T₂ 更好的轉換目標(§12.6.4.7
  • E 是方法群組(),T₁ 與方法群組中轉換 C₁的單一最佳方法相容(\20.4),T₂ 與方法群組中轉換 C₂ 的單一最佳方法不相容

12.6.4.6 完全匹配的表達式

假設表達式 E 和類型 T,如果下列其中一項保留,E完全符合T

  • E 的類型是 S,且從 ST 存在自我轉換
  • E 是匿名函式,T 是委派類型 D 或表達式樹狀結構類型 Expression<D>,且下列其中一項保留:
    • 推斷的傳回型別 X 存在於 D 參數列表的內容中 E##12.6.3.12),且識別轉換存在從 X 轉換成 D 的傳回型別
    • E 是沒有傳回值的 async lambda 表達式,且 D 具有非泛型的 «TaskType»
    • E 為非異步,且 D 具有傳回類型 Y,或 E 為異步,且 D 具有傳回類型 «TaskType»<Y>§15.15.1),且下述條件之一必須成立:
      • E 的主體是一個完全符合 Y 的表達式
      • E 主體是區塊,其中每個 return 語句都會傳回完全符合 Y 的表達式

12.6.4.7 更好的轉換目標

假設有兩種類型 T₁T₂,如果下列其中一項保留,T₁T₂ 更好的轉換 目標:

  • 存在從 T₁T₂ 的隱含轉換,而且不存在從 T₂T₁ 的隱含轉換
  • T₁«TaskType»<S₁>§15.15.1),T₂«TaskType»<S₂>S₁ 是比 S₂ 更好的轉換目標
  • T₁«TaskType»<S₁>§15.15.1),T₂«TaskType»<S₂>T₁T₂ 更專門
  • T₁S₁S₁?,其中 S₁ 是帶正負號整數型別,T₂S₂S₂?,其中 S₂ 是無符號整數型別。 具體說來:
    • S₁sbyteS₂byteushortuintulong
    • S₁shortS₂ushortuintulong
    • S₁int,且 S₂uintulong
    • S₁long,和 S₂ulong

12.6.4.8 泛型類別中的多載

附注:雖然宣告的簽章應是唯一的(§8.6),但型別參數的替換可能會產生相同的簽章。 在這種情況下,多載解析會挑選在替換型別參數之前,原始簽名中最具體的(§12.6.4.3),如果存在該簽名,則使用該簽名,否則會報告錯誤。 尾注

範例:下列範例顯示根據此規則有效且無效的多載:

public interface I1<T> { ... }
public interface I2<T> { ... }

public abstract class G1<U>
{
    public abstract int F1(U u);           // Overload resolution for G<int>.F1
    public abstract int F1(int i);         // will pick non-generic

    public abstract void F2(I1<U> a);      // Valid overload
    public abstract void F2(I2<U> a);
}

abstract class G2<U,V>
{
    public abstract void F3(U u, V v);     // Valid, but overload resolution for
    public abstract void F3(V v, U u);     // G2<int,int>.F3 will fail

    public abstract void F4(U u, I1<V> v); // Valid, but overload resolution for
    public abstract void F4(I1<V> v, U u); // G2<I1<int>,int>.F4 will fail

    public abstract void F5(U u1, I1<V> v2);   // Valid overload
    public abstract void F5(V v1, U u2);

    public abstract void F6(ref U u);      // Valid overload
    public abstract void F6(out V v);
}

結束範例

12.6.5 動態成員調用的編譯時間檢查

即使動態繫結操作的多載解析是在執行階段進行,但有時在編譯階段仍能預知可供選擇多載的函數成員清單:

  • 對於委派調用(§12.8.10.4),這個清單是一個具有與調用的委派類型 delegate_type 相同參數列表的單一函式成員。
  • 對於類型或靜態類型不是動態的值上的方法調用(§12.8.10.2),在編譯時間即可知道方法群組中可存取的一組方法。
  • 針對物件建立表達式(§12.8.17.2),類型中的一組可存取的建構函式在編譯時是已知的。
  • 對於索引器存取(§12.8.12.3),在編譯時期,接收者中可以使用的索引器集合是已知的。

在這些情況下,對已知函式成員集中的每個成員進行有限的編譯時間檢查,以確定是否能確定它在運行時間絕對不會被叫用。 針對每個函式成員 F 會建構已修改的參數和自變數清單:

  • 首先,如果 F 是一個泛型方法,並且提供了型別引數,則將這些引數代入參數清單中的類型參數。 不過,如果未提供型別自變數,則不會發生這類替代。
  • 然後,類型為開放的任何參數(亦即包含類型參數;請參閱 §8.4.3)以及其對應的參數將被省略。

若要 F 通過檢查,必須滿足以下所有條件:

  • F 的修改參數列表適用於 第 12.6.4.2 條中修改的自變數清單。
  • 修改參數清單中的所有建構型別都滿足其條件約束 (8.4.5)。
  • 如果上述步驟中取代了 F 的類型參數,則會滿足其條件約束。
  • 如果 F 是靜態方法,則方法群組不應由在編譯時期已知為變數或值的 member_access 產生。
  • 如果 F 是實例方法,則不應從接收端在編譯時期已經確定為類型的 member_access 中產生方法群組。

如果沒有候選者通過此測試,就會發生編譯時錯誤。

12.6.6 函式成員調用

12.6.6.1 一般

此子條款描述在執行時(run-time)調用特定函式成員的過程。 假設繫結時間程序已經決定了要調用的特定成員,這可能是通過將多載決議應用到一組候選函式成員上來實現的。

為了描述調用程式,函式成員分成兩個類別:

  • 靜態函式成員。 這些是靜態方法、靜態屬性存取子和使用者定義運算符。 靜態函式成員一律為非虛擬。
  • 實例函式成員。 這些是實例方法、實例建構函式、實例屬性存取子和索引器存取子。 實例函式成員為非虛擬或虛擬,且一律在特定實例上叫用。 實例是透過實例運算式計算出來的,並在函式成員中以 this 形式變得可存取 (§12.8.14)。 對於構造函數,實例表達式指的是新分配的物件。

函式成員調用的運行時間處理包含下列步驟,其中 M 是函式成員,如果 M 是實例成員,E 是實例表達式:

  • 如果 M 是靜態函式成員:

    • 引數列表的評估方式,如 §12.6.2中所述。
    • M 被調用。
  • 否則,如果 E 的類型是實值型別 V,且 MV中宣告或覆寫:

    • E 已被評估。 如果此評估造成例外狀況,則不會執行任何進一步的步驟。 針對實例建構函式,此評估包含為新物件配置記憶體(通常是從執行堆棧)。 在此情況下,E 分類為變數。
    • 如果 E 未分類為變數,或 V 不是只讀結構類型(“\16.2.2),E 為下列其中一項:

    然後會建立 E類型的暫存局部變數,並將 E 的值指派給該變數。 然後,E 被重新定義為該暫存局部變數的參考。 暫存變數可以存取為 M內的 this,但無法以任何其他方式存取。 因此,只有在可以寫入 E 時,呼叫端才能觀察 Mthis所做的變更。

    • 引數列表的評估如 §12.6.2中所述。
    • M 調用。 E 所參考的變數會成為 this所參考的變數。
  • 否則:

    • E 已被評估。 如果此評估造成例外狀況,則不會執行任何進一步的步驟。
    • 參數列表的評估方式,如 §12.6.2中所述。
    • 如果 E 的類型是 型別值,則會執行裝箱轉換(§10.2.9),將 E 轉換為 類型,並在以下步驟中將 E 視為該 類型。 如果 value_typeenum_type,則 class_typeSystem.Enum;;否則,則為 System.ValueType
    • E 的值被檢查是否有效。 如果 E 的值為 null,則會擲回 System.NullReferenceException,而且不會執行任何進一步的步驟。
    • 要調用的函式成員實作已確定:
      • 如果 E 的綁定時間類型是介面,則要調用的函數成員是由 E所參考的實例運行時類型提供的 M 實現。 函式成員是藉由套用介面映射規則(§18.6.5)來確定由 E所指實例的運行時類型所提供的 M 實作。
      • 否則,如果 M 是虛擬函式成員,要叫用的函式成員就是由 E所參考實例的運行時間類型所提供之 M 實作。 此函式成員的確定是藉由套用規則,以決定關於 E參考的實例在運行時類型上的最衍生實作§15.6.4M
      • 否則,M 是非虛擬函式成員,而要叫用的函式成員則是 M 本身。
    • 調用在上述步驟中確定的函式成員實作。 E 所參考的物件會成為這個 所參考的物件。

實例建構函式呼叫 (§12.8.17.2) 的結果是創建的值。 任何其他函式成員呼叫的結果是主體傳回的值(如果有的話)(§13.10.5)。

12.6.6.2 在 Boxed 實例上的呼叫

在以下情況下,可以透過該 value_type 的 Boxed 實例調用 value_type 中實作的函式成員:

  • 當函式成員是繼承自類型 class_type 的方法覆寫時,會透過該 class_type的實例表達式叫用。

    附注class_type 一律為 System.ObjectSystem.ValueTypeSystem.Enum之一。  尾注

  • 當函式成員是介面函式成員的實作時,會透過 interface_type的實例表達式叫用。
  • 當函式成員透過委派被叫用時。

在這些情況下,Boxed 實例會被視為包含 value_type的變數,而此變數會成為函式成員調用中這個 所參考的變數。

注意:特別是,當 Boxed 實例上叫用函式成員時,函式成員可以修改 Boxed 實例中包含的值。 尾注

12.7 解構

解構是一個過程,其中表達式會轉換成個別表達式的元組。 當簡單指派的目標是元組運算式時,將使用解構操作,以取得賦予元組每個元素的值。

運算式 E 會以下列方式,解構 成具有 n 個元素的 tuple 運算式:

  • 如果 E 是具有 n 元素的元組表達式,解構的結果就是表達式本身 E
  • 否則,如果 E 具有 (T1, ..., Tn) 並且包含 n 個元素的元組類型,則 E 會被評估為暫存變數 __v,而解構的結果為表達式 (__v.Item1, ..., __v.Itemn)
  • 否則,如果表達式 E.Deconstruct(out var __v1, ..., out var __vn) 在編譯階段解析為唯一實例或擴充方法,則會評估該表達式,解構的結果是表達式 (__v1, ..., __vn)。 這類方法稱為 解構函式
  • 否則,無法解構 E

在這裡,__v__v1, ..., __vn 指的是平常不可見且無法存取的暫時性變數。

Note:無法解構類型 dynamic 的運算式。 尾注

12.8 主要運算式

12.8.1 一般

主要表達式包括最簡單的表達形式。

primary_expression
    : primary_no_array_creation_expression
    | array_creation_expression
    ;

primary_no_array_creation_expression
    : literal
    | interpolated_string_expression
    | simple_name
    | parenthesized_expression
    | tuple_expression
    | member_access
    | null_conditional_member_access
    | invocation_expression
    | element_access
    | null_conditional_element_access
    | this_access
    | base_access
    | post_increment_expression
    | post_decrement_expression
    | null_forgiving_expression
    | object_creation_expression
    | delegate_creation_expression
    | anonymous_object_creation_expression
    | typeof_expression
    | sizeof_expression
    | checked_expression
    | unchecked_expression
    | default_value_expression
    | nameof_expression    
    | anonymous_method_expression
    | pointer_member_access     // unsafe code support
    | pointer_element_access    // unsafe code support
    | stackalloc_expression
    ;

附註:這些文法規則尚未為 ANTLR 準備好,因為它們是一些相互左遞歸規則的一部分(primary_expressionprimary_no_array_creation_expressionmember_accessinvocation_expressionelement_accesspost_increment_expressionpost_decrement_expressionnull_forgiving_expressionpointer_member_accesspointer_element_access),而 ANTLR 無法處理這些規則。 標準技術可用來轉換文法,以移除相互的左遞歸。 這尚未完成,因為並非所有剖析策略都需要它(例如 LALR 剖析器不會),這樣做會模糊化結構和描述。 尾注

pointer_member_access)和 pointer_element_access•23.6.4)僅適用於不安全的程式碼(•23)。

主要表達式分為 array_creation_expressionprimary_no_array_creation_expression。 透過這種方式處理 array_creation_expression,而不是將它與其他簡單的表達式形式一起列出,可讓文法不允許容易混淆的代碼,例如

object o = new int[3][1];

否則會解譯為

object o = (new int[3])[1];

12.8.2 字面量

常值6.4.5) 組成的 primary_expression 會分類為值。

12.8.3 插補字串表達式

interpolated_string_expression 包含 $$@@$,後面緊接著 " 字元內的文字。 在引號文字中,有零或多個 插補點,{} 字元分隔,每個插補都會括住 表達式 和選擇性格式規格。

插值字串表示式有兩種形式:一般的(interpolated_regular_string_expression)和逐字的(interpolated_verbatim_string_expression);在語彙上類似,但在語意上則與字串常值的兩種形式(§6.4.5.6)有所不同。

interpolated_string_expression
    : interpolated_regular_string_expression
    | interpolated_verbatim_string_expression
    ;

// interpolated regular string expressions

interpolated_regular_string_expression
    : Interpolated_Regular_String_Start Interpolated_Regular_String_Mid?
      ('{' regular_interpolation '}' Interpolated_Regular_String_Mid?)*
      Interpolated_Regular_String_End
    ;

regular_interpolation
    : expression (',' interpolation_minimum_width)?
      Regular_Interpolation_Format?
    ;

interpolation_minimum_width
    : constant_expression
    ;

Interpolated_Regular_String_Start
    : '$"'
    ;

// the following three lexical rules are context sensitive, see details below

Interpolated_Regular_String_Mid
    : Interpolated_Regular_String_Element+
    ;

Regular_Interpolation_Format
    : ':' Interpolated_Regular_String_Element+
    ;

Interpolated_Regular_String_End
    : '"'
    ;

fragment Interpolated_Regular_String_Element
    : Interpolated_Regular_String_Character
    | Simple_Escape_Sequence
    | Hexadecimal_Escape_Sequence
    | Unicode_Escape_Sequence
    | Open_Brace_Escape_Sequence
    | Close_Brace_Escape_Sequence
    ;

fragment Interpolated_Regular_String_Character
    // Any character except " (U+0022), \\ (U+005C),
    // { (U+007B), } (U+007D), and New_Line_Character.
    : ~["\\{}\u000D\u000A\u0085\u2028\u2029]
    ;

// interpolated verbatim string expressions

interpolated_verbatim_string_expression
    : Interpolated_Verbatim_String_Start Interpolated_Verbatim_String_Mid?
      ('{' verbatim_interpolation '}' Interpolated_Verbatim_String_Mid?)*
      Interpolated_Verbatim_String_End
    ;

verbatim_interpolation
    : expression (',' interpolation_minimum_width)?
      Verbatim_Interpolation_Format?
    ;

Interpolated_Verbatim_String_Start
    : '$@"'
    | '@$"'
    ;

// the following three lexical rules are context sensitive, see details below

Interpolated_Verbatim_String_Mid
    : Interpolated_Verbatim_String_Element+
    ;

Verbatim_Interpolation_Format
    : ':' Interpolated_Verbatim_String_Element+
    ;

Interpolated_Verbatim_String_End
    : '"'
    ;

fragment Interpolated_Verbatim_String_Element
    : Interpolated_Verbatim_String_Character
    | Quote_Escape_Sequence
    | Open_Brace_Escape_Sequence
    | Close_Brace_Escape_Sequence
    ;

fragment Interpolated_Verbatim_String_Character
    : ~["{}]    // Any character except " (U+0022), { (U+007B) and } (U+007D)
    ;

// lexical fragments used by both regular and verbatim interpolated strings

fragment Open_Brace_Escape_Sequence
    : '{{'
    ;

fragment Close_Brace_Escape_Sequence
    : '}}'
    ;

上述所定義的語彙規則中有六個 是上下文敏感的, 如下所示:

規則 情境需求
Interpolated_Regular_String_Mid 只有在 Interpolated_Regular_String_Start之後、在任何後續的插值之間,以及在對應的 Interpolated_Regular_String_End之前,才會被辨識。
Regular_Interpolation_Format 當只有在一個 regular_interpolation 中且開始的冒號(:)沒有嵌套在任何類型的括號(小括號/大括號/方括號)內時才被識別。
Interpolated_Regular_String_End 只有在 Interpolated_Regular_String_Start 之後才被辨識,而且只有在任何插補令牌都 Interpolated_Regular_String_Mid或可屬於 regular_interpolation的令牌時,才會辨識,包括這類插補中包含的任何 interpolated_regular_string_expression令牌。
Interpolated_Verbatim_String_MidVerbatim_Interpolation_FormatInterpolated_Verbatim_String_End 辨識這三個規則的方式是參照上述對應規則的辨識方式,不同之處在於將每個提及的 一般 文法規則替換為對應的 逐字 規則。

附注:上述規則依賴於上下文,因為其定義與語言中其他標記的定義重疊。 尾注

附注:由於內容敏感的語彙規則的原因,上述文法不符合 ANTLR 的要求。 如同其他語匯器產生器,ANTLR 支援內容敏感的語彙規則,例如使用其 語彙模式,但這是實作詳細數據,因此不屬於此規格的一部分。 尾注

interpolated_string_expression 被視為一個值。 如果它立即通過隱式插值字串轉換成 System.IFormattableSystem.FormattableString§10.2.5),則插值字串表達式就具有該類型。 否則,其類型 string

附註interpolated_string_expression 可能類型之間的差異,可以從 System.String 文件(第C.2節)和 System.FormattableString第C.3節)中判斷。 尾注

插補點的意義,包括 regular_interpolationverbatim_interpolation是要將 表達式 的值格式化為 string,這可以是根據 Regular_Interpolation_FormatVerbatim_Interpolation_Format所指定的格式,或者根據 表達式類型的默認格式。 然後,格式化字串將由 interpolation_minimum_width做修改(如果有的話),從而生成要插入到 interpolated_string_expression的最終 string

附註:類型的預設格式如何決定的詳情,可以參閱 System.String§C.2)和 System.FormattableString§C.3)的檔案。 標準格式的描述,與 Regular_Interpolation_FormatVerbatim_Interpolation_Format完全相同,可以在 System.IFormattable 的文件中(§C.4)以及標準庫中的其他類型(§C)找到。 尾注

interpolation_minimum_width 中,constant_expression 必須被隱式轉換為 int。 讓 字段寬度 為此 常量表達式 的絕對值,而 對齊 則為該 常量表達式的正負符號:

  • 如果欄位寬度的值小於或等於格式化字串的長度,則格式化字串不會修改。
  • 否則,格式化字串會以空格符填補,使其長度等於字段寬度:
    • 如果對齊方式是正數,則格式化字串會靠右對齊,方法是通過在前面加上填充字符來實現。
    • 否則,透過填充會使其靠左對齊。

interpolated_string_expression的整體意義,包括上述內插值的格式和填充,是由將表達式轉換為方法調用來定義:如果表達式的類型是 System.IFormattableSystem.FormattableString,該方法的類型是 System.Runtime.CompilerServices.FormattableStringFactory.Create§C.3),它會返回類型 System.FormattableString的值;否則,類型是 string,方法是 string.Format§C.2),其會返回類型 string的值。

在這兩種情況下,呼叫的參數清單包含 格式字串常值,並且每個插補都有 格式規格,以及對應於這些格式規格的每個表達式的參數。

格式字串常值的建構方式如下,其中 Ninterpolated_string_expression中的插補數目。 格式字串字面值會依序包含:

  • Interpolated_Regular_String_StartInterpolated_Verbatim_String_Start 的字元
  • Interpolated_Regular_String_MidInterpolated_Verbatim_String_Mid的字元,如果有的話
  • 如果對於從 0N-1的每個數字 IN ≥ 1,然後:
    • 佔位符規範
      • 左大括弧 ({) 字元
      • I 的十進位表示法
      • 然後,如果對應的 regular_interpolationverbatim_interpolation 具有 interpolation_minimum_width,則在逗號(,)後接著 constant_expression 值的十進位表示法。
      • 對應的 regular_interpolationverbatim_interpolation 中,如果有的話,Regular_Interpolation_FormatVerbatim_Interpolation_Format的字元
      • 右大括弧 (}) 字元
    • 在對應的插補之後緊接著出現的字元是 Interpolated_Regular_String_MidInterpolated_Verbatim_String_Mid,如果有的話。
  • 最後,字元屬於 Interpolated_Regular_String_EndInterpolated_Verbatim_String_End

後續自變數是內插補點的 表示式,如果有的話,依序排列。

插值字串表達式 包含多個插值時,這些插值中的表達式會按文字順序從左到右進行評估。

範例

此範例使用下列格式規格功能:

  • X 格式規格,將整數格式化為大寫十六進位,
  • string 值的預設格式是值本身,
  • 在指定的最小欄位寬度內靠右對齊的正對齊值,
  • 在指定的最小欄位寬度內靠左對齊的負數對齊值,
  • interpolation_minimum_width定義的常數有:
  • {{}} 分別格式化為 {}

假設:

string text = "red";
int number = 14;
const int width = -4;

然後:

內插字串表達式 相當於 string
$"{text}" string.Format("{0}", text) "red"
$"{{text}}" string.Format("{{text}}) "{text}"
$"{ text , 4 }" string.Format("{0,4}", text) " red"
$"{ text , width }" string.Format("{0,-4}", text) "red "
$"{number:X}" string.Format("{0:X}", number) "E"
$"{text + '?'} {number % 3}" string.Format("{0} {1}", text + '?', number % 3) "red? 2"
$"{text + $"[{number}]"}" string.Format("{0}", text + string.Format("[{0}]", number)) "red[14]"
$"{(number==0?"Zero":"Non-zero")}" string.Format("{0}", (number==0?"Zero":"Non-zero")) "Non-zero"

結束範例

12.8.4 簡單名稱

simple_name 是由識別碼所組成,選擇性地後面接著類型自變數清單:

simple_name
    : identifier type_argument_list?
    ;

simple_name 是形式 I 或形式 I<A₁, ..., Aₑ>,其中 I 是單一標識符,I<A₁, ..., Aₑ> 是選擇性的 型別參數列表。 未指定任何 type_argument_list 時,請將 e 視為零。 simple_name 被評估並進行如下分類:

  • 如果 e 為零,而且 simple_name 會出現在局部變數宣告空間中({7.3),而該局部變數宣告空間會直接包含名稱為 I的局部變數、參數或常數,則 simple_name 會參考該局部變數、參數或常數,並分類為變數或值。
  • 如果 為零,且 simple_name 出現在泛型方法宣告中,但在其 method_declaration 屬性之外,而且如果該宣告包含名稱為 的類型參數,則 simple_name 會參考該類型參數。
  • 否則,針對每個實例類型 T),從立即封入型別宣告的實例類型開始,並繼續每個封入類別或結構宣告的實例類型(如果有的話):
    • 如果 e 為零,且 T 宣告包含名稱為 I的類型參數,則 simple_name 會參考該類型參數。
    • 否則,若在 T 中查找成員 I 並使用 e 類型參數(§12.5)產生匹配:
      • 如果 T 是立即封入類別或結構類型的實體類型,而查閱會識別一或多個方法,則結果是具有相關聯實例表示式的方法群組 this。 如果指定了類型自變數清單,則會用於呼叫泛型方法 (\12.8.10.2)。
      • 否則,如果 是直接封閉類別或結構類型的實例類型,並且查找識別出了一個實例成員,且該引用發生在實例構造函數、實例方法或實例存取子 的 區塊中,則結果與形式 的成員存取相同(§12.2.1;§12.8.7)。 這隻能在 e 為零時發生。
      • 否則,結果與 T.IT.I<A₁, ..., Aₑ>類型的成員存取(§12.8.7)相同。
  • 否則,對於每個命名空間 N,從 simple_name 所在的命名空間開始,然後依序進行每個封閉的命名空間(如果有的話),直到全域命名空間為止,將評估下列步驟,直到找到實體為止:
    • 如果 e 為零,且 IN中的命名空間名稱,則:
      • 如果 simple_name 發生的位置被 N 的命名空間宣告所包住,且該命名空間宣告包含 extern_alias_directiveusing_alias_directive,用來將名稱 I 與命名空間或類型產生關聯,那麼 simple_name 會變得模棱兩可,並且會發生編譯時錯誤。
      • 否則,simple_name 會參考 N中名為 I 的命名空間。
    • 否則,如果 N 包含具有名稱 Ie 類型參數的可存取類型,則:
      • 如果 e 為零,且 simple_name 發生在用於 N 的命名空間宣告中,而該命名空間宣告包含 extern_alias_directiveusing_alias_directive,將名稱 I 與命名空間或類型產生關聯,則 simple_name 會模棱兩可,並發生編譯時期錯誤。
      • 否則,namespace_or_type_name 會參考由指定型別引數構成的類型。
    • 如果 simple_name 的位置被 N的名稱空間宣告所括住,否則:
      • 如果 e 為零,且命名空間宣告包含 extern_alias_directiveusing_alias_directive,使名稱 I 與匯入的命名空間或類型產生關聯,則 simple_name 會參考該命名空間或類型。
      • 否則,如果命名空間宣告的 using_namespace_directive所匯入的命名空間中恰好包含一個有名稱 I 且有 e 型別參數的類型,則 simple_name 會參考以給定型別引數建構的該類型。
      • 否則,如果命名空間宣告的 using_namespace_directive所匯入的命名空間包含多個具有名稱 Ie 類型參數的類型,則 simple_name 模棱兩可,而且會發生編譯時期錯誤。

    附注:整個步驟與處理 namespace_or_type_name 的對應步驟完全相同(§7.8)。 尾注

  • 否則,如果 e 為零且 I 是標識符 _,則 simple_name簡單捨棄,這是一種宣告表示式(§12.17)的形式。
  • 否則,simple_name 未定義,而且會發生編譯時期錯誤。

12.8.5 括弧表達式

parenthesized_expression 包含以括弧括住的 表示式

parenthesized_expression
    : '(' expression ')'
    ;

透過評估括號內的 表達式 來評估 括弧表示式。 如果括弧內的 表示式 表示命名空間或類型,則會發生編譯時期錯誤。 否則,括號運算式 的結果就是對內含的 表達式進行評估的結果

12.8.6 Tuple 運算式

tuple_expression 代表一個元組,由兩個或多個逗號分隔並且可以選擇命名的 運算式組成,這些運算式被括號括住。 deconstruction_expression 是一種包含隱含型別宣告表達式的元組的速記語法。

tuple_expression
    : '(' tuple_element (',' tuple_element)+ ')'
    | deconstruction_expression
    ;
    
tuple_element
    : (identifier ':')? expression
    ;
    
deconstruction_expression
    : 'var' deconstruction_tuple
    ;
    
deconstruction_tuple
    : '(' deconstruction_element (',' deconstruction_element)+ ')'
    ;

deconstruction_element
    : deconstruction_tuple
    | identifier
    ;

tuple_expression 會分類為元組。

deconstruction_expressionvar (e1, ..., en)tuple_expression(var e1, ..., var en) 的簡寫,並具有相同的行為。 這會以遞歸方式套用至 deconstruction_expression中的任何巢狀 deconstruction_tuple。 因此,每個巢狀於 deconstruction_expression 內的標識符都會導入宣告表達式(§12.17)。 因此,deconstruction_expression 只能出現在簡單賦值的左側。

只有當每個元素表達式 Ei 都具有類型 Ti時,元組運算式才會具有類型。 此類型應該是與元組運算式具有相同元數的元組類型,其中每個元素都由下列規則指定:

  • 如果對應位置中的 Tuple 元素具有名稱 Ni,則 Tuple 類型元素應為 Ti Ni
  • 否則,如果 Ei 的型態為 NiE.NiE?.Ni,元組類型元素應為 Ti Ni, ,除非滿足以下任意條件
    • Tuple 表達式中的另一個元素名稱為 Ni或其他名稱
    • 另一個未命名的元組元素具有像 NiE.NiE?.Ni這樣的元組元素表示式,或
    • Ni 的格式為 ItemX,其中 X 是一連串非0起始的小數位數,可代表元組專案的位置,而 X 不代表專案的位置。
  • 否則,該元組類型元素應為Ti

元組表達式的評估方式是從左至右評估每個元素表達式。

可以透過將一個元組表達式轉換為元組類型(§10.2.13),重新分類為值(§12.2.2),或將其設定為解構賦值的目標(§12.21.2),從而獲得一個元組值。

範例

(int i, string) t1 = (i: 1, "One");
(long l, string) t2 = (l: 2, null);
var t3 = (i: 3, "Three");          // (int i, string)
var t4 = (i: 4, null);             // Error: no type

在此範例中,這四個 Tuple 運算式都是有效的。 前兩個 t1t2,不使用元組表達式的類型,而是改為套用隱含的元組轉換。 在 t2的情況下,隱含的元組轉換依賴於從 2long 的隱含轉換,以及從 nullstring的轉換。 第三個元組表達式的類型為 (int i, string),因此可以將其重新分類為該類型的值。 另一方面,t4的宣告是錯誤的:元組運算式沒有類型,因為其第二個元素沒有類型。

if ((x, y).Equals((1, 2))) { ... };

此範例顯示元組有時可能會導致多層括弧,特別是當元組運算式是方法呼叫的唯一參數時。

結束範例

12.8.7 成員存取

12.8.7.1 一般

member_access 包含 primary_expressionpredefined_typequalified_alias_member,後面接著 “.” token,後面接著 標識符;可以接著 type_argument_list

member_access
    : primary_expression '.' identifier type_argument_list?
    | predefined_type '.' identifier type_argument_list?
    | qualified_alias_member '.' identifier type_argument_list?
    ;

predefined_type
    : 'bool' | 'byte' | 'char' | 'decimal' | 'double' | 'float' | 'int'
    | 'long' | 'object' | 'sbyte' | 'short' | 'string' | 'uint' | 'ulong'
    | 'ushort'
    ;

qualified_alias_member 生成定義於 第14.8條中。

member_access 是形式 E.I 或形式 E.I<A₁, ..., Aₑ>,其中 Eprimary_expressionpredefined_typequalified_alias_member,I 是單一識別子,<A₁, ..., Aₑ> 是可選的 type_argument_list。 未指定 type_argument_list 時,e 可視為零。

具有 dynamic 類型的 primary_expression 中的 member_access 會動態系結(§12.3.3)。 在這種情況下,編譯器會將成員存取分類為 dynamic類型的屬性存取。 下列規則會在執行時以執行時類型來確定 member_access 的意義,而不是使用 primary_expression的編譯時類型。 如果此運行時間分類導致方法群組,則成員存取權應該是 invocation_expressionprimary_expression

member_access 被評估並被分類如下所示:

  • 如果 e 為零,且 E 是命名空間,且 E 包含名稱為 I的巢狀命名空間,則結果是該命名空間。
  • 否則,如果 E 是命名空間,且 E 包含具有名稱 IK 類型參數的可存取類型,則結果就是使用指定型別自變數建構的類型。
  • 如果 E 被分類為一種型別,並且 E 不是型別參數,並且在具有 K 型別參數的 E 中對 I 的成員查閱(§12.5)會產生匹配項,則 E.I 會被評估並分類如下:

    附注:當這類成員查閱的結果是方法群組,且 K 為零時,方法群組可以包含具有類型參數的方法。 這可讓這類方法被考慮用於類型參數推斷。 尾注

    • 如果 I 識別類型,則結果就是使用任何指定型別自變數建構的類型。
    • 如果 I 識別一或多個方法,則結果會是沒有相關聯實例表達式的方法群組。
    • 如果 I 識別靜態屬性,則結果會是沒有相關聯實例表達式的屬性存取。
    • 如果 I 識別靜態字段:
      • 如果欄位是唯讀的,而且參考發生在宣告欄位的類別或結構靜態建構函式之外,則結果為值,也就是 E中靜態字段的值 I
      • 否則,結果是變數,也就是 E中的靜態字段 I
    • 如果 I 識別靜態事件:
      • 如果參考發生在宣告事件的類別或結構內,而且宣告事件時沒有 event_accessor_declarations\15.8.1),則 E.I 會如同 I 是靜態字段一樣處理。
      • 否則,結果是事件存取,沒有相關聯的實例表達式。
    • 如果 I 識別常數,則結果為值,也就是該常數的值。
    • 如果 I 識別列舉成員,則結果為值,也就是該列舉成員的值。
    • 否則,E.I 是無效的成員參考,而且會發生編譯時期錯誤。
  • 如果 E 是屬性存取、索引器存取、變數或值,且其類型是 T,並且在 T 中,使用 K 類型參數對 I 進行成員查閱(§12.5)會產生匹配項,則 E.I 會被評估並分類如下:
    • 首先,如果 E 是屬性或索引器存取,則會取得屬性或索引器存取的值(\12.2.2),而 E 會重新分類為值。
    • 如果 I 識別出一個或多個方法,那麼結果將是一個方法群組,具有與 E相關聯的實例表示式。
    • 如果 I 識別的是一個實例屬性,那麼結果將是一個具有 E 相關聯實例表達式的屬性存取,並且它的相關聯型別為屬性類型。 如果 T 是類別類型,則會從從 T開始尋找的屬性的第一個宣告或覆寫中挑選相關聯的類型,並搜尋其基類。
    • 如果 Tclass_type,且 I 識別該 class_type的實例字段:
      • 如果E的值是null,則會拋出System.NullReferenceException
      • 否則,如果欄位是唯讀的,而且參考發生在宣告欄位之類別的實例建構函式之外,則結果為值,也就是 E所參考之 物件中 I 域的值。
      • 否則,結果是變數,也就是 E所參考之物件中的欄位 I
    • 如果 Tstruct_type,且 I 識別該 struct_type的實例字段:
      • 如果 E 為值,或者如果欄位是唯讀的,而且參考發生在宣告欄位之結構的實例建構函式之外,則結果為值,也就是由 E所指定之結構實例中的字段值 I
      • 否則,結果是變數,也就是 E所指定結構實例中的欄位 I
    • 如果 I 識別實例事件:
      • 如果參考是在宣告事件的類別或結構內發生,而且事件是在未宣告 event_accessor_declarations 的情況下宣告的,而且\15.8.1),而且參考不會當做 a +=-= 運算符的左側發生,則 E.I 會如同 I 是實例字段一樣處理。
      • 否則,結果是事件存取,並且具有與之相關的實例表達式 E
  • 否則,將嘗試把 E.I 當作擴充方法呼叫來處理(§12.8.10.3)。 如果失敗,E.I 是無效的成員引用,而且會發生綁定時間錯誤。

12.8.7.2 完全相同的簡單名稱和類型名稱

在形式為 E.I的成員存取中,如果 E 是單一標識符,而且如果 E 作為 simple_name§12.8.4)的意義是常數、欄位、屬性、局部變數或參數,其類型與 E 作為 type_name§7.8.1)的意義相同,那麼 E 的這兩種可能意義都是允許的。 E.I 的成員檢索絕不會模糊不清,因為在這兩種情況下,I 必定為 E 類型的成員。 換句話說,規則只允許存取 E 的靜態成員和巢狀類型,否則會發生編譯錯誤。

範例

struct Color
{
    public static readonly Color White = new Color(...);
    public static readonly Color Black = new Color(...);
    public Color Complement() => new Color(...);
}

class A
{
    public «Color» Color;              // Field Color of type Color

    void F()
    {
        Color = «Color».Black;         // Refers to Color.Black static member
        Color = Color.Complement();  // Invokes Complement() on Color field
    }

    static void G()
    {
        «Color» c = «Color».White;       // Refers to Color.White static member
    }
}

僅作為說明用途,在 A 類別中,參考 Color 類型的 Color 標識符的出現次數以 «...»定界,而參考 Color 字段的標識符則不定界。

結束範例

12.8.8 Null條件式成員存取

null_conditional_member_accessmember_access 的條件式版本(\12.8.7),如果結果類型是 void,則為系結時間錯誤。 如需結果類型可能為 void 的 null 條件表示式,請參閱 (.12.8.11)。

null_conditional_member_access 包含一個 primary_expression,後面接著兩個標記 “?” 和 “.”,後面接著具有選擇性 type_argument_list標識符,後面接著零個或多個 dependent_accesses,其中任一標記都可以由 null_forgiving_operator前置。

null_conditional_member_access
    : primary_expression '?' '.' identifier type_argument_list?
      (null_forgiving_operator? dependent_access)*
    ;
    
dependent_access
    : '.' identifier type_argument_list?    // member access
    | '[' argument_list ']'                 // element access
    | '(' argument_list? ')'                // invocation
    ;

null_conditional_projection_initializer
    : primary_expression '?' '.' identifier type_argument_list?
    ;

E null_conditional_member_access 表示式的格式為 P?.AE 的意義定義如下:

  • 如果 P 的類型是可為 Null 的數值型別:

    T 成為 P.Value.A的類型。

    • 如果 T 是未知為參考型別或不可為 Null 的實值型別的類型參數,則會發生編譯時期錯誤。

    • 如果 T 是不可為 Null 的實值型別,則 E 的類型是 T?,而 E 的意義與下列意義相同:

      ((object)P == null) ? (T?)null : P.Value.A
      

      唯一的不同是 P 只被評估一次。

    • 否則,E 的類型是 T,而 E 的意義與下列意義相同:

      ((object)P == null) ? (T)null : P.Value.A
      

      不同之處在於 P 只會評估一次。

  • 否則:

    T 為表示式 P.A的類型。

    • 如果 T 是類型參數,尚未確定是參考型別或非可空值類型,則會發生編譯時錯誤。

    • 如果 T 是不可為 Null 的實值型別,則 E 的類型是 T?,而 E 的意義與下列意義相同:

      ((object)P == null) ? (T?)null : P.A
      

      不同之處在於 P 只會評估一次。

    • 否則,E 的類型是 T,而 E 的意義與下列意義相同:

      ((object)P == null) ? (T)null : P.A
      

      不同之處在於 P 只會評估一次。

附註:在表單的運算式中:

P?.A₀?.A₁

然後,如果 P 評估為 null 不會評估 A₀A₁。 如果表達式是 null_conditional_member_accessnull_conditional_element_access的序列 ,則為 true , 作業 null_conditional_element_access。12.8.13。

尾注

null_conditional_projection_initializernull_conditional_member_access 的限制,並具有相同的語意。 它只會在匿名物件建立表達式中以投影初始化表達式的形式發生(\12.8.17.7)。

12.8.9 Null-forgiving(允許空值)表達式

12.8.9.1 一般

null 表示表達式的值、類型、分類(•12.2)和安全內容(16.4.12)是其 primary_expression的值、類型、分類和安全內容。

null_forgiving_expression
    : primary_expression null_forgiving_operator
    ;

null_forgiving_operator
    : '!'
    ;

附註:後置 null 允許運算符和前置邏輯否定運算符(§12.9.4),雖然都用相同的語彙標記(!)表示,但它們是不同的。 只有後者可以被覆寫(§15.10),空接受運算子的定義是固定的。 尾注

對相同的運算式多次套用空值寬容運算子,即使有括弧干擾,仍然會造成編譯時錯誤。

範例:下列全都無效:

var p = q!!;            // error: applying null_forgiving_operator more than once
var s = ( ( m(t) ! ) )! // error: null_forgiving_operator applied twice to m(t)

結束範例

本子句的其餘部分及以下同級子句為附帶條件的規範性。

執行靜態 Null 狀態分析的編譯程式 (8.9.5)必須符合下列規格。

空值寬容運算符是一種編譯時期的伪操作,用來協助編譯器的靜態空值狀態分析。 它有兩個用途:一是覆寫編譯器對一個表達式 可能為 null的判斷;二是覆寫編譯器發出與空值性相關的警告。

將 null 容許運算子套用到編譯器的靜態 null 狀態分析不會產生任何警告的運算式時,這不是錯誤。

12.8.9.2 覆寫「可能為空值」判斷

在某些情況下,編譯程式的靜態 Null 狀態分析可能會判斷表達式具有 null 狀態,可能是 null,並在其他資訊指出表達式不能為 Null 時發出診斷警告。 將 null 釋放運算子應用於這類運算式時,會通知編譯器的靜態 null 狀態分析,說明該 null 狀態實際上處於 而非 null,這樣做不僅能防止診斷警告,也可能對任何正在進行的分析提供信息。

範例:請考慮下列事項:

#nullable enable
public static void M()
{
    Person? p = Find("John");                  // returns Person?
    if (IsValid(p))
    {
       Console.WriteLine($"Found {p!.Name}");  // p can't be null
    }
}

public static bool IsValid(Person? person) =>
    person != null && person.Name != null;

如果 IsValid 傳回 truep 可以安全地解參考以存取其 Name 屬性,而且可以使用 !來抑制「可能為 Null 值的取值」警告。

結束範例

範例: 應謹慎使用 null 放棄運算符,請考慮:

#nullable enable
int B(int? x)
{
    int y = (int)x!; // quash warning, throw at runtime if x is null
    return y;
}

在這裡,null 放棄運算符會套用至實值型別,並在 x取消任何警告。 不過,如果在運行時 xnull,則會拋出例外狀況,因為 null 無法轉換成 int

結束範例

12.8.9.3 覆寫其他 Null 分析警告

除了如上所述覆寫 可能為 null 的判斷外,還可能有其他情況需要覆寫編譯器的靜態 null 狀態分析,針對表達式做出的需要一個或多個警告的判斷。 將 null 寬容運算子套用至這類表達式,要求編譯器對此類表達式不發出任何警告。 在回應中,編譯程式可能會選擇不發出警告,也可以修改其進一步分析。

範例:請考慮下列事項:

#nullable enable
public static void Assign(out string? lv, string? rv) { lv = rv; }

public string M(string? t)
{
    string s;
    Assign(out s!, t ?? "«argument was null»");
    return s;
}

方法 Assign的參數類型 lv & rvstring?lv 為輸出參數,而且會執行簡單的指派。

方法 M 傳遞類型為 string的變數 s作為 Assign的輸出參數,使用的編譯器發出警告,因為 s 不是可為 Null 的變數。 由於 Assign的第二個參數不可為 null,因此使用空值容忍運算符來取消警告。

結束範例

條件規範文本的結尾。

12.8.10 呼叫表達式

12.8.10.1 一般

調用運算式 可用來呼叫一個方法。

invocation_expression
    : primary_expression '(' argument_list? ')'
    ;

僅當存在 delegate_type時,primary_expression 才可能是 null_forgiving_expression

invocation_expression 會動態綁定(§12.3.3)如果至少有下列其中一項成立:

  • primary_expression 具有編譯時間類型 dynamic
  • 可選的 參數列表 中至少有一個參數具有編譯時類型 dynamic

在此情況下,編譯程式會將 invocation_expression 分類為 類型 dynamic的值。 接著,會在運行時間使用運行時間類型來套用下列規則,而非使用 primary_expression 和具有編譯時間類型 dynamic的自變數的編譯時間類型,以判斷 invocation_expression 的意義。 如果 primary_expression 並非編譯時期型別 dynamic,則方法呼叫會進行有限度的編譯時期檢查,如 §12.6.5中所述。

invocation_expressionprimary_expression 應為方法集或 委派類型的值。 如果 primary_expression 是方法群組,則 invocation_expression 是方法調用(第12.8.10.2節)。 如果 primary_expressiondelegate_type的值,則 invocation_expression 是委派呼叫(§12.8.10.4)。 如果 primary_expression 既不是方法群組,也不是 delegate_type的值,則會發生系結時間錯誤。

選擇性 argument_list§12.6.2)會為方法的參數提供值或變數參考。

評估 invocation_expression 的結果分類如下:

  • 如果 invocation_expression 叫用不返回任何值的方法(§15.6.1)或不返回任何值的委派,則結果不會產生任何值。 只有在 statement_expression§13.7) 或作為 lambda_expression§12.19) 的主體中,才允許將一個表達式歸類為「無」。 否則,會發生系結時間錯誤。
  • 否則,如果 invocation_expression 呼叫引用回傳的方法(§15.6.1)或引用回傳的委派,則結果是與該方法或委派的傳回型別相關聯的變數。 如果調用是實例方法,而接收者是類別類型 T,則會從從 T 開始並搜尋其基類時找到之方法的第一個宣告或覆寫中挑選相關聯的類型。
  • 否則,invocation_expression 會叫用傳回值方法(§15.6.1),或是逐值返回的委派,其結果為一個值,對應的型別是該方法或委派的傳回型別。 如果調用是實例方法,而接收者是類別類型 T,則會從從 T 開始並搜尋其基類時找到之方法的第一個宣告或覆寫中挑選相關聯的類型。

12.8.10.2 方法調用

方法呼叫中的 invocation_expressionprimary_expression 應該是方法群組。 方法群組識別出要叫用的一個方法,或從一系列多載方法中選擇一個特定方法來叫用。 在後一種情況下,判斷要叫用的特定方法是根據 argument_list中引數的類型所提供的上下文。

表單 M(A)的方法調用的系結時間處理,其中 M 是方法群組(可能包括 type_argument_list),而 A 是選擇性的 argument_list,包含下列步驟:

  • 方法調用的候選方法集合已建立。 針對每個與方法群組 M相關聯的方法 F
    • 如果 F 非泛型,F 為候選項,當:
      • M 沒有類型自變數清單,且
      • F 適用於 A12.6.4.2)。
    • 如果 F 是泛型且 M 沒有類型自變數清單,則 F 為候選項目,當:
      • 類型推斷(§12.6.3)成功,推斷出一個呼叫的類型參數清單,並
      • 一旦推斷的類型自變數取代對應的方法類型參數,F 參數清單中的所有建構型別都滿足其條件約束(\8.4.5),而且 F 的參數列表適用於 A12.6.4.2
    • 如果 F 為泛型,並且 M 包含型別參數列表,F 是潛在候選者時:
      • F 方法類型參數數目與類型自變數清單中提供的方法類型參數數目相同,以及
      • 一旦型別自變數取代對應的方法類型參數,F 參數清單中的所有建構型別都滿足其條件約束(\8.4.5),而且 F 的參數列表適用於 A\12.6.4.2)。
  • 候選方法集合會縮減為只包含來自大多數衍生型別的方法:針對集合中的每個方法 C.F,其中 C 是宣告方法 F 的類型,則會從集合中移除以基底類型宣告的所有方法 C。 此外,如果 Cobject以外的類別類型,則會從集合中移除介面類型中宣告的所有方法。

    附註:此後面的規則只有在方法群組是針對型別參數進行成員查詢的結果時才有作用,且該型別參數擁有非 object 的有效基類和非空的有效介面集。 尾注

  • 如果產生的候選方法集是空的,則會放棄下列步驟的進一步處理,而是嘗試將調用當作擴充方法調用處理(“”\12.8.10.3)。 如果失敗,則沒有任何適用的方法存在,而且會發生系結時間錯誤。
  • 一組候選方法的最佳方法是使用 12.6.4的多載解析規則來識別。 如果無法識別單一最佳方法,則方法調用模棱兩可,而且會發生系結時間錯誤。 執行多載解析時,會考慮泛型方法的參數,再取代對應方法類型參數的類型自變數(提供或推斷)。

上述步驟在系結時間選取並驗證方法之後,會根據 中所述的函式成員調用規則來處理實際的運行時間調用。

附註:上述解析規則的直觀效果如下:為了找出方法調用中實際執行的方法,請從方法調用指示的類型開始,沿著繼承鏈尋找,直到找到至少一個適用、可存取且非覆寫的方法宣告為止。 然後在該類型中宣告的適用、可存取、非覆寫方法集合上執行類型推斷和多載解析,並叫用選取的方法。 如果找不到方法,請嘗試改為將調用當做擴充方法調用來處理。 尾注

12.8.10.3 擴充方法調用

在方法呼叫(§12.6.6.2) 的其中一種形式中

«expr» . «identifier» ( )  
«expr» . «identifier» ( «args» )  
«expr» . «identifier» < «typeargs» > ( )  
«expr» . «identifier» < «typeargs» > ( «args» )

如果調用的一般處理找不到適用的方法,則會嘗試將建構當做擴充方法調用來處理。 如果 «expr» 或任何 «args» 具有編譯時間類型 dynamic,則不會套用擴充方法。

目標是尋找最佳的 type_nameC,以便進行對應的靜態方法調用:

C . «identifier» ( «expr» )  
C . «identifier» ( «expr» , «args» )  
C . «identifier» < «typeargs» > ( «expr» )  
C . «identifier» < «typeargs» > ( «expr» , «args» )

擴充方法 Cᵢ.Mₑ符合資格 的條件是:

  • Cᵢ 是非泛型非巢狀類別
  • Mₑ 的名稱是 識別碼
  • Mₑ 作為靜態方法,在套用至參數時是可存取且適用的,如上所示
  • 隱式標識、參考或 boxing 轉換存在於從 exprMₑ的第一個參數類型。

C 的搜尋過程如下:

  • 從最接近的封入命名空間宣告開始,然後繼續每個封入命名空間宣告,並以包含的編譯單位結尾,接著會嘗試尋找一組候選的擴充方法群組:
    • 如果指定的命名空間或編譯單位直接包含非泛型型別宣告 Cᵢ,並且這些宣告具有合格的擴充方法 Mₑ,則這些擴充方法的集合即為候選集合。
    • 如果指定的命名空間或編譯單位使用命名空間指示詞來匯入的命名空間中,直接包含具有合格擴充方法的非泛型型別宣告 Cᵢ 並列出相應的擴充方法 Mₑ,那麼這些擴充方法的集合將成為候選集。
  • 如果在任何封入命名空間宣告或編譯單位中找不到候選集,就會發生編譯時期錯誤。
  • 否則,多載解析會套用至候選集,如 •12.6.4中所述。 如果找不到任何最佳方法,就會發生編譯時間錯誤。
  • C 是宣告最佳方法為擴充方法的類型。

使用 C 作為目標,接著將方法呼叫處理為靜態方法調用(§12.6.6)。

Note:不同於實例方法呼叫,當 expr 計算結果為 null 參考時,不會擲回任何例外狀況。 相反地,這個 null 值會傳遞至擴充方法,彷彿是透過一般靜態方法調用。 由擴充方法的實作方式來決定如何回應這類呼叫。 尾注

上述規則表示實例方法優先於擴充方法,內部命名空間宣告中的擴充方法優先於外部命名空間宣告中的擴充方法,且直接在命名空間中宣告的擴充方法優先於使用 namespace 指令匯入到相同命名空間的擴充方法。

範例

public static class E
{
    public static void F(this object obj, int i) { }
    public static void F(this object obj, string s) { }
}

class A { }

class B
{
    public void F(int i) { }
}

class C
{
    public void F(object obj) { }
}

class X
{
    static void Test(A a, B b, C c)
    {
        a.F(1);            // E.F(object, int)
        a.F("hello");      // E.F(object, string)
        b.F(1);            // B.F(int)
        b.F("hello");      // E.F(object, string)
        c.F(1);            // C.F(object)
        c.F("hello");      // C.F(object)
    }
}

在此範例中,B的方法優先於第一個擴充方法,而 C的方法優先於這兩個擴充方法。

public static class C
{
    public static void F(this int i) => Console.WriteLine($"C.F({i})");
    public static void G(this int i) => Console.WriteLine($"C.G({i})");
    public static void H(this int i) => Console.WriteLine($"C.H({i})");
}

namespace N1
{
    public static class D
    {
        public static void F(this int i) => Console.WriteLine($"D.F({i})");
        public static void G(this int i) => Console.WriteLine($"D.G({i})");
    }
}

namespace N2
{
    using N1;

    public static class E
    {
        public static void F(this int i) => Console.WriteLine($"E.F({i})");
    }

    class Test
    {
        static void Main(string[] args)
        {
            1.F();
            2.G();
            3.H();
        }
    }
}

此範例的輸出如下:

E.F(1)
D.G(2)
C.H(3)

D.G 優先於 C.GE.F 優先於 D.FC.F

結束範例

12.8.10.4 委派調用

對於委派調用,invocation_expressionprimary_expression 應該是 delegate_type的值。 此外,將 delegate_type 視為與 delegate_type相同的參數清單的函式成員,delegate_type 應適用於 invocation_expressionargument_list\12.6.4.2

針對形式 D(A)的委派調用之執行時處理,其中 Ddelegate_typeprimary_expression,而 A 是選擇性的 argument_list,包括以下步驟:

  • 評估 D。 如果此評估造成例外狀況,則不會執行任何進一步的步驟。
  • 參數列表 A 被評估。 如果此評估造成例外狀況,則不會執行任何進一步的步驟。
  • D 的值會被檢查是否有效。 如果 D 的值是 null,則會擲回 System.NullReferenceException,而且不會執行任何進一步的步驟。
  • 否則,D 是委派實例的參考。 函數成員呼叫(§12.6.6)在委派呼叫清單中的每個可呼叫實體上進行。 對於由實例和實例方法所組成的可呼叫實體,調用的實例是可呼叫實體中包含的實例。

如需沒有參數的多個調用清單的詳細數據,請參閱 •20.6

12.8.11 空條件呼叫表達式

null_conditional_invocation_expression 是語法上的 null_conditional_member_access§12.8.8)或 null_conditional_element_access§12.8.13),其中最後一個 dependent_access 是调用表达式(§12.8.10)。

null_conditional_invocation_expression 發生在 statement_expression§13.7)、anonymous_function_body§12.19.1)或 method_body§15.6.1)。

與語法上對等的 null_conditional_member_accessnull_conditional_element_access不同,null_conditional_invocation_expression 可能被視為無。

null_conditional_invocation_expression
    : null_conditional_member_access null_forgiving_operator? '(' argument_list? ')'
    | null_conditional_element_access null_forgiving_operator? '(' argument_list? ')'
    ;

只有在 null_conditional_member_accessnull_conditional_element_access 具有 delegate_type時,才可能包含選擇性 null_forgiving_operator

E null_conditional_invocation_expression 表示式的格式為 P?A;其中 A 是語法相等 null_conditional_member_accessnull_conditional_element_access的其餘部分,A 會從 .[開始。 讓 PA 表示 PA的統一。

E 作為 statement_expression 時,E 的意義與 語句的意義相同:

if ((object)P != null) PA

不同之處在於 P 只會評估一次。

E 作為 anonymous_function_bodymethod_body 時,E 的意義取決於其分類:

  • 如果 E 被歸類為「無」,那麼其意義等同於 區塊的意義。

    { if ((object)P != null) PA; }
    

    不同之處在於 P 只會評估一次。

  • 否則,E 的意義與 區塊的意義相同:

    { return E; }
    

    進而,這個 區塊 的意義取決於 E 在語法上是否相當於 null_conditional_member_access§12.8.8)或 null_conditional_element_access§12.8.13)。

12.8.12 元素存取

12.8.12.1 一般

element_access 包含 primary_no_array_creation_expression,然後接著「[」符號,再接著 argument_list,最後是「]」符號。 argument_list 包含一或多個 自變數,並以逗號分隔。

element_access
    : primary_no_array_creation_expression '[' argument_list ']'
    ;

不允許 element_accessargument_list 包含 outref 引數。

如果滿足下列其中一項,element_access 就會動態綁定(§12.3.3):

  • primary_no_array_creation_expression 的編譯時間類型是 dynamic
  • argument_list 中至少有一個運算式具有編譯時期類型 dynamic,而 primary_no_array_creation_expression 並非陣列類型。

在此情況下,編譯程式會將 element_access 分類為 類型 dynamic的值。 接著,將套用以下規則來判斷 element_access 的意義,這些規則在運行時會使用運行時類型,而非 primary_no_array_creation_expressionargument_list 運算式的編譯時間類型,這些運算式具有編譯時間類型 dynamic。 如果 primary_no_array_creation_expression 沒有編譯時類型 dynamic,則元素存取只會進行有限的編譯時檢查,如 §12.6.5中所述。

如果 element_accessprimary_no_array_creation_expressionarray_type的值,則 element_access 是陣列存取權(\12.8.12.2)。 否則,primary_no_array_creation_expression 應該是具有一或多個索引器成員之類別、結構或介面類型的變數或值,在此情況下,element_access 是索引器存取權(.12.8.12.3)。

12.8.12.2 陣組存取

針對陣列存取,primary_no_array_creation_expressionelement_access 中應該是 array_type的一個值。 此外,陣列存取的 argument_list 不允許包含具名參數。 argument_list 中的表達式數目應與 array_type的等級相同,而且每個表達式的類型應為 intuintlongulong,,或可隱含轉換成其中一或多個類型。

評估陣列存取的結果是陣列元素類型的變數,即由 argument_list中表達式的值所選取的陣列元素。

形式 P[A]的陣列存取在執行期間的處理,其中 Parray_typeprimary_no_array_creation_expression,而 Aargument_list,包含以下步驟:

  • P 被評估。 如果此評估造成例外狀況,則不會執行任何進一步的步驟。
  • argument_list 的索引表達式會依左至右的順序進行評估。 在評估每個索引表示式之後,會執行下列其中一種類型的隱含轉換(\10.2intuintlongulong。 在此清單中,選擇具有隱含轉換的第一個類型。 例如,如果索引表達式的類型為 short,則會執行隱含轉換至 int,因為可能會將隱含轉換從 short 轉換成 int,以及從 short 轉換成 long。 如果評估索引表達式或後續的隱含轉換會造成例外狀況,則不會評估任何進一步的索引表達式,也不會執行任何進一步的步驟。
  • P 的值會被檢查是否有效。 如果 P 的值是 null,則會擲回 System.NullReferenceException,而且不會執行任何進一步的步驟。
  • argument_list 中的每個運算式值都會根據 P所參考之陣列實例的每個維度的實際界限進行檢查。 如果一或多個值超出範圍,則會擲回 System.IndexOutOfRangeException,而且不會執行任何進一步的步驟。
  • 計算由索引表達式指定的陣列元素位置,該位置將成為陣列存取的結果。

12.8.12.3 索引器存取

對於索引器存取,element_accessprimary_no_array_creation_expression 應該是類別、結構或介面類型的變數或值,而此類型應實作一或多個適用於 element_accessargument_list 的索引器。

P[A]形式的索引器存取的綁定時間處理,其中,P 是類別、結構或介面類型 Tprimary_no_array_creation_expression,而 Aargument_list,包含以下步驟:

  • 建構 T 所提供的索引器集合。 此集合包含所有在 T 或其基底類型 T 中宣告、且非覆寫宣告的索引器,並且可以在目前上下文中存取(§7.5)。
  • 此集合會縮減為適用且未由其他索引器隱藏的索引器。 下列規則會套用至集合中的每個索引器 S.I,其中 S 是宣告索引器 I 的類型:
    • 如果 I 不適用於 A\12.6.4.2),則會從集合中移除 I
    • 如果 I 適用於 A§12.6.4.2),則會從集合中移除在 S 的基底類型中宣告的所有索引器。
    • 如果 I 適用於 A§12.6.4.2),且 S 是非 object的類別類型,則會從集合中移除所有宣告於介面的索引器。
  • 如果產生的候選索引器集是空的,則沒有任何適用的索引器存在,而且會發生系結時間錯誤。
  • 使用 §12.6.4的多載解析規則來識別一組候選索引器中的最佳索引器。 如果無法識別單一最佳索引器,索引器存取就會模棱兩可,而且會發生系結時間錯誤。
  • argument_list 的索引表達式會依左至右的順序進行評估。 處理索引器存取的結果是分類為索引器存取的表達式。 索引器存取表達式會參考在上述步驟中判斷的索引器,並具有 P 的關聯實例表達式和 A的相關自變數清單,以及索引器類型的關聯型別。 如果 T 是類別類型,則會從從 T 開始並搜尋其基類時找到的第一個宣告或索引器覆寫中挑選相關聯的類型。

根據所使用的內容,索引器存取會導致叫用 get 存取子或索引器的 set 存取子。 如果索引器存取是指派的目標,則會調用 set 存取子來設定新的值(§12.21.2)。 在其他所有情況下,會叫用 get 存取子來取得目前的值(§12.2.2)。

12.8.13 Null條件式元素存取

null_conditional_element_access 包含一個 primary_no_array_creation_expression,後面接著兩個標記“?”和“[”,再接著 argument_list,然後是“]”標記,接著可以有零個或多個 dependent_access,其中每個都可以用 null_forgiving_operator作為前置運算符。

null_conditional_element_access
    : primary_no_array_creation_expression '?' '[' argument_list ']'
      (null_forgiving_operator? dependent_access)*
    ;

null_conditional_element_accesselement_access 的條件式版本(.12.8.12),如果結果類型是 void,則為系結時間錯誤。 如需結果類型可能為 void 的 null 條件表示式,請參閱 (.12.8.11)。

null_conditional_element_access 表示式 E 的格式為 P?[A]B;其中 Bdependent_access,如果有的話。 E 的意義如下所示:

  • 如果 P 的類型是可為空的值型別:

    T 成為 P.Value[A]B的表示式類型。

    • 如果 T 是一個未知會是參考型別或非 null 值類型的型別參數,則會發生編譯時期錯誤。

    • 如果 T 是不可為 Null 的實值型別,則 E 的類型是 T?,而 E 的意義與下列意義相同:

      ((object)P == null) ? (T?)null : P.Value[A]B
      

      不同之處在於 P 只會被計算一次。

    • 否則,E 的類型是 T,而 E 的意義與下列意義相同:

      ((object)P == null) ? null : P.Value[A]B
      

      不同之處在於 P 只會評估一次。

  • 否則:

    TP[A]B的表示類型。

    • 如果 T 是不確定是否為參考型別或不可為 Null 的值型別的類型參數,則會發生編譯時錯誤。

    • 如果 T 是不可為 Null 的實值型別,則 E 的類型是 T?,而 E 的意義與下列意義相同:

      ((object)P == null) ? (T?)null : P[A]B
      

      唯一的不同是 P 只會被評估一次。

    • 否則,E 的類型是 T,而 E 的意義與下列意義相同:

      ((object)P == null) ? null : P[A]B
      

      不同之處在於 P 只會評估一次。

附註:在某種形式的運算式中:

P?[A₀]?[A₁]

如果 P 的結果為 null,則不會評估 A₀A₁。 如果一個表達式是由 null_conditional_element_accessnull_conditional_member_access§12.8.8 作業組成的序列,情況也是如此。

尾注

12.8.14 此存取

this_access 是由 關鍵詞 this所組成。

this_access
    : 'this'
    ;

只有在實例建構函式、實例方法、實例存取子(§12.2.1)或完成項的 區塊 中,才允許 this_access。 它有下列其中一個意義:

  • this 用於類別實例建構函式內的 primary_expression 時,它會分類為值。 值的型別是使用發生的類別的實例型別(§15.3.2),而該值則是建構中物件的參考。
  • this 用於類別實例方法或實例存取子內的 primary_expression 時,它會分類為值。 值的型別是用法所在的類別的實例類型(§15.3.2),而值是方法或存取子的被叫用對象之引用。
  • this 用於結構實例建構函式內的 primary_expression 時,它會分類為變數。 變數的類型是該變數使用所在結構的實例類型(§15.3.2),並且該變數代表正在建構的結構。
    • 如果建構函式宣告沒有建構函式初始化表達式,則 this 變數的行為與結構類型的輸出參數完全相同。 特別是,這表示變數應該在實例建構函式的每個執行路徑中明確指派。
    • 否則,this 變數的行為與結構類型的 ref 參數完全相同。 特別是,這表示變數一開始會被視為已指派。
  • this 用於結構實例方法或實例存取子內的 primary_expression 時,它會分類為變數。 變數的類型是其用法所處結構的實例類型(§15.3.2)。
    • 如果方法或存取子不是反覆運算器(•15.14)或異步函式(\15.15),則 this 變數代表叫用方法或存取子的結構。
      • 如果結構是 readonly struct,則 this 變數的行為與結構類型的輸入參數完全相同
      • 否則,this 變數的行為與結構類型的 ref 參數完全相同
    • 如果方法或存取子是反覆運算器或異步函式, 變數代表叫用方法或存取子之結構 複本,其行為與結構類型的 參數完全相同。

在上述情境之外使用 thisprimary_expression 是編譯時錯誤。 特別是,無法在靜態方法、靜態屬性存取子或欄位宣告的 變數初始化器 中參考 this

12.8.15 基本存取

base_access 包含關鍵詞基底,後面接著 「.」 token 和標識碼和選擇性 type_argument_list 或以方括弧括住的 argument_list

base_access
    : 'base' '.' identifier type_argument_list?
    | 'base' '[' argument_list ']'
    ;

base_access 可用來存取目前類別或結構中類似具名成員所隱藏的基類成員。 只有在實例建構函式、實例方法、實例存取器(§12.2.1)或終結器的主體內,才允許 base_access。 當 base.I 出現在類別或結構中時,我將用來表示該類別或結構的基類成員。 同樣地,當 base[E] 發生在類別中時,適用的索引器應該存在於基類中。

在綁定時,base_access 表達式形式 base.Ibase[E] 的評估方式,正如同它們被寫成 ((B)this).I((B)this)[E]一樣,其中 B 是構造所在類別或結構的基類。 因此,base.Ibase[E] 對應至 this.Ithis[E],但 this 視為基類的實例。

base_access 參考虛擬函式成員(方法、屬性或索引器)時,決定在運行時間叫用的函式成員(\12.6.6)。 被呼叫的函式成員是透過尋找與 B 相關的函式成員的最衍生實作來確定的(§15.6.4),而不是根據 this的運行時型別,這是通常在非基礎存取中執行的方式。 因此,在虛擬函式成員的覆寫過程中,可以使用 base_access 來呼叫該函式成員的繼承實作。 如果 base_access 所參考的函式成員是抽象的,則會發生系結時間錯誤。

Note:不同於 thisbase 本身不是表達式。 它是只用於 base_accessconstructor_initializer 語境中的關鍵詞(§15.11.2)。 尾注

12.8.16 後置遞增和遞減運算符

post_increment_expression
    : primary_expression '++'
    ;

post_decrement_expression
    : primary_expression '--'
    ;

後置遞增或遞減操作的操作數應該是被歸類為變數、屬性存取或索引器存取的表達式。 作業的結果是與操作數相同的類型值。

如果 primary_expression 具有編譯時間類型 dynamic,則運算符會動態系結(!12.3.3),post_increment_expressionpost_decrement_expression 具有編譯時間類型 dynamic,而且下列規則會在運行時間使用 primary_expression的運行時間類型來套用。

如果後置遞增或遞減運算的操作數是屬性或索引器存取,那麼該屬性或索引器必須同時具有 get 和 set 存取子。 如果情況並非如此,則會發生系結時間錯誤。

一元運算子多載解析 (•12.4.4) 會套用至選取特定運算符實作。 下列類型存在預先定義的 ++-- 運算符:sbytebyteshortushortintuintlongulongcharfloatdoubledecimal及任何列舉類型。 預先定義的 ++ 運算符會傳回將 1 加入操作數所產生的值,而預先定義的 -- 運算符會傳回從操作數減去 1 所產生的值。 在已檢查的內容中,如果這個加法或減法的結果超出結果型別的範圍,而結果型別是整數型別或列舉型別,則會擲回 System.OverflowException

將選取一元運算符的傳回型別隱含轉換成 primary_expression型別,否則會發生編譯時期錯誤。

表單 x++x-- 後置遞增或遞減作業的運行時間處理包含下列步驟:

  • 如果 x 分類為變數:
    • x 被評估以生成變數。
    • x 的值已保存。
    • x 的已儲存值會轉換成所選取運算子的操作數類型,並使用此值作為其自變數來叫用 運算符。
    • 運算符傳回的值會轉換成 x 類型,並儲存在先前評估 x所指定的位置。
    • 已儲存的 x 值會成為運算的結果。
  • 如果 x 分類為屬性或索引器存取:
    • 實例表達式(如果 x 不是 static)和與 x 相關聯的參數清單(如果 x 是索引器存取)會被評估,這些結果將在隨後的 get 和 set 存取子調用中用到。
    • 會叫用 x 的 get 存取子,並儲存傳回的值。
    • x 的已儲存值會轉換成所選取運算子的操作數類型,並使用此值作為其自變數來叫用 運算符。
    • 運算子返回的值會轉換成 x 類型,並使用這個值作為其 value 參數來調用 x 的 set 存取子。
    • 儲存於 x 的數值成為此操作的結果。

++-- 運算符也支援前置表示法 (§12.9.6)。 的結果是作業 之前 的值,而 的結果則是 作業之後 的值。 不論是哪一種情況,x 本身在作業之後都有相同的值。

您可以使用後置或前置表達法來調用運算子 ++-- 的實作。 這兩個表示法不可能有個別的運算符實作。

12.8.17 新運算符

12.8.17.1 一般

new 運算子可用來建立型別的新實例。

新運算式有三種形式:

  • 物件建立表達式和匿名物件建立表達式可用來建立類別類型和實值型別的新實例。
  • 陣列建立運算式用於創建陣列類型的新實例。
  • 委派創建表達式可用來獲得委派類型的實例。

new 運算符意味著創建一個類型的實例,但不一定意味著分配記憶體。 特別是,實值型別的實例不需要超出其所在變數的任何額外記憶體,並且當使用 new 建立實值型別的實例時,不會發生任何記憶體配置。

附注:委派建立表達式不一定會建立新的實例。 當表達式以與方法群組轉換相同的方式處理時(“\10.8)或匿名函式轉換(\10.7),可能會導致重複使用現有的委派實例。 尾注

12.8.17.2 物件建立表達式

object_creation_expression 可用來建立 class_typevalue_type的新實例。

object_creation_expression
    : 'new' type '(' argument_list? ')' object_or_collection_initializer?
    | 'new' type object_or_collection_initializer
    ;

object_or_collection_initializer
    : object_initializer
    | collection_initializer
    ;

object_creation_expression類型 應該是 class_typevalue_typetype_parameter類型 不能是 元組類型 或抽象或靜態的 類別類型

當且僅當 類型class_typestruct_type時,才允許選擇性的 argument_list(第§12.6.2節)。

物件建立表達式可以省略建構函式自變數清單和括住括弧,前提是它包含物件初始化表達式或集合初始化表達式。 省略建構函式自變數清單和括住括弧相當於指定空的自變數清單。

處理包含物件初始化器或集合初始化器的物件創建表達式包括首先處理實例構造函數,然後處理物件初始化器所指定的成員或元素初始化(§12.8.17.3)或集合初始化器(§12.8.17.4)。

如果選擇性 argument_list 中的任何自變數具有編譯時間類型 dynamic,則 object_creation_expression 會動態系結 (\12.3.3),而且下列規則會在運行時間使用具有編譯時間類型之 argument_list 的運行時間類型套用 dynamic。 不過,物件建立會經過有限的編譯時間檢查,如 §12.6.5中所述。

T(A)窗體 object_creation_expression 的系結時間處理,其中 Tclass_type,或 value_type,而 A 是選擇性的 argument_list,包含下列步驟:

  • 如果 Tvalue_typeA 不存在:
    • object_creation_expression 是預設建構函式呼叫。 object_creation_expression 的結果是類型為 T的值,也就是 T 的預設值,如 第8.3.3節中所定義。
  • 否則,如果 Ttype_parameterA 不存在:
    • 如果沒有針對 T指定任何實值型別條件約束或建構函式條件約束 (\15.2.5),則會發生系結時間錯誤。
    • object_creation_expression 的結果是類型參數已繫結至的運行時類型的值,也就是調用該類型的預設建構函式的結果。 運行時間類型可以是參考型別或實值型別。
  • 否則,如果 Tclass_typestruct_type
    • 如果 T 是抽象或靜態 class_type,則會發生編譯時期錯誤。
    • 要呼叫的實體建構函式將按照 的多載解析規則來決定(見)。 候選實例建構函式集合是由 T中宣告的所有可存取實例建構函式所組成,這些建構函式適用於 A(.12.6.4.2)。 如果候選實例建構函式集合是空的,或無法識別單一最佳實例建構函式,就會發生系結時間錯誤。
    • object_creation_expression 的結果是類型為 T的值,也就是叫用上述步驟中判斷的實例建構函式所產生的值。
    • 否則,object_creation_expression 無效,而且會發生系結時間錯誤。

即使 object_creation_expression 是動態系結,其編譯時類型仍為 T

T(A)物件創建表達式 object_creation_expression 的執行時處理,其中 Tclass_typestruct_type,而 A 是可選的 argument_list,包含以下步驟:

  • 如果 Tclass_type
    • 已分配類別 T 的一個新實例。 如果記憶體不足而無法配置新的實例,則會擲回 System.OutOfMemoryException,而且不會執行任何進一步的步驟。
    • 新實例的所有欄位都會初始化為預設值 (\9.3)。
    • 實例建構函式會根據函式成員調用(§12.6.6)的規則來叫用。 新配置的實例參考會自動傳遞至實例建構函式,而且實例可以從該建構函式中存取,如下所示。
  • 如果 Tstruct_type
    • T 類型的實例是藉由配置暫存局部變數來建立。 由於 struct_type 的實例建構函式必須明確將值指派給所建立實例的每個欄位,因此不需要初始化暫存變數。
    • 實例建構函式會根據函式成員呼叫的規則來調用(§12.6.6)。 新配置的實例參考會自動傳遞至實例建構函式,而且實例可以從該建構函式中存取,如下所示。

12.8.17.3 物件初始化器

物件初始化表達式 指定物件零個或多個欄位、屬性或索引元素的值。

object_initializer
    : '{' member_initializer_list? '}'
    | '{' member_initializer_list ',' '}'
    ;

member_initializer_list
    : member_initializer (',' member_initializer)*
    ;

member_initializer
    : initializer_target '=' initializer_value
    ;

initializer_target
    : identifier
    | '[' argument_list ']'
    ;

initializer_value
    : expression
    | object_or_collection_initializer
    ;

物件初始化表達式是由成員初始化表達式序列所組成,由 {} 標記括住,並以逗號分隔。 每個 member_initializer 應指定初始化的目標。 識別子 應為要初始化之物件的可存取字段或屬性命名,而以方括弧括住的 argument_list 應指定初始化物件上可存取索引器自變數。 若物件初始化包含超過一個對於相同欄位或屬性的成員初始化,則會出現錯誤。

附註:雖然物件初始化表達式不允許多次設定相同的欄位或屬性,但索引器沒有這類限制。 物件初始化表達式可能包含參考索引器的多個初始化表達式目標,甚至可能多次使用相同的索引器自變數。 尾注

每個 initializer_target 後跟著等號,然後是表達式、物件初始化表達式或集合初始化表達式之一。 物件初始化器中的表達式無法引用正在初始化的新建物件。

"指定在等號後的表達式的成員初始化器,其處理方式與對目標的指派(§12.21.2)相同。"

在等號之後指定物件初始化表達式的成員初始化表達式是 巢狀物件初始化表達式,也就是內嵌物件的初始化。 巢狀物件初始化表達式中的賦值操作被視為欄位或屬性成員的賦值,而不是將新值直接賦予給欄位或屬性。 巢狀物件初始化表達式無法套用至具有實值類型的屬性,或套用至具有實值類型的唯讀字段。

成員初始化器在等號後指定集合初始化器時,就是在初始化內嵌集合。 初始化表達式中指定的元素會新增至目標所參考的集合,而不是將一個新的集合指派至目標欄位、屬性或索引器。 目標應為符合 .12.8.17.4中所指定需求的集合類型。

當初始化表達式目標參考索引器時,索引器的參數應該只評估一次。 因此,即使自變數最終永遠不會被使用(例如,因為空的巢狀初始化表達式),它們也會評估其副作用。

範例:下列類別代表具有兩個座標的點:

public class Point
{
    public int X { get; set; }
    public int Y { get; set; }
}

您可以建立和初始化 Point 實例,如下所示:

Point a = new Point { X = 0, Y = 1 };

這具有與...相同的效果

Point __a = new Point();
__a.X = 0;
__a.Y = 1;
Point a = __a;

其中 __a 為不可見且無法存取的暫存變數。

下列類別顯示從兩個點建立的矩形,以及建立和初始化 Rectangle 實例:

public class Rectangle
{
    public Point P1 { get; set; }
    public Point P2 { get; set; }
}

您可以建立和初始化 Rectangle 實例,如下所示:

Rectangle r = new Rectangle
{
    P1 = new Point { X = 0, Y = 1 },
    P2 = new Point { X = 2, Y = 3 }
};

這具有相同的效果

Rectangle __r = new Rectangle();
Point __p1 = new Point();
__p1.X = 0;
__p1.Y = 1;
__r.P1 = __p1;
Point __p2 = new Point();
__p2.X = 2;
__p2.Y = 3;
__r.P2 = __p2;
Rectangle r = __r;

其中 __r__p1__p2 是暫時變數,否則為看不見且無法存取。

如果 Rectangle的建構函式配置兩個內嵌 Point 實例,則可以用來初始化內嵌 Point 實例,而不是指派新的實例:

public class Rectangle
{
    public Point P1 { get; } = new Point();
    public Point P2 { get; } = new Point();
}

下列建構可用來初始化內嵌 Point 實例,而不是指派新的實例:

Rectangle r = new Rectangle
{
    P1 = { X = 0, Y = 1 },
    P2 = { X = 2, Y = 3 }
};

這具有相同的效果

Rectangle __r = new Rectangle();
__r.P1.X = 0;
__r.P1.Y = 1;
__r.P2.X = 2;
__r.P2.Y = 3;
Rectangle r = __r;

結束範例

12.8.17.4 集合初始化表達式

集合初始化設定式會指定集合的元素。

collection_initializer
    : '{' element_initializer_list '}'
    | '{' element_initializer_list ',' '}'
    ;

element_initializer_list
    : element_initializer (',' element_initializer)*
    ;

element_initializer
    : non_assignment_expression
    | '{' expression_list '}'
    ;

expression_list
    : expression
    | expression_list ',' expression
    ;

集合初始化表達式是由元素初始化表達式序列所組成,由 {} 標記括住,並以逗號分隔。 每個元素初始化器會指定一個要新增到初始化集合物件中的元素,並包含一個由 {} 標記括住的表達式列表,以逗號分隔。 單一表達式初始化器可以不加上大括號來撰寫,但不能接著是指派表達式,以避免與成員初始化器混淆。 non_assignment_expression 產生式定義於 §12.22

範例:以下是包含集合初始化表達式的物件建立表達式範例:

List<int> digits = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

結束範例

套用集合初始化表達式的集合物件必須是實作 System.Collections.IEnumerable 的型別,否則會發生編譯時錯誤。 針對每個指定元素依左至右的順序,會套用一般成員查找方法來尋找名為 Add的成員。 如果成員查找的結果不是方法群組,就會發生編譯時期錯誤。 否則,多載解析會套用專案初始化表達式的表達式清單做為自變數清單,而集合初始化表達式會叫用產生的方法。 因此,集合物件應包含一個適用的實例或擴充方法,其方法名稱為 Add,對應每個元素初始化器。

範例:下列顯示類別,代表具有名稱和電話號碼清單的聯繫人,以及建立和初始化 List<Contact>

public class Contact
{
    public string Name { get; set; }
    public List<string> PhoneNumbers { get; } = new List<string>();
}

class A
{
    static void M()
    {
        var contacts = new List<Contact>
        {
            new Contact
            {
                Name = "Chris Smith",
                PhoneNumbers = { "206-555-0101", "425-882-8080" }
            },
            new Contact
            {
                Name = "Bob Harris",
                PhoneNumbers = { "650-555-0199" }
            }
        };
    }
}

其效果與相同

var __clist = new List<Contact>();
Contact __c1 = new Contact();
__c1.Name = "Chris Smith";
__c1.PhoneNumbers.Add("206-555-0101");
__c1.PhoneNumbers.Add("425-882-8080");
__clist.Add(__c1);
Contact __c2 = new Contact();
__c2.Name = "Bob Harris";
__c2.PhoneNumbers.Add("650-555-0199");
__clist.Add(__c2);
var contacts = __clist;

其中 __clist__c1__c2 是暫時變數,否則為看不見且無法存取。

結束範例

12.8.17.5 陣列建立表達式

array_creation_expression 用來創建 array_type的新的實例。

array_creation_expression
    : 'new' non_array_type '[' expression_list ']' rank_specifier*
      array_initializer?
    | 'new' array_type array_initializer
    | 'new' rank_specifier array_initializer
    ;

第一種形式的陣列創建表達式會配置一個該型別的陣列實例,該型別來自於假設刪除表達式清單中每個個別運算式後得到的型別。

範例:陣列建立表達式 new int[10,20] 會產生類型為 int[,]的陣列實例,而陣列建立表達式新 int[10][,] 會產生類型為 int[][,]的陣列實例。 結束範例

表達式清單中的每個運算式都必須是類型 intuintlongulong,或隱式可轉換為其中一或多個類型。 每個表達式的值都會決定新分配的陣列實例中對應維度的長度。 由於陣列維度的長度應該是非負值,所以在表達式清單中具有負值的常數表達式是編譯時期錯誤。

除了不安全的情境(§23.2),陣列的排列未指定。

如果第一種形式的陣列建立運算式包含一個陣列初始化子句,則表示式清單中的每個運算式應該是常數,且表示式清單所指定的階數和維度長度應符合陣列初始化子句中的設置。

在第二或第三種格式的陣列建立運算式中,指定的陣列類型或秩規範的秩必須與陣列初始化器的秩一致。 個別維度長度是從數位初始化表達式的每個對應巢狀層級中的元素數目推斷而來。 因此,下列宣告中的初始化表達式

var a = new int[,] {{0, 1}, {2, 3}, {4, 5}};

完全符合

var a = new int[3, 2] {{0, 1}, {2, 3}, {4, 5}};

第三種形式的陣列建立表示式稱為 隱含型別陣列建立表示式。 它類似於第二種形式,除了未明確指定陣列的元素類型,而是判斷為陣列初始化中運算式的最佳通用類型(§12.6.3.15)。 對於多維度陣列,也就是其中一個 rank_specifier 至少包含一個逗號的陣列,此集合包含巢狀 array_initializers 中找到的所有 表示式

陣列初始化器會在 第17.7節中進一步說明。

運算陣列生成表達式的結果會被分類為一個值,即新分配的陣列實例的引用。 陣列建立表示式的執行時間處理包含下列步驟:

  • expression_list 的維度長度表達式會依左至右的順序進行評估。 在評估每個表達式之後,會執行其中一種類型的隱含轉換(§10.2):intuintlongulong。 在此清單中,選擇存在隱含轉換的第一個類型。 如果表達式或後續的隱含轉換評估造成例外狀況,則不會評估任何進一步的表達式,也不會執行任何進一步的步驟。
  • 維度長度的計算值會經過驗證,如下所示:如果一或多個值小於零,則會擲回 System.OverflowException,而且不會執行任何進一步的步驟。
  • 已分配具指定維度長度的陣列實例。 如果記憶體不足而無法配置新的實例,則會擲回 System.OutOfMemoryException,而且不會執行任何進一步的步驟。
  • 新陣列實例的所有元素都會初始化為預設值(。9.3)。
  • 如果陣列建立表示式包含數位初始化運算式,則會評估數位初始化運算式中的每個運算式,並將其指派給其對應的數位元素。 評估與指派會依照表達式在陣列初始化程序中寫入的順序執行。也就是說,元素會按索引遞增的順序進行初始化,最右邊的維度將優先增加。 如果評估指定的表達式或對應數位元素的後續指派造成例外狀況,則不會再初始化任何元素(因此其餘元素會有其預設值)。

陣列建立運算式允許實例化具有陣列類型元素的陣列,但這類陣列的元素必須手動初始化。

範例:聲明

int[][] a = new int[100][];

會建立一個具有 100 個 int[]類型元素的單維陣列。 每個項目的初始值都是 null。 無法讓相同的數位建立表達式同時具現化子陣列和語句

int[][] a = new int[100][5]; // Error

會產生編譯時錯誤。 您可以改為手動執行子陣列的實例化,如同在

int[][] a = new int[100][];
for (int i = 0; i < 100; i++)
{
    a[i] = new int[5];
}

結束範例

附注:當陣列的陣列具有“矩形”圖形時,也就是當子陣列的長度都相同時,使用多維度陣列會更有效率。 在上述範例中,陣列數位的具現化會建立101個物件,也就是一個外部數位和100個子數組。 相反地,

int[,] a = new int[100, 5];

只會建立單一物件、二維陣列,並在單一語句中完成配置。

尾注

範例:以下是隱含型別陣列建立表達式的範例:

var a = new[] { 1, 10, 100, 1000 };                     // int[]
var b = new[] { 1, 1.5, 2, 2.5 };                       // double[]
var c = new[,] { { "hello", null }, { "world", "!" } }; // string[,]
var d = new[] { 1, "one", 2, "two" };                   // Error

最後一個表達式會造成編譯時期錯誤,因為 intstring 都無法隱含轉換成另一個表達式,因此沒有最佳的常見類型。 在此情況下,需使用明確型別的陣列建立運算式,例如將類型指定為 object[]。 或者,其中一個元素可以轉換為通用的基底類型,然後成為推斷的元素類型。

結束範例

隱含型別陣列建立表達式可以與匿名物件初始設定式(12.8.17.7)結合,以建立匿名型別的資料結構。

範例

var contacts = new[]
{
    new
    {
        Name = "Chris Smith",
        PhoneNumbers = new[] { "206-555-0101", "425-882-8080" }
    },
    new 
    {
        Name = "Bob Harris",
       PhoneNumbers = new[] { "650-555-0199" }
    }
};

結束範例

12.8.17.6 委派建立表達式

delegate_creation_expression 可用來取得 delegate_type的實例。

delegate_creation_expression
    : 'new' delegate_type '(' expression ')'
    ;

委派創建表達式的自變數應該是方法群組、匿名函數,或編譯時間類型 dynamicdelegate_type的值。 如果自變數是方法群組,它會識別 方法,而針對實例方法,則會識別要為其建立委派的物件。 如果自變數是匿名函式,它會直接定義委派目標的參數和方法主體。 如果參數是值,它會識別出要建立複本的委派實例。

如果 表達式 具有編譯時類型 dynamic,則會動態綁定 delegate_creation_expression12.8.17.6),而下列規則會使用 表達式的執行時類型在執行時套用。 否則,規則會在編譯階段套用。

D(E)表單 delegate_creation_expression 的系結時間處理,其中 Ddelegate_typeE表示式,包含下列步驟:

  • 如果 E 是方法群組,則委派建立表達式會以與方法群組轉換相同的方式(§10.8)從 E 轉換成 D來處理。

  • 如果 E 是匿名函式,委派建立表達式會以從 ED的匿名函式轉換(§10.7)相同的方式來處理。

  • 如果 E 是一個值,則 E 應該與 D相容(§20.2),而結果是參考一個新建立的委派,該委派具有調用 E的單一項目調用清單。

D(E)表單 delegate_creation_expression 的運行時間處理,其中 Ddelegate_typeE表示式,包含下列步驟:

  • 如果 E 是方法群組,委派運算式會評估為方法群組轉換(§10.8),從 ED
  • 如果 E 是匿名函式,則會將委派建立評估為從 ED 的匿名函式轉換(\10.7)。
  • 如果 Edelegate_type的值:
    • E 被評估。 如果此評估造成例外狀況,則不會執行任何進一步的步驟。
    • 如果 E 的值是 null,則會擲回 System.NullReferenceException,而且不會執行任何進一步的步驟。
    • 已分配委派型別 D 的新實例。 如果記憶體不足而無法配置新的實例,則會擲回 System.OutOfMemoryException,而且不會執行任何進一步的步驟。
    • 新的委派實例會使用單一項目呼叫清單初始化以呼叫 E

委派的調用清單會在實例化委派時決定,然後在委派的整個生命周期內維持不變。 換句話說,在建立委派之後,就無法變更委派的目標可呼叫實體。

附註:請記住,當兩個委派合併或一個委派從另一個委派中移除時,就會產生新的委派結果:沒有任何現有的委派已變更其內容。 尾注

無法建立指向屬性、索引子、使用者定義的運算子、實例建構函式、finalizer或靜態建構函式的委派物件。

範例:如上所述,當從方法群組建立委派時,委派的參數清單和返回類型會決定要選擇的哪一個多載方法。 在範例中

delegate double DoubleFunc(double x);

class A
{
    DoubleFunc f = new DoubleFunc(Square);

    static float Square(float x) => x * x;
    static double Square(double x) => x * x;
}

A.f 欄位會使用參考第二個 Square 方法的委派初始化,因為該方法完全符合參數清單,並傳回 DoubleFunc的型別。 如果沒有第二個 Square 方法,就會發生編譯時期錯誤。

結束範例

12.8.17.7 匿名物件建立表達式

anonymous_object_creation_expression 用來建立匿名型別的物件。

anonymous_object_creation_expression
    : 'new' anonymous_object_initializer
    ;

anonymous_object_initializer
    : '{' member_declarator_list? '}'
    | '{' member_declarator_list ',' '}'
    ;

member_declarator_list
    : member_declarator (',' member_declarator)*
    ;

member_declarator
    : simple_name
    | member_access
    | null_conditional_projection_initializer
    | base_access
    | identifier '=' expression
    ;

匿名物件初始化表達式會宣告匿名型別,並傳回該類型的實例。 匿名類型是直接繼承自 object的無名稱類別類型。 匿名型別的成員是從用來建立型別實例的匿名物件初始化表達式推斷的只讀屬性序列。 具體而言,表單的匿名物件初始化表達式

new { p₁=e₁,p₂=e₂, ... pv=ev}

宣告表單的匿名類型

class __Anonymous1
{
    private readonly «T1» «f1»;
    private readonly «T2» «f2»;
    ...
    private readonly «Tn» «fn»;

    public __Anonymous1(«T1» «a1», «T2» «a2»,..., «Tn» «an»)
    {
        «f1» = «a1»;
        «f2» = «a2»;
        ...
        «fn» = «an»;
    }

    public «T1» «p1» { get { return «f1»; } }
    public «T2» «p2» { get { return «f2»; } }
    ...
    public «Tn» «pn» { get { return «fn»; } }
    public override bool Equals(object __o) { ... }
    public override int GetHashCode() { ... }
}

其中每個 «Tx» 都是對應表達式 «ex»的類型。 member_declarator 中使用的運算式應具有型別。 因此,在 member_declarator 中,若表達式為 null 或匿名函式,則會產生編譯期錯誤。

編譯程式會自動產生匿名型別及其 Equals 方法的參數名稱,而且無法在程式文字中參考。

在同一個程式中,兩個匿名物件初始化表達式會以相同順序指定相同名稱和編譯時間類型的屬性序列,將產生相同匿名類型的實例。

範例:在範例中

var p1 = new { Name = "Lawnmower", Price = 495.00 };
var p2 = new { Name = "Shovel", Price = 26.95 };
p1 = p2;

允許最後一行的指派,因為 p1p2 屬於相同的匿名類型。

結束範例

匿名型別中的 EqualsGetHashcode 方法覆寫了從 object繼承的方法,並根據屬性的 EqualsGetHashcode 來定義。只有當相同匿名型別的兩個實例的所有屬性都相等時,這兩個實例才會相等。

成員宣告子可以縮寫為簡單名稱(§12.8.4)、成員存取(§12.8.7)、null 條件式投影初始化器 §12.8.8 或基底存取(§12.8.15)。 這稱為 初始化器,是宣告並指派給具有相同名稱的屬性的速記方式。 具體來說,表單的成員宣告子

«identifier»«expr» . «identifier»«expr» ? . «identifier»

分別精確等同於以下內容:

«identifer» = «identifier»«identifier» = «expr» . «identifier»«identifier» = «expr» ? . «identifier»

因此,在投影初始設定中,標識符會同時選擇要指派的值以及要指派值的欄位或屬性。 以直覺方式,投影初始化表達式不僅會投影值,也會投影值的名稱。

12.8.18 typeof 運算符

typeof 運算子用於取得某類型的 System.Type 物件。

typeof_expression
    : 'typeof' '(' type ')'
    | 'typeof' '(' unbound_type_name ')'
    | 'typeof' '(' 'void' ')'
    ;

unbound_type_name
    : identifier generic_dimension_specifier?
    | identifier '::' identifier generic_dimension_specifier?
    | unbound_type_name '.' identifier generic_dimension_specifier?
    ;

generic_dimension_specifier
    : '<' comma* '>'
    ;

comma
    : ','
    ;

第一種 typeof_expression 形式由 typeof 關鍵詞組成,後面接著括號中的類型。 這個表單表達式的結果是所指示型別的 System.Type 物件。 任何指定型別只有一個 System.Type 物件。 這表示對於類型 T而言,typeof(T) == typeof(T) 一律為 true。 類型不能是 dynamic

第二種形式的 typeof_expression 包含 typeof 關鍵詞,後面接著括號 unbound_type_name

附註unbound_type_nametype_name§7.8)非常類似,但不同之處在於 unbound_type_name 包含 generic_dimension_specifier,而 type_name 則包含 type_argument_list尾注

typeof_expression 的操作數是一個同時滿足 unbound_type_nametype_name文法的標記序列時,亦即它不包含 generic_dimension_specifiertype_argument_list,此序列即被視為 type_nameunbound_type_name 的意義如下所示:

  • 將標記序列轉換成 type_name,方法是將每個 generic_dimension_specifier 取代為具有相同逗號數目的 type_argument_list 和每個 type_argument的關鍵詞 object
  • 對產生的 type_name進行評估,並忽略所有類型參數限制。
  • unbound_type_name 解析為與生成的構造類型相關聯的未綁定的泛型類型(§8.4)。

當類型名稱是可為 Null 的參考型別時,會發生錯誤。

typeof_expression 的結果是所產生未系結泛型類型的 System.Type 物件。

第三種形式的 typeof_expression 包含 typeof 關鍵詞,後面接著括弧化 void 關鍵詞。 這種形式的運算式的結果是 System.Type 對象,其代表不存在的類型。 typeof(void) 傳回的類型對象與針對任何類型傳回的類型物件不同。

附註:這個特殊的 System.Type 對象在語言中允許方法反射的類別庫中很有用,這些方法希望可以用一個 System.Type實例來表示任何方法的返回類型,包括 void 方法的返回類型。 尾注

typeof 運算子可用於類型參數。 如果已知類型名稱為可為 Null 的參考型別,則為編譯時間錯誤。 結果是系結至類型參數之執行時類型的 System.Type 物件。 如果執行時類型是可為 Null 的參考型別,結果就是對應的非 Null 參考型別。 typeof 運算子也可以用於建構型別或未系結的泛型型別(§8.4.4)。 未系結泛型類型的 System.Type 對象與實例類型的 System.Type 物件不同(\15.3.2)。 實例類型一律是運行時間的封閉式建構型別,因此其 System.Type 物件相依於使用中的運行時間類型自變數。 另一方面,未系結的泛型型別沒有類型自變數,而且不論運行時間類型自變數為何,都會產生相同的 System.Type 物件。

範例:此範例

class X<T>
{
    public static void PrintTypes()
    {
        Type[] t =
        {
            typeof(int),
            typeof(System.Int32),
            typeof(string),
            typeof(double[]),
            typeof(void),
            typeof(T),
            typeof(X<T>),
            typeof(X<X<T>>),
            typeof(X<>)
        };
        for (int i = 0; i < t.Length; i++)
        {
            Console.WriteLine(t[i]);
        }
    }
}

class Test
{
    static void Main()
    {
        X<int>.PrintTypes();
    }
}

會產生下列輸出:

System.Int32
System.Int32
System.String
System.Double[]
System.Void
System.Int32
X`1[System.Int32]
X`1[X`1[System.Int32]]
X`1[T]

請注意,intSystem.Int32 的類型相同。 typeof(X<>) 的結果不取決於類型參數,而 typeof(X<T>) 的結果則取決於類型參數。

結束範例

12.8.19 sizeof 運算符

sizeof 運算符會傳回指定型別變數所佔用的 8 位位元元組數目。 指定為sizeof操作數的類型應該是 unmanaged_type8.8)。

sizeof_expression
    : 'sizeof' '(' unmanaged_type ')'
    ;

針對特定預先定義的類型,sizeof 運算符會產生常數 int 值,如下表所示:

表示式 結果
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(bool) 1
sizeof(decimal) 16

對於列舉類型 T,表達式 sizeof(T) 的結果是等於其基礎類型大小的一個常數值,正如前面所述。 針對所有其他操作數類型,sizeof 運算子會在 .23.6.9中指定。

12.8.20 已核取和未核取的運算符

checkedunchecked 運算子用於控制整數型別算術運算和轉換的溢位檢查上下文。

checked_expression
    : 'checked' '(' expression ')'
    ;

unchecked_expression
    : 'unchecked' '(' expression ')'
    ;

checked 運算符會評估已核取內容中的內含表達式,而 unchecked 運算符會評估未核取內容中的內含表達式。 checked_expressionunchecked_expression 完全對應於 parenthesized_expression (§12.8.5),但會在指定的溢位檢查上下文中評估包含的表達式。

溢位檢查內容也可以透過 checkedunchecked 語句來控制(§13.12)。

下列作業會受到已核取和未核取運算符和語句所建立的溢位檢查內容所影響:

  • 當操作數是整數或列舉型別時,預先定義的 ++-- 運算符 (12.8.1612.9.6)。
  • 預先定義的 - 一元運算符(§12.9.3),當操作數是整數型別時。
  • 當兩個操作數都是整數或列舉型別時,預定義的 +-*/ 二元運算符(§12.10)。
  • 從一個整數或列舉型別到另一個整數或列舉型別,或從 floatdouble 轉換成整數或列舉型別的明確數值轉換(10.3.2)。

當上述其中一個作業產生的結果過大以至無法適應於目的類型時,執行的上下文將控制產生的行為。

  • checked 內容中,如果操作是常數運算式(§12.23),則會有一個編譯時錯誤。 否則,在執行時執行作業時,會拋出 System.OverflowException
  • unchecked 環境中,會捨棄不符合目標類型的任何高階位元,以截斷結果。

對於非常數表達式(•12.23)(在運行時間評估的表達式),不會由任何 checkedunchecked 運算符或語句所括住,除非外部因素(例如編譯程式參數和執行環境組態)呼叫已檢查的評估,否則預設溢位檢查內容會取消核取。

對於常數表達式 (•12.23)(可在編譯階段完整評估的表達式),一律會檢查預設溢位檢查內容。 除非明確將常數表示式放在 unchecked 內容中,否則在表達式的編譯時期評估期間發生的溢位一律會導致編譯時間錯誤。

匿名函式的主體不會受到匿名函式所處的 checkedunchecked 情境的影響。

範例:在下列程式代碼中

class Test
{
    static readonly int x = 1000000;
    static readonly int y = 1000000;

    static int F() => checked(x * y);    // Throws OverflowException
    static int G() => unchecked(x * y);  // Returns -727379968
    static int H() => x * y;             // Depends on default
}

不會報告任何編譯時間錯誤,因為兩個表達式都無法於編譯時期進行評估。 在運行時間,F 方法會擲回 System.OverflowException,而 G 方法會傳回 –727379968(超出範圍結果的下層 32 位)。 H 方法的行為取決於編譯的預設溢位檢查內容,但它與 F 相同,或與 G相同。

結束範例

範例:在下列程式代碼中

class Test
{
    const int x = 1000000;
    const int y = 1000000;

    static int F() => checked(x * y);    // Compile-time error, overflow
    static int G() => unchecked(x * y);  // Returns -727379968
    static int H() => x * y;             // Compile-time error, overflow
}

當在 F 中評估常數表達式時發生的溢位,會導致在 H 報告編譯時間錯誤,因為表達式是在 checked 的上下文中進行評估。 評估 G中的常數表達式時,也會發生溢位,但因為評估發生在 unchecked 內容中,因此不會報告溢位。

結束範例

checkedunchecked 運算符只會影響包含在「(」和「)」標記內的操作所屬的溢位檢查的範圍。 運算元對因評估所包含運算式而調用的函式成員沒有任何影響。

範例:在下列程式代碼中

class Test
{
    static int Multiply(int x, int y) => x * y;

    static int F() => checked(Multiply(1000000, 1000000));
}

在 F 中使用 checked 不會影響 Multiplyx * y 的評估,因此會在預設溢位檢查內容中評估 x * y

結束範例

在以十六進位表示法撰寫帶正負號整數型別的常數時,unchecked 運算子很方便。

範例

class Test
{
    public const int AllBits = unchecked((int)0xFFFFFFFF);
    public const int HighBit = unchecked((int)0x80000000);
}

此兩個十六進位常數都是類型 uint。 由於常數位於 int 範圍之外,沒有 unchecked 運算符,因此轉型為 int 的操作會導致編譯時錯誤。

結束範例

附註checkedunchecked 運算符和語句可讓程式設計人員控制某些數值計算的某些層面。 不過,某些數值運算符的行為取決於其操作數的數據類型。 例如,將兩個小數相乘時,即使在明確的未檢查結構內進行,溢位仍然會導致例外發生。 同樣地,相乘兩個浮點數絕不會導致溢位時發生例外狀況,即使在明確檢查的建構內也一樣。 此外,其他運算符永遠不會受到檢查模式的影響,無論是預設還是明確。 尾注

12.8.21 預設值表達式

預設值表達式是用來取得型別的預設值 ()。

default_value_expression
    : explictly_typed_default
    | default_literal
    ;

explictly_typed_default
    : 'default' '(' type ')'
    ;

default_literal
    : 'default'
    ;

default_literal 代表預設值(§9.3)。 它沒有類型,但可以透過預設字面值轉換成任何類型(§10.2.16)。

default_value_expression 的結果是 explitly_typed_default中明確類型的預設值(§9.3),或作為 default_value_expression轉換的目標類型。

如果類型為下列其中一項,則 default_value_expression 是常數表達式(•12.23)。

  • 參考型別
  • 已知為參考型別的類型參數 (§8.2) 。
  • 下列其中一個實值類型:sbytebyteshortushortintuintlongulongcharfloatdoubledecimalbool,;或
  • 任何列舉類型。

12.8.22 堆棧配置

堆疊分配表達式會從執行堆疊分配一個記憶體區塊。 執行堆疊 是儲存局部變數的記憶體區域。 執行堆疊不是受控堆積的一部分。 當目前函式傳回時,用於儲存局部變數的記憶體會被自動回收。

堆疊配置表達式的安全內容規則會在 •16.4.12.7中描述。

stackalloc_expression
    : 'stackalloc' unmanaged_type '[' expression ']'
    | 'stackalloc' unmanaged_type? '[' constant_expression? ']' stackalloc_initializer
    ;

stackalloc_initializer
     : '{' stackalloc_initializer_element_list '}'
     ;

stackalloc_initializer_element_list
     : stackalloc_element_initializer (',' stackalloc_element_initializer)* ','?
     ;
    
stackalloc_element_initializer
    : expression
    ;

unmanaged_type§8.8)表示將儲存在新配置位置的項目類型,而 表達式 表示這些項目的數量。 結合在一起,這些說明了所需的分配大小。 表示式 的類型應可以隱式地轉換到類型 int

由於堆疊配置的大小不可以是負數,因此將項目數目指定為計算為負值的 constant_expression 是編譯時錯誤。

在運行時間,如果要配置的項目數目是負值,則行為未定義。 如果它是零,則不會進行配置,而且傳回的值是實作定義。 如果沒有足夠的記憶體可用來配置項目,則會拋出 System.StackOverflowException

stackalloc_initializer 存在時:

  • 如果省略 unmanaged_type,則會根據 stackalloc_element_initializers 集合的最佳常見類型規則(\12.6.3.15)來推斷。
  • 如果省略 constant_expression,則會推斷為 stackalloc_element_initializer的數量。
  • 如果 constant_expression 存在,它應等於 stackalloc_element_initializer的數目。

每個 stackalloc_element_initializer 都必須具有可轉換成 unmanaged_type 的隱式轉換(§10.2)。 stackalloc_element_initializer會以遞增順序初始化配置記憶體中的元素,從索引為零的元素開始。 如果沒有 stackalloc_initializer,新配置的記憶體內容就會未定義。

如果 stackalloc_expression 直接作為 local_variable_declaration 的初始化表達式(§13.6.2),其中 local_variable_type 是指針類型(§23.3)或推斷(var),則 stackalloc_expression 的結果是類型 T* 的指標(§23.9)。 在此情況下,stackalloc_expression 必須出現在不安全的程式代碼中。 否則,stackalloc_expression 的結果是 Span<T>類型的實例,其中 Tunmanaged_type

  • Span<T>§C.3)是 ref 結構類型(§16.2.3),它將顯示由 stackalloc_expression所配置的記憶體區塊,作為一個可索引的具型別(T)項目集合。
  • 結果的 Length 屬性會傳回已配置的項目數。
  • 結果索引器(§15.9)會將 variable_reference§9.5)傳回已配置區塊的項目,並進行範圍檢查。

catchfinally 區塊中不允許堆疊分配初始化器(§13.11)。

附註:沒有辦法明確釋放使用 stackalloc配置的記憶體。 尾注

當函式成員傳回時,會自動捨棄在函式成員執行期間建立的所有由堆疊配置的記憶體區塊。

除了 stackalloc 運算子之外,C# 不會提供任何預先定義的建構來管理非垃圾收集的記憶體。 這類服務通常是藉由支援類別庫或直接從基礎操作系統匯入來提供。

範例

// Memory uninitialized
Span<int> span1 = stackalloc int[3];
// Memory initialized
Span<int> span2 = stackalloc int[3] { -10, -15, -30 };
// Type int is inferred
Span<int> span3 = stackalloc[] { 11, 12, 13 };
// Error; result is int*, not allowed in a safe context
var span4 = stackalloc[] { 11, 12, 13 };
// Error; no conversion from Span<int> to Span<long>
Span<long> span5 = stackalloc[] { 11, 12, 13 };
// Converts 11 and 13, and returns Span<long> 
Span<long> span6 = stackalloc[] { 11, 12L, 13 };
// Converts all and returns Span<long>
Span<long> span7 = stackalloc long[] { 11, 12, 13 };
// Implicit conversion of Span<T>
ReadOnlySpan<int> span8 = stackalloc int[] { 10, 22, 30 };
// Implicit conversion of Span<T>
Widget<double> span9 = stackalloc double[] { 1.2, 5.6 };

public class Widget<T>
{
    public static implicit operator Widget<T>(Span<double> sp) { return null; }
}

span8的情況下,stackalloc 會產生 Span<int>,然後被隱含運算子轉換為 ReadOnlySpan<int>。 同樣地,針對 span9,產生的 Span<double> 會被轉換成使用者定義的型別 Widget<double>,如下所示。 結束範例

12.8.23 nameof 運算符(或運算子)

nameof_expression 用來取得程式實體的名稱作為常數字串。

nameof_expression
    : 'nameof' '(' named_entity ')'
    ;
    
named_entity
    : named_entity_target ('.' identifier type_argument_list?)*
    ;
    
named_entity_target
    : simple_name
    | 'this'
    | 'base'
    | predefined_type 
    | qualified_alias_member
    ;

由於 nameof 不是關鍵詞,因此 nameof_expression 一律會以語法模棱兩可的方式叫用簡單名稱 nameof。 基於相容性考慮,如果名稱查閱 (\12.8.4) 名稱 nameof 成功,則表達式會視為 invocation_expression,而不論調用是否有效。 否則,它是 nameof_expression

簡單名稱和成員存取查閱會在編譯時期針對 named_entity 進行,遵循 §12.8.4§12.8.7中所述的規則。 不過,在 「和 12.8.7 中所述的查閱會導致錯誤,因為實例成員是在靜態內容中找到,因此 nameof_expression 不會產生這類錯誤。

這是 named_entity 指定方法群組具有 type_argument_list的編譯時間錯誤。 具有類型 dynamicnamed_entity_target 是編譯時間錯誤。

nameof_expression 是類型為 string的常數運算式,而且在執行期間沒有影響。 具體來說,其 named_entity 不會進行評估,且在進行明確指派分析(§9.4.4.22)時會被忽略。 其值是選擇性最終 type_argument_list之前 named_entity 的最後一個標識符,會以下列方式轉換:

  • 若使用此「@」字首,則將移除。
  • 每個 unicode_escape_sequence 都會轉換成其對應的 Unicode 字元。
  • 移除任何 formatting_characters

這些是測試標識符相等性時應用在 §6.4.3 的相同轉換。

範例:下列範例說明各種 nameof 表達式的結果,假設 System.Collections.Generic 命名空間內宣告的泛型型別 List<T>

using TestAlias = System.String;

class Program
{
    static void Main()
    {
        var point = (x: 3, y: 4);

        string n1 = nameof(System);                      // "System"
        string n2 = nameof(System.Collections.Generic);  // "Generic"
        string n3 = nameof(point);                       // "point"
        string n4 = nameof(point.x);                     // "x"
        string n5 = nameof(Program);                     // "Program"
        string n6 = nameof(System.Int32);                // "Int32"
        string n7 = nameof(TestAlias);                   // "TestAlias"
        string n8 = nameof(List<int>);                   // "List"
        string n9 = nameof(Program.InstanceMethod);      // "InstanceMethod"
        string n10 = nameof(Program.GenericMethod);      // "GenericMethod"
        string n11 = nameof(Program.NestedClass);        // "NestedClass"

        // Invalid
        // string x1 = nameof(List<>);            // Empty type argument list
        // string x2 = nameof(List<T>);           // T is not in scope
        // string x3 = nameof(GenericMethod<>);   // Empty type argument list
        // string x4 = nameof(GenericMethod<T>);  // T is not in scope
        // string x5 = nameof(int);               // Keywords not permitted
        // Type arguments not permitted for method group
        // string x6 = nameof(GenericMethod<Program>);
    }

    void InstanceMethod() { }

    void GenericMethod<T>()
    {
        string n1 = nameof(List<T>); // "List"
        string n2 = nameof(T);       // "T"
    }

    class NestedClass { }
}

此範例中可能令人驚訝的部分是將 nameof(System.Collections.Generic) 解析為 「泛型」,而不是完整命名空間,而 nameof(TestAlias) 為 “TestAlias” 而非 “String”。 結束範例

12.8.24 匿名方法表達式

anonymous_method_expression 是定義匿名函式的兩種方式之一。 這些會進一步說明於 #.12.19

12.9 一元運算符

12.9.1 一般

+-!(僅針對邏輯否定 §12.9.4)、~++--、轉換和 await 運算符稱為一元運算符。

附注:後置 null-forgiving 運算符 (12.8.9),!,因為其編譯時間和不可多載的本質,會排除在上述清單中。 尾注

unary_expression
    : primary_expression
    | '+' unary_expression
    | '-' unary_expression
    | logical_negation_operator unary_expression
    | '~' unary_expression
    | pre_increment_expression
    | pre_decrement_expression
    | cast_expression
    | await_expression
    | pointer_indirection_expression    // unsafe code support
    | addressof_expression              // unsafe code support
    ;

pointer_indirection_expression•23.6.2)和 addressof_expression•23.6.5)僅適用於不安全的程式代碼(•23)。

如果 unary_expression 的操作數在編譯時具有類型 dynamic,則會進行動態繫結(§12.3.3)。 在此情況下,unary_expression 的編譯時類型是 dynamic,而以下表示的解析度將會使用操作數的運行時類型在運行時進行。

12.9.2 一元加號運算子

針對形式為 +x的運算,一元運算子重載解析(§12.4.4)會被應用來選擇特定的運算符實作。 操作數會轉換成所選取運算子的參數類型,而結果的類型是運算元的傳回類型。 預先定義的一元加運算符如下:

int operator +(int x);
uint operator +(uint x);
long operator +(long x);
ulong operator +(ulong x);
float operator +(float x);
double operator +(double x);
decimal operator +(decimal x);

針對這些運算符,結果只是操作數的值。

此外,上述未提升預先定義一元加號運算符的提升後的(§12.4.8)形式也會被預先定義。

12.9.3 一元減號運算符

對於形如 –x的運算,會套用一元運算子多載解析(§12.4.4)來選擇特定運算子的實作。 操作數會轉換成所選取運算子的參數類型,而結果的類型是運算元的傳回類型。 預先定義的一元減號運算符如下:

  • 整數否定:

    int operator –(int x);
    long operator –(long x);
    

    結果是透過從零減去 X 計算出來的。 如果 X 的值是操作數類型的最小可表示值(針對 int 是 −2³¹ 或針對 long是 −2⁶³),則操作數類型的 X 數學互補無法在操作數類型內表示。 如果這種情況發生在 checked 內容中,則會擲回 System.OverflowException;如果在 unchecked 內容中發生,結果就是操作數的值,而且不會報告溢位。

    如果負運算子的操作數的類型為 uint,則會轉換成類型 long,且結果的類型為 long。 例外是允許將 int−2147483648 (−2³¹)寫成十進位整數字面值的規則(§6.4.5.3)。

    如果負運算子的操作數類型為 ulong,就會發生編譯時期錯誤。 例外狀況是允許將 long−9223372036854775808 (−2⁶³) 寫入為十進位整數常值的規則 (§6.4.5.3

  • 浮點否定:

    float operator –(float x);
    double operator –(double x);
    

    結果是將 X 的值的正負號反轉。 如果 xNaN,則結果也會 NaN

  • 十進位否定:

    decimal operator –(decimal x);
    

    結果是透過將 X 從零中減去來計算的。 十進位否定相當於使用 類型為 System.Decimal的一元減號運算符。

此外,提升的(§12.4.8)形式之一元減號運算符也已預先定義,未提升的形式同樣如此。

12.9.4 邏輯否定運算符

針對型別 !x的操作,一元運算子超載解析(§12.4.4)將應用於選擇特定的運算符實作。 操作數會轉換成所選取運算子的參數類型,而結果的類型是運算元的傳回類型。 只有一個預先定義的邏輯否定運算元存在:

bool operator !(bool x);

這個運算子會計算操作數的邏輯否定:如果操作數是 true,則結果會 false。 如果操作數是 false,則結果是 true

上述未提升預先定義的邏輯否定運算符的提升形式(§12.4.8)也已預先定義。

附註:雖然前置邏輯否定與後置 null 寬容運算符(§12.8.9)是以相同的語彙標記(!)表示,但它們是不同的。 尾注

12.9.5 位元補數運算子

針對 ~x形式的作業,將套用一元運算符重載解析(§12.4.4),以選擇特定的運算符實作。 操作數會轉換成所選取運算子的參數類型,而結果的類型是運算元的傳回類型。 預先定義的位元補數運算符如下所示:

int operator ~(int x);
uint operator ~(uint x);
long operator ~(long x);
ulong operator ~(ulong x);

針對這些運算符,作業的結果是 x的位補碼。

每個列舉型別 E 隱含地提供下列位補碼運算子:

E operator ~(E x);

評估 ~x的結果,其中 X 是具有基礎類型 U的列舉型別 E 表達式,與評估 (E)(~(U)x)的結果完全一致,唯一的不同在於轉換至 E 總是如同在 unchecked 環境中執行(§12.8.20)。

此外,上述未提升的預先定義位補碼運算符的提升形式(§12.4.8)也已預先定義。

12.9.6 前置式遞增和遞減運算子

pre_increment_expression
    : '++' unary_expression
    ;

pre_decrement_expression
    : '--' unary_expression
    ;

前置遞增或遞減運算的操作數應該是分類為變數、屬性存取或索引器存取的表達式。 作業的結果是與操作數相同的類型值。

如果前置詞遞增或遞減作業的操作數是屬性或索引器存取,則屬性或索引器應同時具有 get 和 set 存取子。 如果情況並非如此,則會發生系結時間錯誤。

一元運算子多載決議(§12.4.4)用於選定特定的運算子實作。 下列類型存在預先定義的 ++-- 運算符:sbytebyteshortushortintuintlongulongcharfloatdoubledecimal及任何列舉類型。 預先定義的 ++ 運算符會傳回將 1 加入操作數所產生的值,而預先定義的 -- 運算符會傳回從操作數減去 1 所產生的值。 在 checked 內容中,如果這個加法或減法的結果超出結果類型的範圍,而結果類型是整數類型或列舉類型,則會拋出 System.OverflowException

應將所選的一元運算子傳回的型別隱含轉換為 unary_expression的型別,否則會發生編譯時期錯誤。

++x--x 形式的前置遞增或遞減運算的執行時處理包含以下步驟:

  • 如果 x 分類為變數:
    • 評估 x 來生成變數。
    • x 的值會轉換成所選取運算子的操作數類型,並使用這個值作為其自變數來叫用 運算符。
    • 運算符傳回的值會轉換成 x類型。 產生的值會儲存在評估 x所指定的位置。
    • 最後成為運算的結果。
  • 如果 x 被歸類為屬性或索引器存取。
    • 實例表達式(如果 x 不是 static)以及與 x 相關聯的引數列表(如果 x 是索引器訪問)將被評估,而結果將用於後續的 get 和 set 存取器調用。
    • 叫用了 x 的 get 存取方法。
    • get 存取子傳回的值會轉換成所選運算子的運算元類型,然後以此值作為引數來調用運算符。
    • 運算符傳回的值會轉換成 x類型。 x 的 set 存取子會以此值作為其值參數來呼叫。
    • 這個值也會成為作業的結果。

++-- 運算符也支援後置表示法 (12.8.16)。 x++x-- 的結果是作業前 x 的值,而 ++x--x 的結果則是作業之後 x 的值。 不論是哪一種情況,x 本身在作業之後都有相同的值。

您可以使用後置表示法或前置詞表示法來叫用運算符 ++ 或運算子 -- 實作。 這兩個表示法不可能有獨立的運算子實作。

此外,提升形式的未提升預先定義前置遞增和遞減運算符(§12.4.8)的運算形式也被預先定義。

12.9.7 轉換表達式

cast_expression 可用來將表達式明確轉換成指定的型別。

cast_expression
    : '(' type ')' unary_expression
    ;

形式為 (T)E型態轉換運算式,其中 T 是一種型別,E一元運算式,對 E 的值執行明確轉換 (§10.3)為型別 T。 如果 ET之間沒有明確轉換,則會發生系結時間錯誤。 否則,結果是明確轉換所產生的值。 即使 E 表示變數,結果一律會分類為值。

cast_expression 的語法會導致某些句法上的模棱兩可。

範例:表達式 (x)–y 可以解譯為 類型轉換表達式(將 –y 轉換成類型 x),或者作為 加法表達式,結合 括號表達式(用來計算值 x – y)。 結束範例

若要解決 cast_expression 模棱兩可的情況,下列規則存在:一或多個標記序列({6.4)以括弧括住,只有在下列其中至少一個成立時,才會被視為 cast_expression 的開頭:

  • 標記序列是類型的正確文法,但不適用於表達式。
  • 標記序列是類型的正確文法,而緊接在右括弧後面的標記是 “~”, “!”, “(”, 識別符號 (§6.4.3)、文字常值 (§6.4.5),或任何關鍵詞 (§6.4.4),但 asis除外。

上述「正確文法」一詞只表示標記序列應符合特定的文法製作。 它特別不會考慮任何組成標識碼的實際意義。

範例:如果 xy 是標識符,則即使 x.y 實際上並未表示類型,x.y 也是正確的文法。 結束範例

附註:根據釐清規則,如果 xy 是標識符,則 (x)y(x)(y)(x)(-y)類型轉換表達式,但 (x)-y 則不是,即使 x 識別的是一個類型。 不過,如果 x 是識別預先定義類型的關鍵詞(例如 int),則所有四種形式都是 cast_expression(因為這類關鍵詞本身不可能是表達式)。 尾注

12.9.8 Await 運算式

12.9.8.1 一般

await 運算符用來暫停執行封閉的非同步函式,直到操作數表示的非同步操作完成為止。

await_expression
    : 'await' unary_expression
    ;

在異步函式的主體中,只允許使用 await_expression§15.15)。 在最接近的封入異步函式中,await_expression 不應發生在下列位置:

  • 在嵌套的(非異步)匿名函式內
  • 鎖定語句 的區塊內
  • 在匿名函式轉換為表達式樹類型時(§10.7.3
  • 在不安全的情境中

附注await_expression 不能出現在 query_expression的大部分位置,因為這些語法會被轉換為使用非同步 Lambda 表達式。 尾注

在異步函式內,await 不得作為 available_identifier 使用,雖然可以使用逐字標識碼 @await。 因此,await_expression與涉及標識符的各種表達式之間沒有語法模棱兩可。 在異步函式之外,await 做為一般標識符。

await_expression 的運算元稱為 工作。 它表示在評估 await_expression 時,異步操作可能已完成或尚未完成。 await 運算子的目的是暫停封入異步函式的執行,直到等候的工作完成,然後取得其結果。

12.9.8.2 可等待的運算式

await_expression 的工作必須 可等候。 如果下列條件之一成立,則表達式 t 可等待:

  • t 是編譯時間類型 dynamic
  • t 有一個可存取的實例或擴充方法,稱為 GetAwaiter,沒有參數和類型參數,且其回傳型別 A 滿足下列所有條件:
    • A 實作介面 System.Runtime.CompilerServices.INotifyCompletion(以下簡稱 INotifyCompletion
    • A 具有可存取且可讀取的實例屬性 IsCompleted 類型 bool
    • A 具有可存取的實例方法,GetResult 沒有參數,也沒有類型參數

GetAwaiter 方法的目的是為了取得任務的「awaiter」A 類型稱為針對 await 表達式的 awaiter 類型

IsCompleted 屬性的目的是判斷工作是否已完成。 如果是,就不需要暫停評估。

INotifyCompletion.OnCompleted 方法的目的是註冊工作的「接續」;亦即,工作完成後,將會叫用的委派(類型為 System.Action)。

GetResult 方法的目的是在工作完成之後取得工作的結果。 此結果可能成功完成,可能附帶一個結果值,或可能是由 GetResult 方法擲回的例外。

12.9.8.3 await 表達式分類

表達式 await t 分類的方式與表達式 (t).GetAwaiter().GetResult()相同。 因此,如果 GetResult 的傳回型別是 void,則 await_expression 會分類為 nothing。 如果它的傳回值不是void 類型 T,則 await_expression 會被歸類為類型 T的值。

12.9.8.4 await 表達式的運行時間評估

在執行時,表達式 await t 會被計算如下。

  • 評估表達式 (t).GetAwaiter(),以獲得 awaiter a
  • 通過評估表達式 (a).IsCompleted來獲得 boolb
  • 如果 bfalse,那麼取決於 a 是否實作介面 System.Runtime.CompilerServices.ICriticalNotifyCompletion(以下簡稱 ICriticalNotifyCompletion)。 在綁定時完成此檢查;亦即,如果 a 具有編譯時類型 dynamic,則在運行時檢查,否則在編譯時檢查。 讓 r 表示恢復委派(§15.15):
    • 如果 a 未實作 ICriticalNotifyCompletion,則會評估表達式 ((a) as INotifyCompletion).OnCompleted(r)
    • 如果 a 實作 ICriticalNotifyCompletion,則會評估表達式 ((a) as ICriticalNotifyCompletion).UnsafeOnCompleted(r)
    • 評估接著會暫停,並將控制權傳回至異步函式的當前呼叫端。
  • 緊接在之後(如果 btrue),或在稍後叫用恢復委派時(如果 bfalse),就會評估表達式 (a).GetResult()。 如果傳回值,該值就是 await_expression的結果。 否則,結果是空的。

實作 awaiter 的介面方法 INotifyCompletion.OnCompletedICriticalNotifyCompletion.UnsafeOnCompleted,應該導致委派 r 被叫用最多一次。 否則,封入異步函式的行為是未定義的。

12.10 算術運算元

12.10.1 一般

*/%+- 運算符稱為算術運算符。

multiplicative_expression
    : unary_expression
    | multiplicative_expression '*' unary_expression
    | multiplicative_expression '/' unary_expression
    | multiplicative_expression '%' unary_expression
    ;

additive_expression
    : multiplicative_expression
    | additive_expression '+' multiplicative_expression
    | additive_expression '-' multiplicative_expression
    ;

如果算術運算子的操作數具有編譯時間類型 dynamic,則表達式會動態系結(\12.3.3)。 在這種情況下,表達式的編譯時類型是 dynamic,而以下所述的解析將會在執行時,使用具有編譯時類型的操作數的執行時類型 dynamic

12.10.2 乘法運算符

針對格式 x * y的操作,會套用二元運算子重載解析(§12.4.5)用來選擇特定的運算子實作。 操作數會轉換成所選取運算子的參數類型,而結果的類型是運算元的傳回類型。

預先定義的乘法運算符如下所列。 運算子都會計算 xy的乘積。

  • 整數乘法:

    int operator *(int x, int y);
    uint operator *(uint x, uint y);
    long operator *(long x, long y);
    ulong operator *(ulong x, ulong y);
    

    checked 內容中,如果結果超出結果類型的範圍,則會擲回 System.OverflowException。 在 unchecked 內容中,不會報告溢位,而且會捨棄結果類型範圍以外的任何重大高階位。

  • 浮點乘法:

    float operator *(float x, float y);
    double operator *(double x, double y);
    

    該產品是根據 IEC 60559 算術規則來計算。 下表列出非零有限值、零、無限和 NaN 的所有可能組合結果。 在數據表中,xy 都是正有限值。 zx * y的結果,四捨五入為最接近的可表示值。 如果結果的大小對目的類型而言太大,則 z 為無限大。 由於四捨五入,即使 xy 都不是零,z 也可能為零。

    +y -y +0 -0 +∞ -∞ NaN
    +x +z -z +0 -0 +∞ -∞ NaN
    -x -z +z -0 +0 -∞ +∞ NaN
    +0 +0 -0 +0 -0 NaN NaN NaN
    -0 -0 +0 -0 +0 NaN NaN NaN
    +∞ +∞ -∞ NaN NaN +∞ -∞ NaN
    -∞ -∞ +∞ NaN NaN -∞ +∞ NaN
    NaN NaN NaN NaN NaN NaN NaN NaN

    (除非另有說明,否則在 §12.10.2§12.10.6 浮點數據表中,使用 “+” 表示值為正數;使用 “-” 表示值為負數;而缺少正負號表示值可能是正數或負數,或無標識符號(NaN)。)

  • 十進位乘法:

    decimal operator *(decimal x, decimal y);
    

    如果產生的值大小太大而無法以十進位格式表示,則會擲回 System.OverflowException。 由於四捨五入,即使兩個操作數都不是零,結果也可能為零。 結果的尺度,在任何四捨五入之前,為兩個操作數的尺度之和。 十進位乘法相當於使用類型為 System.Decimal的乘法運算符。

上述未提升的預先定義乘法運算符的提升形式(§12.4.8)也已經預先定義。

12.10.3 除法運算符

針對形式 x / y的運算,會套用二元運算子重載解析(§12.4.5)來選取特定的運算子實作。 操作數會轉換成所選取運算子的參數類型,而結果的類型是運算元的傳回類型。

預先定義的除法運算符如下所列。 運算子都會計算 xy商數。

  • 整數除法:

    int operator /(int x, int y);
    uint operator /(uint x, uint y);
    long operator /(long x, long y);
    ulong operator /(ulong x, ulong y);
    

    如果右操作數的值為零,則會拋出 System.DivideByZeroException

    此除法會將結果趨近於零。 因此,結果的絕對值是最大可能整數,小於或等於兩個操作數商的絕對值。 當兩個操作數具有相同正負號時,結果為零或正數,而當兩個操作數有相反的正負號時,則結果為零或正數。

    如果左操作數是最小可表示的 intlong 值,而右操作數則為 –1,就會發生溢位。 在 checked 的情境中,這會導致拋出 System.ArithmeticException(或其子類別)。 在 unchecked 內容中,實作定義為擲回 System.ArithmeticException(或子類別),或溢位未回報,產生的值為左操作數。

  • 浮點除法:

    float operator /(float x, float y);
    double operator /(double x, double y);
    

    商數是根據 IEC 60559 算術規則來計算。 下表列出非零有限值、零、無限和 NaN 的所有可能組合結果。 在數據表中,xy 都是正有限值。 zx / y的結果,四捨五入為最接近的可表示值。

    +y -y +0 -0 +∞ -∞ NaN
    +x +z -z +∞ -∞ +0 -0 NaN
    -x -z +z -∞ +∞ -0 +0 NaN
    +0 +0 -0 NaN NaN +0 -0 NaN
    -0 -0 +0 NaN NaN -0 +0 NaN
    +∞ +∞ -∞ +∞ -∞ NaN NaN NaN
    -∞ -∞ +∞ -∞ +∞ NaN NaN NaN
    NaN NaN NaN NaN NaN NaN NaN NaN
  • 十進制除法:

    decimal operator /(decimal x, decimal y);
    

    如果右操作數的值為零,則會拋出 System.DivideByZeroException。 如果產生的值大小太大而無法以十進位格式表示,則會擲回 System.OverflowException。 由於四捨五入,即使第一個操作數不是零,結果也可能為零。 在進行任何捨入之前,結果的縮放比例是最接近所需比例的小數位設置,以保證結果等於精確結果。 所選擇的比例是 x 的比例減去 y的比例。

    十進位除法相當於使用類型為 System.Decimal的除法運算符。

此外,已預先定義上述未提升之預定義除法運算符的提升形式(§12.4.8)。

12.10.4 餘數運算符

形式為 x % y的運算中,會套用二元運算子多載解析(§12.4.5)以選擇特定的運算符實作。 操作數會轉換成所選取運算子的參數類型,而結果的類型是運算元的傳回類型。

預先定義的餘數運算符如下所列。 運算子都會計算 xy之間除法的餘數。

  • 整數餘數:

    int operator %(int x, int y);
    uint operator %(uint x, uint y);
    long operator %(long x, long y);
    ulong operator %(ulong x, ulong y);
    

    x % y 的結果是 x – (x / y) * y所產生的值。 如果 y 為零,則會拋出 System.DivideByZeroException

    如果左操作數是最小的 intlong 值,而右操作數是 –1,則只有在 x / y 擲回例外狀況時,才會擲回 System.OverflowException

  • 浮點餘數:

    float operator %(float x, float y);
    double operator %(double x, double y);
    

    下表列出非零有限值、零、無限和 NaN 的所有可能組合結果。 在數據表中,xy 都是正有限值。 zx % y 的結果,並計算為 x – n * y,其中 n 是小於或等於 x / y的最大可能整數。 這個計算餘數的方法類似於用於整數操作數,但與 IEC 60559 定義不同(其中 n 是最接近 x / y的整數)。

    +y -y +0 -0 +∞ -∞ NaN
    +x +z +z NaN NaN +x +x NaN
    -x -z -z NaN NaN -x -x NaN
    +0 +0 +0 NaN NaN +0 +0 NaN
    -0 -0 -0 NaN NaN -0 -0 NaN
    +∞ NaN NaN NaN NaN NaN NaN NaN
    -∞ NaN NaN NaN NaN NaN NaN NaN
    NaN NaN NaN NaN NaN NaN NaN NaN
  • 十進位餘數:

    decimal operator %(decimal x, decimal y);
    

    如果右操作數的值為零,則會拋出 System.DivideByZeroException。 當擲回 System.ArithmeticException 或子類別時,即會定義實作。 如果 x / y 不會拋出例外狀況,那麼符合規範的實作就不應該拋出 x % y 的例外狀況。 在任何四捨五入之前,結果的小數位數是兩個操作數中較大的小數位數。如果結果的符號非零,則結果的符號與 x相同。

    小數餘數相當於使用 類型為 System.Decimal的餘數運算符。

    附注:這些規則確保針對所有類型,結果永遠不會具有與左操作數相反的符號。 尾注

提升形式(§12.4.8)的上述未提升的預定義餘數運算子也已預先定義。

12.10.5 加法運算符

針對型態 x + y的運算,會套用二元運算子重載解析(§12.4.5)以選擇特定的運算符實作。 操作數會轉換成所選取運算子的參數類型,而結果的類型是運算元的傳回類型。

預先定義的加法運算符如下所列。 針對數值和列舉型別,預先定義的加法運算符會計算兩個操作數的總和。 當一或兩個操作數的類型為 string時,預先定義的加法運算符會串連操作數的字串表示。

  • 整數加法:

    int operator +(int x, int y);
    uint operator +(uint x, uint y);
    long operator +(long x, long y);
    ulong operator +(ulong x, ulong y
    

    checked 內容中,如果總和超出結果類型的範圍,則會引發 System.OverflowException。 在 unchecked 內容中,不會報告溢位,並且會捨棄超出結果類型範圍的所有高位元。

  • 浮點加法:

    float operator +(float x, float y);
    double operator +(double x, double y);
    

    總和是根據 IEC 60559 算術規則計算。 下表列出非零有限值、零、無限和 NaN 的所有可能組合結果。 在數據表中,xy 是非零的有限值,zx + y的結果。 如果 xy 具有相同的大小,但相反的跡象,z 為正零。 如果 x + y 太大而無法表示在目標類型中,z 是和 x + y擁有相同符號的無限值。

    y +0 -0 +∞ -∞ NaN
    x z x x +∞ -∞ NaN
    +0 y +0 +0 +∞ –∞ NaN
    -0 y +0 -0 +∞ -∞ NaN
    +∞ +∞ +∞ +∞ +∞ NaN NaN
    -∞ -∞ -∞ -∞ NaN -∞ NaN
    NaN NaN NaN NaN NaN NaN NaN
  • 十進位加法:

    decimal operator +(decimal x, decimal y);
    

    如果產生的值大小太大而無法以十進位格式表示,則會擲回 System.OverflowException。 結果的縮放比例,在任何四捨五入之前,都是兩個操作數的較大比例。

    十進位加法相當於使用 System.Decimal類型的加法運算符。

  • 列舉加法。 每個列舉型別都會隱含地提供下列預先定義的運算符,其中 E 是列舉類型,而 UE的基礎類型:

    E operator +(E x, U y);
    E operator +(U x, E y);
    

    在執行時,這些運算符的評估結果與 (E)((U)x + (U)y完全相同。

  • 字串連接:

    string operator +(string x, string y);
    string operator +(string x, object y);
    string operator +(object x, string y);
    

    二進位 + 運算符的這些多載會執行字串串連。 如果字串串連的操作數是 null,則會以空字串替代。 否則,任何非string 操作數都會藉由叫用繼承自類型 object的虛擬 ToString 方法,轉換為其字串表示法。 如果 ToString 傳回 null,則會取代空字串。

    範例

    class Test
    {
        static void Main()
        {
            string s = null;
            Console.WriteLine("s = >" + s + "<");  // Displays s = ><
    
            int i = 1;
            Console.WriteLine("i = " + i);         // Displays i = 1
    
            float f = 1.2300E+15F;
            Console.WriteLine("f = " + f);         // Displays f = 1.23E+15
    
            decimal d = 2.900m;
            Console.WriteLine("d = " + d);         // Displays d = 2.900
       }
    }
    

    批注中顯示的輸出是 US-English 系統上的典型結果。 精確的輸出可能取決於執行環境的地區設定。 字串串連運算符本身在每個案例中的行為都相同,但在執行期間隱含呼叫的 ToString 方法可能會受到區域設定的影響。

    結束範例

    字串串連運算符的結果是 string,其中包含左操作數的字元,後面接著右操作數的字元。 字串串連運算符永遠不會傳回 null 值。 如果沒有足夠的記憶體可供配置產生的字串,可能會擲回 System.OutOfMemoryException

  • 委託合併。 每個委派類型都會隱含地提供下列預先定義的運算符,其中 D 是委派類型:

    D operator +(D x, D y);
    

    如果第一個操作數是 null,則作業的結果是第二個操作數的值(即使這也是 null)。 否則,如果第二個操作數是 null,則作業的結果就是第一個操作數的值。 否則,作業的結果是新的委派實例,其調用清單是由第一個操作數調用清單中的元素所組成,後面接著第二個操作數調用清單中的元素。 也就是說,產生的委派調用清單是兩個操作數的調用清單串連。

    附註:如需委派組合的範例,請參閱 < 12.10.6\20.6。 因為 System.Delegate 不是委派類型,所以不會為其定義運算子 + 。尾注

已增強形式的上述未增強預先定義加法運算符(§12.4.8)也已預先定義。

12.10.6 減法運算符

針對 x – y形式的運算,會運用二元操作符重載解析(§12.4.5)以選擇特定的運算符實作。 操作數會轉換成所選取運算子的參數類型,而結果的類型是運算元的傳回類型。

預先定義的減法運算符如下所列。 運算子都會從 x減去 y

  • 整數減法:

    int operator –(int x, int y);
    uint operator –(uint x, uint y);
    long operator –(long x, long y);
    ulong operator –(ulong x, ulong y
    

    checked 的情境中,如果差異超出了結果類型的範圍,則會拋出 System.OverflowException。 在 unchecked 環境中,不會報告溢位,而且會捨棄結果類型範圍以外的所有重要的高階位元。

  • 浮點減法:

    float operator –(float x, float y);
    double operator –(double x, double y);
    

    差異是根據 IEC 60559 算術規則來計算。 下表列出非零有限值、零、無限和 NaN 的所有可能組合結果。 在數據表中,xy 是非零的有限值,zx – y的結果。 如果 xy 相等,z 為正零。 如果 x – y 太大而無法表示在目的地類型中,z 是具有與 x – y相同符號的無限大。

    y +0 -0 +∞ -∞ NaN
    x z x x -∞ +∞ NaN
    +0 -y +0 +0 -∞ +∞ NaN
    -0 -y -0 +0 -∞ +∞ NaN
    +∞ +∞ +∞ +∞ NaN +∞ NaN
    -∞ -∞ -∞ -∞ -∞ NaN NaN
    NaN NaN NaN NaN NaN NaN NaN

    (在上表中,-y 專案表示 否定 y,而不是值為負值。

  • 十進位減法:

    decimal operator –(decimal x, decimal y);
    

    如果產生的值大小太大而無法以十進位格式表示,則會擲回 System.OverflowException。 在任何四捨五入之前,結果的精度是兩個操作數中較大者的精度。

    十進位減法相當於使用 類型為 System.Decimal的減法運算符。

  • 列舉減法。 每個列舉型別都會隱含地提供下列預先定義的運算符,其中 E 是列舉型別,UE的基礎型別:

    U operator –(E x, E y);
    

    此運算子的評估方式與 (U)((U)x – (U)y)完全相同。 換句話說,運算符會計算 xy的序數值之間的差異,而結果的類型是列舉的基礎型別。

    E operator –(E x, U y);
    

    此運算子的評估方式與 (E)((U)x – y)完全相同。 換句話說,運算符會從列舉的基礎型別中減去一個值,並生成一個列舉值。

  • 解除委派。 每個委派類型都會隱含地提供下列預先定義的運算符,其中 D 是委派類型:

    D operator –(D x, D y);
    

    語意如下:

    • 如果第一個操作數是 null,運算的結果是 null
    • 否則,如果第二個操作數是 null,則作業的結果就是第一個操作數的值。
    • 否則,這兩個運算元都代表非空的調用清單(§20.2)。
      • 如果清單比較相等,由委派相等運算符(§12.12.9)判斷,則操作的結果是 null
      • 否則,操作的結果是新的調用列表,其中包含第一個操作數的項目列表,並移除了第二個操作數的項目,如果第二個操作數的項目列表是第一個操作數的子列表。 (若要判斷子清單相等,相對應的專案會與委派相等運算符比較。如果第二個操作數的清單符合第一個操作數清單中連續專案的多個子清單,則會移除連續項目的最後一個相符子清單。
      • 否則,作業的結果就是左操作數的值。

    操作數清單(如果有的話)都不會在程序中變更。

    範例

    delegate void D(int x);
    
    class C
    {
        public static void M1(int i) { ... }
        public static void M2(int i) { ... }
    }
    
    class Test
    {
        static void Main()
        {
            D cd1 = new D(C.M1);
            D cd2 = new D(C.M2);
            D list = null;
    
            list = null - cd1;                             // null
            list = (cd1 + cd2 + cd2 + cd1) - null;         // M1 + M2 + M2 + M1
            list = (cd1 + cd2 + cd2 + cd1) - cd1;          // M1 + M2 + M2
            list = (cd1 + cd2 + cd2 + cd1) - (cd1 + cd2);  // M2 + M1
            list = (cd1 + cd2 + cd2 + cd1) - (cd2 + cd2);  // M1 + M1
            list = (cd1 + cd2 + cd2 + cd1) - (cd2 + cd1);  // M1 + M2
            list = (cd1 + cd2 + cd2 + cd1) - (cd1 + cd1);  // M1 + M2 + M2 + M1
            list = (cd1 + cd2 + cd2 + cd1) - (cd1 + cd2 + cd2 + cd1);  // null
        }
    }
    

    結束範例

提升後(§12.4.8)的形式,與上述未提升的預先定義的減法運算符形式一樣,也都是預先定義的。

12.11 位移運算符

<<>> 運算子可用來執行位元移位作業。

shift_expression
    : additive_expression
    | shift_expression '<<' additive_expression
    | shift_expression right_shift additive_expression
    ;

如果 shift_expression 的操作數具有編譯時期類型 dynamic,則該表示式會動態系結(§12.3.3)。 在這種情況下,表達式的編譯時期類型是 dynamic,而以下所述的解析將會在執行時期使用具有編譯時期類型為 dynamic的操作數進行。

針對形式 x << countx >> count的操作,會套用二元運算子的多載解析(§12.4.5)來選擇特定的運算子實作。 操作數會轉換成所選取運算子的參數類型,而結果的類型是運算元的傳回類型。

宣告多載移位運算符時,第一個操作數的類型一律為包含運算符宣告的類別或結構,而第二個操作數的類型一律 int

預先定義的移位運算符如下所列。

  • 左移:

    int operator <<(int x, int count);
    uint operator <<(uint x, int count);
    long operator <<(long x, int count);
    ulong operator <<(ulong x, int count);
    

    << 操作符將 x 向左移動計算出的位數,如下所述。

    捨棄 x 結果類型範圍以外的高階位、剩餘位會向左移位,並將低階空白位位置設定為零。

  • 右移:

    int operator >>(int x, int count);
    uint operator >>(uint x, int count);
    long operator >>(long x, int count);
    ulong operator >>(ulong x, int count);
    

    >> 運算符會依照下述計算的位元數,將 x 向右移位。

    x 的類型為 intlong時,會捨棄 x 的低階位,其餘位會向右移位,如果 x 為非負數,則會設定為零,如果 x 為負數,則會將高階空位位置設定為零。

    x 類型為 uintulong時,會捨棄 x 的低階位,其餘位會向右移位,而高階空白位位置會設定為零。

針對預先定義的運算子,要移位的位數會計算如下:

  • x 的類型是 intuint時,位移計數由 count的低階五位元決定。 換句話說,班次計數是從 count & 0x1F計算出來的。
  • x 的類型是 longulong時,移位數由 count的低位六位決定。 換句話說,會從 count & 0x3F計算班次計數。

如果產生的位移計數為零,則位移運算子只會傳回 x的值。

位移運算永遠不會造成溢位,且在已檢查和未檢查的環境中會產生相同的結果。

>> 運算子的左操作數是有符號整數型別時,該運算符會執行 算術 右移位,其中操作數的最高有效位數值(符號位)會延伸至高階空位位置。 當 >> 運算子的左操作數是無符號整數類型時,運算符會執行 邏輯 右移,其中高階空白位位置一律設定為零。 若要執行與操作數類型所推斷的相反操作,可以使用明確類型轉換。

範例:如果 xint類型的變數,則操作 unchecked ((int)((uint)x >> y)) 會執行 x的邏輯右移。 結束範例

此外,還預先定義了上述未提升的位移運算子的提升形式(§12.4.8)。

12.12 關係型和型別測試運算符

12.12.1 一般

==!=<><=>=isas 運算符稱為關係型和型別測試運算符。

relational_expression
    : shift_expression
    | relational_expression '<' shift_expression
    | relational_expression '>' shift_expression
    | relational_expression '<=' shift_expression
    | relational_expression '>=' shift_expression
    | relational_expression 'is' type
    | relational_expression 'is' pattern
    | relational_expression 'as' type
    ;

equality_expression
    : relational_expression
    | equality_expression '==' relational_expression
    | equality_expression '!=' relational_expression
    ;

附註:查閱 is 運算子的右操作數必須先測試為 類型,然後作為可能跨越多個標記的 表達式。 如果運算元是 表達式,則模式表達式的優先順序必須至少等於或高於 位移表達式尾注

is 運算子在 §12.12.12 中描述,as 運算子在 §12.12.13中描述。

==!=<><=>= 運算符是 比較運算子

如果 default_literal§12.8.21)作為 <><=>= 運算元,則會發生編譯時期錯誤。 如果 default_literal 當做 ==!= 運算符的兩個操作數使用,則會發生編譯時期錯誤。 如果將 default_literal 作為 isas 運算子的左運算元,會發生編譯時錯誤。

如果比較運算子的操作數具有編譯時間類型 dynamic,則表示式會動態系結 (\12.3.3)。 在這種情況下,表達式的編譯時類型是 dynamic,而以下所述的解析將會在執行時,使用那些編譯時類型為 dynamic的操作數的執行時類型。

對於格式為 x «op» y的運算,其中 «op» 是比較運算符,多載解析 (§12.4.5)將被套用,以選擇特定的運算符實作。 操作數會轉換成所選取運算子的參數類型,而結果的類型是運算元的傳回類型。 如果 equality_expression 的兩個操作數都是 null 常值,則不會執行多載解析,而且表達式會根據運算符是 ==!=,評估為常數值 truefalse

預先定義的比較運算子會在下列子條款中描述。 所有預先定義的比較運算符都會傳回bool類型的結果,如下表所述。

作業 結果
x == y 如果 x 等於 y,則 true,否則 false
x != y true 如果 x 不等於 y,否則為 false
x < y true 如果 x 小於 y,則為 ,否則為 false
x > y 如果 x 大於 y,則使用 true ,否則使用 false
x <= y 如果 x 小於或等於 y,則 true 否則為 false
x >= y 如果 x 大於或等於 y,則 true 否則為 false

12.12.2 整數比較運算符

預先定義的整數比較運算符如下:

bool operator ==(int x, int y);
bool operator ==(uint x, uint y);
bool operator ==(long x, long y);
bool operator ==(ulong x, ulong y);

bool operator !=(int x, int y);
bool operator !=(uint x, uint y);
bool operator !=(long x, long y);
bool operator !=(ulong x, ulong y);

bool operator <(int x, int y);
bool operator <(uint x, uint y);
bool operator <(long x, long y);
bool operator <(ulong x, ulong y);

bool operator >(int x, int y);
bool operator >(uint x, uint y);
bool operator >(long x, long y);
bool operator >(ulong x, ulong y);

bool operator <=(int x, int y);
bool operator <=(uint x, uint y);
bool operator <=(long x, long y);
bool operator <=(ulong x, ulong y);

bool operator >=(int x, int y);
bool operator >=(uint x, uint y);
bool operator >=(long x, long y);
bool operator >=(ulong x, ulong y);

每一個運算符都會比較兩個整數操作數的數值,並傳回 bool 值,指出特定關聯性是 true 還是 false

上述未提升的預定義整數比較運算子的提升(§12.4.8)形式也預先定義。

12.12.3 浮點比較運算符

預先定義的浮點比較運算符如下:

bool operator ==(float x, float y);
bool operator ==(double x, double y);

bool operator !=(float x, float y);
bool operator !=(double x, double y);

bool operator <(float x, float y);
bool operator <(double x, double y);

bool operator >(float x, float y);
bool operator >(double x, double y);

bool operator <=(float x, float y);
bool operator <=(double x, double y);

bool operator >=(float x, float y);
bool operator >=(double x, double y);

運算子會根據 IEC 60559 標準的規則來比較操作數:

如果任一操作數為 NaN,則在所有運算子中,其結果為 false,除非是運算子 !=,在此情況下,結果為 true。 對於任兩個操作數,x != y 一律會產生與 !(x == y)相同的結果。 不過,當一或兩個操作數都是 NaN 時,<><=>= 運算符不會產生與相反運算符邏輯否定相同的結果

範例:如果任一 xy 為 NaN,則 x < yfalse,但 !(x >= y)true結束範例

當兩個操作數都不是 NaN 時,運算子會比較兩個浮點操作數與排序相關的值

–∞ < –max < ... < –min < –0.0 == +0.0 < +min < ... < +max < +∞

其中 minmax 是最小且最大的正有限值,可以用指定的浮點格式表示。 此排序的顯著影響如下:

  • 負數和正零視為相等。
  • 負無限大被認為小於所有其他值,但等於另一個負無限大。
  • 正無限大被視為大於所有其他值,但與另一個正無限大相等。

提升形式的浮點比較運算符(§12.4.8)和未提升的預先定義運算符一樣,都是預先定義的。

12.12.4 十進位比較運算符

預先定義的十進位比較運算符如下:

bool operator ==(decimal x, decimal y);
bool operator !=(decimal x, decimal y);
bool operator <(decimal x, decimal y);
bool operator >(decimal x, decimal y);
bool operator <=(decimal x, decimal y);
bool operator >=(decimal x, decimal y);

每一個運算子都會比較兩個十進位操作數的數值,並傳回 bool 值,指出特定關聯性是 true 還是 false。 每個十進位比較都相當於使用類型 System.Decimal相對應的關係或等式運算子。

預先定義的提升形式的小數比較運算符(§12.4.8)也同樣已預先定義。

12.12.5 布爾值相等運算符

預先定義的布爾等號比較運算符如下:

bool operator ==(bool x, bool y);
bool operator !=(bool x, bool y);

如果 xy 都是 true,或是 xyfalse,則 == 的結果為 true。 否則,結果會是 false

如果 xy 都是 true,或是 xyfalse,則 != 的結果為 false。 否則,結果是 true。 當操作數的類型為 bool時,!= 運算符會產生與 ^ 運算符相同的結果。

上述未提升的預定義布爾等式運算符的提升形式(§12.4.8)也已預先定義。

12.12.6 列舉比較運算子

每個列舉型別都會隱含地提供下列預先定義的比較運算符

bool operator ==(E x, E y);
bool operator !=(E x, E y);

bool operator <(E x, E y);
bool operator >(E x, E y);
bool operator <=(E x, E y);
bool operator >=(E x, E y);

評估 x «op» y的結果,其中 x 和 y 是具有基礎類型 U之列舉型別 E 表達式,而 «op» 是其中一個比較運算符,與評估 ((U)x) «op» ((U)y)完全相同。 換句話說,列舉型別比較運算符只會比較兩個操作數的基礎整數值。

上述未提升的預先定義列舉比較運算符的提升(§12.4.8)形式也已預先定義。

12.12.7 參考型別相等運算符

每個類別類型 C 隱含地提供下列預先定義的參考型別相等運算子:

bool operator ==(C x, C y);
bool operator !=(C x, C y);

除非已存在預先定義的等號運算符,對於 C 而言(例如,當 CstringSystem.Delegate時)。

運算符會傳回比較兩個參考是否相等或不相等的結果。 只有在 xy 參考相同的實例或同時 null時,operator == 才會傳回 true;而只有在具有相同操作數的 operator == 傳回 false時,operator != 才會傳回 true

除了一般適用性規則 (•12.6.4.2),預先定義的參考類型相等運算符還需要下列其中一項才能適用:

  • 這兩個操作數都是已知為 reference_type 或常值 null的類型值。 此外,存在從任一操作數到另一個操作數類型的身份或明確引用轉換(§10.3.5)。
  • 其中一個操作數是字面值 null,另一個操作數是類型 T 的值,其中 T 是一個 類型參數,已知它不是一個值類型,且不具備值類型的條件約束。
    • 如果在執行階段 T 是不可為 Null 的實值類型,那麼 == 的結果是 false,而 != 的結果是 true
    • 如果在執行期間 T 是可為 Null 的實值型別,則結果是從運算元的 HasValue 屬性計算得出的,如 (§12.12.10) 中所述。
    • 如果在執行時 T 是參考型別,當操作數是 null時結果為 true,否則為 false

除非其中一個條件成立,否則會發生系結時間錯誤。

附注:這些規則的顯著影響如下:

  • 使用預先定義的參考型別平等運算符來比較在系結時間已知不同的兩個參考,屬於系結時間的錯誤。 例如,如果操作數的系結時間類型是兩個類別類型,而且兩者都不是衍生自另一個,則兩個操作數不可能參考相同的物件。 因此,作業會被視為系結時間錯誤。
  • 預先定義的參考型別相等運算符不允許比較實值型別操作數(除非類型參數與 null進行比較,這是特別處理的)。
  • 預先定義的參考型別相等運算子的操作數絕不會進行裝箱。 執行這類 Boxing 作業是毫無意義的,因為新配置的 Boxed 實例參考必然與所有其他參考不同。

對於形式 x == yx != y的運算操作,如果存在任何適用的使用者定義 operator ==operator !=,則運算子重載解析規則(§12.4.5)將選擇該運算子,而非預先定義的參考型別相等運算子。 一律可以藉由明確將一或兩個操作數轉換成類型 object,來選取預先定義的參考型別相等運算符。

尾注

範例:下列範例會檢查未限制類型參數類型的自變數是否 null

class C<T>
{
   void F(T x)
   {
      if (x == null)
      {
          throw new ArgumentNullException();
      }
      ...
   }
}

即使 T 可能代表不可為 Null 的實值型別,x == null 建構還是允許使用,而且當 T 為不可為 Null 的實值型別時,結果只會定義為 false

結束範例

對於形式 x == yx != y的運算,如果存在任何適用的 operator ==operator !=,運算符多載解析(§12.4.5)規則會選擇該運算符,而不是預先定義的參考類型相等運算符。

附註:藉由明確將兩個操作數轉換成類型 object,一律可以選取預先定義的參考型別相等運算元。 尾注

範例:此範例

class Test
{
    static void Main()
    {
        string s = "Test";
        string t = string.Copy(s);
        Console.WriteLine(s == t);
        Console.WriteLine((object)s == t);
        Console.WriteLine(s == (object)t);
        Console.WriteLine((object)s == (object)t);
    }
}

會產生輸出

True
False
False
False

st 變數是指包含相同字元的兩個相異字串實例。 第一個比較會輸出 True,因為在兩個操作數的類型都是 string時,會選擇使用預先定義的字串等號運算符(§12.12.8)。 所有其餘的比較輸出 False,因為當任一操作數的系結時間類型為 object時,string 類型的 operator == 多載函數不適用。

請注意,上述技術對實值型別沒有意義。 範例

class Test
{
    static void Main()
    {
        int i = 123;
        int j = 123;
        Console.WriteLine((object)i == (object)j);
    }
}

輸出 False,因為轉換會建立兩個不同的boxed int 值實例的參考。

結束範例

12.12.8 字串相等運算符

預先定義的字串相等運算符如下:

bool operator ==(string x, string y);
bool operator !=(string x, string y);

當下列其中一個值成立時,會將兩個 string 值視為相等:

  • 這兩個值都是 null
  • 兩個值都是對非null 字串實例的參考,這些字串實例具有相同的長度,且每個字元位置的字元也相同。

字串相等運算符會比較字串值,而不是字串參考。 當兩個不同的字串實例包含完全相同的字元序列時,字串的值會相等,但參考不同。

附註:如 •12.12.7中所述,參考型別相等運算符可用來比較字元串參考,而不是字符串值。 尾注

12.12.9 委派相等運算符

預先定義的委派相等運算符如下:

bool operator ==(System.Delegate x, System.Delegate y);
bool operator !=(System.Delegate x, System.Delegate y);

兩個委派實例會視為相等,如下所示:

  • 如果任一個委派實例是 null,那麼只有當兩者都是 null時,它們才相等。
  • 如果委派對象具有不同的執行時類型,則是永遠不相等的。
  • 如果這兩個委派實例都有叫用清單(),則只有在它們的叫用清單長度相同,且其中一個叫用清單中的每個項目依序等於另外一個叫用清單中的對應項目時,這些實例才會相等。

下列規則會控管調用清單項目的相等性:

  • 如果兩個調用清單條目都指向相同的靜態方法,那麼這些條目是相等的。
  • 如果兩個叫用清單條目都參考同一目標物件上的同一個非靜態方法(由參考相等運算子定義),則這些條目是相等的。
  • 允許(但不要求)從語意上相同的匿名函式(§12.19)評估所產生的調用列表條目中,相同(可能為空)的擷取外部變數實例集合是相等的。

如果運算子多載解析解析為委派相等運算符,且兩個操作數的系結時間類型都是 §20 中所述的委派類型而非 System.Delegate中所述,且系結類型操作數類型之間沒有識別轉換,則會發生系結時間錯誤。

附註:此規則可防止因為參考不同類型的委派實例而無法將非null 值視為相等的比較。 尾注

12.12.10 可為 Null 的實值型別與 null 常值之間的相等運算符

==!= 運算符允許一個操作數是可為 Null 的實值型別值,而另一個操作數則為 null 常值,即使作業中沒有預先定義或使用者定義的運算符(以未提升或已解除的形式)存在也一樣。

針對其中一個形式的操作

x == null    null == x    x != null    null != x

其中 x 是可為 Null 實值型別的運算式,如果運算子多載解析(§12.4.5)找不到適用的運算子,則會改為從 xHasValue 屬性計算結果。 具體來說,前兩個窗體會轉譯為 !x.HasValue,最後兩個窗體會轉譯為 x.HasValue

12.12.11 Tuple 相等運算符

Tuple 相等運算符會依據語匯順序,成對地套用至 Tuple 操作數中各元素。

如果 ==!= 運算符的每個操作數 xy 都被分類為元組或具有元組類型的值 (§8.3.11),則該運算符是 元組等式運算符

如果操作數 e 被分類為元組,則 e1...en 的元素須為對元組表達式中元素表達式求值的結果。 否則,如果 e 是 Tuple 類型的值,則元素應為 t.Item1...t.Itemn,其中 t 是評估 e的結果。

元組等值運算子的操作數 xy 應該具有相同的元數,否則會發生編譯時錯誤。 對於每對元素 xiyi,應套用相同的相等運算符,並產生結果為類型 booldynamic,可隱含轉換為 bool的類型,或是定義了 truefalse 運算符的類型。

評估元組相等運算子 x == y 時,計算過程如下:

  • 左側運算元 x 已被評估。
  • 將評估右邊運算元 y
  • 針對每一對元素 xiyi,按照語彙順序:
    • 先評估運算子 xi == yi,然後以下列方式獲得結果,其類型為 bool
      • 如果比較產生 bool,則為結果。
      • 否則,如果比較產生 dynamic,則會動態調用運算符 false,然後將產生的 bool 值用邏輯否定運算符(!)進行否定。
      • 否則,如果比較的類型具有隱含轉換至 bool,則會套用該轉換。
      • 否則,如果比較的類型具有運算子 false,則會叫用該運算元,且產生的 bool 值會與邏輯否定運算符 (!) 否定。
    • 如果產生的 boolfalse,則不會再進行任何評估,而且元組等號運算符的結果會 false
  • 如果所有元素的比較結果為 true,那麼元組相等運算子的結果是 true

元組相等運算子 x != y 如下進行評估:

  • 對左側運算元 x 進行評估。
  • 將評估右側運算元 y
  • 針對每一組元素 xiyi 的語彙順序:
    • 會評估運算子 xi != yi,並以下列方式取得類型 bool 的結果:
      • 如果比較產生 bool 則為結果。
      • 否則,如果比較產生 dynamic,則會動態叫用運算符 true,而產生的 bool 值就是結果。
      • 否則,如果比較的類型具有隱含轉換至 bool,則會套用該轉換。
      • 否則,如果比較的類型具有運算子 true,則會叫用該運算符,而產生的 bool 值就是結果。
    • 如果產生的 booltrue,則將不會進行進一步的評估,且元組等號運算符的結果是 true
  • 如果所有元素比較產生 false,元組相等運算子的結果是 false

12.12.12 The is 運算符

is 運算子有兩種形式。 其中一個是 類型運算子,其右側有類型。 另一個是 is-pattern 運算子,其右側有一個模式。

12.12.12.1 類型判斷運算符

is-type 運算子 用來檢查物件的運行時間類型是否與指定的類型相容。 檢查在執行時進行。 作業 E is T的結果,其中 E 是表達式,T 是非 dynamic類型,是一個布林值,指出 E 是否為非 null,且可透過參考轉換、裝箱轉換、拆箱轉換、包裝轉換或解包轉換成功轉換成類型 T

作業的評估方式如下:

  1. 如果 E 是匿名函式或方法群組,則會發生編譯時期錯誤
  2. 如果 Enull 的常量,或 E 的值是 null,那麼結果就是 false
  3. 否則:
  4. R 成為 E的運行時間類型。
  5. D 衍生自 R,如下所示:
  6. 如果 R 為可為 Null 的數值型別,DR的基礎型別。
  7. 否則,DR
  8. 結果取決於 DT,如下所示:
  9. 如果 T 是參考型別,則結果是 true,條件如下:
    • DT之間存在身分識別轉換。
    • D 是參考型別,而且有從 DT 的隱含參考轉換,或
    • 任一種:D 是值類型,並且存在從 DT 的箱式轉換。
      或者:D 是實值型別,T 是由 D實作的介面類型。
  10. 如果 T 是可為 Null 的實值型別,而 DT的基礎類型,那麼結果是 true
  11. 如果 T 是不可為 Null 的實值型別,則如果 DT 是相同的類型,則結果會 true
  12. 否則,結果是 false

is 運算子不會考慮使用者定義的轉換。

附註:由於在執行時評估 is 運算符,所有型別參數已經替換完畢,且沒有開放型別(§8.4.3)。 尾注

附注:可以根據編譯時期類型和轉換來理解 is 運算符,其中 CE的編譯時期類型。

  • 如果 e 的編譯時類型與 T相同,或存在從編譯時類型 ET的隱含參考轉換(§10.2.8)、裝箱轉換(§10.2.9)、封裝轉換(§10.6),或明確的解除封裝轉換(§10.6):
    • 如果 C 為不可為 Null 的實值型別,則作業的結果會為 true
    • 否則,作業的結果相當於評估 E != null
  • 否則,如果從 CT存在明確的參考轉換(§10.3.5)或拆箱轉換(§10.3.7),或者如果 CT 是開放類型(§8.4.3),則應進行如上所述的運行時檢查。
  • 否則,不可能將 E 的引用、拳套化、包裝或拆裝轉換為類型 T,作業的結果是 false。 編譯程式可以根據編譯時間類型實作優化。

尾注

12.12.12.2 is-模式運算子

is-pattern 運算子 用來檢查表達式 計算的值是否符合給定的模式§11)。 檢查在執行時進行。 如果值符合模式,則is-pattern 運算子的結果為 true;否則為 false。

形如 E is P的運算式,其中 E 是類型 T 的關係表達式,而 P 是模式,如果下列任一項成立,則為編譯時期錯誤:

  • E 未指定值或沒有型別。
  • 模式 P 不適用於類型 T§11.2)。

12.12.13 as 型態轉換運算符

as 運算子可用來明確將值轉換成指定的參考型別或可為 Null 的實值型別。 不同於轉換表達式(§12.9.7),as 運算符永遠不會拋出例外。 相反地,如果無法進行指定的轉換,產生的值會 null

在形式 E as T的操作中,E 應為表達式,T 應為引用型別、已知為引用型別的類型參數或是可為 Null 的實值型別。 此外,下列條件至少應滿足其中一項,否則會發生編譯時期錯誤:

  • 識別(§10.2.2)、隱含可為 Null(§10.2.6)、隱含參考(§10.2.8)、封箱(§10.2.9)、明確可為 Null(§10.3.4)、明確參考(§10.3.5),或包裝(§8.3.12)的轉換存在從 ET
  • ET 的類型是開放型。
  • Enull 文字。

如果 E 的編譯時間類型不是 dynamic,則作業 E as T 會產生與相同的結果

E is T ? (T)(E) : (T)null

不同之處在於 E 只會評估一次。 編譯器可以預期將 E as T 優化,使之最多只有一個執行時類型檢查,而非上述擴展中隱含的兩個執行時類型檢查。

如果 E 的編譯時間類型是 dynamic,與轉換運算符不同,as 運算符不會動態系結 (\12.3.3)。 因此,在此案例中擴展為:

E is T ? (T)(object)(E) : (T)null

請注意,某些轉換,例如使用者定義的轉換,無法使用 as 運算符,而應該改用轉換表達式來執行。

範例:在範例中

class X
{
    public string F(object o)
    {
        return o as string;  // OK, string is a reference type
    }

    public T G<T>(object o)
        where T : Attribute
    {
        return o as T;       // Ok, T has a class constraint
    }

    public U H<U>(object o)
    {
        return o as U;       // Error, U is unconstrained
    }
}

G 的類型參數 T 已知為參考型別,因為它具有類別條件約束。 不過,H 的類型參數 U 不是;因此不允許在 H 中使用 as 運算符。

結束範例

12.13 邏輯運算符

12.13.1 一般

&, ^| 運算符稱為邏輯運算符。

and_expression
    : equality_expression
    | and_expression '&' equality_expression
    ;

exclusive_or_expression
    : and_expression
    | exclusive_or_expression '^' and_expression
    ;

inclusive_or_expression
    : exclusive_or_expression
    | inclusive_or_expression '|' exclusive_or_expression
    ;

如果邏輯運算子的操作數具有編譯時間類型 dynamic,則表示式會動態系結 ()。 在這種情況下,表示式的編譯時期類型是 dynamic,而下文所述的解析將會在執行時使用那些具有編譯時期類型 dynamic的運算元的執行時期類型。

對於形式 x «op» y的操作,其中「op」是某個邏輯運算子,多載解析(第12.4.5節)會被用來選擇特定的運算子實作。 操作數會轉換成所選取運算子的參數類型,而結果的類型是運算元的傳回類型。

預先定義的邏輯運算符號會在下列子條款中描述。

12.13.2 整數邏輯運算符

預先定義的整數邏輯運算符如下:

int operator &(int x, int y);
uint operator &(uint x, uint y);
long operator &(long x, long y);
ulong operator &(ulong x, ulong y);

int operator |(int x, int y);
uint operator |(uint x, uint y);
long operator |(long x, long y);
ulong operator |(ulong x, ulong y);

int operator ^(int x, int y);
uint operator ^(uint x, uint y);
long operator ^(long x, long y);
ulong operator ^(ulong x, ulong y);

& 運算子計算兩個操作數的位元邏輯 AND,| 運算子計算兩個操作數的位元邏輯 OR,^ 運算子計算兩個操作數的位元邏輯互斥 OR。 這些作業不會發生溢位問題。

提升形式的上述未提升的整數邏輯運算符也預先定義(§12.4.8)。

12.13.3 列舉邏輯運算符

每個列舉類型 E 隱含地提供下列預先定義的邏輯運算子:

E operator &(E x, E y);
E operator |(E x, E y);
E operator ^(E x, E y);

評估 x «op» y的結果,其中 xy 是具有基礎類型 U之列舉型別 E 表達式,而 «op» 是其中一個邏輯運算符,與評估 (E)((U)x «op» (U)y)完全相同。 換句話說,列舉型別邏輯運算符只會在兩個操作數的基礎類型上執行邏輯運算。

此外,上述未提升之列舉邏輯運算符的提升(§12.4.8)形式也已預先定義。

12.13.4 布爾邏輯運算符

預先定義的布林邏輯運算符如下:

bool operator &(bool x, bool y);
bool operator |(bool x, bool y);
bool operator ^(bool x, bool y);

如果 xytrue,則 x & y 的結果為 true。 否則,結果會是 false

x | y 的結果為 true ,如果 xytrue。 否則,結果是 false

如果 xtrueyfalse,或 xfalseytrue,則 x ^ y 的結果為 true。 否則,結果會是 false。 當操作數的類型為 bool時,^ 運算符會計算與 != 運算符相同的結果。

12.13.5 可為 Null 的布爾值 & 和 |運算子

可空的布林類型 bool? 可以代表三個值,truefalsenull

與其他二元運算符一樣,邏輯運算子 &|12.13.4)的提升形式也已預先定義:

bool? operator &(bool? x, bool? y);
bool? operator |(bool? x, bool? y);

下表定義了提升 &| 運算符的語意:

x y x & y x \| 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

附注bool? 類型在概念上類似於 SQL 中布爾運算式所使用的三個值型別。 上表遵循與 SQL 相同的語意,而將 §12.4.8 的規則套用至 &| 運算符則不會。 12.4.8 的規則已經為提升 ^ 運算符提供類似 SQL 的語意。 尾注

12.14 條件式邏輯運算元

12.14.1 一般

&&|| 運算符稱為條件式邏輯運算符。 它們也稱為「短路」邏輯運算符。

conditional_and_expression
    : inclusive_or_expression
    | conditional_and_expression '&&' inclusive_or_expression
    ;

conditional_or_expression
    : conditional_and_expression
    | conditional_or_expression '||' conditional_and_expression
    ;

&&|| 運算子是 &| 運算子的條件式版本:

  • 作業 x && y 對應至作業 x & y,但只有當 x 不是 false時,才會評估 y
  • 作業 x || y 對應至作業 x | y,但只有當 x 不為 true時,才會評估 y

附注:短路使用「非真」和「非假」條件的原因是讓使用者可以定義條件運算子來決定短路何時應用。 用戶定義型別可能處於 operator true 傳回 falseoperator false 傳回 false的狀態。 在這些情況下,&&|| 都不會短路。 尾注

如果條件式邏輯運算子的操作數具有編譯時間類型 dynamic,則表示式會動態系結 (#\12.3.3)。 在這種情況下,表達式的編譯時期類型是 dynamic,而以下所述的解析將於執行時期進行,使用具有編譯時期類型 dynamic的操作數的執行時期類型。

形式 x && yx || y 的操作通過套用多載解析(§12.4.5)來處理,就好像該操作是寫成 x & yx | y。 然後

  • 如果多載解析找不到單一最佳運算符,或者多載解析會選取其中一個預先定義的整數邏輯運算符或可為 Null 的布爾邏輯運算元(\12.13.5),則會發生系結時間錯誤。
  • 否則,如果選取的運算符是其中一個預先定義的布爾邏輯運算元(&12.13.4),則會依照 .14.2所述處理作業。
  • 否則,選取的運算符是使用者定義的運算符,而且作業會依照 \12.14.3中所述處理。

無法直接多載條件式邏輯運算元。 不過,由於條件邏輯運算子是基於一般邏輯運算子來進行運算,因此一般邏輯運算子的多載(在某些限制下)也被視為條件邏輯運算子的多載。 這會在 •12.14.3中進一步說明。

12.14.2 布爾條件邏輯運算符

&&|| 的操作數屬於類型 bool,或操作數不是定義適用 operator &operator |的類型時,但會定義對 bool的隱含轉換,作業會如下所示處理:

  • 作業 x && y 被評估為 x ? y : false。 換句話說,x 會先評估並轉換成類型 bool。 然後,如果 xtrue,則會評估 y 並轉換成類型 bool,這會成為作業的結果。 否則,作業的結果為 false
  • 操作 x || y 被評估為 x ? true : y。 換句話說,x 會先評估並轉換成類型 bool。 然後,如果 xtrue,則作業的結果會 true。 否則,y 會評估並轉換成類型 bool,這會成為作業的結果。

12.14.3 使用者定義的條件式邏輯運算符

&&|| 的運算元是屬於宣告適用的使用者定義 operator &operator |的類型時,則以下兩者皆為真,其中 T 是所選運算符被宣告的類型:

  • 選取運算子的傳回型別和每個參數的型別應為 T。 換句話說,運算符應對兩個 T類型的操作數進行邏輯 AND 或邏輯 OR 計算,並返回 T類型的結果。
  • T 應包含 operator trueoperator false的宣告。

如果不符合上述任一需求,就會發生系結時間錯誤。 否則,會結合使用者定義的 operator trueoperator false 與選取的使用者定義運算符,評估 &&|| 作業:

  • 作業 x && y 會評估為 T.false(x) ? x : T.&(x, y),其中 T.false(x)T中宣告的 operator false 調用,而 T.&(x, y) 是選取 operator &的調用。 換句話說,系統會先評估 x,並在結果上叫用 operator false,以判斷 x 是否絕對為 false。 然後,如果 x 絕對為 false,作業的結果就是先前針對 x計算的值。 否則,會評估 y,並在先前針對 x 計算的值和針對 y 計算的值上叫用選取的 operator &,以產生作業的結果。
  • 作業 x || y 被評估為 T.true(x) ? x : T.|(x, y),其中 T.true(x) 調用的是在 T宣告的 operator true,而 T.|(x, y) 調用的是選定的 operator |。 換句話說,系統會先評估 x,並在結果上叫用 operator true,以判斷 x 是否絕對正確。 然後,如果 x 絕對成立,作業的結果就是先前針對 x計算的值。 否則,會評估 y,然後將選定的 operator | 套用於先前計算出的 x 值和 y 值,以產生作業結果。

在這些運算中的任一個中,由 x 指定的運算式只會被評估一次,而由 y 指定的運算式要麼不被評估,要麼只被評估一次。

12.15 空合併運算子

?? 運算符稱為 null 聯合運算符。

null_coalescing_expression
    : conditional_or_expression
    | conditional_or_expression '??' null_coalescing_expression
    | throw_expression
    ;

a ?? b的 null 合併運算式中,如果 anull,則結果為 a;否則,結果為 b。 當 anull時,作業才會評估 b

Null 聯合運算子是右關聯運算元,表示作業會從右至左分組。

範例a ?? b ?? c 的運算式會評估為 ?? (b ?? c)。 一般而言,表單的表達式 E1 ?? E2 ?? ... ?? EN 會傳回非null的第一個操作數,如果所有操作數都 null,則傳回 null結束範例

表達式 a ?? b 的類型取決於操作數上可用的隱含轉換。 依喜好設定,a ?? b 的類型是 A₀AB,其中 Aa 類型(前提是 a 具有類型),Bb類型(前提是 b 具有類型),而如果 A 為可為 Null 的實值型別,則 A₀A 的基礎類型,否則為 A。 具體來說,a ?? b 會依照下列方式進行處理:

  • 如果 A 存在且不是可為 null 的值類型或參考類型,則會發生編譯時期錯誤。
  • 否則,如果 A 存在且 b 為動態表示式,則結果類型會 dynamic。 在執行時,首先會評估 a。 如果 a 不是 null,則 a 會轉換成 dynamic,而這會成為結果。 否則,b 會被評估,並成為結果。
  • 否則,如果 A 存在,而且是可為 Null 的實值型別,而且有從 bA₀的隱含轉換,則結果類型會 A₀。 在執行時,a 會先被評估。 如果 a 不是 null,那麼 a 會解包成類型 A₀,這將成為結果。 否則,b 會評估並轉換成類型 A₀,這會成為結果。
  • 否則,如果 A 存在,且隱含轉換從 bA,則結果類型會 A。 在執行時間,首先會評估變數 a。 如果 a 不是 Null,則 a 會成為結果。 否則,b 會被評估並轉換成類型 A,結果便會如此。
  • 否則,如果 A 存在且為可為 Null 的實值型別,b 具有類型 B,而且有從 A₀B的隱含轉換,則結果類型會 B。 在執行時,會先評估 a。 如果 a 不是 null,則 a 將解封裝為類型 A₀,並轉換成類型 B,作為結果。 否則,b 會被評估並成為結果。
  • 否則,如果 b 具有類型 B,且隱含轉換會從 aB,則結果類型會 B。 在執行期間,會先評估 a。 如果 a 不是 nulla 會轉換成類型 B,這會成為結果。 否則,會評估 b,並以其作為結果。

否則,ab 不相容,並且會在編譯時發生錯誤 a

12.16 擲回表達式運算子

throw_expression
    : 'throw' null_coalescing_expression
    ;

throw_expression 會擲回評估 null_coalescing_expression所產生的值。 表達式應可隱含轉換成 System.Exception,而且評估表達式的結果會在擲回之前轉換為 System.Exception。 在運行時間評估 throw 表達式 的行為,與 throw 語句 所指定的行為相同(§13.10.6)。

throw_expression 沒有類型。 throw_expression 可透過 隱式拋出轉換轉換為每種類型。

拋出表達式 只能出現在下列語法情境中:

  • 做為三元條件運算符的第二或第三個操作數(?:)。
  • 做為 空合併運算子的第二個操作數(??)。
  • 做為具表達式主體的 Lambda 或成員。

12.17 宣告表達式

宣告表達式會宣告局部變數。

declaration_expression
    : local_variable_type identifier
    ;

local_variable_type
    : type
    | 'var'
    ;

如果簡單名稱查閱找不到相關的宣告,則 simple_name_ 也被視為宣告表達式(§12.8.4)。 當作宣告表示式使用時,_ 稱為 簡單捨棄。 其語意相當於 var _,但允許在更多地方使用。

宣告表達式應該只會發生在下列語法內容中:

  • 做為 argument_list中的 outargument_value
  • 作為包含在簡單指派左側的簡單捨棄 _12.21.2)。
  • 做為一或多個遞歸巢狀 tuple_expressions 中的 tuple_element,其中最外層包含解構指派的左側。 deconstruction_expression 在此位置產生宣告表達式,儘管宣告表達式在語法上並不存在。

附注:這表示宣告表達式不能加上括弧。 尾注

若在 argument_list 中引用了以 declaration_expression 宣告的隱含型別變數,則會發生錯誤。

宣告了 declaration_expression 的變數在其所在的解構指派中被引用,這是錯誤的。

簡單捨棄的宣告表達式,或其中 local_variable_typevar 標識子的情況下,被分類為隱含型別 變數。 運算式沒有類型,而且局部變數的類型會根據語法內容推斷,如下所示:

  • argument_list 中,變數的推斷類型是對應參數的宣告類型。
  • 在簡單賦值運算中的左側,變數的推斷的類型等同於賦值運算右側的類型。
  • 在簡單賦值左側的 tuple_expression 中,變數的型別會被推斷為賦值右側對應元組元素的類型(解構之後)。

否則,宣告表達式會分類為明確具類型的 變數,而表達式的類型以及宣告的變數,則由 local_variable_type所指定。

識別符號為 _ 的宣告表達式是捨棄符(§9.2.9.2),且不會導入變數的名稱。 具有 _ 以外的標識碼的宣告表達式會將該名稱引入到最接近的封入局部變數宣告空間中(7.3)。

範例

string M(out int i, string s, out bool b) { ... }

var s1 = M(out int i1, "One", out var b1);
Console.WriteLine($"{i1}, {b1}, {s1}");
// Error: i2 referenced within declaring argument list
var s2 = M(out var i2, M(out i2, "Two", out bool b2), out b2);
var s3 = M(out int _, "Three", out var _);

s1 的宣告同時顯示明確和隱含型別的宣告表達式。 b1 的推斷類型是 bool,因為這是 M1中對應輸出參數的類型。 後續 WriteLine 能夠存取已經被引入封閉範圍的 i1b1

s2 的宣告顯示嘗試在巢狀呼叫 M時使用 i2,但這是不允許的,因為該參考出現在宣告 i2 的參數列表中。 另一方面,允許在最終參數中引用 b2,因為它是在 b2 宣告的巢狀參數列表結束之後出現的。

s3 的宣告顯示了使用隱含和明確型別的捨棄宣告表達式。 因為丟棄不會宣告具名變數,所以允許多次出現 _ 這個識別碼。

(int i1, int _, (var i2, var _), _) = (1, 2, (3, 4), 5);

這個範例示範在解構賦值中對變數和捨棄符使用隱式和顯式型別的宣告表達式。 當找不到 _ 宣告時,simple_name_ 相當於 var _

void M1(out int i) { ... }

void M2(string _)
{
    M1(out _);      // Error: `_` is a string
    M1(out var _);
}

此範例示範如何使用 var _,在無法使用 _ 時提供隱含型別的捨棄,因為它會在封入範圍中指定變數。

結束範例

12.18 條件運算符

?: 運算符稱為條件運算元。 有時也稱為三元運算符。

conditional_expression
    : null_coalescing_expression
    | null_coalescing_expression '?' expression ':' expression
    | null_coalescing_expression '?' 'ref' variable_reference ':'
      'ref' variable_reference
    ;

如果 ref 存在,則在條件運算符中不允許拋出表達式(§12.16)。

表單的條件表示式 b ? x : y 會先評估條件 b。 然後,如果 btrue,將評估 x 並將其作為作業結果。 否則,y 會被評估,並成為作業的結果。 條件表示式絕不會同時評估 xy

條件運算子是右關聯運算元,這表示作業會從右至左分組。

範例:形如 a ? b : c ? d : e 的運算式計算結果為 a ? b : (c ? d : e)結束範例

?: 運算子的第一個操作數應該是可以隱含轉換成 bool的表達式,或是實作 operator true之型別的表達式。 如果這兩個需求都未滿足,就會發生編譯時期錯誤。

如果 ref 存在:

  • 識別轉換應該存在於兩個 variable_reference的類型之間,而結果的類型可以是任一類型。 如果任一類型為 dynamic,則類型推斷會更偏好 dynamic§8.7)。 如果任一個類型是 Tuple 類型(§8.3.11),則當兩個 Tuple 中位於相同序數位置的元素名稱相符時,類型推斷會包含元素名稱。
  • 結果是變數參考,若兩個 variable_reference都可寫入,則該變數參考可寫入。

附註:當 ref 存在時,條件表達式 會傳回變數參考,可以利用 = ref 運算符將其指派給參考變數,或作為參考/輸入/輸出參數傳遞。 尾注

如果 ref 不存在,則 ?: 運算子的第二個操作數 x 和第三個操作數 y會控制條件表達式的類型。

  • 如果 x 具有類型 Xy 類型 Y,則
    • 如果 XY之間存在識別轉換,則結果是一組運算式的最佳共有類型(§12.6.3.15)。 如果任一類型為 dynamic,則類型推斷偏好 dynamic§8.7)。 如果其中一個類型是元組類型(§8.3.11),則當兩個元組中相同序數位置的元素名稱相符時,類型推斷會包括這些元素名稱。
    • 否則,如果隱含轉換 (\10.2) 從 XY存在,但不是從 YX,則 Y 是條件表達式的類型。
    • 否則,如果存在從 XY的隱含列舉轉換(§10.2.4),則 Y 是條件表達式的類型。
    • 否則,如果從 YX存在隱含的列舉轉換(§10.2.4),則 X 是條件表達式的類型。
    • 否則,如果隱含轉換 (\10.2) 從 YX存在,但不是從 XY,則 X 是條件表達式的類型。
    • 否則,無法判斷表達式類型,而且會發生編譯時期錯誤。
  • 如果只有其中一個 xy 具有類型,而且 xy 都會隱含地轉換成該類型,則為條件表達式的類型。
  • 否則,無法判斷表達式類型,而且會發生編譯時期錯誤。

表單的 ref 條件表示式的執行時間處理 b ? ref x : ref y 包含下列步驟:

  • 首先,會評估 b,並判斷 bbool 值:
    • 如果存在從 b 類型到 bool 類型的隱含轉換,則會執行此隱含轉換來產生 bool 值。
    • 否則,會叫用 b 類型所定義的 operator true,以產生 bool 值。
  • 如果上述步驟所產生的 bool 值是 true,則會評估 x,而產生的變數參考會成為條件表達式的結果。
  • 否則,會評估 y,而產生的變數參考會成為條件表達式的結果。

運行時處理條件運算式的形式 b ? x : y 由以下步驟組成:

  • 首先,會評估 b,並判斷 bbool 值:
    • 如果存在從 bbool 類型的隱含轉換,則會執行該隱含轉換以產生 bool 值。
    • 否則,會調用 b 類型所定義的 operator true,以產生 bool 值。
  • 如果上述步驟所產生的 booltrue,則會評估 x 並轉換成條件表達式的類型,這會成為條件表達式的結果。
  • 否則,會評估 y 並轉換為條件表達式的類型,這就成為條件表達式的結果。

12.19 匿名函式表達式

12.19.1 一般

匿名函式 是代表「內嵌」方法定義的表達式。 匿名函式本身沒有 值或型別,但可轉換成相容的委派或表達式樹狀結構類型。 匿名函式轉換的評估取決於轉換的目標類型:如果目標類型是委託類型,轉換的結果將評估為參考匿名函式所定義方法的委託值。 如果是表達樹類型,則轉換結果將被評估為一個表達樹,該樹表示方法的結構為對象結構。

附註:基於歷史原因,匿名函式有兩種語法類型,即 lambda_expressionanonymous_method_expression。 為了幾乎所有的目的,lambda_expressionanonymous_method_expression更簡潔且表達更為豐富,而後者僅為了確保向後兼容而仍然存在於語言中。 尾注

lambda_expression
    : 'async'? anonymous_function_signature '=>' anonymous_function_body
    ;

anonymous_method_expression
    : 'async'? 'delegate' explicit_anonymous_function_signature? block
    ;

anonymous_function_signature
    : explicit_anonymous_function_signature
    | implicit_anonymous_function_signature
    ;

explicit_anonymous_function_signature
    : '(' explicit_anonymous_function_parameter_list? ')'
    ;

explicit_anonymous_function_parameter_list
    : explicit_anonymous_function_parameter
      (',' explicit_anonymous_function_parameter)*
    ;

explicit_anonymous_function_parameter
    : anonymous_function_parameter_modifier? type identifier
    ;

anonymous_function_parameter_modifier
    : 'ref'
    | 'out'
    | 'in'
    ;

implicit_anonymous_function_signature
    : '(' implicit_anonymous_function_parameter_list? ')'
    | implicit_anonymous_function_parameter
    ;

implicit_anonymous_function_parameter_list
    : implicit_anonymous_function_parameter
      (',' implicit_anonymous_function_parameter)*
    ;

implicit_anonymous_function_parameter
    : identifier
    ;

anonymous_function_body
    : null_conditional_invocation_expression
    | expression
    | 'ref' variable_reference
    | block
    ;

anonymous_function_body 中,若辨識出同時適用 null_conditional_invocation_expression表示式 替代方案,應選擇前者。

附注:此處的替代方案重疊和優先權僅供描述性之用,文法規則可以進一步詳細說明以消除重疊。 ANTLR 和其他文法系統採用相同的便利性,因此 anonymous_function_body 自動具有指定的語意。 尾注

附註:當視為 表達式時,語法形式如 x?.M(),如果 M 的結果類型是 void,則會是錯誤(§12.8.13)。 但是,當視為 null_conditional_invocation_expression時,結果型別允許為 void尾注

範例List<T>.Reverse 的結果類型 void。 在下列程式碼中,匿名表達式的主體是一個 null_conditional_invocation_expression,所以這不是錯誤。

Action<List<int>> a = x => x?.Reverse();

結束範例

=> 運算子的優先順序與賦值(=)相同,且具有右結合性。

具有 async 修飾詞的匿名函式是異步函式,並遵循 中所述的規則。

匿名函式以 lambda_expression 形式時,其參數的類型可以是明確指定的或隱含推斷的。 在明確具類型的參數清單中,會明確說明每個參數的類型。 在隱含型別參數清單中,參數的類型是從匿名函式發生的內容推斷而來,特別是當匿名函式轉換成相容的委派類型或表達式樹狀結構類型時,該類型會提供參數類型 ($$\10.7)。

在具有單一隱含型別參數的 lambda_expression 中,可以從參數清單中省略括號。 換句話說,表單的匿名函式

( «param» ) => «expr»

可以縮寫為

«param» => «expr»

anonymous_method_expression 形式匿名函式的參數清單是選擇性的。 如果指定,則參數應明確輸入。 如果沒有,匿名函式會轉換成具有任何參數清單且不包含輸出參數的委派。

匿名函式的 區塊 主體總是可達到(§13.2)。

範例:下列匿名函式的一些範例如下:

x => x + 1                             // Implicitly typed, expression body
x => { return x + 1; }                 // Implicitly typed, block body
(int x) => x + 1                       // Explicitly typed, expression body
(int x) => { return x + 1; }           // Explicitly typed, block body
(x, y) => x * y                        // Multiple parameters
() => Console.WriteLine()              // No parameters
async (t1,t2) => await t1 + await t2   // Async
delegate (int x) { return x + 1; }     // Anonymous method expression
delegate { return 1 + 1; }             // Parameter list omitted

結束範例

lambda_expressionanonymous_method_expression的行為相同,但下列幾點除外:

  • anonymous_method_expression允許完全省略參數清單,使其能轉換為任何值參數清單的委派類型。
  • lambda_expression允許省略和推斷參數類型,而 anonymous_method_expression需要明確陳述參數類型。
  • lambda_expression 的主體可以是表達式或區塊,而 anonymous_method_expression 主體應該是區塊。
  • 只有 lambda_expression轉換成相容的表達式樹狀結構類型 ()。

12.19.2 匿名函式簽章

匿名函式 anonymous_function_signature 會定義匿名函式的名稱,並選擇性地定義匿名函式的參數類型。 匿名函式的參數範圍是 anonymous_function_body§7.7)。 匿名方法主體與參數清單(如果指定)一起構成宣告空間({7.3)。 因此,如果匿名函式的參數名稱與其範圍內的局部變數、局部常數或參數的名稱相同,則會造成編譯時錯誤。這些範圍包括 anonymous_method_expressionlambda_expression

如果匿名函式具有 explicit_anonymous_function_signature,則相容的委派類型和表達式樹狀結構類型集合會限制為具有相同參數類型和修飾詞的相同順序 (\10.7)。 與方法群組轉換 (“”10.8不同,不支援匿名函式參數類型的反變異數。 如果匿名函式沒有 anonymous_function_signature,則相容的委派類型和表達式樹狀目錄類型集合會限制為沒有輸出參數的委派類型。

請注意,anonymous_function_signature 不能包含屬性或參數陣列。 不過,anonymous_function_signature 可能與參數清單包含參數陣列的委派類型相容。

另請注意,即使類型相容,轉換至表達式樹類型在編譯時仍可能會失敗(§8.6)。

12.19.3 匿名函式主體

匿名函式的主體(表達式區塊)受限於下列規則:

  • 如果匿名函式包含簽章,則本文中會提供簽章中指定的參數。 如果匿名函式沒有簽名,則可以轉換成具有參數的委派類型或運算式類型(§10.7),但無法在函式主體中存取這些參數。
  • 除了在最近的封閉匿名函式的簽章中指定的傳址參數(如果有的話)之外,在函式主體中存取傳址參數會導致編譯時期錯誤。
  • 除了最接近封入其中的匿名函式的簽名(若有)中指定的參數外,如果函式體嘗試存取 ref struct 類型的參數,則會出現編譯時期錯誤。
  • this 的類型是結構類型時,程式碼存取 this會產生編譯時錯誤。 無論存取方式是明確的(如 this.x中),還是隱含的(如 x 中,其中 x 是該結構的實例成員)。 此規則僅禁止這類存取,不會影響成員查詢是否屬於結構中的成員。
  • 函數體可以存取匿名函式的外部變數(§12.19.6)。 外部變數的存取會參考在評估 lambda 表達式匿名方法表達式 時,當下作用的變數實例(§12.19.7)。
  • 若內文包含目標在內文外部或在內文包含的匿名函式的主體內的 goto 語句、break 語句或 continue 語句,則會產生編譯時期錯誤。
  • 本文中的 return 語句會從最接近封入匿名函式的調用傳回控件,而不是從封入函式成員傳回。

尚未明確指定是否有其他方法可執行匿名函式的區塊,除了透過評估及調用 lambda_expressionanonymous_method_expression。 特別是,編譯程式可以選擇藉由合成一或多個具名方法或類型來實作匿名函式。 任何這類合成項目的名稱應為保留供編譯程式使用的格式(4.3)。

12.19.4 多載解析

自變數清單中的匿名函式會參與類型推斷和多載解析。 如需確切的規則,請參閱 •12.6.3#12.6.4

範例:下列範例說明匿名函式對多載解析的影響。

class ItemList<T> : List<T>
{
    public int Sum(Func<T, int> selector)
    {
        int sum = 0;
        foreach (T item in this)
        {
            sum += selector(item);
        }
        return sum;
    }

    public double Sum(Func<T, double> selector)
    {
        double sum = 0;
        foreach (T item in this)
        {
            sum += selector(item);
        }
        return sum;
    }
}

ItemList<T> 類別有兩個 Sum 方法。 每個函式都接受一個 selector 參數,負責從清單項目中擷取要加總的數值。 擷取的值可以是 intdouble,產生的總和同樣可以是 intdouble

例如,Sum 方法可用來計算訂單中詳細數據行清單的總和。

class Detail
{
    public int UnitCount;
    public double UnitPrice;
    ...
}

class A
{
    void ComputeSums()
    {
        ItemList<Detail> orderDetails = GetOrderDetails( ... );
        int totalUnits = orderDetails.Sum(d => d.UnitCount);
        double orderTotal = orderDetails.Sum(d => d.UnitPrice * d.UnitCount);
        ...
    }

    ItemList<Detail> GetOrderDetails( ... )
    {
        ...
    }
}

orderDetails.Sum的第一個調用中,這兩個 Sum 方法都適用,因為匿名函式 d => d.UnitCountFunc<Detail,int>Func<Detail,double>兼容。 不過,多載解析會挑選第一個 Sum 方法,因為轉換為 Func<Detail,int> 的轉換比轉換成 Func<Detail,double>更好。

在第二個 orderDetails.Sum調用中,只有第二個 Sum 方法適用,因為匿名函式 d => d.UnitPrice * d.UnitCount 會產生類型為 double的值。 因此,重載解析會選擇該次呼叫的第二個 Sum 方法。

結束範例

12.19.5 匿名函式和動態系結

匿名函式不能是動態系結作業的接收者、自變數或操作數。

12.19.6 外部變數

12.19.6.1 一般

任何範圍包含 lambda_expressionanonymous_method_expression 的局部變數、值參數或參數陣列,稱為匿名函式的 外部變數。 在類別的實例函式成員中,這個值會被視為 value 參數,而且是函式成員內所含之任何匿名函式的外部變數。

12.19.6.2 擷取的外部變數

當匿名函式參考外部變數時,據說外部變數已被匿名函式 擷取。 一般而言,局部變數的存留期僅限於執行與之相關的區塊或語句(§9.2.9.1)。 不過,擷取的外部變數的生命周期至少延伸至從匿名函式建立的委派或表達式樹狀結構符合垃圾收集資格為止。

範例:在範例中

delegate int D();

class Test
{
    static D F()
    {
        int x = 0;
        D result = () => ++x;
        return result;
    }

    static void Main()
    {
        D d = F();
        Console.WriteLine(d());
        Console.WriteLine(d());
        Console.WriteLine(d());
    }
}

匿名函式會堲取局部變數 x,而且 x 的存留期至少會延伸,直到從 F 傳回的委派才有資格進行垃圾收集。 由於匿名函式的每個調用都會在相同的 x實例上運作,因此範例的輸出為:

1
2
3

結束範例

當匿名函式擷取局部變數或值參數時,局部變數或參數不再被視為固定變數(),而是被視為可移動的變數。 不過,擷取的外部變數不能用於 fixed 語句中(§23.7),因此無法取得擷取外部變數的位址。

附注:不同於未擷取的變數,擷取的局部變數可以同時公開至多個執行線程。 尾注

12.19.6.3 局部變數的具現化

當執行進入變數範圍時,局部變數會被視為 實例化

範例:例如,當呼叫下列方法時,局部變數 x 將針對迴圈的每次迭代具現化並初始化三次。

static void F()
{
    for (int i = 0; i < 3; i++)
    {
        int x = i * 2 + 1;
        ...
    }
}

x 的宣告移到迴圈外會只生成一個 x

static void F()
{
    int x;
    for (int i = 0; i < 3; i++)
    {
        x = i * 2 + 1;
        ...
    }
}

結束範例

未擷取時,很難準確觀察到局部變數被實例化的頻率,因為實例化的生命週期互不重疊,因此每次實例化都可能使用相同的儲存位置。 不過,當匿名函式擷取局部變數時,具現化的效果就會變得明顯。

範例:此範例

delegate void D();
class Test
{
    static D[] F()
    {
        D[] result = new D[3];
        for (int i = 0; i < 3; i++)
        {
            int x = i * 2 + 1;
            result[i] = () => Console.WriteLine(x);
        }
        return result;
    }

    static void Main()
    {
        foreach (D d in F())
        {
            d();
        }
    }
}

會產生輸出:

1
3
5

不過,當宣告 x 移動到迴圈外時:

delegate void D();

class Test
{
    static D[] F()
    {
        D[] result = new D[3];
        int x;
        for (int i = 0; i < 3; i++)
        {
            x = i * 2 + 1;
            result[i] = () => Console.WriteLine(x);
        }
        return result;
   }

   static void Main()
   {
       foreach (D d in F())
       {
           d();
       }
   }
}

輸出為:

5
5
5

請注意,允許編譯器(但不要求)將三個實例優化為單一委派實例(§10.7.2)。

結束範例

如果 for 迴圈宣告了一個迭代變數,那麼該變數本身會被視為在迴圈之外宣告。

範例:因此,如果範例改為直接擷取迭代變數本身:

delegate void D();

class Test
{
    static D[] F()
    {
        D[] result = new D[3];
        for (int i = 0; i < 3; i++)
        {
            result[i] = () => Console.WriteLine(i);
        }
        return result;
   }

   static void Main()
   {
       foreach (D d in F())
       {
           d();
       }
   }
}

只會擷取一個迭代變數的實例,這會產生輸出:

3
3
3

結束範例

匿名函式委派可以共用一些已擷取的變數,但也可以擁有其他各自的實例。

範例:例如,如果 F 變更為

static D[] F()
{
    D[] result = new D[3];
    int x = 0;
    for (int i = 0; i < 3; i++)
    {
        int y = 0;
        result[i] = () => Console.WriteLine($"{++x} {++y}");
    }
    return result;
}

這三個代理捕捉到相同的 x 實例,但 y的個別實例,並且其輸出為:

1 1
2 1
3 1

結束範例

個別的匿名函式可以擷取外部變數的相同實例。

範例:在範例中:

delegate void Setter(int value);
delegate int Getter();

class Test
{
    static void Main()
    {
        int x = 0;
        Setter s = (int value) => x = value;
        Getter g = () => x;
        s(5);
        Console.WriteLine(g());
        s(10);
        Console.WriteLine(g());
    }
}

這兩個匿名函式會擷取本機變數的相同實例 x,因此可以透過該變數「通訊」。 此範例的輸出為:

5
10

結束範例

12.19.7 匿名函式表達式的評估

匿名函式 F 一律會轉換成委派類型 D 或表示式樹狀結構類型 E,直接或透過執行委派建立表達式 new D(F)。 此轉換會決定匿名函數的結果,如 §10.7中所述。

12.19.8 實作範例

這個子章節具有資訊性。

此子段描述其他 C# 建構中匿名函式轉換的可能的實作。 此處所述的實作是以商業 C# 編譯程式所使用的相同原則為基礎,但絕不是授權實作,也不是唯一可能的實作。 它只會簡要提及轉換成表達式樹,因為其確切語意不在此規格的範圍之內。

這個子程式代碼的其餘部分提供數個程式代碼範例,其中包含具有不同特性的匿名函式。 針對每個範例,會提供只使用其他 C# 建構之程式代碼的對應轉譯。 在範例中,會假設標識碼 D 代表下列委派類型:

public delegate void D();

匿名函式最簡單的形式是不擷取任何外部變數的函式。

delegate void D();

class Test
{
    static void F()
    {
        D d = () => Console.WriteLine("test");
    }
}

這可以翻譯成委派實例化,參考編譯器生成的靜態方法,其中放置匿名函數的代碼:

delegate void D();

class Test
{
    static void F()
    {
        D d = new D(__Method1);
    }

    static void __Method1()
    {
        Console.WriteLine("test");
    }
}

在下列範例中,匿名函式會參考 this的實例成員:

delegate void D();

class Test
{
    int x;

    void F()
    {
        D d = () => Console.WriteLine(x);
    }
}

這可以轉譯成編譯程式產生的實例方法,其中包含匿名函式的程式代碼:

delegate void D();

class Test
{
   int x;

   void F()
   {
       D d = new D(__Method1);
   }

   void __Method1()
   {
       Console.WriteLine(x);
   }
}

在此範例中,匿名函式會擷取局部變數:

delegate void D();

class Test
{
    void F()
    {
        int y = 123;
        D d = () => Console.WriteLine(y);
    }
}

局部變數的存留期現在必須延伸至至少匿名函式委派的存留期。 這可藉由將局部變數「吊接」到編譯程式產生類別的欄位來達成。 局部變數的具現化(•12.19.6.3),然後對應至建立編譯程式產生的類別實例,而存取局部變數會對應至存取編譯程式產生的類別實例中的字段。 此外,匿名函式會成為編譯程式產生類別的實例方法:

delegate void D();

class Test
{
    void F()
    {
        __Locals1 __locals1 = new __Locals1();
        __locals1.y = 123;
        D d = new D(__locals1.__Method1);
    }

    class __Locals1
    {
        public int y;

        public void __Method1()
        {
            Console.WriteLine(y);
        }
    }
}

最後,下列匿名函式會擷取 this,以及具有不同存留期的兩個局部變數:

delegate void D();

class Test
{
   int x;

   void F()
   {
       int y = 123;
       for (int i = 0; i < 10; i++)
       {
           int z = i * 2;
           D d = () => Console.WriteLine(x + y + z);
       }
   }
}

在這裡,會針對每個區塊建立編譯程式產生的類別,其中會擷取局部變數,讓不同區塊中的局部變數可以有獨立的存留期。 __Locals2的實例,編譯程式為內部區塊產生的類別,包含局部變數 z,以及參考 __Locals1實例的欄位。 __Locals1的實例,編譯程式為外部區塊產生的類別,包含局部變數 y,以及參考封入函式成員 this 的欄位。 透過這些數據結構,您可以透過 __Local2的實例連線到所有擷取的外部變數,因此匿名函式的程式代碼可以實作為該類別的實例方法。

delegate void D();

class Test
{
    int x;

    void F()
    {
        __Locals1 __locals1 = new __Locals1();
        __locals1.__this = this;
        __locals1.y = 123;
        for (int i = 0; i < 10; i++)
        {
            __Locals2 __locals2 = new __Locals2();
            __locals2.__locals1 = __locals1;
            __locals2.z = i * 2;
            D d = new D(__locals2.__Method1);
        }
    }

    class __Locals1
    {
        public Test __this;
        public int y;
    }

    class __Locals2
    {
        public __Locals1 __locals1;
        public int z;

        public void __Method1()
        {
            Console.WriteLine(__locals1.__this.x + __locals1.y + z);
        }
    }
}

將匿名函式轉換成表達式樹狀結構時,也可以使用此處所套用的相同方法來擷取局部變數:編譯程式產生的對象的參考可以儲存在表達式樹狀結構中,而局部變數的存取可以表示為這些物件的欄位存取。 這種方法的優點在於,它允許委派和運算式樹共享提升的局部變數。

資訊文本結束。

12.20 查詢表達式

12.20.1 一般

查詢表示式 為類似 SQL 和 XQuery 等關係型和階層式查詢語言的查詢提供語言整合語法。

query_expression
    : from_clause query_body
    ;

from_clause
    : 'from' type? identifier 'in' expression
    ;

query_body
    : query_body_clauses? select_or_group_clause query_continuation?
    ;

query_body_clauses
    : query_body_clause
    | query_body_clauses query_body_clause
    ;

query_body_clause
    : from_clause
    | let_clause
    | where_clause
    | join_clause
    | join_into_clause
    | orderby_clause
    ;

let_clause
    : 'let' identifier '=' expression
    ;

where_clause
    : 'where' boolean_expression
    ;

join_clause
    : 'join' type? identifier 'in' expression 'on' expression
      'equals' expression
    ;

join_into_clause
    : 'join' type? identifier 'in' expression 'on' expression
      'equals' expression 'into' identifier
    ;

orderby_clause
    : 'orderby' orderings
    ;

orderings
    : ordering (',' ordering)*
    ;

ordering
    : expression ordering_direction?
    ;

ordering_direction
    : 'ascending'
    | 'descending'
    ;

select_or_group_clause
    : select_clause
    | group_clause
    ;

select_clause
    : 'select' expression
    ;

group_clause
    : 'group' expression 'by' expression
    ;

query_continuation
    : 'into' identifier query_body
    ;

查詢表達式會以 from 子句開頭,並以 selectgroup 子句結尾。 初始 from 子句之後可以接零個或多個 fromletwherejoinorderby 子句。 每個 from 子句都是一個產生器,引入 範圍變數,範圍涵蓋 序列的元素。 每個 let 子句都會引進一個範圍變數,代表以先前範圍變數計算的值。 每個 where 子句都是用來過濾從結果中排除項目的條件。 每個 join 子句都會比較來源序列的指定索引鍵與另一個序列的索引鍵,產生相符的配對。 每個 orderby 子句都會根據指定的準則重新排序專案。最終 selectgroup 子句會以範圍變數來指定結果的形狀。 最後,into 子句可用來「擷取」查詢,方法是將一個查詢的結果視為後續查詢中的產生器。

12.20.2 查詢表達式中的模棱兩可

查詢表達式使用一些內容關鍵詞 ():ascendingbydescendingequalsfromgroupintojoinletonorderbyselectwhere

為了避免這些識別符在查詢表達式中作為關鍵字和簡單名稱時產生歧義,除非這些識別符前加上 `@`(§6.4.4),否則它們在任何位置都被視為關鍵字。 針對此目的,查詢表達式是開頭為 “from標識符” 的任何表達式,後面接著 “;”、“=” 或 “,” 以外的任何標記。

12.20.3 查詢表達式翻譯

12.20.3.1 一般

C# 語言不會指定查詢表達式的執行語意。 相反地,查詢表達式會被轉譯成遵循查詢表達式模式的方法的調用(§12.20.4)。 具體而言,查詢表達式會轉譯成名為 WhereSelectSelectManyJoinGroupJoinOrderByOrderByDescendingThenByThenByDescendingGroupByCast的方法調用。 這些方法預期具有特定的特徵和傳回型別,如 第12.20.4節中所述。 這些方法可能是所查詢之對象的實例方法,或是物件外部的擴充方法。 這些方法會實作查詢的實際執行。

從查詢表達式到方法調用的轉譯是語法對應,會在執行任何類型系結或多載解析之前發生。 在查詢表達式轉譯之後,所產生的方法呼叫會被當作一般的方法呼叫來處理,這可能會進一步發現編譯時錯誤。 這些錯誤狀況包括但不限於不存在的方法、錯誤的型別自變數,以及類型推斷失敗的泛型方法。

查詢表達式會藉由重複套用下列翻譯來處理,直到無法進一步縮減為止。 翻譯將按照應用次序列出:每個區段假定之前各節的翻譯已經完全執行,且一旦某個區段完成,其內容將不會在處理相同查詢表達式時再次被訪問。

查詢表達式中,如果包含對範圍變數的賦值操作,或者將範圍變數作為參考或輸出參數的引數使用,這會在編譯時出錯。

某些翻譯會插入範圍變數,其中包含 透明標識符, 以 *表示。 這些會在 §12.20.3.8中進一步說明。

12.20.3.2 含接續的查詢表達式

查詢表達式,在其查詢主體之後有一個延續部分

from «x1» in «e1» «b1» into «x2» «b2»

已轉譯成

from «x2» in ( from «x1» in «e1» «b1» ) «b2»

下列各節中的翻譯假設查詢沒有接續。

範例:範例:

from c in customers
group c by c.Country into g
select new { Country = g.Key, CustCount = g.Count() }

被翻譯成:

from g in
   (from c in customers
   group c by c.Country)
select new { Country = g.Key, CustCount = g.Count() }

最後的翻譯如下:

customers.
GroupBy(c => c.Country).
Select(g => new { Country = g.Key, CustCount = g.Count() })

結束範例

12.20.3.3 明確範圍變數類型

明確指定範圍變數類型的 from 子句

from «T» «x» in «e»

已轉譯成

from «x» in ( «e» ) . Cast < «T» > ( )

明確指定範圍變數類型的 join 子句

join «T» «x» in «e» on «k1» equals «k2»

已轉譯成

join «x» in ( «e» ) . Cast < «T» > ( ) on «k1» equals «k2»

下列各節中的翻譯假設查詢沒有明確的範圍變數類型。

範例:此範例

from Customer c in customers
where c.City == "London"
select c

已轉譯成

from c in (customers).Cast<Customer>()
where c.City == "London"
select c

其最終翻譯為

customers.
Cast<Customer>().
Where(c => c.City == "London")

結束範例

附註:明確範圍變數類型適用於查詢實作非泛型 IEnumerable 介面的集合,但不適用於泛型 IEnumerable<T> 介面。 在上述範例中,如果客戶的類型為 ArrayList,則會出現這種情況。 尾注

12.20.3.4 變質查詢表達式

表單的查詢表達式

from «x» in «e» select «x»

已轉譯成

( «e» ) . Select ( «x» => «x» )

範例:此範例

from c in customers
select c

已轉譯成

(customers).Select(c => c)

結束範例

變質的查詢表達式是可簡單選取來源元素的查詢表達式。

附注:翻譯的後續階段(§12.20.3.6§12.20.3.7)通過將其替換為其來源來移除其他翻譯步驟所引進的退化查詢。 不過,請務必確保查詢表達式的結果絕不是來源物件本身。 否則,傳回這類查詢的結果可能會不小心將私用數據(例如元素陣列)公開給呼叫端。 因此,此步驟會藉由在來源上明確呼叫 Select,保護直接在原始程式碼中撰寫的變質查詢。 然後,Select 和其他查詢運算子的實作者負責確保這些方法永遠不會回傳來源物件本身。 尾注

12.20.3.5 From、let、where、join 和 orderby 子句

具有第二個 from 子句的查詢表達式,後面接著 select 子句

from «x1» in «e1»  
from «x2» in «e2»  
select «v»

已轉譯成

( «e1» ) . SelectMany( «x1» => «e2» , ( «x1» , «x2» ) => «v» )

範例:此範例

from c in customers
from o in c.Orders
select new { c.Name, o.OrderID, o.Total }

已轉譯成

(customers).
SelectMany(c => c.Orders,
(c,o) => new { c.Name, o.OrderID, o.Total }
)

結束範例

具有第二個 from 子句的查詢表達式,隨後接著的查詢主體 Q 包含一組非空的查詢主體子句:

from «x1» in «e1»
from «x2» in «e2»
Q

已轉譯成

from * in («e1») . SelectMany( «x1» => «e2» ,
                              ( «x1» , «x2» ) => new { «x1» , «x2» } )
Q

範例:此範例

from c in customers
from o in c.Orders
orderby o.Total descending
select new { c.Name, o.OrderID, o.Total }

已轉譯成

from * in (customers).
   SelectMany(c => c.Orders, (c,o) => new { c, o })
orderby o.Total descending
select new { c.Name, o.OrderID, o.Total }

其最終翻譯為

customers.
SelectMany(c => c.Orders, (c,o) => new { c, o }).
OrderByDescending(x => x.o.Total).
Select(x => new { x.c.Name, x.o.OrderID, x.o.Total })

其中 x 是編譯程式產生的標識符,是本質上看不見且無法存取的。

結束範例

一個 let 表示式及其前面的 from 子句:

from «x» in «e»  
let «y» = «f»  
...

已轉譯成

from * in ( «e» ) . Select ( «x» => new { «x» , «y» = «f» } )  
...

範例:此範例

from o in orders
let t = o.Details.Sum(d => d.UnitPrice * d.Quantity)
where t >= 1000
select new { o.OrderID, Total = t }

已轉譯成

from * in (orders).Select(
    o => new { o, t = o.Details.Sum(d => d.UnitPrice * d.Quantity) })
where t >= 1000
select new { o.OrderID, Total = t }

其最終翻譯為

orders
    .Select(o => new { o, t = o.Details.Sum(d => d.UnitPrice * d.Quantity) })
    .Where(x => x.t >= 1000)
    .Select(x => new { x.o.OrderID, Total = x.t })

其中 x 是編譯程式產生的識別符號,否則為隱藏且不可存取。

結束範例

where 表达式及其之前的 from 子句:

from «x» in «e»  
where «f»  
...

已轉譯成

from «x» in ( «e» ) . Where ( «x» => «f» )  
...

join 子句緊接在 select 子句後面

from «x1» in «e1»  
join «x2» in «e2» on «k1» equals «k2»  
select «v»

已轉譯成

( «e1» ) . Join( «e2» , «x1» => «k1» , «x2» => «k2» , ( «x1» , «x2» ) => «v» )

範例:此範例

from c in customersh
join o in orders on c.CustomerID equals o.CustomerID
select new { c.Name, o.OrderDate, o.Total }

已轉譯成

(customers).Join(
   orders,
   c => c.CustomerID, o => o.CustomerID,
   (c, o) => new { c.Name, o.OrderDate, o.Total })

結束範例

join 子句後面接著一個查詢內容子句:

from «x1» in «e1»  
join «x2» in «e2» on «k1» equals «k2»  
...

已轉譯成

from * in ( «e1» ) . Join(  
«e2» , «x1» => «k1» , «x2» => «k2» ,
( «x1» , «x2» ) => new { «x1» , «x2» })  
...

一個 join-into 子句後面緊跟著一個 select 子句

from «x1» in «e1»  
join «x2» in «e2» on «k1» equals «k2» into «g»  
select «v»

已轉譯成

( «e1» ) . GroupJoin( «e2» , «x1» => «k1» , «x2» => «k2» ,
                     ( «x1» , «g» ) => «v» )

join into 子句後面接著查詢主體子句

from «x1» in «e1»  
join «x2» in «e2» on «k1» equals «k2» into *g»  
...

已轉譯成

from * in ( «e1» ) . GroupJoin(  
   «e2» , «x1» => «k1» , «x2» => «k2» , ( «x1» , «g» ) => new { «x1» , «g» })
...

範例:此範例

from c in customers
join o in orders on c.CustomerID equals o.CustomerID into co
let n = co.Count()
where n >= 10
select new { c.Name, OrderCount = n }

已轉譯成

from * in (customers).GroupJoin(
    orders,
    c => c.CustomerID,
    o => o.CustomerID,
    (c, co) => new { c, co })
let n = co.Count()
where n >= 10
select new { c.Name, OrderCount = n }

其最終翻譯為

customers
    .GroupJoin(
        orders,
        c => c.CustomerID,
        o => o.CustomerID,
        (c, co) => new { c, co })
    .Select(x => new { x, n = x.co.Count() })
    .Where(y => y.n >= 10)
    .Select(y => new { y.x.c.Name, OrderCount = y.n })

其中 xy 是編譯器產生的識別符號,這些符號通常是隱形的並且無法存取。

結束範例

orderby 子句及其先前的 from 子句:

from «x» in «e»  
orderby «k1» , «k2» , ... , «kn»  
...

已轉譯成

from «x» in ( «e» ) .
OrderBy ( «x» => «k1» ) .
ThenBy ( «x» => «k2» ) .
... .
ThenBy ( «x» => «kn» )
...

如果 ordering 子句指定遞減方向指標,則會改為產生 OrderByDescendingThenByDescending 的叫用。

範例:此範例

from o in orders
orderby o.Customer.Name, o.Total descending
select o

具有最終翻譯

(orders)
    .OrderBy(o => o.Customer.Name)
    .ThenByDescending(o => o.Total)

結束範例

下列翻譯假設每個查詢表達式中沒有任何 letwherejoinorderby 子句,而且每個查詢表達式中最多只有一個初始 from 子句。

12.20.3.6 Select 子句

表單的查詢表達式

from «x» in «e» select «v»

已轉譯成

( «e» ) . Select ( «x» => «v» )

除了 «v» 是標識碼 «x»以外,翻譯只是簡單的

( «e» )

範例:此範例

from c in customers.Where(c => c.City == "London")
select c

只是轉譯成

(customers).Where(c => c.City == "London")

結束範例

12.20.3.7 群組子句

group 子句

from «x» in «e» group «v» by «k»

已轉譯成

( «e» ) . GroupBy ( «x» => «k» , «x» => «v» )

除非 «v» 是標識碼 «x»時,則翻譯為

( «e» ) . GroupBy ( «x» => «k» )

範例:此範例

from c in customers
group c.Name by c.Country

已轉譯成

(customers).GroupBy(c => c.Country, c => c.Name)

結束範例

12.20.3.8 透明標識符

某些翻譯會將範圍變數插入 透明標識符,*表示。 透明標識碼只存在於查詢表達式轉譯程式中做為中繼步驟。

當查詢轉譯插入透明標識符時,進一步的翻譯步驟會將透明標識符傳播至匿名函式和匿名物件初始化表達式。 在這些內容中,透明標識碼具有下列行為:

  • 當透明識別符作為匿名函式中的參數出現時,相關匿名類型的成員會自動在匿名函式主體中可用。
  • 如果一個包含透明識別碼的成員在範圍內,那麼該成員的所有成員也在範圍內。
  • 當透明標識符作為匿名物件初始化器中的成員宣告符時,它會引入一個具有透明標識符的成員。

在上述的翻譯步驟中,透明標識符一律會與匿名類型一起導入,目的是將多個範圍變數擷取為單一對象的成員。 C# 的實作可以使用與匿名類型不同的機制,將多個範圍變數分組在一起。 下列翻譯範例假設使用匿名類型,並顯示一個可能的透明標識符轉譯。

範例:此範例

from c in customers
from o in c.Orders
orderby o.Total descending
select new { c.Name, o.Total }

已轉譯成

from * in (customers).SelectMany(c => c.Orders, (c,o) => new { c, o })
orderby o.Total descending
select new { c.Name, o.Total }

進一步轉譯成

customers
    .SelectMany(c => c.Orders, (c,o) => new { c, o })
    .OrderByDescending(* => o.Total)
    .Select(\* => new { c.Name, o.Total })

這相當於清除透明標識時的結果

customers
    .SelectMany(c => c.Orders, (c,o) => new { c, o })
    .OrderByDescending(x => x.o.Total)
    .Select(x => new { x.c.Name, x.o.Total })

其中 x 是由編譯器生成的識別符,並且本身是不可見且無法存取的。

範例

from c in customers
join o in orders on c.CustomerID equals o.CustomerID
join d in details on o.OrderID equals d.OrderID
join p in products on d.ProductID equals p.ProductID
select new { c.Name, o.OrderDate, p.ProductName }

已轉譯成

from * in (customers).Join(
    orders,
    c => c.CustomerID,
    o => o.CustomerID,
    (c, o) => new { c, o })
join d in details on o.OrderID equals d.OrderID
join p in products on d.ProductID equals p.ProductID
select new { c.Name, o.OrderDate, p.ProductName }

進一步縮減為

customers
    .Join(orders, c => c.CustomerID,
        o => o.CustomerID, (c, o) => new { c, o })
    .Join(details, * => o.OrderID, d => d.OrderID, (*, d) => new { *, d })
    .Join(products, * => d.ProductID, p => p.ProductID,
        (*, p) => new { c.Name, o.OrderDate, p.ProductName })

其最終翻譯為

customers
    .Join(orders, c => c.CustomerID,
        o => o.CustomerID, (c, o) => new { c, o })
    .Join(details, x => x.o.OrderID, d => d.OrderID, (x, d) => new { x, d })
    .Join(products, y => y.d.ProductID, p => p.ProductID,
        (y, p) => new { y.x.c.Name, y.x.o.OrderDate, p.ProductName })

其中 xy 是編譯程式產生的標識符,否則為看不見且無法存取的標識符。 結束範例

12.20.4 查詢表達式模式

查詢表達式的模式 建立了一種方法模式,類型可以實作這種方法以支持查詢表達式。

如果泛型型別 C<T> 的公共成員方法和可公開存取的擴充方法可以用下列類別定義取代,則其支援查詢表達式模式。 成員和可存取的擴展方法被稱為泛型類型的「形狀」,C<T>。 泛型型別可用來說明參數和傳回型別之間的適當關聯性,但也可以實作非泛型型別的模式。

delegate R Func<T1,R>(T1 arg1);
delegate R Func<T1,T2,R>(T1 arg1, T2 arg2);

class C
{
    public C<T> Cast<T>() { ... }
}

class C<T> : C
{
    public C<T> Where(Func<T,bool> predicate) { ... }
    public C<U> Select<U>(Func<T,U> selector) { ... }
    public C<V> SelectMany<U,V>(Func<T,C<U>> selector,
        Func<T,U,V> resultSelector) { ... }
    public C<V> Join<U,K,V>(C<U> inner, Func<T,K> outerKeySelector,
        Func<U,K> innerKeySelector, Func<T,U,V> resultSelector) { ... }
    public C<V> GroupJoin<U,K,V>(C<U> inner, Func<T,K> outerKeySelector,
        Func<U,K> innerKeySelector, Func<T,C<U>,V> resultSelector) { ... }
    public O<T> OrderBy<K>(Func<T,K> keySelector) { ... }
    public O<T> OrderByDescending<K>(Func<T,K> keySelector) { ... }
    public C<G<K,T>> GroupBy<K>(Func<T,K> keySelector) { ... }
    public C<G<K,E>> GroupBy<K,E>(Func<T,K> keySelector,
        Func<T,E> elementSelector) { ... }
}

class O<T> : C<T>
{
    public O<T> ThenBy<K>(Func<T,K> keySelector) { ... }
    public O<T> ThenByDescending<K>(Func<T,K> keySelector) { ... }
}

class G<K,T> : C<T>
{
    public K Key { get; }
}

上述方法會使用泛型委派類型 Func<T1, R>Func<T1, T2, R>,但是它們同樣可以使用參數和傳回型別中具有相同關聯性的其他委派或表達式樹狀結構類型。

附注C<T>O<T> 之間建議的關聯性,可確保 ThenByThenByDescending 方法只能在 OrderByOrderByDescending的結果上使用。 尾注

附注:建議的 GroupBy結果圖形,其中每個內部序列都有額外的 Key 屬性。 尾注

附註:由於查詢表達式會透過語法對應轉譯為方法調用,因此類型在實作任何或所有查詢表達式模式的方式方面具有相當大的彈性。 例如,模式的方法可以實作為實例方法或擴充方法,因為兩者具有相同的調用語法,而且方法可以要求委派或表達式樹狀結構,因為匿名函式可轉換成兩者。 只實作某些查詢表示式模式的類型僅支持對應至類型所支援方法的查詢表達式翻譯。 尾注

NoteSystem.Linq 命名空間提供一個查詢運算式模式的實作,適用於任何實作 System.Collections.Generic.IEnumerable<T> 介面的類型。 尾注

12.21 指派運算符

12.21.1 一般

除了其中一個指派運算符以外,所有運算子都會將新值指派給變數、屬性、事件或索引器元素。 例外狀況 = ref會將變數參考(§9.5)指派給參考變數(§9.7)。

assignment
    : unary_expression assignment_operator expression
    ;

assignment_operator
    : '=' 'ref'? | '+=' | '-=' | '*=' | '/=' | '%=' | '&=' | '|=' | '^=' | '<<='
    | right_shift_assignment
    ;

指派的左操作數應該是分類為變數的表達式,或者,除了 = ref、屬性存取、索引器存取、事件存取或 Tuple 之外。 宣告表達式不能直接作為左操作數,但可能會在解構指派的評估過程中出現。

= 運算子稱為 簡單指派運算子。 它會將右操作數的一個或多個值指派給左操作數所指定的變數、屬性、索引器元素或元組元素。 簡單指派運算子的左操作數不應是事件存取器(除非 §15.8.2中另有說明)。 簡單指派運算子描述於 .12.21.2

運算子 = ref 稱為 ref 指派運算子。 它會讓右操作數成為 variable_reference),左操作數所指定之參考變數的參考值。 ref 指派運算符於 §12.21.3中描述。

== ref 運算子以外的指派運算元稱為 複合指派運算子。 這些運算子會在兩個操作數上執行指示的作業,然後將產生的值指派給左操作數所指定的變數、屬性或索引器元素。 複合指派運算子在 §12.21.4中描述。

+=-= 運算符,當左操作數是事件存取表達式時,稱為 事件指派運算子。 作為左操作數的情況下,事件存取不接受任何其他指派運算符。 事件指派運算符描述於 .12.21.5

指派運算子是右關聯運算元,這表示作業會從右至左分組。

範例:將表達式 a = b = c 計算為 a = (b = c)結束範例

12.21.2 簡單指派

= 運算符稱為簡單指派運算符。

如果簡單賦值的左元件是形如 E.PE[Ei],而 E 在編譯時的類型是 dynamic,則該賦值會被動態綁定(§12.3.3)。 在此情況下,指派表達式的編譯時間類型是 dynamic,以下所述的解析過程將會根據執行時期類型 E在執行時期進行。 如果左操作數的格式為 E[Ei],其中至少有一個 Ei 元素的編譯時間類型為 dynamic,且 E 的編譯時間類型不是陣列,則產生的索引器存取會動態繫結,但具有有限的編譯時間檢查(§12.6.5)。

一個將左操作數分類為元組(Tuple)的簡單指派,也稱為 解構賦值。 如果左操作數的任何元組元素具有元素名稱,就會發生編譯時期錯誤。 如果左操作數的任何元組元素是 declaration_expression,而任何其他元素不是 declaration_expression 或簡單丟棄,編譯時會發生錯誤。

簡單賦值的類型 x = y 是對 xy語句的賦值類型,其遞歸決定如下:

  • 如果 x 是元組表達式 (x1, ..., xn),並且 y 可以解構為具有 n 元素的元組表達式 (y1, ..., yn)§12.7),則每個對 yi 的指派 xi 具有類型 Ti,且該指派具有類型 (T1, ..., Tn)
  • 否則,如果 x 分類為變數,則變數不會 readonlyx 具有類型 T,而且 y 隱含轉換成 T,則指派的類型為 T
  • 否則,如果 x 分類為隱含型別變數(亦即隱含型別宣告表達式),且 y 具有類型 T,則變數的推斷型別 T,且指派具有類型 T
  • 否則,如果 x 分類為屬性或索引器存取,則屬性或索引器具有可存取的 set 存取子、x 具有類型 T,且 y 隱含轉換成 T,則指派具有類型 T
  • 否則指派作業無效,而且會發生綁定時間錯誤。

執行具有類型 T 之表單 x = y 簡單指派的運行時間處理,會執行指派給具有類型 Tyx,其中包含下列遞歸步驟:

  • 如果尚未評估,則評估 x
  • 如果 x 分類為變數,則會評估 y,並視需要透過隱含轉換轉換為 T\10.2)。
    • 如果 x 指定的變數是 reference_type的陣列元素,則會執行執行時檢查,以確保為 y 計算的值與 x 所屬的陣列實例相容。 檢查會成功的條件是:如果 ynull,或如果存在從 y 所參考實例的類型到包含 x的陣列實例的實際元素類型的隱含參考轉換(§10.2.8)。 否則,會拋出 System.ArrayTypeMismatchException
    • 評估和轉換 y 所產生的值會儲存到由評估 x所給出的位置,並作為指派的結果返回。
  • 如果 x 分類為屬性或索引器存取:
    • 會評估 y,並在需要時透過隱含轉換(§10.2)轉換為 T
    • x 的 set 存取子被叫用,其值參數是由對 y 的評估與轉換所得出。
    • y 的評估與轉換所得之值,被作為賦值的結果。
  • 如果 x 被分類為具有元數 n的元組 (x1, ..., xn)
    • yn 元素解構為元組表示式 e
    • 使用隱含元組轉換,將 e 轉換成 T,以建立結果元組 t
    • 針對每個 xi,依左至右的順序進行將 t.Itemi 賦值給 xi 的操作,但不會再次評估 xi
    • t 會因為指派而產生。

注意:如果 x 的編譯時期型別是 dynamic,且從 y 的編譯時期型別有隱含轉換到 dynamic,則不需要執行時間解析。 尾注

附註:陣列共變數規則(§17.6)允許陣列類型的值 A[] 可以是陣列類型 B[]的實例參考,前提是從 BA存在隱含參考轉換。 由於這些規則,指派給 reference_type 的陣列元素需要在執行時檢查,以確保指派的值與陣列實例相容。 在範例中

string[] sa = new string[10];
object[] oa = sa;
oa[0] = null;              // OK
oa[1] = "Hello";           // OK
oa[2] = new ArrayList();   // ArrayTypeMismatchException

最後一個指派作業會導致拋出 System.ArrayTypeMismatchException,因為無法將 ArrayList 的參考儲存在 string[]的元素中。

尾注

struct_type 中宣告的屬性或索引器是指派的目標時,與屬性或索引器存取相關聯的實例表達式應分類為變數。 如果實例表達式分類為值,則會發生系結時間錯誤。

附註:由於 §12.8.7,相同的規則也適用於欄位。 尾注

範例:給定以下宣告:

struct Point
{
   int x, y;

   public Point(int x, int y)
   {
      this.x = x;
      this.y = y;
   }

   public int X
   {
      get { return x; }
      set { x = value; }
   }

   public int Y {
      get { return y; }
      set { y = value; }
   }
}

struct Rectangle
{
    Point a, b;

    public Rectangle(Point a, Point b)
    {
        this.a = a;
        this.b = b;
    }

    public Point A
    {
        get { return a; }
        set { a = value; }
    }

    public Point B
    {
        get { return b; }
        set { b = value; }
    }
}

在範例中

Point p = new Point();
p.X = 100;
p.Y = 100;
Rectangle r = new Rectangle();
r.A = new Point(10, 10);
r.B = p;

允許 p.Xp.Yr.Ar.B 指派,因為 pr 是變數。 不過,在範例中

Rectangle r = new Rectangle();
r.A.X = 10;
r.A.Y = 10;
r.B.X = 100;
r.B.Y = 100;

指派全都無效,因為 r.Ar.B 不是變數。

結束範例

12.21.3 參考指派

= ref 運算符稱為 ref 賦值 運算符。

左操作數必須是綁定至參考變數的表達式(§9.7)、參考參數(除了this)、輸出參數或輸入參數。 右操作數應該是一個運算結果,其中生成 variable_reference 並指定一個與左操作數相同類型的值。

如果左操作數的 ref-safe-context (\9.7.2)比右操作數的 ref-safe-context 寬,則為編譯時間錯誤。

右操作數應該在 ref 指派的點上明確指派。

當左操作數係結至輸出參數時,如果在 ref 指派運算子的開頭未明確指派該輸出參數,就會發生錯誤。

如果左操作數是一個可寫的引用(亦即,它指定的不是 ref readonly 本地或輸入參數),那麼右操作數必須是可寫的 變數引用。 如果右操作數變數是可寫的,則左操作數可能是可寫的或只讀的參考 (ref)。

此作業會使左操作數成為右操作數變數的別名。 即使變數的右運算元是可寫的,別名也可能是唯讀的。

ref 指派運算子會產生一個屬於指派類型的 variable_reference。 如果左操作數可寫入,則此項也為可寫入。

ref 指派運算符不得讀取右操作數所參考的儲存位置。

範例:以下是使用 = ref的一些範例:

public static int M1() { ... }
public static ref int M2() { ... }
public static ref uint M2u() { ... }
public static ref readonly int M3() { ... }
public static void Test()
{
int v = 42;
ref int r1 = ref v; // OK, r1 refers to v, which has value 42
r1 = ref M1();      // Error; M1 returns a value, not a reference
r1 = ref M2();      // OK; makes an alias
r1 = ref M2u();     // Error; lhs and rhs have different types
r1 = ref M3();    // error; M3 returns a ref readonly, which r1 cannot honor
ref readonly int r2 = ref v; // OK; make readonly alias to ref
r2 = ref M2();      // OK; makes an alias, adding read-only protection
r2 = ref M3();      // OK; makes an alias and honors the read-only
r2 = ref (r1 = ref M2());  // OK; r1 is an alias to a writable variable,
              // r2 is an alias (with read-only access) to the same variable
}

結束範例

附註:在使用 = ref 運算符閱讀程式碼時,可能很容易錯誤地將 ref 部分視為操作數的一部分。 當操作數是條件式 ?: 表示式時,這特別令人困惑。 例如,在讀取 ref int a = ref b ? ref x : ref y; 時,務必要理解為 = ref 是運算子,而 b ? ref x : ref y 是右操作數:ref int a = ref (b ? ref x : ref y);。 重要的是,表達式 ref b而不是 該語句的一部分,即使乍看之下似乎是如此。 尾注

12.21.4 複合指派

如果複合指派的左操作數是 E.PE[Ei],而 E 具有編譯時類型 dynamic,則該指派是動態綁定(§12.3.3)。 在此情況下,指派表達式的編譯期間類型是 dynamic,而所述的解析將根據 E的運行期間類型在運行時進行。 如果左操作數的格式為 E[Ei],其中至少有一個 Ei 元素具有編譯時類型 dynamic,且 E 的編譯時類型不是陣列,則產生的索引器存取會動態繫結,但具有限的編譯時檢查(§12.6.5)。

形式 x «op»= y 的運算會通過套用二元運算子的多載解析(§12.4.5)來進行處理,如同該運算被寫作 x «op» y。 然後

  • 如果所選的運算子的傳回型別可以隱含轉換為 x的型別,則運算將被評估為 x = x «op» y,只是 x 只會被評估一次。
  • 否則,如果選取的運算符是預先定義的運算符,如果所選取運算符的傳回型別明確轉換成 x 類型,而且如果 y 隱含轉換成 x 類型,或運算符是 shift 運算符,則會將作業評估為 x = (T)(x «op» y),其中 Tx類型, 不同之處在於 x 只會評估一次。
  • 否則,複合指派無效,而且會發生綁定時錯誤。

「只評估一次」一詞表示,在評估 x «op» y時,會暫時儲存 x 的任何組成表達式結果,然後在執行指派至 x時重複使用。

範例:在工作分派 A()[B()] += C()中,A 是傳回 int[]的方法,而 BC 是傳回 int的方法,方法只會依 ABC的順序叫用一次。 結束範例

當複合指派的左操作數是屬性存取或索引器存取時,屬性或索引器應同時具有 get 存取子和 set 存取子。 如果情況並非如此,則會發生系結時間錯誤。

上述第二個規則允許 x «op»= y 在特定情境中被評估為 x = (T)(x «op» y)。 規則存在,因此當左操作數的類型為 sbytebyteshortushortchar時,預先定義的運算子就可以當做複合運算符使用。 即使這兩個自變數都是其中一種類型,預先定義的運算符還是會產生類型 int的結果,如 .12.4.7.3中所述。 因此,如果沒有轉換,就無法將結果指派給左操作數。

預先定義運算子的規則的直觀效果就是,若允許x «op» yx = y,也就允許x «op»= y

範例:在下列程式代碼中

byte b = 0;
char ch = '\0';
int i = 0;
b += 1;           // OK
b += 1000;        // Error, b = 1000 not permitted
b += i;           // Error, b = i not permitted
b += (byte)i;     // OK
ch += 1;          // Error, ch = 1 not permitted
ch += (char)1;    // OK

每個錯誤的直覺原因是對應的簡單指派也會是錯誤。

結束範例

附註:這也表示複合賦值運算支援提升的運算子。 由於複合指派 x «op»= y 評估為 x = x «op» yx = (T)(x «op» y),因此評估規則隱含地涵蓋提升運算子。 尾注

12.21.5 事件指派

如果 a += or -= 運算子左邊的操作數被分類為事件存取,則該表達式會按如下方式進行評估:

  • 評估事件存取的實例表達式,如果有的話。
  • 會評估 +=-= 運算子的右運算元,如果需要,會透過隱含轉換轉變成左運算元的類型(§10.2)。
  • 事件的存取子會被叫用,其參數清單包含上一個步驟中計算的值。 如果運算子 +=,則會叫用 add 存取子;如果運算子 -=,則會叫用 remove 存取子。

事件指派表達式不會產生值。 因此,事件指派表達式只有在 statement_expression 的語境中有效(§13.7)。

12.22 表達式

表示式non_assignment_expression賦值

expression
    : non_assignment_expression
    | assignment
    ;

non_assignment_expression
    : declaration_expression
    | conditional_expression
    | lambda_expression
    | query_expression
    ;

12.23 常數表達式

常數表達式是應在編譯時期完整評估的表達式。

constant_expression
    : expression
    ;

常數表示式應具有值 null 或下列其中一種類型:

  • sbytebyteshortushortintuintlongulongcharfloatdoubledecimalboolstring;
  • 列舉型別;或
  • 參考型別的預設值表達式(§12.8.21)。

常數表示式中只允許下列建構:

  • 字面值(包括 null 字面值)。
  • 類別和結構型別 const 成員的引用。
  • 列舉型別成員的參考。
  • 區域常數的引用。
  • 括號內的子表達式,其本身是常數表達式。
  • 轉換表達式。
  • checkedunchecked 表達式。
  • nameof 表示式。
  • 預先定義的 +-!(邏輯否定)和 ~ 一元運算符。
  • 預先定義的 +-*/%<<>>&|^&&||==!=<><=>= 二元運算符。
  • ?: 條件運算符。
  • ! null-forgiving 運算子 (.12.8.9)。
  • sizeof 表達式,前提是 unmanaged-type 是 §23.6.9 中指定的類型之一,且 sizeof 會傳回一個常數值。
  • 默認值表達式,前提是類型是上述其中一種類型。

常數表示式允許下列轉換:

  • 身分識別轉換
  • 數值轉換
  • 列舉轉換
  • 常數表達式轉換
  • 隱含和明確的參考轉換,前提是轉換的來源是評估為 null 值的常數表達式。

附註:常數運算式中不允許其他轉換,包括非null 值的裝箱、拆箱和隱含參考轉換。 尾注

範例:在下列程式代碼中

class C
{
    const object i = 5;         // error: boxing conversion not permitted
    const object str = "hello"; // error: implicit reference conversion
}

i 的初始化是錯誤,因為需要 Boxing 轉換。 str 初始化是錯誤,因為需要來自非null 值的隱含參考轉換。

結束範例

每當表達式符合上述需求時,表達式就會在編譯階段進行評估。 即使表達式是包含非常數建構之較大表達式的子表達式,也是如此。

常數表達式的編譯時期評估會使用與非常數的表達式在執行時期評估相同的規則,但在執行時期評估會拋出異常的情況下,編譯時期評估則會導致編譯時期錯誤發生。

除非在 unchecked 內容中明確放置常數表達式,否則在執行編譯時期評估時,發生於整數類型算術運算和轉換中的溢位,將一律導致編譯時錯誤(§12.8.20)。

在下列情境中需要常數表達式,這在文法中以使用 constant_expression來表示。 在這些內容中,如果無法在編譯時期完全評估表達式,就會發生編譯時期錯誤。

隱含常數表達式轉換 (10.2.11)允許將類型 int 常數表達式轉換成 sbytebyteshortushortuintulong,前提是常數表達式的值在目的型別的範圍內。

12.24 布爾表達式

boolean_expression 是產生類型 bool結果的表達式;直接或透過在特定內容中套用 operator true,如下所述:

boolean_expression
    : expression
    ;

if_statement 的控制條件表達式(§13.8.2),while_statement§13.9.2),do_statement§13.9.3),或 for_statement§13.9.4)是 boolean_expression?: 運算子的控制用的條件表達式(§12.18)遵循與 boolean_expression相同的規則,但因為運算符優先順序的原因,被分類為 null_coalescing_expression

boolean_expressionE 必須能夠產生類型為 bool的值,如下所示:

  • 如果 E 可以隱含轉換成 bool 則會在運行時間套用隱含轉換。
  • 否則,一元運算符多載解析(§12.4.4)用於在 E上尋找唯一的最佳 operator true 實作,並在執行時期套用該實作。
  • 如果找不到這類運算符,就會發生系結時間錯誤。