12.1 一般
表達式是運算子和操作數序列。 這個子句會定義語法、操作數和運算子的評估順序,以及表達式的意義。
12.2 運算式分類
12.2.1 一般
運算式的結果會分類為下列其中一項:
- 一個值。 每個值都有相關聯的類型。
- 變數。 除非另有指定,否則變數會明確類型化,而且具有關聯類型,也就是變數的宣告類型。 隱含型別變數沒有相關聯的型別。
- 空字面量。 具有此分類的表達式可以隱含地轉換成參考類型或可為 Null 的實值類型。
- 匿名函式。 具有此分類的表達式可以隱含轉換成相容的委派類型或表達式樹狀結構類型。
- 一個元組。 每個元組都有固定數目的元素,每個元素都有運算式和選用的元素名稱。
- 屬性存取。 每個屬性存取都有相關聯的類型,也就是 屬性的類型。 此外,屬性存取可能會有相關聯的實例表達式。 叫用實例屬性存取的存取子時,評估實例表達式的結果會變成以
this表示的實例(#.12.8.14)。 - 使用索引器進行存取。 每個索引器存取都有相關聯的類型,也就是索引器的項目類型。 此外,索引器存取具有相關聯的實例表達式和相關聯的自變數清單。 當叫用索引器的存取子時,評估實例表達式的結果會轉變為由
this代表的實例(§12.8.14),而評估引數清單的結果會轉變為調用的參數清單。 - 無。 當表達式是傳回型別為
void的方法調用時,就會發生這種情況。 分類為無的表達式僅在 statement_expression (§13.7) 或 lambda_expression 主體 (§12.21) 的上下文中有效。
對於作為較大表達式的子表達式出現的表達式,在已指出的限制下,結果也可以分類為以下之一:
- 命名空間。 具有此分類的表達式只能顯示為 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.5),而且可以隱含地轉換成相容的委派類型(\10.8)。 在任何其他情況下,分類為方法群組的表達式會產生編譯時期的錯誤。 - 事件存取。 每個事件存取都有相關聯的類型,也就是事件的型別。 此外,事件存取可能會有相關聯的實例表達式。 事件存取可能會顯示為 和
+=-=運算子的左運算元 (§12.23.5)。 在任何其他情境中,分類為事件存取的表達式會導致編譯時錯誤。 當實例事件存取的存取子被叫用時,評估實例表達式的結果將成為由this表示的實例(§12.8.14)。 - throw 表達式,可用於多種情境,以擲回例外狀況至表達式中。 擲出運算式可能會透過隱含轉換轉為任何類型。
屬性存取或索引器存取一律會藉由執行 get 存取子或 set 存取子的調用,重新分類為值。 特定存取子是由屬性或索引子存取的內容所決定:如果存取是指派的目標,則會叫用 set 存取子來指派新值 (§12.23.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ᵥ) - 多載一元運算符:
+、-、!(僅限邏輯否定)、~、++、--、true、false - 多載二進位運算符:
+、-、*、/、%、&、&&、|、||、??、^<<、>>、==、!=、>、<、>=、<= - 指定派運算子:
=,= 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)中,會使用F的舊值呼叫 方法i,然後使用G的舊值呼叫方法i,最後會以 i 的新值呼叫方法H。 這與運算符優先順序無關。 結束範例
某些運算子可以 多載。 運算子多載 (•12.4.3) 允許針對一或兩個操作數屬於使用者定義類別或結構類型的作業指定使用者定義的運算子實作。
12.4.2 運算元優先順序和關聯性
當表達式包含多個運算符時,運算子 優先順序 會控制個別運算符的評估順序。
Note:例如,表達式
x + y * z會評估為x + (y * z),因為*運算符的優先順序高於二進位+運算符。 結尾註釋
運算符的優先順序是由其相關聯文法產生式的定義所建立。
附註:例如,additive_expression 是由 或
+運算符分隔的-序列所組成,因此讓+和-運算符的優先順序低於*、/和%運算符。 結尾註釋
附註:下表摘要說明優先等級從最高到最低的所有運算子:
子條款 類別 運算子 §12.8 主要 x.yx?.yf(x)a[x]a?[x]x++x--x!newtypeofdefaultcheckeduncheckeddelegatestackalloc§12.9 單一運算 +-!x~^++x--x(T)xawait x§12.10 範圍 ..§12.11 開關 switch { … }§12.12 乘法 */%§12.12 添加劑 +-§12.13 轉變 <<>>§12.14 關係型和類型測試 <><=>=isas§12.14 平等 ==!=§12.15 邏輯與運算 &§12.15 邏輯 XOR ^§12.15 邏輯或 \|§12.16 條件式 AND &&§12.16 條件式 OR \|\|第 12.17 條 和 第 12.18 條 空合併運算子和拋出表達式 ??throw x§12.20 有條件的 ?:第 12.23 條 和 第 12.21 條 指派和 Lambda 運算式 == ref*=/=%=+=-=<<=>>=&=^=\|==>??=結尾註釋
當操作數發生在具有相同優先順序的兩個運算符之間時,運算子的 關聯性 會控制執行作業的順序:
- 除了賦值運算子、範圍運算子和空值合併運算子之外,所有二進位運算子都是 左關聯的,這表示從左到右執行運算。
範例:
x + y + z評估為(x + y) + z。 結束範例 - 指派運算符、null 聯合運算子和條件運算符 (
?:) 是 右關聯,這表示作業是從右至左執行。範例:
x = y = z評估為x = (y = z)。 結束範例 - 範圍運算子是 非關聯的,這表示範圍運算子的左運算元或右運算元都不能是 range_expression。
範例:兩者 和
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中所述。
可 重載的一元運算子s 是:
+ - !(僅限邏輯否定)~ ++ -- true false
只有上述運算子可以多載。 特別是,無法重載 null 容恕運算子 (前置詞 !, §12.8.9) 或一元索引從結尾運算子 (前置詞 ^, (§12.9.6)) 。
附註: 雖然 和
true未在運算式中明確使用 (因此未包含在false的優先順序資料表中) ,但它們會被視為運算子,因為它們會在數個運算式內容中叫用:布林運算式 (§12.26) ,以及涉及條件式 (§12.20) 和條件式邏輯運算子 (§12.16) 的運算式。 結尾註釋
可 重載的二進位運算子是:
+ - * / % & | ^ << >> == != > < <= >=
只有上述運算子可以多載。 特別是,無法多載成員存取、方法呼叫或 ..、 =&&||???:=>checkeduncheckednewtypeofdefaultasis
當二元運算子多載時,對應的複合指派運算子(如果有的話)也會被隱含多載。
範例:運算子
*的多載也是運算子*=的多載。 這在 §12.23 中進一步描述。 結束範例
指派運算符本身 (=) 無法多載。 賦值一律會執行將值簡單儲存到變數中 (§12.23.2) 。
轉換作業,例如 (T)x,會藉由提供使用者定義的轉換來重載(§10.5)。
注意:使用者定義轉換不會影響
is或as運算符的行為。 結尾註釋
元素存取,例如 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.9 到 §12.23 中個別運算子的描述會指定運算子的預先定義實作,以及套用至每個運算子的任何其他規則。 這些描述會使用 一元運算子多載解析、二元運算子多載解析、數值升階這些詞彙,以及在下列子章節中找到的提升運算子定義。
12.4.4 一元運算子多載解析
操作的形式為 «op» x 或 x «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的表達式,會依下列方式處理:
- 由
X及Y提供的用於操作operator «op»(x, y)的候選使用者定義運算子的集合已被確定。 此集合包含X所提供的候選運算符聯集,以及由Y提供的候選運算符聯集,每個運算符都是使用 -12.4.6的規則所決定。 針對合併集,候選者將合併如下:- 如果
X和Y可轉換身分識別,或X和Y衍生自一般基底類型,則共用候選運算符只會發生在合併集一次。 - 如果
X與Y之間有身份轉換,«op»Y所提供的運算子Y的傳回類型與«op»X所提供的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。 - 對於
operator «op»中的所有T₀宣告,以及這類運算子的所有提升形式,如果至少有一個運算子適用於自變數清單§12.6.4.2的A,則候選運算子集合由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,其中b是byte,而s是short,多載解析會選取operator *(int, int)為最佳運算符。 因此,效果是b和s會轉換成int,而結果的類型int。 同樣地,針對作業i * d,而其中i是int,d是double,overload解析過程中選擇operator *(double, double)作為最佳操作員。 結束範例
說明文字結尾。
12.4.7.2 一元數值升階
這個子句供參考。
針對預先定義的一元運算子 +、-和 ~ 的操作數,會發生一元數值升階。 一元數值升階只包含將類型 sbyte、byte、short、ushort或 char 轉換成類型 int的操作數。 此外,針對一元 – 運算符,一元數值升階會將類型 uint 的操作數轉換為類型 long。
說明文字結尾。
12.4.7.3 二進位數值升階
這個子句供參考。
預先定義的 +、-、*、/、%、&、|、^、==、!=、>、<、>=和 <= 二元運算符的操作數會發生二進位數值提升。 二進位數值升階會隱含地將這兩個操作數轉換成共同類型,如果是非關係運算符,這個類型也會成為運算結果的類型。 二進位數值升級包括依次套用以下規則,其順序如下:
- 如果任一操作數的類型為
decimal,則另一個操作數會轉換成類型decimal,或者如果另一個操作數的類型為float或double,則會發生係結時間錯誤。 - 否則,如果任一操作數的類型為
double,則另一個操作數會轉換成類型double。 - 否則,如果任一操作數的類型為
float,則另一個操作數會轉換成類型float。 - 否則,如果任一操作數的類型為
ulong,則另一個操作數會轉換成類型ulong,或者如果另一個操作數是type sbyte、short、int或long,則會發生系結時間錯誤。 - 否則,如果任一操作數的類型為
long,則另一個操作數會轉換成類型long。 - 否則,如果任一操作數的類型為
uint,而另一個操作數的類型為sbyte、short或int,則兩個操作數都會轉換成類型long。 - 否則,如果任一操作數的類型為
uint,則另一個操作數會轉換成類型uint。 - 否則,這兩個操作數都會轉換成類型
int。
Note:第一個規則不允許將
decimal類型與double和float類型混合的任何作業。 規則遵循的事實是,decimal類型與double和float類型之間沒有隱含轉換。 結尾註釋
附註:另請注意,當另一個操作數是帶正負號整數型別時,操作數不可能是類型
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是類型的 and&運算子|,如bool?中所述) ,則提升的運算子會產生值。 否則,提升運算符會解包操作數,應用底層運算符,並封裝結果。 - 對於等號運算子
==和!=,如果操作數型別都是不可為 Null 的實值型別,而且如果結果型別是bool,則存在運算子的增益形式。 提升形式是通過在每個運算元類型中添加一個?修飾詞來構建的。 提昇運算子會將兩個null值視作相等,且null值與任何非null值視作不相等。 如果兩個操作數都是非null,則提升運算符會解開操作數,並套用基礎運算符來產生bool結果。 - 對於關係運算子
<、>、<=和>=,如果操作數的類型都是不可為 Null 的實值型別,並且結果型別是bool,則該運算子具有提升形式。 提升形式是通過在每個運算元類型中添加一個?修飾詞來構建的。 若其中一個或兩個運算元是false,則提升運算子會產生值null。 否則,增益運算符會解除包裝操作數,並套用基礎運算符來產生bool結果。
12.5 成員查找
12.5.1 一般
成員查找指的是在一個類型的上下文中確定名稱定義的過程。 成員查詢可以作為評估表達式中的simple_name(§12.8.4)或member_access(§12.8.7)的一部分。 如果 simple_name 或 member_access 作為 invocation_expression 的 primary_expression(§12.8.10.2),則成員稱為 調用。
如果成員是方法或事件,或是委派類型 (§21) 或類型 dynamic (§8.2.4) 的常數、欄位或屬性,則稱為 可叫用成員。
成員查閱不僅會考慮成員的名稱,也會考慮成員擁有的類型參數數目,以及成員是否可存取。 為了進行成員查閱,泛型方法和巢狀泛型型別具有各自宣告中指出的類型參數數目,而所有其他成員都有零個類型參數。
在類型 N 中,名稱 K 具有 T 型別引數的成員查找按以下方式處理:
- 首先,會決定一組名為
N的可存取成員: - 接下來,如果
K為零,則會移除宣告包含類型參數的所有巢狀類型。 如果K不是零,則會移除具有不同類型參數數目的所有成員。 當K為零時,不會移除具有型別參數的方法,因為型別推斷過程(§12.6.3)可能會推斷型別參數。 - 接下來,如果叫用成員,則會從集合中移除所有不可叫用的成員。
- 接下來,會從集合中移除其他成員隱藏的成員。 對於集合中每個成員
S.M,其中S是宣告成員M的類型,則會套用下列規則:- 如果
M是常數、欄位、屬性、事件或列舉成員,則會從以S作為基底類型所宣告的集合中移除所有成員。 - 如果
M是類型宣告,則會從集合中移除在S基底類型中宣告的所有非型別成員,並且會移除在M基底類型中宣告且型別參數數目與S相同的所有型別宣告。 - 如果
M是一個方法,那麼在S的基類型中宣告的所有非方法成員都會從集合中刪除。
- 如果
- 接下來,類別成員所隱藏的介面成員會從集合中移除。 如果
T是類型參數,而且T除了object之外,還有有效的基類,以及非空白的有效介面集(\15.2.5),這個步驟才有效果。 對於集合中每個成員S.M,其中S是宣告成員M的類型,如果S是object以外的類別宣告,則會套用下列規則:- 如果
M是常數、字段、屬性、事件、列舉成員或類型宣告,則介面宣告中的所有成員都會從集合中移除。 - 如果
M是方法,則會從集合中移除介面宣告中宣告的所有非方法成員,而且所有簽章與介面宣告中宣告M相同的方法都會從集合中移除。
- 如果
- 最後,移除隱藏成員之後,查找的結果是:
- 如果集合是由不是方法的單一成員所組成,則此成員是查閱的結果。
- 否則,如果集合只包含方法,則這個方法群組就是查閱的結果。
- 否則,查找會出現模糊不清,並且會發生綁定時間錯誤。
在類型參數和介面之外的類型中查詢成員,以及在只允許單一繼承的介面中查詢成員(繼承鏈中的每個介面最多只有一個直接基底介面),查詢規則的效果僅為衍生成員會隱藏具有相同名稱或簽名的基底成員。 這類單一繼承查閱從不會有模糊不清的情況。 §19.4.11 中描述了多重繼承介面中成員查閱可能產生的模糊性。
附注:此階段僅涉及一種模棱兩可的情況。 如果成員查詢結果是一個方法群組,則方法群組的進一步使用可能會因為模棱兩可而失敗,如 \12.6.4.1 和 \12.6.6.2 中所述。 結尾註釋
12.5.2 基底類型
為了方便成員查閱,T 類型會被視為具有下列基底類型:
- 如果
T是object或dynamic,則T沒有基底類型。 - 如果
T是 enum_type,則T的基底類型是類別類型System.Enum、System.ValueType和object。 - 如果
T是 struct_type,T的基底類型是類別類型System.ValueType和object。附註:nullable_value_type 是 struct_type (§8.3.1)。 結尾註釋
- 如果
T是 class_type,T的基底類型是T的基類,包括類別類型object。 - 如果
T是 interface_type,則T的基底類型是T的基底介面,而類別類型object。 - 如果
T是 array_type,則T的基底類型是類別類型System.Array和object。 - 如果
T是 delegate_type,則T的基底類型是類別類型System.Delegate和object。
12.6 函式成員
12.6.1 一般
函式成員是包含可執行語句的成員。 函式成員一律是型別的成員,不能是命名空間的成員。 C# 會定義下列函式成員類別:
- 方法
- 屬性
- 事件
- 索引員
- 使用者定義運算子
- 實例建構函式
- 靜態建構函式
- 終結器
除了終結器和靜態建構函式(無法被明確呼叫)之外,函式中包含的語句會透過函式呼叫來執行。 撰寫函式成員調用的實際語法取決於特定函式成員類別。
函式成員的呼叫的引數清單(§12.6.2)提供函式成員參數的實際值或變數參考。
泛型方法的調用可能會採用型別推斷來判斷要傳遞至方法的類型自變數集。 此過程在 §12.6.3中描述。
方法、索引器、運算符和實例建構函式的調用會採用多載解析,以判斷要叫用的候選函式成員集。 此過程在 §12.6.4中描述。
在繫結時間識別出特定的函式成員後,可能透過多載解析,叫用該函式成員的實際運行過程描述於 §12.6.6。
附注:下表摘要說明在涉及可明確叫用之函式成員六個類別的建構中發生的處理。 在數據表中,
e、x、y和value表示分類為變數或值的表達式,T表示分類為類型的運算式,F是方法的簡單名稱,P是屬性的簡單名稱。
構建 例 描述 方法調用 F(x, y)重載解析用來在包含的類別或結構中選出最佳方法 F。 方法會使用自變數清單(x, y)叫用。 如果方法不是static,則實例表達式為this。T.F(x, y)多載解析會套用至 類別或結構中選取最佳方法 FT。 如果方法未為static,就會發生系結時間錯誤。 方法會使用自變數清單(x, y)叫用。e.F(x, y)多載解析用於選擇由 F類型所指定之類別、結構或介面中的最佳方法e。 如果方法static,就會發生系結時間錯誤。 使用實例表達式e和自變數清單(x, y)叫用 方法。屬性存取 P屬性 P的 get 存取子在其包含的類別或結構中被呼叫。 如果P為僅限寫入,就會發生編譯時期錯誤。 如果P不是static,則實例表示式會this。P = value包含類別或結構中屬性 P的 set 存取子會使用自變數清單(value)叫用。 如果P是唯讀的,就會發生編譯時錯誤。 如果P不是static,則實例表示式會this。T.P在類別或結構 P中,屬性T的 get 存取子已被呼叫。 如果P不是static,或P為僅限寫入,就會發生編譯時期錯誤。T.P = value類別或結構 P中屬性T的 set 存取子會叫用自變數清單(value)。 如果P不是static,或者P是唯讀,就會發生編譯時錯誤。e.P在由 P型別提供的類別、結構或介面中,由實例表達式E叫用屬性e的 get 存取子。 如果P為static,或P為僅限寫入,則會發生系結時間錯誤。e.P = value實例表達式 PE和自變數清單e,叫用類別、結構或介面中(value)型別所指定之屬性的 set 存取子。 如果P是static,或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調用類別或結構 E中事件T的 remove 存取子。 如果E不是static,就會發生系結時間錯誤。e.E += value事件 E的 add 存取子由E型別的類別、結構或介面使用實例表達式e進行呼叫。 如果E是static,就會發生系結時間錯誤。e.E -= value類別、結構或介面中,由型別 E指定的事件E的 remove 存取子被實例運算式e叫用。 如果E是static,就會發生系結時間錯誤。索引器存取 e[x, y]重載解析將應用於類別、結構或介面中,以選擇由 e類型提供的最佳索引器。 索引器的 get 存取子會使用實例表示式e和自變數清單(x, y)叫用。 如果索引器是唯寫的,就會發生系結時間錯誤。e[x, y] = value重載解析將應用於類別、結構或介面中,以選擇由 e類型提供的最佳索引器。 索引器的 set 存取子被調用實例表達式e和參數列表(x, y, value)。 如果索引器是只讀的,就會發生系結時間錯誤。運算符調用 -x多載決策適用於依據類型 x的類別或結構中,以選擇最佳的一元運算子。 選取的運算子會叫用自變數清單(x)。x + y多載解析會在類別或結構中,根據 x和y類型來選擇最佳的二進位運算元。 選取的運算子會叫用自變數清單(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 存取子宣告的隱含值參數。
- 針對使用者定義的一元運算元(包括轉換),單一操作數會對應至運算符宣告的單一參數。
- 針對使用者定義的二元運算符,左操作數會對應至第一個參數,而右操作數會對應至運算符宣告的第二個參數。
- 未命名的自變數會在位外命名自變數或對應至參數陣列的具名自變數之後,對應至無參數。
附注:這可防止
void M(bool a = true, bool b = true, bool c = true);叫用M(c: false, valueB);。 第一個自變數會用在位置外(自變數用於第一個位置,但名為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結束範例
當具有參數陣列的函式成員以擴展形式調用並至少包含一個擴展參數時,此調用將像在擴展的參數周圍插入了一個包含陣列初始值設定項的陣列建立運算式一樣被處理。 當參數陣列沒有自變數時,會傳遞空陣列;未指定傳遞的參考是新配置的或現有的空陣列。
範例:給定的宣告
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> } }透過類型推斷,型別參數
int和string是從方法的參數中確定的。結束範例
類型推斷會在方法調用的系結時間處理中發生(\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.15),並尋找一組表達式的最佳常見類型(\12.6.3.16)。 結尾註釋
12.6.3.2 第一個階段
針對每個方法自變數 Eᵢ,輸入類型推斷 (~12.6.3.7) 會從 Eᵢ 到對應的參數類型 Tⱼ。
12.6.3.3 第二階段
第二個階段會繼續進行,如下所示:
- 所有不相依於的
Xᵢ類型變數(\12.6.3.6)都Xₑ固定 (\12.6.3.13)。 - 如果不存在這類類型變數,則符合下列所有條件的所有 未固定 類型變數
Xᵢ都會 固定:- 至少有一個類型變數
Xₑ相依於Xᵢ -
Xᵢ有一組非空白的界限
- 至少有一個類型變數
- 如果不存在這類類型變數,但仍 未修正 類型變數,則類型推斷會失敗。
- 否則,如果沒有進一步 未修正 類型變數存在,類型推斷就會成功。
- 否則,對於具有對應參數類型的所有自變數
Eᵢ,Tⱼ(~12.6.3.5) 包含未修正的類型變數Xₑ,但輸入類型 (\12.6.3.4) 則不會,則輸出類型推斷 (~12.6.3.8) 會從Eᵢ到Tⱼ。 然後重複第二個階段。
12.6.3.4 輸入類型
如果
12.6.3.5 輸出類型
如果
12.6.3.6 相依性
如果 Xₑ直接相依於Xᵢ,或 Xₑ直接相依於Xᵢ,而 Xᵢ則取決於Xᵥ,則 Xᵥ相依於Xₑ。 因此,“取決於”是“直接依賴”的傳遞但不具反射閉包。
12.6.3.7 輸入類型推斷
輸入類型推斷 是從一種 表達式 E 一種 類型 以如下方式T:
- 如果
E是具有 arity 和 elements 的 Tuple 運算式 (N),而且Eᵢ是具有對應項目類型的TArityNTₑ的 Tuple 類型,或是T可為 Null 的實值型別,而且是具有對應元素類型的T0?TupleT0類型NTₑ,則針對每個Eᵢ,輸入類型推斷會從Eᵢ設為Tₑ。 - 如果
E是匿名函式,則會從 到 建立明確的參數類型推斷 (E)T - 否則,如果
E具有型U別,且對應的參數是值參數(~15.6.2.2),則會從到U下限推斷(T)。 - 否則,如果
E具有型U別,且對應的參數是參考參數 (~15.6.2.3.3.3),或輸出參數 (~15.6.2.3.4),則會從U進行確切的推斷(T)。 - 否則,如果
E具有型U別,且對應的參數是輸入參數(~15.6.2.2.3.2),而且E是輸入自變數,則會從到U進行確切推斷(T)。 - 否則,如果
E具有型U別,且對應的參數是輸入參數 (~15.6.2.2.3.2),則會從到U下限推斷 (T) 。 - 否則,不會針對這個參數進行推斷。
12.6.3.8 輸出類型推斷
- 如果
E是具有 arity 和 elementsN的 Tuple 運算式,而且Eᵢ是具有對應項目類型的TArityNTₑ的 Tuple 類型,或是T可為 Null 的實值型T0?別,而且T0是具有對應元素類型的NTuple 類型Tₑ,則針對每個Eᵢ輸出類型推斷都是從Eᵢ到Tₑ。 - 如果
E是具有推斷傳回類型的U匿名函式(T),而且Tₓ是具有傳回型別的委派型別或表達式樹狀結構類型,則會從U建立下限推斷(Tₓ)。 - 否則,如果
是方法群組,而 是具有參數類型 且傳回型別 的委派型別或表達式樹狀結構類型,且具有型別 的重載 解析會產生具有傳回型別 的單一方法,則 下限推斷 會從到 。 - 否則,如果
是類型為 的表達式,則會 從 到 建立 下限推斷。 - 否則,不會進行推斷。
12.6.3.9 明確參數類型推斷
- 如果
E是具有參數類型的U₁...Uᵥ明確具型別匿名函式,而且T是具有參數型V₁...Vᵥ別的委派型別或表達式樹狀結構類型,則針對每個Uᵢ確切的推斷,會從Uᵢ建立至對應的Vᵢ。
12.6.3.10 確切推斷
從 推斷,如下所示:
- 如果
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.11 下限推斷
從類型 到類型 U 的下限推斷,如下所示:
- 如果
V是未固定的Xᵢ之一,那麼會將U添加至Xᵢ的下界集合。 - 否則,如果
V是類型V₁?,且U是類型U₁?,則會從U₁到V₁進行下限推斷。 - 否則,會藉由檢查下列任何情況是否適用來決定
U₁...Uₑ和V₁...Vₑ:-
V是陣列類型V₁[...],U是相同順位的陣列類型U₁[...] -
V是IEnumerable<V₁>、ICollection<V₁>、IReadOnlyList<V₁>>、IReadOnlyCollection<V₁>或IList<V₁>之一,U是單維數位類型U₁[] -
V是建構的class、struct、interface或delegate類型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₁可以是X或Y。
如果適用上述任一情況,則會從每個Uᵢ到對應的Vᵢ進行推斷,如下所示: - 若未知
Uᵢ為參考類型,則會進行 精確推斷。 - 否則,如果
U是陣列類型,則會進行 下限推斷 - 否則,如果
V為C<V₁...Vₑ>,則推斷取決於i-th的C類型參數:- 如果它是共變數,則會建立 下限推斷。
- 如果是反變數,則會進行 上限推斷。
- 如果它是不變的,則會建立 精確的推論。
-
- 否則,不會進行推斷。
12.6.3.12 上限推斷
從 類型
- 如果
V是為數其中一個未固定的Xᵢ,則將U加入為Xᵢ的上界集合。 - 否則,會藉由檢查下列任何情況是否適用來決定
V₁...Vₑ和U₁...Uₑ:-
U是陣列類型U₁[...],V是相同順位的陣列類型V₁[...] -
U是IEnumerable<Uₑ>、ICollection<Uₑ>、IReadOnlyList<Uₑ>、IReadOnlyCollection<Uₑ>或IList<Uₑ>之一,V是單維數位類型Vₑ[] -
U是類型U1?,V是類型V1? -
U是由類別、結構、介面或委派類型C<U₁...Uₑ>構成,V是一種class, struct, interface或delegate類型,該類型能(直接或間接)轉換為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是陣列類型,則會建立 上限推斷 - 否則,如果
U為C<U₁...Uₑ>,則推斷取決於i-th的C類型參數:- 如果它是共變數,則會建立 上限推斷。
- 如果是反變數,則會進行 下限推斷。
- 如果它是不變的,則會建立 精確的推論。
-
- 否則,不會進行推斷。
12.6.3.13 修正
具有一組界限 的未固定 類型變數 Xᵢ 已 修正 ,如下所示:
- 一組 候選型別
Uₑ最初是Xᵢ界限中的所有型別。 - 接著會逐一檢查每個系結
Xᵢ:針對每個Xᵢ的確切系結 U,從候選集中移除所有與Uₑ不一致的U類型。 針對所有不能從U隱含轉換而來的Xᵢ型別,這些型別的Uₑ下限將從候選集移除。 對於Xᵢ的每個上界 U,從候選集合中移除所有類型Uₑ,這些類型是沒有隱含轉換到 的U)。 - 如果在剩餘的候選類型
Uₑ之中存在一個可以從所有其他候選類型進行隱式轉換的唯一類型V,則Xᵢ被固定為V。 - 否則,類型推斷會失敗。
12.6.3.14 推斷傳回類型
匿名函式推斷出的傳回型別 F 在類型推斷及多載解析過程中會被使用。 推斷的返回類型只能針對已知所有參數類型的匿名函數來確定,可能是因為它們被明確指定,透過匿名函數轉換提供,或在封閉泛型方法調用中的類型推斷過程中推斷。
推斷的有效返回類型,決定如下:
- 如果
F的主體是具有類型的 表示式,則F的推斷有效傳回類型是該表示式的類型。 - 如果 的主體
F是區塊,且區塊return語句中的運算式集合具有最佳通用類型T(~12.6.3.16),則推斷的有效傳回類型F為T。 - 否則,無法針對
F推斷有效的傳回型別。
推斷的傳回類型,如下所示:
- 如果
F是異步,且F的主體是被分類為 nothing 的表達式(§12.2),或者是沒有return語句的區塊,則推斷的返回類型為«TaskType»(§15.14.1)。 - 如果
F是異步且具有推斷的有效傳回型別,則推斷的傳回類型T為«TaskType»<T>»(~15.14.1)。 - 如果
F為非異步,且具有推斷的有效傳回類型T,則推斷的傳回類型會T。 - 否則,無法推斷
F的傳回型別。
範例:作為涉及匿名函式的類型推斷範例,請考慮在
Select類別中宣告的System.Linq.Enumerable擴充方法: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具有類型Name的string屬性,則Select方法可以用來選取客戶清單的名稱:List<Customer> customers = GetCustomerList(); IEnumerable<string> names = customers.Select(c => c.Name);的擴展方法呼叫(
Select)是透過將呼叫重寫為靜態方法呼叫來處理:IEnumerable<string> names = Enumerable.Select(customers, c => c.Name);由於未明確指定類型自變數,因此會使用類型推斷來推斷類型自變數。 首先,客戶的參數與來源參數相關,推斷
TSource為Customer。 然後,使用上述匿名函式類型推斷程式,c會指定類型Customer,而表達式c.Name與選取器參數的傳回型別有關,推斷TResult為string。 因此,調用相當於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” 與值參數相關,推斷
X為string。 然後,第一個匿名函式的 參數s會得到推斷的類型string,而表達式TimeSpan.Parse(s)與f1的傳回型別有關,推斷Y為System.TimeSpan。 最後,第二個匿名函式t的參數會得到推斷的類型System.TimeSpan,而表達式t.TotalHours與傳回型別f2相關,推斷Z為double。 因此,呼叫的結果的類型為double。結束範例
12.6.3.15 方法群組轉換的類型推斷
類似於泛型方法的呼叫,當包含泛型方法的方法群組 M 被轉換為指定的委派類型 D (§10.8)時,也需要應用型別推斷。 給定的方法
Tₑ M<X₁...Xᵥ>(T₁ x₁ ... Tₑ xₑ)
將方法群組 M 指派給委派類型 D,類型推斷的任務是尋找類型參數 S₁...Sᵥ,使表達式:
M<S₁...Sᵥ>
與 相容 (§21.2)。D
不同於泛型方法呼叫的類型推斷演算法,在此情況下,只有自變數 類型,沒有自變數 表達式。 特別是沒有匿名函式,因此不需要多個推斷階段。
相反地,所有Xᵢ項目都會視為未修正,且下限推斷是從的每個自變數類型UₑD到的對應參數類型TₑM所產生。 如果沒有找到任何 Xᵢ 的界限,則類型推斷將失敗。 否則,所有 Xᵢ 都會 固定 對應 Sᵢ,這是類型推斷的結果。
12.6.3.16 尋找一組表達式的最佳常見類型
在某些情況下,必須推斷一組表達式的一般型別。 特別是透過這種方式來確定隱含型別陣列的元素類型,以及具有 區塊 主體的匿名函式的傳回型別。
一組表達式 E₁...Eᵥ 的最佳常見類型取決於如下:
- 引進新的 未固定的 類型變數
X。 - 針對每個運算式
Ei,輸出類型推斷會從它執行到X。 -
X是 固定 的 (\12.6.3.13),可能的話,產生的類型是最好的常見類型。 - 否則推斷會失敗。
附註:直覺上,此推斷相當於呼叫方法
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中的每個自變數,自變數的參數傳遞模式與對應參數的參數傳遞模式相同,以及
對於包含參數陣列的函式成員,如果函式成員適用於上述規則,則表示其 一般形式適用。 如果包含參數陣列的函式成員不適用於其一般形式,該函式成員可能會改為適用於其 展開的形式:
- 展開形式是透過在函式成員宣告中將參數陣列替換為該陣列元素型別的零或多個值參數,從而使引數清單中的引數數目
A與參數總數相符。 如果A自變數比函數成員宣告中的固定參數數目少,則無法建構函式成員的展開形式,因此不適用。 - 否則,若對於
A中的每個參數,下列條件之一成立,則展開形式適用:
當從引數的類型隱含轉換成輸入參數的參數類型是動態隱含轉換時(§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_name 或 member_access 產生時,才適用靜態方法。
- 只有當方法組由 simple_name、透過變數或值的 member_access 或 base_access所產生時,才適用實例方法。
- 當方法群組來自 member_access,並且可以透過實例或類型取得時,如 §12.8.7.2所述,則實例和靜態方法皆適用。
- 類型參數(無論是明確指定還是推斷)的泛型方法,若沒有全部符合其約束條件,將不可用。
- 在方法群組轉換的上下文中,應該存在從方法傳回型別到委派傳回型別的識別轉換(§10.2.2)或隱含參考轉換(§10.2.8)。 否則,候選方法不適用。
12.6.4.3 功能較佳的函式成員
為了判斷更好的函式成員,會建構一個精簡後的引數清單 A,該清單只包含引數表達式本身,並按它們在原始引數清單中的順序排列,並排除任何 out 或 ref 引數。
每個候選函式成員的參數清單會以下列方式建構:
- 如果函數成員僅適用於展開的形式,則會使用展開的形式。
- 沒有對應自變數的選擇性參數會從參數清單中移除
- 從參數清單中移除參考和輸出參數
- 參數會重新排序,使其與引數清單中的對應引數位於相同位置。
假設自變數清單
- 針對每個自變數,從
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不具體,且至少有一個參數,Rx比Sx更具體。- 類型參數比非類型參數更不明確。
- 遞歸來看,一個建構類型比另一個建構類型更具體(類型參數數量相同),如果至少有一個類型參數更具體,且沒有任何類型參數比另一個中的對應類型參數更不具體。
- 如果兩個陣列類型的維度數目相同,且第一個陣列的元素類型比第二個的具體,那麼第一個陣列類型就是比第二個更具體。
- 否則,如果一個成員是非提升運算符,另一個是提升運算符,則非提升的運算符比較好。
- 如果兩個函式成員都找不到較佳,而且
Mᵥ的所有參數都有對應的自變數,而預設自變數則至少要取代Mₓ中的一個選擇性參數,則Mᵥ比Mₓ更好。 - 如果至少有一個參數
Mᵥ使用 比 中的對應參數更好的參數傳遞選擇(Mₓ),而且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₁,還有將表達式 C₂ 轉換為類型 E的隱含轉換 T₂,若下列條件之一成立,C₁ 是 :
-
E完全符合T₁,E與T₂不完全相符(§12.6.4.6) -
E完全符合或完全不符合T₁和T₂,且T₁是比T₂更好的轉換目標(§12.6.4.7) -
E是一個方法組(§12.2),T₁與用於轉換的方法組中的單一最佳方法相容(C₁),並且T₂與用於轉換的方法組中的單一最佳方法不相容C₂
12.6.4.6 完全匹配的表達式
假設表達式 E 和類型 T,如果下列其中一項保留,E完全符合T:
-
E的類型是S,且從S到T存在自我轉換 -
E是匿名函式,T是委派類型D或表達式樹狀結構類型Expression<D>,且下列其中一項保留:- 參數清單
X的內容中存在E推斷的傳回型D別(~12.6.3.13),且識別轉換存在從X到 傳回型別的 。D -
E是沒有傳回值的asynclambda 表達式,且D具有非泛型的«TaskType» -
E不是異步且D具有傳回類型Y或E是異步且D具有傳回類型«TaskType»<Y>(§15.14.1),並且符合下列其中一項:-
E的主體是一個完全符合Y的表達式 -
E主體是區塊,其中每個 return 語句都會傳回完全符合Y的表達式
-
- 參數清單
12.6.4.7 更好的轉換目標
假設有兩種類型 T₁ 和 T₂,如果下列其中一項保留,T₁ 是 目標:
- 存在從
T₁到T₂的隱含轉換,而且不存在從T₂到T₁的隱含轉換 -
T₁是«TaskType»<S₁>(§15.14.1),T₂是«TaskType»<S₂>,而且S₁是比S₂更好的轉換目標 -
T₁是«TaskType»<S₁>(§15.14.1),T₂是«TaskType»<S₂>,而T₁比T₂更專業。 -
T₁是S₁或S₁?,其中S₁是帶正負號整數型別,T₂是S₂或S₂?,其中S₂是無符號整數型別。 具體說來:-
S₁是sbyte,S₂是byte、ushort、uint或ulong -
S₁是short,S₂是ushort、uint或ulong -
S₁為int,且S₂為uint或ulong -
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.4) ,接收器中的可存取索引子集在編譯階段是已知的。
在這些情況下,對已知函式成員集中的每個成員進行有限的編譯時間檢查,以確定是否能確定它在運行時間絕對不會被叫用。 針對每個函式成員 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別 ,且M在中V宣告或覆寫 :-
E已被評估。 如果此評估造成例外狀況,則不會執行任何進一步的步驟。 針對實例建構函式,此評估包含為新物件配置記憶體(通常是從執行堆棧)。 在此情況下,E分類為變數。 - 如果
E未分類為變數,或 不是V只讀結構類型 (~16.2.2), 且M不是唯讀函式成員 (~16.4.12),則E為下列其中一項:- 輸入參數(.15.6.2.3.2),或
-
readonly欄位(§15.5.3),或 - 參考
readonly變數或傳回 (~9.7),然後會建立 型別的E暫存局部變數,並將的值E指派給該變數。 然後,E被重新定義為該暫存局部變數的參考。 暫存變數可以存取為this內的M,但無法以任何其他方式存取。 因此,只有在可以寫入E時,呼叫端才能觀察M對this所做的變更。
- 引數列表的評估方式,如 §12.6.2中所述。
-
M被調用。E所參考的變數會成為this所參考的變數。
-
- 否則:
-
E已被評估。 如果此評估造成例外狀況,則不會執行任何進一步的步驟。 - 引數列表的評估方式,如 §12.6.2中所述。
- 如果
E的類型是 型別值,則會執行裝箱轉換(§10.2.9),將E轉換為 類型,並在以下步驟中將E視為該 類型。 如果 value_type 是 enum_type,則 class_type 為System.Enum;;否則,則為System.ValueType。 -
E的值被檢查是否有效。 如果E的值為 null,則會擲回System.NullReferenceException,而且不會執行任何進一步的步驟。 - 要調用的函式成員實作已確定:
- 如果
E的綁定時間類型是介面,則要調用的函數成員是由M所參考的實例運行時類型提供的E實現。 此函式成員是藉由套用介面對應規則 (§19.6.5) 來決定,以判斷所參考M實例執行階段類型所提供的實E作。 - 否則,如果
M是虛擬函式成員,要叫用的函式成員就是由M所參考實例的運行時間類型所提供之E實作。 此函式成員的確定是藉由套用規則,以決定關於 參考的實例在運行時類型上的最衍生實作M的E。 - 否則,
M是非虛擬函式成員,而要叫用的函式成員則是M本身。
- 如果
- 調用在上述步驟中確定的函式成員實作。
E所參考的物件會成為這個 所參考的物件。
-
注意:\12.2 會將屬性存取分類為叫用對應的函式成員,也就是
get存取子或set存取子。 後面會遵循上述程式來叫用該存取子。 結尾註釋
實例建構函式呼叫 (§12.8.17.2) 的結果是創建的值。 任何其他函式成員呼叫的結果是主體傳回的值(如果有的話)(§13.10.5)。
12.6.6.2 在 Boxed 實例上的呼叫
在以下情況下,可以透過該 value_type 的 Boxed 實例調用 value_type 中實作的函式成員:
- 當函式成員是繼承自類型 class_type 的方法覆寫時,會透過該 class_type的實例表達式叫用。
附注:class_type 一律為
System.Object、System.ValueType或System.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
: 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
| array_creation_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,因為它是 ANTLR 無法處理的一組互相左遞歸的規則(
primary_expression、member_access、invocation_expression、element_access、post_increment_expression、post_decrement_expression、null_forgiving_expression、pointer_member_access和pointer_element_access)的一部分。 標準技術可用來轉換文法,以移除相互的左遞歸。 此標準不會這樣做,因為並非所有剖析策略都需要它(例如 LALR 剖析器不會),這樣做會模糊化結構和描述。 結尾註釋
pointer_member_access (§24.6.3) 和 pointer_element_access (§24.6.4) 僅在不安全代碼 (§24) 中可用。
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.IFormattable 或 System.FormattableString(§10.2.5),則插值字串表達式就具有該類型。 否則,其類型 string。
附註:interpolated_string_expression 可能類型之間的差異,可以從
System.String文件(第C.2節)和System.FormattableString(第C.3節)中判斷。 結尾註釋
插補點的意義,包括 regular_interpolation 和 verbatim_interpolation是要將 表達式 的值格式化為 string,這可以是根據 Regular_Interpolation_Format 或 Verbatim_Interpolation_Format所指定的格式,或者根據 表達式類型的默認格式。 然後,格式化字串將由 interpolation_minimum_width做修改(如果有的話),從而生成要插入到 string的最終 。
附註:類型的預設格式如何決定的詳情,可以參閱
System.String(§C.2)和System.FormattableString(§C.3)的檔案。 標準格式的描述,與 Regular_Interpolation_Format 和 Verbatim_Interpolation_Format完全相同,可以在System.IFormattable的文件中(§C.4)以及標準庫中的其他類型(§C)找到。 結尾註釋
在 interpolation_minimum_width 中,constant_expression 必須被隱式轉換為 int。 讓 字段寬度 為此 常量表達式 的絕對值,而 對齊 則為該 常量表達式的正負符號:
- 如果欄位寬度的值小於或等於格式化字串的長度,則格式化字串不會修改。
- 否則,格式化字串會以空格符填補,使其長度等於字段寬度:
- 如果對齊方式是正數,則格式化字串會靠右對齊,方法是通過在前面加上填充字符來實現。
- 否則,透過填充會使其靠左對齊。
interpolated_string_expression的整體意義,包括上述內插值的格式和填充,是由將表達式轉換為方法調用來定義:如果表達式的類型是 System.IFormattable 或 System.FormattableString,該方法的類型是 System.Runtime.CompilerServices.FormattableStringFactory.Create(§C.3),它會返回類型 System.FormattableString的值;否則,類型是 string,方法是 string.Format(§C.2),其會返回類型 string的值。
在這兩種情況下,呼叫的參數清單包含 格式字串常值,並且每個插補都有 格式規格,以及對應於這些格式規格的每個表達式的參數。
格式字串常值的建構方式如下,其中 N 是 interpolated_string_expression中的插補數目。 格式字串字面值會依序包含:
- Interpolated_Regular_String_Start 或 Interpolated_Verbatim_String_Start 的字元
- Interpolated_Regular_String_Mid 或 Interpolated_Verbatim_String_Mid的字元,如果有的話
- 如果對於從
N ≥ 1到I的每個數字0N-1,然後:- 佔位符規範
- 左大括弧 (
{) 字元 -
I的十進位表示法 - 然後,如果對應的 regular_interpolation 或 verbatim_interpolation 具有 interpolation_minimum_width,則在逗號(
,)後接著 constant_expression 值的十進位表示法。 - 對應的 regular_interpolation 或 verbatim_interpolation 中,如果有的話,Regular_Interpolation_Format 或 Verbatim_Interpolation_Format的字元
- 右大括弧 (
}) 字元
- 左大括弧 (
- 在對應的插補之後緊接著出現的字元是 Interpolated_Regular_String_Mid 或 Interpolated_Verbatim_String_Mid,如果有的話。
- 佔位符規範
- 最後,字元屬於 Interpolated_Regular_String_End 或 Interpolated_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 會參考該局部變數、參數或常數,並分類為變數或值。 - 如果
e為零,且 simple_name 出現在泛型方法宣告內,但在其 method_declaration 的 屬性區段之外,而該宣告包含一個名稱為I的類型參數,則simple_name會參考該類型參數。 - 否則,針對每個實例類型
T(§15.3.2),從最內層的封閉型別宣告的實例類型開始,並依序繼續檢查每個封閉類別或結構宣告的實例類型(如果有的話):- 如果
e為零,且T宣告包含名稱為I的類型參數,則 simple_name 會參考該類型參數。 - 否則,如果在 中使用
I型別參數對T進行成員檢索(e)找到符合項目:- 如果
T是直接封閉的類別或結構體的實例類型,而查閱會識別一或多個方法,則結果是具有相關聯實例表示式的方法群組this。 如果指定了類型自變數清單,則會用於呼叫泛型方法 (\12.8.10.2)。 - 否則,如果
是直接封閉類別或結構類型的實例類型,並且查找識別出了一個實例成員,且該引用發生在實例構造函數、實例方法或實例存取子 的 區塊中,則結果與形式 的成員存取相同( §12.2.1 ;§12.8.7 )。 這隻能在e為零時發生。 - 否則,結果與 或
T.I類型的成員存取(T.I<A₁, ..., Aₑ>)相同。
- 如果
- 如果
- 否則,對於每個命名空間
N,從 simple_name 所在的命名空間開始,然後依序進行每個封閉的命名空間(如果有的話),直到全域命名空間為止,將評估下列步驟,直到找到實體為止:- 如果
e為零,且I為N中的命名空間名稱,則:- 如果 simple_name 發生的位置被
N的命名空間宣告所包住,且該命名空間宣告包含 extern_alias_directive 或 using_alias_directive,用來將名稱I與命名空間或類型產生關聯,那麼 simple_name 會變得模棱兩可,並且會發生編譯時錯誤。 - 否則,simple_name 會參考
I中名為N的命名空間。
- 如果 simple_name 發生的位置被
- 否則,如果
N包含具有名稱I和e類型參數的可存取類型,則:- 如果
e為零,且 simple_name 發生在用於N的命名空間宣告中,而該命名空間宣告包含 extern_alias_directive 或 using_alias_directive,將名稱I與命名空間或類型產生關聯,則 simple_name 會模棱兩可,並發生編譯時期錯誤。 - 否則,namespace_or_type_name 指的是由給定的類型參數建構的類型。
- 如果
- 否則,如果 simple_name 的位置被
N的命名空間宣告所包圍:- 如果
e為零,且命名空間宣告包含 extern_alias_directive 或 using_alias_directive,使名稱I與匯入的命名空間或類型產生關聯,則 simple_name 會參考該命名空間或類型。 - 否則,如果命名空間宣告的 using_namespace_directive所匯入的命名空間中恰好包含一個有名稱
I且有e型別參數的類型,則 simple_name 會參考以給定型別引數建構的該類型。 - 否則,如果命名空間宣告的 using_namespace_directive所匯入的命名空間包含多個具有名稱
I和e類型參數的類型,則 simple_name 模棱兩可,而且會發生編譯時期錯誤。
- 如果
附注:整個步驟與處理 namespace_or_type_name 的對應步驟完全相同(§7.8)。 結尾註釋
- 如果
- 否則,如果 為零 且
e是標識符I,則_simple_name是簡單的捨棄,這是宣告表達式的一種形式 (§12.19)。 - 否則,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.19)。 因此,deconstruction_expression 只能出現在簡單賦值的左側。
範例:下列程式代碼會宣告三個變數:a、b 和 c。 其中每一個都是整數,並從賦值運算式右側的 tuple 中得到其值。
var (a, b, c) = (1, 2, 3); // a is 1, b is 2, and c is 3. var sum = a + b + c; // sum is 6.指派的任何個別元素本身都可以是解構表達式。 例如,下列解構表達式會指派從
a到f的六個變數。var (a, b, (c, d, (e, f))) = (1, 2, (3, 4, (5, 6)));在此範例中,請注意,巢狀 Tuple 的結構必須符合指派的兩端。
如果左邊的變數是隱式類型,對應的運算式必須具有類型:
(int a, string? b) = (42, null); //OK var (c, d) = (42, null); // Invalid as type of d cannot be inferred (int e, var f) = (42, null); // Invalid as type of f cannot be inferred結束範例
只有當每個元素表達式 Ei 都具有類型 Ti時,元組運算式才會具有類型。 此類型應該是與元組運算式具有相同元數的元組類型,其中每個元素都由下列規則指定:
- 如果對應位置中的 Tuple 元素具有名稱
Ni,則 Tuple 類型元素應為Ti Ni。 - 否則,如果
Ei的型態為Ni或E.Ni或E?.Ni,元組類型元素應為Ti Ni, ,除非滿足以下任意條件:- Tuple 表達式中的另一個元素名稱為
Ni或其他名稱 - 另一個未命名的元組元素具有像
Ni、E.Ni、E?.Ni這樣的元組元素表示式,或 -
Ni的格式為ItemX,其中X是一連串非0起始的小數位數,可代表元組專案的位置,而X不代表專案的位置。
- Tuple 表達式中的另一個元素名稱為
- 否則,該元組類型元素應為
Ti。
元組表達式的評估方式是從左至右評估每個元素表達式。
元組值可以從元組運算式取得,方法是將元組運算式轉換成元組類型 (§10.2.13)、將它重新分類為值 (§12.2.2)) ,或讓它成為解構指派的目標 (§12.23.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 運算式都是有效的。 前兩個
t1和t2,不使用元組表達式的類型,而是改為套用隱含的元組轉換。 在t2的情況下,隱含的元組轉換依賴於從2到long的隱含轉換,以及從null到string的轉換。 第三個元組表達式的類型為(int i, string),因此可以將其重新分類為該類型的值。 另一方面,t4的宣告是錯誤的:元組運算式沒有類型,因為其第二個元素沒有類型。if ((x, y).Equals((1, 2))) { ... };此範例顯示元組有時可能會導致多層括弧,特別是當元組運算式是方法呼叫的唯一參數時。
結束範例
12.8.7 成員存取
12.8.7.1 一般
member_access 包含 primary_expression、predefined_type或 qualified_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ₑ>,其中 E 是 primary_expression、predefined_type 或 qualified_alias_member,I 是單一識別子,<A₁, ..., Aₑ> 是可選的 type_argument_list。 未指定任何 type_argument_list 時,請將 e 視為零。
具有 類型的 primary_expression 中的 dynamic 會動態系結(§12.3.3)。 在此情況下,成員存取權會分類為類型為 dynamic的屬性存取權。 下列規則會在執行時以執行時類型來確定 member_access 的意義,而不是使用 primary_expression的編譯時類型。 如果此運行時間分類導致方法群組,則成員存取權應該是 invocation_expression的 primary_expression。
member_access 被評估並被分類如下所示:
- 如果
e為零,且E是命名空間,且E包含名稱為I的巢狀命名空間,則結果是該命名空間。 - 否則,如果
E是命名空間,且E包含具有名稱I和K類型參數的可存取類型,則結果就是使用指定型別自變數建構的類型。 - 如果
E被分類為一種型別,並且E不是型別參數,並且在具有 型別參數的I中對E的成員查閱(K)會產生匹配項,則E.I會被評估並分類如下:附注:當這類成員查閱的結果是方法群組,且
K為零時,方法群組可以包含具有類型參數的方法。 這可讓這類方法被考慮用於類型參數推斷。 結尾註釋- 如果
I識別類型,則結果就是使用任何指定型別自變數建構的類型。 - 如果
I識別一或多個方法,則結果會是沒有相關聯實例表達式的方法群組。 - 如果
I識別靜態屬性,則結果會是沒有相關聯實例表達式的屬性存取。 - 如果
I識別靜態字段:- 如果欄位是唯讀的,而且參考發生在宣告欄位的類別或結構靜態建構函式之外,則結果為值,也就是
I中靜態字段的值E。 - 否則,結果是變數,也就是
I中的靜態字段E。
- 如果欄位是唯讀的,而且參考發生在宣告欄位的類別或結構靜態建構函式之外,則結果為值,也就是
- 如果
I識別靜態事件:- 如果參考發生在宣告事件的類別或結構內,而且宣告事件時沒有 event_accessor_declarations (\15.8.1),則
E.I會如同I是靜態字段一樣處理。 - 否則,結果是事件存取,沒有相關聯的實例表達式。
- 如果參考發生在宣告事件的類別或結構內,而且宣告事件時沒有 event_accessor_declarations (\15.8.1),則
- 如果
I識別常數,則結果為值,也就是該常數的值。 - 如果
I識別列舉成員,則結果為值,也就是該列舉成員的值。 - 否則,
E.I是無效的成員參考,而且會發生編譯時期錯誤。
- 如果
- 如果
E是屬性存取、索引器存取、變數或值,且其類型是T,並且在 中,使用I類型參數對T進行成員查閱(K)會產生匹配項,則E.I會被評估並分類如下:- 首先,如果
E是屬性或索引器存取,則會取得屬性或索引器存取的值(\12.2.2),而 E 會重新分類為值。 - 如果
I識別出一個或多個方法,那麼結果將是一個方法群組,具有與E相關聯的實例表示式。 - 如果
I識別的是一個實例屬性,那麼結果將是一個具有E相關聯實例表達式的屬性存取,並且它的相關聯型別為屬性類型。 如果T是類別類型,則會從從T開始尋找的屬性的第一個宣告或覆寫中挑選相關聯的類型,並搜尋其基類。 - 如果
T是 class_type,且I識別該 class_type的實例字段:- 如果
E的值是null,則會拋出System.NullReferenceException。 - 否則,如果欄位是唯讀的,而且參考發生在宣告欄位之類別的實例建構函式之外,則結果為值,也就是
I所參考之 物件中E域的值。 - 否則,結果是變數,也就是
I所參考之物件中的欄位E。
- 如果
- 如果
T是 struct_type,且I識別該 struct_type的實例字段:- 如果
E為值,或者如果欄位是唯讀的,而且參考發生在宣告欄位之結構的實例建構函式之外,則結果為值,也就是由I所指定之結構實例中的字段值E。 - 否則,結果是變數,也就是
I所指定結構實例中的欄位E。
- 如果
- 如果
I識別實例事件:- 如果參考發生在宣告事件的類別或結構內,而且事件是未使用event_accessor_declarations聲明的(§15.8.1),而且參考不是出現在
a +=或-=運算符的左側,則E.I將像實例欄位一樣被I處理。 - 否則,結果是事件存取,並且具有與之相關的實例表達式
E。
- 如果參考發生在宣告事件的類別或結構內,而且事件是未使用event_accessor_declarations聲明的(§15.8.1),而且參考不是出現在
- 首先,如果
- 否則,將嘗試把
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_access 是 member_access 的條件式版本(\12.8.7),如果結果類型是 void,則為系結時間錯誤。 如需結果類型可能為 void 的 null 條件表示式,請參閱 (.12.8.11)。
null_conditional_member_access由一個primary_expression組成,後面接著兩個標記 “?” 和 “.”,後面接著一個帶有可選type_argument_list的標識符,後面接著零個或多個dependent_access,其中任何一個都可以在前面加上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 表示式的格式為 P?.A。
E 的意義定義如下:
如果
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是未知為參考型別或不可為 Null 的實值型別的類型參數,則會發生編譯時期錯誤。如果
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_access或null_conditional_element_access12.8.13 作業的序列,則也是如此。結尾註釋
null_conditional_projection_initializer 是 null_conditional_member_access 的限制,並具有相同的語意。 它只會在匿名物件建立表達式中以投影初始化表達式的形式發生(~12.8.17.3)。
12.8.9 Null-forgiving(允許空值)表達式
12.8.9.1 一般
null 寬容表達式的值、類型、分類(§12.2)和安全內容(§16.4.15)是其 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傳回true,p可以安全地解參考以存取其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取消任何警告。 不過,如果在運行時x是null,則會拋出例外狀況,因為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&rv為string?,lv為輸出參數,而且會執行簡單的指派。方法
M傳遞類型為s的變數string作為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 的意義。 如果 primary_expression 並非編譯時期型別 dynamic,則方法呼叫會進行有限度的編譯時期檢查,如 §12.6.5中所述。
invocation_expression 的 primary_expression 應為方法集或 委派類型的值。 如果 primary_expression 是方法群組,則 invocation_expression 是方法調用(第12.8.10.2節)。 如果 primary_expression 是 delegate_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.21) 的上下文中。 否則,會發生系結時間錯誤。
- 否則,如果 invocation_expression 呼叫引用回傳的方法(§15.6.1)或引用回傳的委派,則結果是與該方法或委派的傳回型別相關聯的變數。 如果調用是實例方法,且接收者為類別類型
T,則相關聯的類型將從從T開始,搜尋其基類時找到的該方法的第一個宣告或覆寫中挑選出。 - 否則,invocation_expression 會叫用傳回值方法(§15.6.1),或是逐值返回的委派,其結果為一個值,對應的型別是該方法或委派的傳回型別。 如果調用是實例方法,且接收者為類別類型
T,則相關聯的類型將從從T開始,搜尋其基類時找到的該方法的第一個宣告或覆寫中挑選出。
12.8.10.2 方法調用
方法呼叫中的 invocation_expression 的 primary_expression 應該是方法群組。 方法群組識別出要叫用的一個方法,或從一系列多載方法中選擇一個特定方法來叫用。 在後一種情況下,判斷要叫用的特定方法是根據 argument_list中引數的類型所提供的上下文。
表單 M(A)的方法調用的系結時間處理,其中 M 是方法群組(可能包括 type_argument_list),而 A 是選擇性的 argument_list,包含下列步驟:
- 方法調用的候選方法集合已建立。 針對每個與方法群組
F相關聯的方法M:- 如果
F非泛型,F為候選項,當:-
M沒有類型自變數清單,且 -
F適用於A(12.6.4.2)。
-
- 如果
F是泛型且M沒有類型自變數清單,則F為候選項目,當: - 如果
F為泛型,並且M包含型別參數列表,F是潛在候選者時:
- 如果
- 候選方法集合會縮減為只包含來自大多數衍生型別的方法:針對集合中的每個方法
C.F,其中C是宣告方法F的類型,則會從集合中移除以基底類型宣告的所有方法C。 此外,如果C是object以外的類別類型,則會從集合中移除介面類型中宣告的所有方法。附註:此後面的規則只有在方法群組是針對型別參數進行成員查詢的結果時才有作用,且該型別參數擁有非
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 轉換存在於從 expr 到
Mₑ的第一個參數類型。
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.G於 ,並E.FD.F優先於 和C.F。結束範例
12.8.10.4 委派調用
對於委派調用,invocation_expression 的 primary_expression 應該是 delegate_type的值。 此外,將 delegate_type 視為具有相同參數列表的函式成員,因此 delegate_type 應適用於 §12.6.4.2,並且符合 invocation_expression 的 argument_list。
針對形式 D(A)的委派調用之執行時處理,其中 D 是 delegate_type 的 primary_expression,而 A 是選擇性的 argument_list,包括以下步驟:
-
D已被評估。 如果此評估造成例外狀況,則不會執行任何進一步的步驟。 - 參數列表
A被評估。 如果此評估造成例外狀況,則不會執行任何進一步的步驟。 -
D的值被檢查是否有效。 如果D的值是null,則會擲回System.NullReferenceException,而且不會執行任何進一步的步驟。 - 否則,
D是委派實例的參考。 函數成員呼叫(§12.6.6)在委派呼叫清單中的每個可呼叫實體上進行。 對於由實例和實例方法所組成的可呼叫實體,調用的實例是可呼叫實體中包含的實例。
如需沒有參數的多個調用清單的詳細資訊,請參閱 §21.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.21.1) 或method_body (§15.6.1) 的上下文中。
與語法上對等的 null_conditional_member_access 或 null_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_access 或 null_conditional_element_access 具有 delegate_type時,才可能包含選擇性 null_forgiving_operator。
null_conditional_invocation_expression 表達式 E 的格式 P?A 為;其中 A 是與語法等價的 null_conditional_member_access 或 null_conditional_element_access 的其餘部分,因此 A 將以 . 或 [ 開頭。 讓我們PA表示 和 P的A串聯。
當 E 作為 statement_expression 時,E 的意義與 語句的意義相同:
if ((object)P != null) PA
不同之處在於 P 只會評估一次。
當 E 作為 anonymous_function_body 或 method_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_expression組成,後面接著 “[” 標記,後面接著argument_list,再接著 “]” 標記。
argument_list 包含一或多個 自變數,並以逗號分隔。
element_access
: primary_expression '[' argument_list ']'
;
在識別 primary_expression 時,如果 element_access 和 pointer_element_access (§24.6.4) 替代方案都適用,則如果嵌入 primary_expression 是指標類型 (§24.3),則應選擇後者。
若primary_expression為array_creation_expression,則必須包含array_initializer;若為stackalloc_expression,則必須包含stackalloc_initializer。否則,不得作為element_access的primary_expression。
注意:這項限制存在,不允許可能令人混淆的程序代碼,例如:
var a = new int[3][1];否則將被解釋為:
var a = (new int[3])[1];類似的限制適用於 null_conditional_element_access (~12.8.13)。 結尾註釋
如果滿足下列其中一項,element_access 就會動態綁定(§12.3.3):
-
primary_expression 具有編譯時間類型
dynamic。 -
argument_list的至少一個運算式具有編譯時類型
dynamic。
在此情況下, element_access 的編譯時類型取決於其 primary_expression的編譯時類型:如果它具有陣列類型,則編譯時類型是該陣列類型的元素類型;否則編譯階段類型為 dynamic ,且 element_access 被分類為類型 dynamic的值。 接著會在運行階段應用下列規則,使用運行時間類型而非編譯時間類型,來判斷element_access的意義,這裡的primary_expression和argument_list的編譯時間類型為dynamic。 如果 primary_expression 沒有編譯時間類型 dynamic,則元素存取會經歷有限的編譯時間檢查,如 12.6.5 中所述。
範例:
var index = (dynamic)1; // index has compile-time type dynamic int[] a = {0, 1, 2}; var a_elem = a[index]; // dynamically bound, a_elem has compile-time type int string s = "012"; var s_elem = s[index]; // dynamcially bound, s_elem has compile-time type dynamic結束範例
如果element_access的primary_expression為:
- 陣列類型的值, element_access 是陣列存取(§12.8.12.2);
- 類型的值
string, element_access 是字串存取 (§12.8.12.3); - 否則, primary_expression 應該是具有一或多個索引子成員的類別、結構或介面類型的變數或值,在此情況下, element_access 是索引子存取 (§12.8.12.4) 。
12.8.12.2 陣組存取
針對陣列存取, argument_list 不得包含具名引數或依參考引數 (§15.6.2.3) 。
argument_list中的表達數量應與array_type的等級相同,並且每個表達方式應為:
- 類型
int,uint,long, 或ulong;或 - 僅適用於單階陣列存取,類型為
Index或Range;或 - 隱含地可轉換為上述一種或多種類型。
格式為 P[A]的陣列存取的執行階段處理,其中 P 是array_type的primary_expression,且A是索引表示式的argument_list,包含下列步驟:
-
P已被評估。 如果此評估造成例外狀況,則不會執行任何進一步的步驟。 - 針對 argument_list 中的每個索引運算式,依序從左到右:
- 對索引表達式進行求值,令結果值的類型為 T;
- 然後,此值會轉換成第一個類型:
int、uint、longulong、 ,或僅用於單秩陣列存取,Index或Range;存在從 T 的隱含轉換 (§10.2)。 - 如果評估索引表達式或後續的隱含轉換會造成例外狀況,則不會評估任何進一步的索引表達式,也不會執行任何進一步的步驟。
-
P的值被檢查是否有效。 如果P的值是null,則會擲回System.NullReferenceException,而且不會執行任何進一步的步驟。 - 如果上述步驟已產生類型的
Range單一索引值,則:- 設 L 是 引用的
P數組的長度。 -
A被檢查為對 L 有效 (§18.3)。 如果不是,則會擲回 aSystem.ArgumentOutOfRangeException,且不會執行進一步的步驟。 - 相對於 L 的起始偏移
A和項目數 N 的確定,如 (GetOffsetAndLength) 所述。 - 陣列存取的結果是一個陣列,其中包含從索引 S 開始的
PN 個元素的淺層副本。如果 N 為零,則陣列具有零元素。
- 設 L 是 引用的
便條:S 和 N 都可能為零(24.3 美元)。 索引空陣列通常是無效的,但是使用從零開始的空範圍的索引是有效的,並返回一個空陣列。 定義也允許 S 是 L,即過去結束索引 (§18.1),在此情況下, N 會為零,並傳回空陣列。 結尾註釋
便條: 陣列的元素範圍無法使用陣列存取指派給。 這與索引子存取 (§12.8.12.4) 不同,索引子存取可能但不需要支援指派給值所
Range指定的索引範圍。 結尾註釋
- 否則:
- 評估陣列存取的結果是陣列元素類型的變數參考 (§9.5)。
-
argument_list 中的每個運算式值都會根據
P所參考之陣列實例的每個維度的實際界限進行檢查。 如果一或多個值超出範圍,則會擲回System.IndexOutOfRangeException,而且不會執行任何進一步的步驟。 - 計算索引表達式給出的陣列元素的變數引用,這成為陣列存取的結果。
12.8.12.3 字串存取
針對字串存取,element_access的argument_list應包含單一未命名值引數 (§15.6.2.2) ,該引數應為:
- 的類型
int,Index或Range;或 - 隱含地可轉換為上述一種或多種類型。
表單P[A]字串存取的執行階段處理 ,其中 P 是型別的string,A且是單一運算式,包含下列步驟:
-
P已被評估。 如果此評估造成例外狀況,則不會執行任何進一步的步驟。 - 對索引表達式進行求值,令結果值的類型為 T;
- 然後,此值會轉換成第一個類型:
int、或IndexRange;,其中存在從 T 的隱含轉換 (§10.2)。 - 如果評估索引表達式或後續的隱含轉換會造成例外狀況,則不會評估任何進一步的索引表達式,也不會執行任何進一步的步驟。
-
P的值被檢查是否有效。 如果P的值是null,則會擲回System.NullReferenceException,而且不會執行任何進一步的步驟。 - 如果上述步驟已產生類型
Range為 的索引值,則:- 評估字串存取的結果是 type 的
string值。 - 設 L 為 所引用
P的字串長度。 -
A檢查為相對於 L 有效 (§18.3),如果不是,則擲回 aSystem.ArgumentOutOfRangeException並且不會執行進一步的步驟。 - 相對於 L 的起始偏移
A和項目數 N 的確定,如 (GetOffsetAndLength) 所述。 - 字串存取的結果是複製從S開始的
PN個字元形成的字串,如果N為零,則字串為空。
- 評估字串存取的結果是 type 的
便條:S 和 N 都可以為零 (§18.3)。 索引空字串通常是無效的,但是,從零開始的空白範圍的索引是有效的,並傳回空字串。 定義也允許 S 是 L,即過去結束索引 (§18.1),在此情況下, N 會為零,並傳回空字串。 結尾註釋
- 否則:
- 評估字串存取的結果是 type 的
char值。 - 轉換後的索引運算式的值會根據所參照的
P字串實例的實際界限進行檢查。 如果值超出範圍,則會擲回 aSystem.IndexOutOfRangeException,且不會執行進一步的步驟。 - 具有字串
P的轉換索引運算式偏移處的字元值會成為字串存取的結果。
- 評估字串存取的結果是 type 的
12.8.12.4 索引子存取
對於索引器存取,element_access的primary_expression必須是類別、結構或介面類型的變數或值,而此類型必須實作一或多個適用於argument_list的索引器。
argument_list不得包含out或ref論點。
表單P[A]之索引器存取的系結時間處理,其中 P 是類別、結構或介面類型的T,而 A 是argument_list,包含下列步驟:
- 建構
T所提供的索引器集合。 此集合包含所有在T或其基底類型T中宣告、且非覆寫宣告的索引器,並且可以在目前上下文中存取(§7.5)。 - 此集合會縮減為適用且未由其他索引器隱藏的索引器。 下列規則會套用至集合中的每個索引器
S.I,其中S是宣告索引器I的類型: - 如果產生的候選索引器集是空的,則沒有任何適用的索引器存在,而且會發生系結時間錯誤。
- 使用 §12.6.4的多載解析規則來識別一組候選索引器中的最佳索引器。 如果無法識別單一最佳索引器,索引器存取就會模棱兩可,而且會發生系結時間錯誤。
- 會檢查最佳索引子的存取子:
- 如果索引子存取是指派的目標,則索引子應該具有 set 或 ref get 存取子,否則會發生繫結時間錯誤;
- 否則,索引子應該具有 get 或 ref get 存取子,否則會發生繫結時間錯誤。
索引子存取的執行階段處理包含下列步驟:
- 評估目標 primary_expression
P。 -
argument_list
A的索引運算式會依順序從左到右計算。 - 使用繫結時決定的最佳索引子:
12.8.13 Null條件式元素存取
null_conditional_element_access由一個primary_expression組成,後面接著兩個標記“?”和“[”,再接著一個argument_list,然後是一個“]”標記,後面可以有零個或多個dependent_access,其中任何一個都可以在null_forgiving_operator前。
null_conditional_element_access
: primary_expression '?' '[' argument_list ']'
(null_forgiving_operator? dependent_access)*
;
null_conditional_element_access的argument_list不得包含 out 或 ref 自變數。
除非array_initializer包含在內,否則null_conditional_element_access的primary_expression不得為array_creation_expression;除非包含stackalloc_initializer,否則不得為stackalloc_expression。
注意:此限制可用來禁止可能令人混淆的程序代碼。 類似的限制適用於element_access(§12.8.12),您可能在其中找到被排除的範例。 結尾註釋
null_conditional_element_access 是 element_access 的條件式版本(.12.8.12),如果結果類型是 void,則為系結時間錯誤。 如需結果類型可能為 void 的 null 條件表示式,請參閱 (.12.8.11)。
null_conditional_element_access 表示式 E 的格式為 P?[A]B;其中 B 是 dependent_access,如果有的話。
E 的意義定義如下:
如果
P的類型是可為 Null 的數值型別:讓
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只被評估一次。
否則:
讓
T為表示式P[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_access 或 null_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)。
在上述情境之外使用 thisprimary_expression 是編譯時錯誤。 特別是,無法在靜態方法、靜態屬性存取子或欄位宣告的 this 中參考 。
12.8.15 基本存取
base_access 包含關鍵詞 base,後面接著“.”符號以及標識符和可選的 type_argument_list 或用方括號括住的 argument_list:
base_access
: 'base' '.' identifier type_argument_list?
| 'base' '[' argument_list ']'
;
base_access 可用來存取目前類別或結構中類似具名成員所隱藏的基類成員。 只有在實例建構函式、實例方法、實例存取器(§12.2.1)或終結器的主體內,才允許 base_access。 當 base.I 發生在類別或結構中時,I 應表示該類別或結構之基類的成員。 同樣地,當 base[E] 發生在類別中時,適用的索引器應該存在於基類中。
在綁定時,base_access 表達式形式 base.I 和 base[E] 的評估方式,正如同它們被寫成 ((B)this).I 和 ((B)this)[E]一樣,其中 B 是構造所在類別或結構的基類。 因此,base.I 和 base[E] 對應至 this.I 和 this[E],但 this 視為基類的實例。
當 base_access 參考虛擬函式成員(方法、屬性或索引器)時,決定在執行時要調用的函式成員(§12.6.6)將會有所改變。 被呼叫的函式成員是透過尋找與 相關的函式成員的最衍生實作來確定的(B),而不是根據 this的運行時型別,這是通常在非基礎存取中執行的方式。 因此,在虛擬函式成員的覆寫過程中,可以使用 base_access 來呼叫該函式成員的繼承實作。 如果 base_access 所參考的函式成員是抽象的,則會發生系結時間錯誤。
Note:不同於
this,base本身不是表達式。 它是只用於 base_access 或 constructor_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_expression 或 post_decrement_expression 具有編譯時間類型 dynamic,而且下列規則會在運行時間使用 primary_expression的運行時間類型來套用。
如果後置遞增或遞減運算的操作數是屬性或索引器存取,那麼該屬性或索引器必須同時具有 get 和 set 存取子。 如果情況並非如此,則會發生系結時間錯誤。
一元運算子多載解析 (•12.4.4) 會套用至選取特定運算符實作。 下列類型存在預先定義的 ++ 和 -- 運算符:sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal及任何列舉類型。 預先定義的 ++ 運算符會傳回將 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值會成為運算的結果。
- 實例表達式(如果
++ and -- 運算子也支援前置詞表示法 (§12.9.7) 。 x 本身在作業之後都有相同的值。
您可以使用後置或前置表達法來調用運算子 ++ 或 -- 的實作。 這兩個表示法不可能有個別的運算符實作。
12.8.17 新運算符
12.8.17.1 一般
new 運算子可用來建立型別的新實例。
新運算式有三種形式:
- 物件建立表達式可用來建立類別類型和實值型別的新實例。
- 陣列建立運算式用於創建陣列類型的新實例。
- 委派創建表達式可用來獲得委派類型的實例。
new 運算符意味著創建一個類型的實例,但不一定意味著分配記憶體。 特別是,實值型別的實例不需要超出其所在變數的任何額外記憶體,並且當使用 new 建立實值型別的實例時,不會發生任何記憶體配置。
附注:委派建立表達式不一定會建立新的實例。 當表達式以與方法群組轉換相同的方式處理時(“\10.8)或匿名函式轉換(\10.7),可能會導致重複使用現有的委派實例。 結尾註釋
12.8.17.2 物件建立表達式
12.8.17.2.1 一般
object_creation_expression 可用來建立 class_type 或 value_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_type、value_type或 type_parameter。 類型 不能是 元組類型 或抽象或靜態的 類別類型。
當且僅當 類型 是 class_type 或 struct_type時,才允許選擇性的 argument_list(第§12.6.2節)。
物件建立表達式可以省略建構函式自變數清單和括住括弧,前提是它包含物件初始化表達式或集合初始化表達式。 省略建構函式自變數清單和括住括弧相當於指定空的自變數清單。
物件建立表達式的處理,其中包含物件初始化表達式或集合初始化表達式,包括先處理實例建構函式,然後處理物件初始化表達式所指定的成員或專案初始化表達式(\12.8.17.2.2)或集合初始化表達式(\12.8.17.2.3)。
如果可選 argument_list 中的任何參數具有編譯時間類型 dynamic,則 object_creation_expression 會動態綁定(§12.3.3),而在運行時間,會以那些具有該編譯時間類型的 argument_list 參數的運行時間類型 dynamic 套用規則。 不過,物件建立會經過有限的編譯時間檢查,如 §12.6.5中所述。
形式為 的 new T(A) 的系結時間處理,其中 T 是 類型,或是 值類型,且 A 是選擇性的 參數列表,包含下列步驟:
- 如果
T是 value_type 且A不存在:-
object_creation_expression 是預設建構函式呼叫。
object_creation_expression 的結果是類型為
T的值,也就是T的預設值,如 第8.3.3節中所定義。
-
object_creation_expression 是預設建構函式呼叫。
object_creation_expression 的結果是類型為
- 否則,如果
T是 type_parameter 且A不存在: - 否則,如果
T是 class_type 或 struct_type:- 如果
T是抽象或靜態 class_type,則會發生編譯時期錯誤。 - 要呼叫的實體建構函式將按照 的多載解析規則來決定(見)。 候選實例建構函式集合是由
T中宣告的所有可存取實例建構函式所組成,這些建構函式適用於A(12.6.4.2)。 如果候選實例建構函式集合是空的,或無法識別單一最佳實例建構函式,就會發生系結時間錯誤。 -
object_creation_expression 的結果是類型為
T的值,也就是叫用上述步驟中判斷的實例建構函式所產生的值。 - 否則,object_creation_expression 無效,而且會發生系結時間錯誤。
- 如果
即使 object_creation_expression 是動態系結,其編譯時類型仍為 T。
物件創建表達式 T(A) 的執行時處理,其中 T 是 class_type 或 struct_type,而 A 是可選的 argument_list,包含以下步驟:
- 如果
T是 class_type: - 如果
T是 struct_type:-
T類型的實例是藉由配置暫存局部變數來建立。 由於 struct_type 的實例建構函式必須明確將值指派給所建立實例的每個欄位,因此不需要初始化暫存變數。 - 實例建構函式會根據函式成員調用(§12.6.6)的規則來叫用。 新配置的實例參考會自動傳遞至實例建構函式,而且實例可以從該建構函式中存取,如下所示。
-
12.8.17.2.2 物件初始化表達式
物件初始化表達式 指定物件零個或多個欄位、屬性或索引元素的值。
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 後跟著等號,然後是表達式、物件初始化表達式或集合初始化表達式之一。 物件初始化器中的表達式無法引用正在初始化的新建物件。
在initializer_target的argument_list中,沒有隱含支援類型 (Index) 或 (Range) 類型的引數。
在等號之後指定運算式的成員初始化運算式,其處理方式與目標的指派 (§12.23.2) 相同。
在等號之後指定物件初始化表達式的成員初始化表達式是 巢狀物件初始化表達式,也就是內嵌物件的初始化。 巢狀物件初始化表達式中的賦值操作被視為欄位或屬性成員的賦值,而不是將新值直接賦予給欄位或屬性。 巢狀物件初始化表達式無法套用至具有實值類型的屬性,或套用至具有實值類型的唯讀字段。
成員初始化器在等號後指定集合初始化器時,就是在初始化內嵌集合。 初始化表達式中指定的元素會新增至目標所參考的集合,而不是將一個新的集合指派至目標欄位、屬性或索引器。 目標應為符合 \12.8.17.2.3 中所指定需求的集合類型。
當初始化表達式目標參考索引器時,索引器的參數應該只評估一次。 因此,即使自變數最終永遠不會被使用(例如,因為空的巢狀初始化表達式),它們也會評估其副作用。
範例:下列類別代表具有兩個座標的點:
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.2.3 集合初始化運算式
集合初始化設定式會指定集合的元素。
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)*
;
集合初始化表達式是由元素初始化表達式序列所組成,由 { 和 } 標記括住,並以逗號分隔。 每個元素初始化器會指定一個要新增到初始化集合物件中的元素,並包含一個由 { 和 } 標記括住的表達式列表,以逗號分隔。 單一表達式初始化器可以不加上大括號來撰寫,但不能接著是指派表達式,以避免與成員初始化器混淆。
non_assignment_expression生產在 §12.24 中定義。
範例:以下是包含集合初始化表達式的物件建立表達式範例:
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.3 匿名物件建立表達式
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;允許最後一行的指派,因為
p1和p2屬於相同的匿名類型。結束範例
匿名型別中的 Equals 和 GetHashcode 方法覆寫了從 object繼承的方法,並根據屬性的 Equals 和 GetHashcode 來定義。只有當相同匿名型別的兩個實例的所有屬性都相等時,這兩個實例才會相等。
成員宣告子可以縮寫為簡單名稱(§12.8.4)、成員存取(§12.8.7)、null 條件式投影初始化器 §12.8.8 或基底存取(§12.8.15)。 這稱為 初始化器,是宣告並指派給具有相同名稱的屬性的速記方式。 具體來說,表單的成員宣告子
«identifier»、«expr» . «identifier» 和 «expr» ? . «identifier»
分別精確等同於以下內容:
«identifier» = «identifier»、«identifier» = «expr» . «identifier» 和 «identifier» = «expr» ? . «identifier»
因此,在投影初始設定中,標識符會同時選擇要指派的值以及要指派值的欄位或屬性。 以直覺方式,投影初始化表達式不僅會投影值,也會投影值的名稱。
12.8.17.4 陣列建立表達式
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[][,]的陣列實例。 結束範例
表達式清單中的每個運算式都必須是類型 int、uint、long或 ulong,或隱式可轉換為其中一或多個類型。 每個表達式的值都會決定新分配的陣列實例中對應維度的長度。 由於陣列維度的長度應該是非負值,所以在表達式清單中具有負值的常數表達式是編譯時期錯誤。
除非在不安全的內容 (§24.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.16)。 對於多維度陣列,也就是其中一個 rank_specifier 至少包含一個逗號的陣列,此集合包含巢狀 array_initializers 中找到的所有 表示式。
陣列初始化器會在 第17.7節中進一步說明。
運算陣列生成表達式的結果會被分類為一個值,即新分配的陣列實例的引用。 陣列建立表示式的執行時間處理包含下列步驟:
-
expression_list 的維度長度表達式會依左至右的順序進行評估。 在評估每個表達式之後,會執行其中一種類型的隱含轉換(§10.2):
int、uint、long、ulong。 在此清單中,選擇具有隱含轉換的第一個類型。 如果表達式或後續的隱含轉換評估造成例外狀況,則不會評估任何進一步的表達式,也不會執行任何進一步的步驟。 - 維度長度的計算值會經過驗證,如下所示:如果一或多個值小於零,則會擲回
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最後一個表達式會造成編譯時期錯誤,因為
int或string都無法隱含轉換成另一個表達式,因此沒有最佳的常見類型。 在此情況下,需使用明確型別的陣列建立運算式,例如將類型指定為object[]。 或者,其中一個元素可以轉換為通用的基底類型,然後成為推斷的元素類型。結束範例
隱式型別的陣列建立表達式可以與匿名物件初始化器§12.8.17.3結合,以建立匿名型別的數據結構。
範例:
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.5 委託建立表達式
delegate_creation_expression 可用來取得 delegate_type的實例。
delegate_creation_expression
: 'new' delegate_type '(' expression ')'
;
委派創建表達式的自變數應該是方法群組、匿名函數,或編譯時間類型 dynamic 或 delegate_type的值。 如果自變數是方法群組,它會識別 方法,而針對實例方法,則會識別要為其建立委派的物件。 如果自變數是匿名函式,它會直接定義委派目標的參數和方法主體。 如果參數是值,它會識別出要建立複本的委派實例。
如果 表達式 具有編譯時間類型 dynamic,則 delegate_creation_expression 會動態系結 (§12.8.17.5),而下列規則會在運行時間使用 表達式的運行時間類型來套用。 否則,規則會在編譯階段套用。
新 表單 D(E) 的系結時間處理,其中 D 是 delegate_type,E 是 表示式,包含下列步驟:
如果
E是方法群組,則委派建立表達式會以與方法群組轉換相同的方式(§10.8)從E轉換成D來處理。
新 表單 D(E) 的運行時間處理,其中 D 是 delegate_type,E 是 表示式,包含下列步驟:
- 如果
E是方法群組,委派運算式會評估為方法群組轉換(§10.8),從E到D。 - 如果
E是匿名函式,則會將委派建立評估為從E到D的匿名函式轉換(\10.7)。 - 如果
E為 delegate_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.18 typeof 運算符
typeof 運算子用於取得某類型的 System.Type 物件。
typeof_expression
: 'typeof' '(' type ')'
| 'typeof' '(' unbound_type_name ')'
| 'typeof' '(' 'void' ')'
;
unbound_type_name
: identifier generic_dimension_specifier?
('.' identifier generic_dimension_specifier?)*
| unbound_qualified_alias_member
('.' identifier generic_dimension_specifier?)*
;
unbound_qualified_alias_member
: identifier '::' 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_name 和 unbound_qualified_alias_member 的文法遵循 type_name 文法(7.8)和 qualified_alias_member 文法(~14.8.1),但 generic_dimension_specifier取代 type_argument_list。 結尾註釋
當辨識typeof_expression的運算元時,如果unbound_type_name和type_name皆可適用,即在不包含generic_dimension_specifier或type_argument_list的情況下,應選擇type_name。
注意:由於 typeof_expression的替代選項的排列順序,ANTLR 會自動進行指定選擇。 結尾註釋
unbound_type_name的意義會判斷為:
- 把令牌序列轉換為type_name時,需要將每個generic_dimension_specifier替換為具有相同逗號數目並包含關鍵字的
object,以及每個type_argument。 - 產生的 type_name 解析為建構型別(§7.8)。
- unbound_type_name接著是與已解析建構型別 (~8.4) 相關聯的未系結泛型型別。
注意:實作不需要轉換符號序列,或生成中間建構類型,只是確定的未繫結泛型類型會「如同」遵循此過程。 結尾註釋
當類型名稱是可為 Null 的參考型別時,會發生錯誤。
typeof_expression 的結果是所產生未系結泛型類型的 System.Type 物件。
第三種形式的 typeof_expression 包含 typeof 關鍵詞,後面接著括弧化 void 關鍵詞。 這種形式的運算式的結果是 System.Type 對象,其代表不存在的類型。
typeof(void) 傳回的類型對象與針對任何類型傳回的類型物件不同。
附註:這個特殊的
System.Type對象在語言中允許方法反射的類別庫中很有用,這些方法希望可以用一個void實例來表示任何方法的返回類型,包括System.Type方法的返回類型。 結尾註釋
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]請注意,
int和System.Int32的類型相同。typeof(X<>)的結果不取決於類型參數,而typeof(X<T>)的結果則取決於類型參數。結束範例
12.8.19 sizeof 運算符
sizeof 運算符會傳回指定型別變數所佔用的 8 位位元元組數目。 指定為sizeof操作數的類型應該是 unmanaged_type (8.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§24.6.9 中指定。
12.8.20 已核取和未核取的運算符
checked 和 unchecked 運算子用於控制整數型別算術運算和轉換的溢位檢查上下文。
checked_expression
: 'checked' '(' expression ')'
;
unchecked_expression
: 'unchecked' '(' expression ')'
;
checked 運算符會評估已核取內容中的內含表達式,而 unchecked 運算符會評估未核取內容中的內含表達式。
checked_expression 或 unchecked_expression 完全對應於 parenthesized_expression (§12.8.5),但會在指定的溢位檢查上下文中評估包含的表達式。
溢位檢查內容也可以透過 checked 和 unchecked 語句來控制(§13.12)。
下列作業會受到已核取和未核取運算符和語句所建立的溢位檢查內容所影響:
- 預先定義
++的 and--運算子 (§12.8.16 和 §12.9.7),當運算元是整數或列舉類型時。 - 預先定義的
-一元運算符(§12.9.3),當操作數是整數型別時。 - 預先定義
+的 、-、*和/二進位運算子 (§12.12),當兩個運算元都是整數或列舉類型時。 - 從一個整數或列舉型別到另一個整數或列舉型別,或從 或
float轉換成整數或列舉型別的明確數值轉換(double)。
當上述其中一個作業產生的結果過大以至無法適應於目的類型時,執行的上下文將控制產生的行為。
- 在內容中
checked,如果作業是常數運算式 (§12.25) ,則會發生編譯時間錯誤。 否則,在執行時執行作業時,會拋出System.OverflowException。 - 在
unchecked環境中,會捨棄不符合目標類型的任何高階位元,以截斷結果。
對於未由任何 OR checked 運算子或陳述式括住的非常數運算式 (unchecked) (在執行階段評估的運算式) ,預設溢位檢查內容會取消核取,除非外部因素 (例如編譯器參數和執行環境設定) 呼叫檢查評估。
針對常數運算式 (§12.25) (可在編譯階段完整評估的運算式) ,一律會檢查預設溢位檢查內容。 除非明確將常數表示式放在 unchecked 內容中,否則在表達式的編譯時期評估期間發生的溢位一律會導致編譯時間錯誤。
匿名函式的主體不會受到匿名函式所處的 checked 或 unchecked 情境的影響。
範例:在下列程式代碼中
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內容中,因此不會報告溢位。結束範例
checked 和 unchecked 運算符只會影響包含在「(」和「)」標記內的操作所屬的溢位檢查的範圍。 運算元對因評估所包含運算式而調用的函式成員沒有任何影響。
範例:在下列程式代碼中
class Test { static int Multiply(int x, int y) => x * y; static int F() => checked(Multiply(1000000, 1000000)); }在 F 中使用
checked不會影響x * y中Multiply的評估,因此會在預設溢位檢查內容中評估x * y。結束範例
在以十六進位表示法撰寫帶正負號整數型別的常數時,unchecked 運算子很方便。
範例:
class Test { public const int AllBits = unchecked((int)0xFFFFFFFF); public const int HighBit = unchecked((int)0x80000000); }此兩個十六進位常數都是類型
uint。 由於常數位於int範圍之外,沒有unchecked運算符,因此轉型為int的操作會導致編譯時錯誤。結束範例
附註:
checked和unchecked運算符和語句可讓程式設計人員控制某些數值計算的某些層面。 不過,某些數值運算符的行為取決於其操作數的數據類型。 例如,將兩個小數相乘時,即使在明確的未檢查結構內進行,溢位仍然會導致例外發生。 同樣地,相乘兩個浮點數絕不會導致溢位時發生例外狀況,即使在明確檢查的建構內也一樣。 此外,其他運算符永遠不會受到檢查模式的影響,無論是預設還是明確。 結尾註釋
12.8.21 預設值表達式
default_value_expression
: explicitly_typed_default
| default_literal
;
explicitly_typed_default
: 'default' '(' type ')'
;
default_literal
: 'default'
;
default_literal 代表預設值(§9.3)。 它沒有類型,但可以透過預設字面值轉換成任何類型(§10.2.16)。
default_value_expression的結果是explicitly_typed_default中明確類型的預設值 (§9.3) ,或default_value_expression轉換的目標類型。
如果類型是下列類型之一,則 default_value_expression 是常數運算式 (§12.25):
- 參考型別
- 已知為參考型別的類型參數 (§8.2) 。
- 下列其中一個實值類型:
sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal、bool,;或 - 任何列舉類型。
12.8.22 堆棧配置
堆疊分配表達式會從執行堆疊分配一個記憶體區塊。 執行堆疊 是儲存局部變數的記憶體區域。 執行堆疊不是受控堆積的一部分。 當目前函式傳回時,用於儲存局部變數的記憶體會被自動回收。
堆疊配置表達式的安全內容規則會在 {16.4.15.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 的最佳常見類型規則來推斷它。
- 如果省略 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 是指標類型 (§24.3) 或推斷 (var),則 stackalloc_expression 的結果是類型的 T* 指標 (§24.9) 。 在此情況下,stackalloc_expression 必須出現在不安全的程式代碼中。 否則,stackalloc_expression 的結果是 Span<T>類型的實例,其中 T 是 unmanaged_type:
-
Span<T>(§C.3)是 ref 結構類型(§16.2.3),它將顯示由 stackalloc_expression所配置的記憶體區塊,作為一個可索引的具型別(T)項目集合。 - 結果的
Length屬性會傳回已配置的項目數。 - 結果索引器(§15.9)會將 variable_reference(§9.5)傳回已配置區塊的項目,並進行範圍檢查。
catch 或 finally 區塊中不允許堆疊分配初始化器(§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 ,則無論調用是否有效,表達式都會被視為 調用表達式 。 否則,它是 nameof_expression。
簡單名稱和成員存取查閱會在編譯時期針對 named_entity 進行,遵循 §12.8.4 和 §12.8.7中所述的規則。 不過,當§12.8.4和§12.8.7中所描述的查找因在靜態上下文中找到實例成員而導致錯誤時,nameof_expression不會產生這類錯誤。
這是 named_entity 指定方法群組具有 type_argument_list的編譯時間錯誤。 具有類型 的 dynamic 是編譯時間錯誤。
nameof_expression 是類型為 string的常數運算式,而且在執行期間沒有影響。 具體來說,其 named_entity 不會進行評估,且在進行明確指派分析(§9.4.4.22)時會被忽略。 其值是選擇性最終 type_argument_list之前 named_entity 的最後一個標識符,會以下列方式轉換:
- 若使用此「
@」字首,則將移除。 - 每個 unicode_escape_sequence 都會轉換成其對應的 Unicode 字元。
- 移除任何 formatting_characters。
這些是測試標識符相等性時應用在 §6.4.3 的相同轉換。
範例:下列範例說明各種
nameof表達式的結果,假設List<T>命名空間內宣告的泛型型別System.Collections.Generic: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.21 中進一步描述。
12.9 一元運算符
12.9.1 一般
, , (僅限邏輯否定 +)、 -、 !、 ~、 cast 和^運算子稱為一元運算子。 ++--await
附注:後置 null-forgiving 運算符 (12.8.9),
!,因為其編譯時間和不可多載的本質,會排除在上述清單中。 結尾註釋
unary_expression
: primary_expression
| '+' unary_expression
| '-' unary_expression
| logical_negation_operator unary_expression
| '~' 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 (§24.6.2) 和 addressof_expression (§24.6.5) 僅在不安全代碼 (§24) 中可用。
如果 unary_expression 的操作數在編譯時具有類型 dynamic,則會進行動態繫結(§12.3.3)。 在此案例中:
-
unary_expression的編譯時間類型為:
-
Index針對索引^從結尾運算子 (§12.9.6) -
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的值的正負號反轉。 如果x是NaN,則結果也會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 是具有基礎類型 E的列舉型別 U 表達式,與評估 (E)(~(U)x)的結果完全一致,唯一的不同在於轉換至 E 總是如同在 unchecked 環境中執行(§12.8.20)。
此外,上述未提升的預先定義位補碼運算符的提升形式(§12.4.8)也已預先定義。
12.9.6 索引從端運算子
一元 ^ 運算子稱為 索引從結尾運算子 (通俗地稱為 帽子運算子)。 此運算子不可多載 (§12.4.3) ,而且有一個預先定義的運算子:
Index operator ^(int x);
形式 ^x 運算的結果是 from-end Index (§18.2) 值,相當於運算式的結果:
new Index(x, true)
與其他 unary_expression一樣,運算元的編譯階段類型 dynamic 可能為 (§12.9.1) ,並動態系結 (§12.3.3) 。 結果的編譯時間類型一律 Index為 。
索引從端運算子的提升 (§12.4.8) 形式也會預先定義。
12.9.7 前綴遞增和遞減運算子
pre_increment_expression
: '++' unary_expression
;
pre_decrement_expression
: '--' unary_expression
;
前置遞增或遞減運算的操作數應該是分類為變數、屬性存取或索引器存取的表達式。 作業的結果是與操作數相同的類型值。
如果前置詞遞增或遞減作業的操作數是屬性或索引器存取,則屬性或索引器應同時具有 get 和 set 存取子。 如果情況並非如此,則會發生系結時間錯誤。
一元運算子多載解析 (•12.4.4) 會套用至選取特定運算符實作。 下列類型存在預先定義的 ++ 和 -- 運算符:sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal及任何列舉類型。 預先定義的 ++ 運算符會傳回將 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.8 轉換運算式
cast_expression 可用來將表達式明確轉換成指定的型別。
cast_expression
: '(' type ')' unary_expression
;
形式為 的 (T)E,其中 T 是一種型別,E 是 一元運算式,對 的值執行明確轉換 (E)為型別 T。 如果 E 到 T之間沒有明確轉換,則會發生系結時間錯誤。 否則,結果是明確轉換所產生的值。 即使 E 表示變數,結果一律會分類為值。
cast_expression 的語法會導致某些句法上的模棱兩可。
範例:表達式
(x)–y可以解譯為 類型轉換表達式(將–y轉換成類型x),或者作為 加法表達式,結合 括號表達式(用來計算值x – y)。 結束範例
若要解決 cast_expression 模棱兩可的情況,下列規則存在:一或多個標記序列({6.4)以括弧括住,只有在下列其中至少一個成立時,才會被視為 cast_expression 的開頭:
- 標記序列是類型的正確文法,但不適用於表達式。
- 標記序列是類型的正確文法,而緊接在右括弧後面的標記是 “
~”, “!”, “(”, 識別符號 (§6.4.3)、文字常值 (§6.4.5),或任何關鍵詞 (§6.4.4),但as和is除外。
上述「正確文法」一詞只表示標記序列應符合特定的文法製作。 它特別不會考慮任何組成標識碼的實際意義。
範例:如果
x和y是標識符,則即使x.y實際上並未表示類型,x.y也是正確的文法。 結束範例
附註:根據釐清規則,如果
x和y是標識符,則(x)y、(x)(y)和(x)(-y)是 類型轉換表達式,但(x)-y則不是,即使x識別的是一個類型。 不過,如果x是識別預先定義類型的關鍵詞(例如int),則所有四種形式都是 cast_expression(因為這類關鍵詞本身不可能是表達式)。 結尾註釋
12.9.9 等待運算式
12.9.9.1 一般規定
await 運算符用來暫停執行封閉的非同步函式,直到操作數表示的非同步操作完成為止。
await_expression
: 'await' unary_expression
;
await_expression 只允許在異步函式的主體中(§15.14)。 在最接近的封入異步函式中,await_expression 不應發生在下列位置:
- 在嵌套的(非異步)匿名函式內
- 在 鎖定語句 的區塊內
- 在匿名函式轉換為表達式樹類型時(§10.7.3)
- 在不安全的情境中
附注:await_expression 不能出現在 query_expression的大部分位置,因為這些語法會被轉換為使用非同步 Lambda 表達式。 結尾註釋
在異步函式內,await 不得作為 available_identifier 使用,雖然可以使用逐字標識碼 @await。 因此,await_expression與涉及標識符的各種表達式之間沒有語法模棱兩可。 在異步函式之外,await 做為一般標識符。
await_expression 的運算元稱為 工作。 它表示在評估 await_expression 時可能已完成或尚未完成的異步操作。
await 運算子的目的是暫停封入異步函式的執行,直到等候的工作完成,然後取得其結果。
12.9.9.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.9.3 等待表達式的分類
表達式 await t 分類的方式與表達式 (t).GetAwaiter().GetResult()相同。 因此,如果 GetResult 的傳回型別是 void,則 await_expression 會分類為 nothing。 如果它的傳回值不是void 類型 T,則 await_expression 會被歸類為類型 T的值。
12.9.9.4 await 運算式的執行階段評估
在執行時,表達式 await t 會被計算如下。
- 評估表達式
a,以獲得 awaiter(t).GetAwaiter()。 - 通過評估表達式
bool來獲得b(a).IsCompleted。 - 如果
b是false,那麼取決於a是否實作介面System.Runtime.CompilerServices.ICriticalNotifyCompletion(以下簡稱ICriticalNotifyCompletion)。 在綁定時完成此檢查;亦即,如果a具有編譯時類型dynamic,則在運行時檢查,否則在編譯時檢查。 令r表示恢復委派 (§15.14):- 如果
a未實作ICriticalNotifyCompletion,則會評估表達式((a) as INotifyCompletion).OnCompleted(r)。 - 如果
a實作ICriticalNotifyCompletion,則會評估表達式((a) as ICriticalNotifyCompletion).UnsafeOnCompleted(r)。 - 評估接著會暫停,並將控制權傳回至異步函式的當前呼叫端。
- 如果
- 緊接在之後(如果
btrue),或在稍後叫用恢復委派時(如果b已false),就會評估表達式(a).GetResult()。 如果傳回值,該值就是 await_expression的結果。 否則,結果是空的。
實作 awaiter 的介面方法 INotifyCompletion.OnCompleted 和 ICriticalNotifyCompletion.UnsafeOnCompleted,應該導致委派 r 被叫用最多一次。 否則,封入異步函式的行為是未定義的。
12.10 範圍運算子
運算子 .. 稱為 範圍 運算子。
range_expression
: unary_expression
| unary_expression? '..' unary_expression?
;
預先定義的範圍運算子為:
Range operator ..(Index x, Index y);
範圍運算子無法多載 (§12.4.3)。
所有範圍運算式都會被視為具有 的形式 x..y,其中:
-
x如果存在,則為左運算元,否則為表達式0;並且 -
y如果存在,則為正確的運算元,否則為運算式^0。
作業的結果是 Range (§18.3) 值,相當於運算式的結果:
new Range(x, y)
如果範圍運算式中的其中一個或兩個運算元具有編譯階段類型 dynamic,則運算式會動態系結 (§12.3.3) 。 結果的編譯時間類型一律 Range為 。
範圍運算子的提升 (§12.4.8) 形式也已預先定義。
範圍運算子是非關聯的 (§12.4.2)。
12.11 切換表達式
switch_expression在運算式內容中提供switch類似語意的語意。
switch_expression
: range_expression
| switch_expression 'switch' '{' switch_expression_arms? '}'
;
switch_expression_arms
: switch_expression_arm (',' switch_expression_arm)* ','?
;
switch_expression_arm
: pattern case_guard? '=>' switch_expression_arm_expression
;
switch_expression_arm_expression
: expression
;
如果每個 switch 運算式的每個switch_expression_arm_expression都有隱含轉換,則會從 switch 運算式switch_expression_armT轉換成類型的 switch 運算式轉換 (T) 。
如果開關運算式不受 開關運算式轉換的約束,則
- switch_expression的類型是 switch_expression_arms 的 switch_expression_arm_expressions 的最佳常見類型 §12.6.3.16) (如果存在此類類型),並且每個switch_expression_arm_expression都可以隱含地轉換為該類型。
- 如果不存在這類類型,就會發生錯誤。
如果某些 switch_expression_arm的模式無法影響結果,則為錯誤,因為某些先前的模式和防護將始終匹配。
如果 switch 運算式的輸入每個值都由 switch 運算式的至少一個臂處理,則稱為 switch 運算式是 詳盡的 。 如果 switch 運算式不是詳盡的,編譯器應該會產生警告。
在執行時期,switch_expression的結果是第一個switch_expression_arm的運算式值,其中switch_expression左側的運算式符合switch_expression_arm的模式,且switch_expression_arm的case_guard (如果存在) 會評估為 true。 若不存在此類switch_expression_arm,switch_expression 會拋出該例外System.InvalidOperationException的實例(或由此衍生的類別)。
範例:下列會將線上地圖上表示視覺方向的列舉值轉換為對應的基本方向:
static Orientation ToOrientation(Direction direction) => direction switch { Direction.Up => Orientation.North, Direction.Right => Orientation.East, Direction.Down => Orientation.South, Direction.Left => Orientation.West, _ => throw new ArgumentOutOfRangeException(direction.ToString()), }; public enum Direction { Up, Down, Right, Left } public enum Orientation { North, South, East, West }結束範例
12.12 算術運算子
12.12.1 一般
*、/、%、+和 - 運算符稱為算術運算符。
multiplicative_expression
: switch_expression
| multiplicative_expression '*' switch_expression
| multiplicative_expression '/' switch_expression
| multiplicative_expression '%' switch_expression
;
additive_expression
: multiplicative_expression
| additive_expression '+' multiplicative_expression
| additive_expression '-' multiplicative_expression
;
如果算術運算子的操作數具有編譯時間類型 dynamic,則表達式會動態系結(\12.3.3)。 在這種情況下,表達式的編譯時類型是 dynamic,而以下所述的解析將會在執行時,使用具有編譯時類型的操作數的執行時類型 dynamic。
12.12.2 乘法運算子
針對格式 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 的所有可能組合結果。 在數據表中,
x和y都是正有限值。z是x * y的結果,四捨五入為最接近的可表示值。 如果結果的大小對目的類型而言太大,則z為無限大。 由於四捨五入,即使z或x都不是零,y也可能為零。+y-y+0-0+∞-∞NaN+x+z-z+0-0+∞-∞NaN-x-z+z-0+0-∞+∞NaN+0+0-0+0-0NaNNaNNaN-0-0+0-0+0NaNNaNNaN+∞+∞-∞NaNNaN+∞-∞NaN-∞-∞+∞NaNNaN-∞+∞NaNNaNNaNNaNNaNNaNNaNNaNNaN(除非另有說明,否則在 §12.12.2–§12.12.6 的浮點表中,使用 “
+” 表示值為正數,使用 “-” 表示值為負數,缺少符號表示值可能是正數或負數,或沒有符號 (NaN)。十進位乘法:
decimal operator *(decimal x, decimal y);如果產生的值大小太大而無法以十進位格式表示,則會擲回
System.OverflowException。 由於四捨五入,即使兩個操作數都不是零,結果也可能為零。 結果的尺度,在任何四捨五入之前,為兩個操作數的尺度之和。 十進位乘法相當於使用類型為System.Decimal的乘法運算符。
上述未提升的預先定義乘法運算符的提升形式(§12.4.8)也已經預先定義。
12.12.3 除法運算子
針對格式 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);如果右操作數的值為零,則會拋出
System.DivideByZeroException。此除法會將結果趨近於零。 因此,結果的絕對值是最大可能整數,小於或等於兩個操作數商的絕對值。 當兩個操作數具有相同正負號時,結果為零或正數,而當兩個操作數有相反的正負號時,則結果為零或正數。
如果左操作數是最小可表示的
int或long值,而右操作數則為–1,就會發生溢位。 在checked的情境中,這會導致拋出System.ArithmeticException(或其子類別)。 在unchecked內容中,實作定義為擲回System.ArithmeticException(或子類別),或溢位未回報,產生的值為左操作數。浮點除法:
float operator /(float x, float y); double operator /(double x, double y);商數是根據 IEC 60559 算術規則來計算。 下表列出非零有限值、零、無限和 NaN 的所有可能組合結果。 在數據表中,
x和y都是正有限值。z是x / y的結果,四捨五入為最接近的可表示值。+y-y+0-0+∞-∞NaN+x+z-z+∞-∞+0-0NaN-x-z+z-∞+∞-0+0NaN+0+0-0NaNNaN+0-0NaN-0-0+0NaNNaN-0+0NaN+∞+∞-∞+∞-∞NaNNaNNaN-∞-∞+∞-∞+∞NaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaN十進制除法:
decimal operator /(decimal x, decimal y);如果右操作數的值為零,則會拋出
System.DivideByZeroException。 如果產生的值大小太大而無法以十進位格式表示,則會擲回System.OverflowException。 由於四捨五入,即使第一個操作數不是零,結果也可能為零。 在進行任何捨入之前,結果的縮放比例是最接近所需比例的小數位設置,以保證結果等於精確結果。 所選擇的比例是x的比例減去y的比例。十進位除法相當於使用類型為
System.Decimal的除法運算符。
此外,已預先定義上述未提升之預定義除法運算符的提升形式(§12.4.8)。
12.12.4 餘數運算子
針對格式 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);x % y的結果是x – (x / y) * y所產生的值。 如果y為零,則會拋出System.DivideByZeroException。如果左操作數是最小的
int或long值,而右操作數是–1,則只有在System.OverflowException擲回例外狀況時,才會擲回x / y。浮點餘數:
float operator %(float x, float y); double operator %(double x, double y);下表列出非零有限值、零、無限和 NaN 的所有可能組合結果。 在數據表中,
x和y都是正有限值。z是x % y的結果,並計算為x – n * y,其中 n 是小於或等於x / y的最大可能整數。 這個計算餘數的方法類似於用於整數操作數,但與 IEC 60559 定義不同(其中n是最接近x / y的整數)。+y-y+0-0+∞-∞NaN+x+z+zNaNNaN+x+xNaN-x-z-zNaNNaN-x-xNaN+0+0+0NaNNaN+0+0NaN-0-0-0NaNNaN-0-0NaN+∞NaNNaNNaNNaNNaNNaNNaN-∞NaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaN十進位餘數:
decimal operator %(decimal x, decimal y);如果右操作數的值為零,則會拋出
System.DivideByZeroException。 當擲回System.ArithmeticException或子類別時,即會定義實作。 如果x % y不會拋出例外狀況,那麼符合規範的實作就不應該拋出x / y的例外狀況。 在任何四捨五入之前,結果的小數位數是兩個操作數中較大的小數位數。如果結果的符號非零,則結果的符號與x相同。小數餘數相當於使用 類型為
System.Decimal的餘數運算符。附注:這些規則確保針對所有類型,結果永遠不會具有與左操作數相反的符號。 結尾註釋
提升形式(§12.4.8)的上述未提升的預定義餘數運算子也已預先定義。
12.12.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 的所有可能組合結果。 在數據表中,
x和y是非零的有限值,z是x + y的結果。 如果x和y具有相同的大小,但相反的跡象,z為正零。 如果x + y太大而無法表示在目標類型中,z是和x + y擁有相同符號的無限值。y+0-0+∞-∞NaNxzxx+∞-∞NaN+0y+0+0+∞–∞NaN-0y+0-0+∞-∞NaN+∞+∞+∞+∞+∞NaNNaN-∞-∞-∞-∞NaN-∞NaNNaNNaNNaNNaNNaNNaNNaN十進位加法:
decimal operator +(decimal x, decimal y);如果產生的值大小太大而無法以十進位格式表示,則會擲回
System.OverflowException。 結果的縮放比例,在任何四捨五入之前,都是兩個操作數的較大比例。十進位加法相當於使用
System.Decimal類型的加法運算符。列舉加法。 每個列舉型別都會隱含地提供下列預先定義的運算符,其中
E是列舉類型,而U是E的基礎類型: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操作數都會藉由叫用繼承自類型ToString的虛擬object方法,轉換為其字串表示法。 如果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.12.6 和 §21.6。 因為
System.Delegate不是委派類型,所以不會為其定義運算子 + 。尾注
已增強形式的上述未增強預先定義加法運算符(§12.4.8)也已預先定義。
12.12.6 減法運算子
針對格式 x – y的操作,會套用二元運算子重載解析(§12.4.5)用來選擇特定的運算子實作。 操作數會轉換成所選取運算子的參數類型,而結果的類型是運算元的傳回類型。
預先定義的減法運算符如下所列。 運算子都會從 y減去 x。
整數減法:
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 的所有可能組合結果。 在數據表中,
x和y是非零的有限值,z是x – y的結果。 如果x和y相等,z為正零。 如果x – y太大而無法表示在目標類型中,z是和x – y擁有相同符號的無限值。y+0-0+∞-∞NaNxzxx-∞+∞NaN+0-y+0+0-∞+∞NaN-0-y-0+0-∞+∞NaN+∞+∞+∞+∞NaN+∞NaN-∞-∞-∞-∞-∞NaNNaNNaNNaNNaNNaNNaNNaNNaN(在上表中,
-y專案表示否定y,而不是表示值為負的意思。)十進位減法:
decimal operator –(decimal x, decimal y);如果產生的值大小太大而無法以十進位格式表示,則會擲回
System.OverflowException。 結果的縮放比例,在任何四捨五入之前,都是兩個操作數的較大比例。十進位減法相當於使用 類型為
System.Decimal的減法運算符。列舉減法。 每個列舉型別都會隱含地提供下列預先定義的運算符,其中
E是列舉型別,U是E的基礎型別:U operator –(E x, E y);此運算子的評估方式與
(U)((U)x – (U)y)完全相同。 換句話說,運算符會計算x和y的序數值之間的差異,而結果的類型是列舉的基礎型別。E operator –(E x, U y);此運算子的評估方式與
(E)((U)x – y)完全相同。 換句話說,運算符會從列舉的基礎型別中減去一個值,並生成一個列舉值。解除委派。 每個委派類型都會隱含地提供下列預先定義的運算符,其中
D是委派類型:D operator –(D x, D y);語意如下:
- 如果第一個操作數是
null,運算的結果是null。 - 否則,如果第二個操作數是
null,則作業的結果就是第一個操作數的值。 - 否則,這兩個運算元都代表非空調用清單 (§21.2)。
- 如果清單比較相等,如委派相等運算子 (§12.14.9) 所決定,則運算的結果為
null。 - 否則,操作的結果是新的調用列表,其中包含第一個操作數的項目列表,並移除了第二個操作數的項目,如果第二個操作數的項目列表是第一個操作數的子列表。 (若要判斷子清單相等,相對應的專案會與委派相等運算符比較。如果第二個操作數的清單符合第一個操作數清單中連續專案的多個子清單,則會移除連續項目的最後一個相符子清單。
- 否則,作業的結果就是左操作數的值。
- 如果清單比較相等,如委派相等運算子 (§12.14.9) 所決定,則運算的結果為
操作數清單(如果有的話)都不會在程序中變更。
範例:
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.13 班次操作員
<< 和 >> 運算子可用來執行位元移位作業。
shift_expression
: additive_expression
| shift_expression '<<' additive_expression
| shift_expression right_shift additive_expression
;
如果 shift_expression 的操作數具有編譯時期類型 dynamic,則該表示式會動態系結(§12.3.3)。 在這種情況下,表達式的編譯時類型是 dynamic,而以下所述的解析將會在執行時,使用具有編譯時類型的操作數的執行時類型 dynamic。
針對形式 x << count 或 x >> 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的類型為int或long時,會捨棄x的低階位,其餘位會向右移位,如果x為非負數,則會設定為零,如果x為負數,則會將高階空位位置設定為零。當
x類型為uint或ulong時,會捨棄x的低階位,其餘位會向右移位,而高階空白位位置會設定為零。
針對預先定義的運算子,要移位的位數會計算如下:
- 當
x的類型是int或uint時,位移計數由count的低階五位元決定。 換句話說,班次計數是從count & 0x1F計算出來的。 - 當
x的類型是long或ulong時,移位計數由count的低位六個位元決定。 換句話說,班次計數是從count & 0x3F計算出來的。
如果產生的位移計數為零,則位移運算子只會傳回 x的值。
位移運算永遠不會造成溢位,且在已檢查和未檢查的環境中會產生相同的結果。
當 >> 運算子的左操作數是有符號整數型別時,該運算符會執行 算術 右移位,其中操作數的最高有效位數值(符號位)會延伸至高階空位位置。 當 >> 運算子的左操作數是無符號整數類型時,運算符會執行 邏輯 右移,其中高階空白位位置一律設定為零。 若要執行與操作數類型所推斷的相反操作,可以使用明確類型轉換。
範例:如果
x是int類型的變數,則操作unchecked ((int)((uint)x >> y))會執行x的邏輯右移。 結束範例
此外,還預先定義了上述未提升的位移運算子的提升形式(§12.4.8)。
12.14 關聯式運算子和類型測試運算子
12.14.1 一般
==、!=、<、>、<=、>=、is和 as 運算符稱為關係型和型別測試運算符。
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運算子的右操作數必須先測試為 類型,然後作為可能跨越多個標記的 表達式。 若運算元是 表達式,則模式表達式的優先順序不得低於 shift_expression。 結尾註釋
注意:類型和constant_pattern 之間存在語法歧義,位於 的
relational_expression右側 ;is其中任何一個都可能是限定識別碼的有效剖析。 在這種情況下,只有當它無法綁定為類型(為了與語言的舊版相容)時,它才會被解析為第一個找到的東西(必須是常數或類型)。 這種歧義只存在於這種表達式的右側。
運算子在 is§12.14.12 中描述, as 運算子在 §12.14.13 中描述。
==、!=、<、>、<= 和 >= 運算符是 比較運算子。
如果 default_literal (§12.8.21)作為 <、>、<=或 >= 運算元,則會發生編譯時期錯誤。
如果 default_literal 當做 == 或 != 運算符的兩個操作數使用,則會發生編譯時期錯誤。 如果將 default_literal 作為 is 或 as 運算子的左運算元,會發生編譯時錯誤。
如果比較運算子的操作數具有編譯時間類型 dynamic,則表示式會動態系結 (\12.3.3)。 在這種情況下,表達式的編譯時類型是 dynamic,而以下所述的解析將會在執行時,使用那些編譯時類型為 dynamic的操作數的執行時類型。
對於格式為 x «op» y的運算,其中 «op» 是比較運算符,多載解析 (§12.4.5)將被套用,以選擇特定的運算符實作。 操作數會轉換成所選取運算子的參數類型,而結果的類型是運算元的傳回類型。 如果 equality_expression 的兩個操作數都是 null 常值,則不會執行多載解析,而且表達式會根據運算符是 true 或 false,評估為常數值 == 或 !=。
預先定義的比較運算子會在下列子條款中描述。 所有預先定義的比較運算符都會傳回bool類型的結果,如下表所述。
| 運算 | 結果 |
|---|---|
x == y |
如果 true 等於 x,則 y,否則 false |
x != y |
true 如果 x 不等於 y,否則為 false |
x < y |
true 如果 x 小於 y,則為 ,否則為 false |
x > y |
如果 true 大於 x,則使用 y ,否則使用 false |
x <= y |
如果 true 小於或等於 x,則 y 否則為 false |
x >= y |
如果 true 大於或等於 x,則 y 否則為 false |
12.14.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.14.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 時,<、>、<=和 >= 運算符不會產生與相反運算符邏輯否定相同的結果 。
範例:如果任一
x和y為 NaN,則x < y為false,但!(x >= y)為true。 結束範例
當兩個操作數都不是 NaN 時,運算子會比較兩個浮點操作數與排序相關的值
–∞ < –max < ... < –min < –0.0 == +0.0 < +min < ... < +max < +∞
其中 min 和 max 是最小且最大的正有限值,可以用指定的浮點格式表示。 此排序的顯著影響如下:
- 負數和正零視為相等。
- 負無限大被認為小於所有其他值,但等於另一個負無限大。
- 正無限大被視為大於所有其他值,但與另一個正無限大相等。
提升形式的浮點比較運算符(§12.4.8)和未提升的預先定義運算符一樣,都是預先定義的。
12.14.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.14.5 布林相等運算子
預先定義的布爾等號比較運算符如下:
bool operator ==(bool x, bool y);
bool operator !=(bool x, bool y);
如果 == 和 true 都是 x,或是 y 和 true 都 x,則 y 的結果為 false。 否則,結果會是 false。
如果 != 和 false 都是 x,或是 y 和 true 都 x,則 y 的結果為 false。 否則,結果會是 true。 當操作數的類型為 bool時,!= 運算符會產生與 ^ 運算符相同的結果。
上述未提升的預定義布爾等式運算符的提升形式(§12.4.8)也已預先定義。
12.14.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 是具有基礎類型 E之列舉型別 U 表達式,而 «op» 是其中一個比較運算符,與評估 ((U)x) «op» ((U)y)完全相同。 換句話說,列舉型別比較運算符只會比較兩個操作數的基礎整數值。
上述未提升的預先定義列舉比較運算符的提升(§12.4.8)形式也已預先定義。
12.14.7 參考類型相等運算子
每個類別類型 C 隱含地提供下列預先定義的參考型別相等運算子:
bool operator ==(C x, C y);
bool operator !=(C x, C y);
除非已存在預先定義的等號運算符,對於 C 而言(例如,當 C 是 string 或 System.Delegate時)。
運算符會傳回比較兩個參考是否相等或不相等的結果。 只有在 operator == 和 true 參考相同的實例或同時 x時,y 才會傳回 null;而只有在具有相同操作數的 operator != 傳回 true時,operator == 才會傳回 false。
除了一般適用性規則 (•12.6.4.2),預先定義的參考類型相等運算符還需要下列其中一項才能適用:
- 這兩個操作數都是已知為 reference_type 或常值
null的類型值。 此外,存在從任一操作數到另一個操作數類型的身份或明確引用轉換(§10.3.5)。 - 其中一個操作數是字面值
null,另一個操作數是類型T的值,其中T是一個 類型參數,已知它不是一個值類型,且不具備值類型的條件約束。- 如果在執行階段
T是不可為 Null 的實值類型,那麼==的結果是false,而!=的結果是true。 - 如果執行階段
T是可為 Null 的值類型,則會從運算元的屬性計算HasValue結果,如 (§12.14.10) 中所述。 - 如果在執行時
T是參考型別,當操作數是true時結果為null,否則為false。
- 如果在執行階段
除非其中一個條件成立,否則會發生系結時間錯誤。
附注:這些規則的顯著影響如下:
- 使用預先定義的參考型別平等運算符來比較在系結時間已知不同的兩個參考,屬於系結時間的錯誤。 例如,如果操作數的系結時間類型是兩個類別類型,而且兩者都不是衍生自另一個,則兩個操作數不可能參考相同的物件。 因此,作業會被視為系結時間錯誤。
- 預先定義的參考型別相等運算符不允許比較實值型別操作數(除非類型參數與
null進行比較,這是特別處理的)。- 預先定義的參考型別相等運算子的操作數絕不會進行裝箱。 執行這類 Boxing 作業是毫無意義的,因為新配置的 Boxed 實例參考必然與所有其他參考不同。
對於形式
x == y或x != y的運算操作,如果存在任何適用的使用者定義operator ==或operator !=,則運算子重載解析規則(§12.4.5)將選擇該運算子,而非預先定義的參考型別相等運算子。 一律可以藉由明確將一或兩個操作數轉換成類型object,來選取預先定義的參考型別相等運算符。結尾註釋
範例:下列範例會檢查未限制類型參數類型的自變數是否
null。class C<T> { void F(T x) { if (x == null) { throw new ArgumentNullException(); } ... } }即使
x == null可能代表不可為 Null 的實值型別,T建構還是允許使用,而且當false為不可為 Null 的實值型別時,結果只會定義為T。結束範例
對於形式 x == y 或 x != 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
s和t變數是指包含相同字元的兩個相異字串實例。 第一個比較會輸出True,因為當兩個運算元的類型都為 時,會選取預先定義的字串相等運算子 (string) 。 所有其餘的比較輸出False,因為當任一操作數的系結時間類型為operator ==時,string類型的object多載函數不適用。請注意,上述技術對實值型別沒有意義。 範例
class Test { static void Main() { int i = 123; int j = 123; Console.WriteLine((object)i == (object)j); } }輸出
False,因為轉換會建立兩個不同的boxedint值實例的參考。結束範例
12.14.8 字串相等運算子
預先定義的字串相等運算符如下:
bool operator ==(string x, string y);
bool operator !=(string x, string y);
當下列其中一個值成立時,會將兩個 string 值視為相等:
- 這兩個值都是
null。 - 兩個值都是對非
null字串實例的參考,這些字串實例具有相同的長度,且每個字元位置的字元也相同。
字串相等運算符會比較字串值,而不是字串參考。 當兩個不同的字串實例包含完全相同的字元序列時,字串的值會相等,但參考不同。
附註: 如 §12.14.7 所述,參考類型相等運算子可用來比較字串參考,而不是字串值。 結尾註釋
12.14.9 委派相等運算子
預先定義的委派相等運算符如下:
bool operator ==(System.Delegate x, System.Delegate y);
bool operator !=(System.Delegate x, System.Delegate y);
兩個委派實例會視為相等,如下所示:
- 如果任一個委派實例是
null,那麼只有當兩者都是null時,它們才相等。 - 如果委派有不同的運行時間類型,則永遠不會相等。
- 如果兩個委派實例都有調用清單 (§21.2),則當且僅當它們的調用清單長度相同,且一個調用清單中的每個條目都相等於 (如下所述) ,依序在另一個調用清單中對應的條目,這些實例才相等。
下列規則會控管調用清單項目的相等性:
- 如果兩個調用清單條目都指向相同的靜態方法,那麼這些條目是相等的。
- 如果兩個叫用清單條目都參考同一目標物件上的同一個非靜態方法(由參考相等運算子定義),則這些條目是相等的。
- 允許 (但不是必要) 相等的呼叫清單專案,以評估語意上相同的匿名函式 (§12.21) 產生,具有相同 (可能是空的) 擷取外部變數實例集。
如果運算子多載解析解析為任一委派相等運算子,且這兩個運算元的繫結時間類型都是 §21System.Delegate中所述的委派類型,而不是 ,而且繫結類型運算元類型之間沒有身分轉換,則會發生繫結時間錯誤。
附註:此規則可防止因為參考不同類型的委派實例而無法將非
null值視為相等的比較。 結尾註釋
12.14.10 可為空值類型與空文字之間的相等運算子
== 和 != 運算符允許一個操作數是可為 Null 的實值型別值,而另一個操作數則為 null 常值,即使作業中沒有預先定義或使用者定義的運算符(以未提升或已解除的形式)存在也一樣。
針對其中一個形式的操作
x == null null == x x != null null != x
其中 x 是可為 Null 實值型別的運算式,如果運算子多載解析(§12.4.5)找不到適用的運算子,則會改為從 HasValue的 x 屬性計算結果。 具體來說,前兩個窗體會轉譯為 !x.HasValue,最後兩個窗體會轉譯為 x.HasValue。
12.14.11 元組相等運算子
Tuple 相等運算符會依據語匯順序,成對地套用至 Tuple 操作數中各元素。
如果 x 或 y 運算符的每個操作數 == 和 != 都被分類為元組或具有元組類型的值 (§8.3.11),則該運算符是 元組等式運算符。
如果操作數 e 被分類為元組,則 e1...en 的元素須為對元組表達式中元素表達式求值的結果。 否則,如果 e 是 Tuple 類型的值,則元素應為 t.Item1...t.Itemn,其中 t 是評估 e的結果。
元組等值運算子的操作數 x 和 y 應該具有相同的元數,否則會發生編譯時錯誤。 對於每對元素 xi 和 yi,應套用相同的相等運算符,並產生結果為類型 bool、dynamic,可隱含轉換為 bool的類型,或是定義了 true 和 false 運算符的類型。
評估元組相等運算子 x == y 時,計算過程如下:
- 左側運算元
x已被評估。 - 將評估右邊運算元
y。 - 針對每一對元素
xi和yi,按照語彙順序:- 先評估運算子
xi == yi,然後以下列方式獲得結果,其類型為bool:- 如果比較產生
bool,則為結果。 - 否則,如果比較產生
dynamic,則會動態調用運算符false,然後將產生的bool值用邏輯否定運算符(!)進行否定。 - 否則,如果比較的類型具有隱含轉換至
bool,則會套用該轉換。 - 否則,如果比較的類型具有運算子
false,則會叫用該運算元,且產生的bool值會與邏輯否定運算符 (!) 否定。
- 如果比較產生
- 如果產生的
bool是false,則不會再進行任何評估,而且元組等號運算符的結果會false。
- 先評估運算子
- 如果所有元素的比較結果為
true,那麼元組相等運算子的結果是true。
評估元組相等運算子 x != y 時,計算過程如下:
- 左側運算元
x已被評估。 - 將評估右邊運算元
y。 - 針對每一對元素
xi和yi,按照語彙順序:- 先評估運算子
xi != yi,然後以下列方式獲得結果,其類型為bool:- 如果比較產生
bool,則為結果。 - 否則,如果比較產生
dynamic,則會動態叫用運算符true,而產生的bool值就是結果。 - 否則,如果比較的類型具有隱含轉換至
bool,則會套用該轉換。 - 否則,如果比較的類型具有運算子
true,則會叫用該運算符,而產生的bool值就是結果。
- 如果比較產生
- 如果產生的
bool是true,則不會再進行任何評估,而且元組等號運算符的結果會true。
- 先評估運算子
- 如果所有元素的比較結果為
false,那麼元組相等運算子的結果是false。
12.14.12 is 運算子
is 運算子有兩種形式。 其中一個是 類型運算子,其右側有類型。 另一個是 is-pattern 運算子,其右側有一個模式。
12.14.12.1 is 類型運算子
is-type 運算子 用來檢查物件的運行時間類型是否與指定的類型相容。 檢查在執行時進行。 作業 E is T的結果,其中 E 是表達式,T 是非 dynamic類型,是一個布林值,指出 E 是否為非 null,且可透過參考轉換、裝箱轉換、拆箱轉換、包裝轉換或解包轉換成功轉換成類型 T。
作業 E is T 的評估如下:
- 如果
E是匿名函式或方法群組,則會發生編譯時期錯誤。 - 如果是可為 Null 的參考類型 (
T),則會發生編譯階段錯誤。 - 如果
E是null的常量,或E的值是null,那麼結果就是false。 - 否則:
- 讓
R成為E的運行時間類型。 - 讓我們
D推導如下R:- 如果
R為可為 Null 的數值型別,D是R的基礎型別。 - 否則,
D是R。
- 如果
- 結果取決於
D和T如下所示:- 如果
T是參考型別,則結果是true,條件如下:- 身分轉換存在於 和
D之間T,或 -
D是參考型別,而且有從D到T的隱含參考轉換,或 -
D是值類型,且從D到 存在T的裝箱轉換。
- 身分轉換存在於 和
- 如果
T是可為 Null 的實值型別,而true是D的基礎類型,那麼結果是T。 - 如果
T是不可為 Null 的實值型別,則如果true和D是相同的類型,則結果會T。 - 否則,結果會是
false。
- 如果
- 讓
is 運算子不會考慮使用者定義的轉換。
附註:由於在執行時評估
is運算符,所有型別參數已經替換完畢,且沒有開放型別(§8.4.3)。 結尾註釋
附注:可以根據編譯時期類型和轉換來理解
is運算符,其中C是E的編譯時期類型。
- 如果
e的編譯時類型與T相同,或存在從編譯時類型 到 的隱含參考轉換(§10.2.8)、裝箱轉換(§10.2.9)、封裝轉換(E),或明確的解除封裝轉換(T):
- 如果
C為不可為 Null 的實值型別,則作業的結果會為true。- 否則,作業的結果相當於評估
E != null。- 否則,如果明確的參考轉換 (§10.3.5) 或取消封裝轉換 (§10.3.7) 存在
C於 ,T或 是C開啟類型 (T),則應執行上述執行階段檢查。- 否則,不可能將
E的引用、拳套化、包裝或拆裝轉換為類型T,作業的結果是false。 編譯程式可以根據編譯時間類型實作優化。結尾註釋
12.14.12.2 is-pattern運算子
is-pattern 運算子 用來檢查表達式 計算的值是否符合給定的模式(§11)。 檢查在執行時進行。 如果值符合模式,則is-pattern 運算子的結果為 true;否則為 false。
形如 E is P的運算式,其中 E 是類型 T 的關係表達式,而 P 是模式,如果下列任一項成立,則為編譯時期錯誤:
模式的每個single_variable_designation都會引入一個新的局部變量,當相應的relational_expression測試時,該變量肯定被分配 (true)。
12.14.13 as 運算子
as 運算子可用來明確將值轉換成指定的參考型別或可為 Null 的實值型別。 不同於強制轉換運算式 (§12.9.8),運算 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)的轉換存在從
E到T。 -
E或T的類型是開放型。 -
E是null文字。
如果 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 } }
T的類型參數G已知為參考型別,因為它具有類別條件約束。 不過,U的類型參數H不是;因此不允許在as中使用H運算符。結束範例
12.15 邏輯運算子
12.15.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.15.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.15.3 列舉邏輯運算子
每個列舉類型 E 隱含地提供下列預先定義的邏輯運算子:
E operator &(E x, E y);
E operator |(E x, E y);
E operator ^(E x, E y);
評估 x «op» y的結果,其中 x 和 y 是具有基礎類型 E之列舉型別 U 表達式,而 «op» 是其中一個邏輯運算符,與評估 (E)((U)x «op» (U)y)完全相同。 換句話說,列舉型別邏輯運算符只會在兩個操作數的基礎類型上執行邏輯運算。
此外,上述未提升之列舉邏輯運算符的提升(§12.4.8)形式也已預先定義。
12.15.4 布林邏輯運算子
預先定義的布林邏輯運算符如下:
bool operator &(bool x, bool y);
bool operator |(bool x, bool y);
bool operator ^(bool x, bool y);
如果 x & y 和 true 都 x,則 y 的結果為 true。 否則,結果會是 false。
x | y 的結果為 true ,如果 x 或 y 是 true。 否則,結果會是 false。
如果 x ^ y 是 true 且 x 是 true,或 y 是 false 且 x 是 false,則 y 的結果為 true。 否則,結果會是 false。 當操作數的類型為 bool時,^ 運算符會計算與 != 運算符相同的結果。
12.15.5 可為空的布林值 & 和 |運營商
可空的布林類型 bool? 可以代表三個值,true、false和 null。
與其他二進位運算子一樣,邏輯運算子 & 和 | (§12.15.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.16 條件邏輯運算子
12.16.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,但只有當y不是x時,才會評估false。 - 作業
x || y對應至作業x | y,但只有當y不是x時,才會評估true。
附注:短路使用「非真」和「非假」條件的原因是讓使用者可以定義條件運算子來決定短路何時應用。 用戶定義型別可能處於
operator true傳回false且operator false傳回false的狀態。 在這些情況下,&&和||都不會短路。 結尾註釋
如果條件式邏輯運算子的操作數具有編譯時間類型 dynamic,則表示式會動態系結 (#\12.3.3)。 在這種情況下,表達式的編譯時類型是 dynamic,而以下所述的解析將會在執行時,使用那些編譯時類型為 dynamic的操作數的執行時類型。
形式 x && y 或 x || y 的操作通過套用多載解析(§12.4.5)來處理,就好像該操作是寫成 x & y 或 x | y。 然後
- 如果多載解析找不到單一最佳運算子,或多載解析選取其中一個預先定義的整數邏輯運算子或可為 Null 的布林邏輯運算子 (§12.15.5),則會發生繫結時間錯誤。
- 否則,如果選取的運算子是其中一個預先定義的布林邏輯運算子 (§12.15.4) ,則會依照 §12.16.2 中所述處理作業。
- 否則,選取的運算子是使用者定義的運算子,而且作業會依 §12.16.3 中所述進行處理。
無法直接多載條件式邏輯運算元。 不過,由於條件邏輯運算子是基於一般邏輯運算子來進行運算,因此一般邏輯運算子的多載(在某些限制下)也被視為條件邏輯運算子的多載。 這在 §12.16.3 中進一步描述。
12.16.2 布林條件邏輯運算子
當 && 或 || 的操作數屬於類型 bool,或操作數不是定義適用 operator & 或 operator |的類型時,但會定義對 bool的隱含轉換,作業會如下所示處理:
- 作業
x && y被評估為x ? y : false。 換句話說,x會先評估並轉換成類型bool。 然後,如果x是true,則會評估y並轉換成類型bool,這會成為作業的結果。 否則,作業的結果為false。 - 作業
x || y被評估為x ? true : y。 換句話說,x會先評估並轉換成類型bool。 然後,如果x是true,則作業的結果會true。 否則,y會評估並轉換成類型bool,這會成為作業的結果。
12.16.3 使用者定義的條件邏輯運算子
當 && 或 || 的運算元是屬於宣告適用的使用者定義 operator & 或 operator |的類型時,則以下兩者皆為真,其中 T 是所選運算符被宣告的類型:
- 選取運算子的傳回型別和每個參數的型別應為
T。 換句話說,運算符應對兩個T類型的操作數進行邏輯 AND 或邏輯 OR 計算,並返回T類型的結果。 -
T應包含operator true和operator false的宣告。
如果不符合上述任一需求,就會發生系結時間錯誤。 否則,會結合使用者定義的 && 或 || 與選取的使用者定義運算符,評估 operator true 或 operator false 作業:
- 作業
x && y會評估為T.false(x) ? x : T.&(x, y),其中T.false(x)是operator false中宣告的T調用,而T.&(x, y)是選取operator &的調用。 換句話說,系統會先評估x,並在結果上叫用operator false,以判斷x是否絕對為 false。 然後,如果x絕對為 false,作業的結果就是先前針對x計算的值。 否則,會評估y,並在先前針對operator &計算的值和針對x計算的值上叫用選取的y,以產生作業的結果。 - 作業
x || y會評估為T.true(x) ? x : T.|(x, y),其中T.true(x)是operator true中宣告的T調用,而T.|(x, y)是選取operator |的調用。 換句話說,系統會先評估x,並在結果上叫用operator true,以判斷x是否絕對正確。 然後,如果x絕對成立,作業的結果就是先前針對x計算的值。 否則,會評估y,並在先前針對operator |計算的值和針對x計算的值上叫用選取的y,以產生作業的結果。
在這些運算中的任一個中,由 x 指定的運算式只會被評估一次,而由 y 指定的運算式要麼不被評估,要麼只被評估一次。
12.17 空合併運算子
?? 運算符稱為 null 聯合運算符。
null_coalescing_expression
: conditional_or_expression
| conditional_or_expression '??' null_coalescing_expression
| throw_expression
;
在 a ?? b的 null 合併運算式中,如果 a 非null,則結果為 a;否則,結果為 b。 當 b 是 a時,作業才會評估 null。
Null 聯合運算子是右關聯運算元,表示作業會從右至左分組。
範例:形如
a ?? b ?? c的運算式計算結果為a ?? (b ?? c)。 一般而言,表單的表達式E1 ?? E2 ?? ... ?? EN會傳回非null的第一個操作數,如果所有操作數都null,則傳回null。 結束範例
表達式 a ?? b 的類型取決於操作數上可用的隱含轉換。 依喜好設定,a ?? b 的類型是 A₀、A或 B,其中 A 是 a 類型(前提是 a 具有類型),B 是 b類型(前提是 b 具有類型),而如果 A₀ 為可為 Null 的實值型別,則 A 是 A 的基礎類型,否則為 A。 具體來說,a ?? b 會依照下列方式進行處理:
- 如果
A存在且 是 Unmanaged 類型 (~8.8) 或已知為不可為 Null 的實值類型,則會發生編譯時期錯誤。 - 否則,如果
A存在且b為動態表示式,則結果類型會dynamic。 在執行時,首先會評估a。 如果a不是null,則a會轉換成dynamic,而這會成為結果。 否則,b會被評估,並成為結果。 - 否則,如果
A存在,而且是可為 Null 的實值型別,而且有從b到A₀的隱含轉換,則結果類型會A₀。 在執行時,首先會評估a。 如果a不是null,那麼a會解包成類型A₀,這將成為結果。 否則,b會評估並轉換成類型A₀,這會成為結果。 - 否則,如果
A存在,且隱含轉換從b到A,則結果類型會A。 在執行時,首先會評估a。 如果a不是null,a會變成結果。 否則,b會評估並轉換成類型A,這會成為結果。 - 否則,如果
A存在且為可為 Null 的實值型別,b具有類型B,而且有從A₀到B的隱含轉換,則結果類型會B。 在執行時,首先會評估a。 如果a不是null,則a將解封裝為類型A₀,並轉換成類型B,作為結果。 否則,b會被評估並成為結果。 - 否則,如果
b具有類型B,且隱含轉換會從a到B,則結果類型會B。 在執行時,首先會評估a。 如果a不是null,a會轉換成類型B,這會成為結果。 否則,b會被評估並成為結果。 - 否則,
a和b不相容,而且會發生編譯時期錯誤。
範例:
T M<T>(T a, T b) => a ?? b; string s = M(null, "text"); int i = M(19, 23);方法
T的類型參數M不受限制。 因此,類型參數可以是參考型別或可為 Null 的值型別,如第一次呼叫M所示。 型別引數也可以是不可為 Null 的實值型別,如第二次呼叫M所示。 當 type 自變數是不可為 Null 的實值型別時,表示式a ?? b的值會是a。結束範例
12.18 throw 運算式運算子
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.19 宣告表達式
宣告表達式會宣告局部變數。
declaration_expression
: local_variable_type identifier
;
local_variable_type
: type
| 'var'
;
如果簡單名稱查閱找不到相關的宣告,則 simple_name_ 也被視為宣告表達式(§12.8.4)。 當作宣告表示式使用時,_ 稱為 簡單捨棄。 其語意相當於 var _,但允許在更多地方使用。
宣告表達式應該只會發生在下列語法內容中:
- 做為
out中的 argument_value。 - 作為包含簡單賦值左側的簡單丟棄
_(§12.23.2)。 - 做為一或多個遞歸巢狀 tuple_expressions 中的 tuple_element,其中最外層包含解構指派的左側。 deconstruction_expression 在此位置產生宣告表達式,儘管宣告表達式在語法上並不存在。
附注:這表示宣告表達式不能加上括弧。 結尾註釋
若在 argument_list 中引用了以 declaration_expression 宣告的隱含型別變數,則會發生錯誤。
宣告了 declaration_expression 的變數在其所在的解構指派中被引用,這是錯誤的。
簡單捨棄的宣告表達式,或其中 local_variable_type 是 var 標識子的情況下,被分類為隱含型別 變數。 運算式沒有類型,而且局部變數的類型會根據語法內容推斷,如下所示:
- 在 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能夠存取已經被引入封閉範圍的i1和b1。
s2的宣告顯示嘗試在巢狀呼叫i2時使用M,但這是不允許的,因為該參考出現在宣告i2的參數列表中。 另一方面,允許在最終參數中引用b2,因為它是在b2宣告的巢狀參數列表結束之後出現的。
s3的宣告顯示了使用隱含和明確型別的捨棄宣告表達式。 因為丟棄不會宣告具名變數,所以允許多次出現_這個識別碼。(int i1, int _, (var i2, var _), _) = (1, 2, (3, 4), 5);這個範例示範在解構賦值中對變數和捨棄符使用隱式和顯式型別的宣告表達式。 當找不到 宣告時,
_var _相當於_。void M1(out int i) { ... } void M2(string _) { M1(out _); // Error: `_` is a string M1(out var _); }此範例示範如何使用
var _,在無法使用_時提供隱含型別的捨棄,因為它會在封入範圍中指定變數。結束範例
12.20 條件運算子
?: 運算符稱為條件運算元。 有時也稱為三元運算符。
conditional_expression
: null_coalescing_expression
| null_coalescing_expression '?' expression ':' expression
| null_coalescing_expression '?' 'ref' variable_reference ':'
'ref' variable_reference
;
表單的條件表示式 b ? x : y 會先評估條件 b。 然後,如果 b 是 true,將評估 x 並將其作為作業結果。 否則,y 會被評估,並成為作業的結果。 條件表示式絕不會同時評估 x 和 y。
條件運算子是右關聯運算元,這表示作業會從右至左分組。
範例:形如
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具有類型X且y類型Y,則- 如果 和 之間
X存在身分識別轉換,則結果是一組運算式的最佳常見類型(Y)。 如果任一類型為dynamic,則類型推斷會更偏好dynamic(§8.7)。 如果任一個類型是 Tuple 類型(§8.3.11),則當兩個 Tuple 中位於相同序數位置的元素名稱相符時,類型推斷會包含元素名稱。 - 否則,如果隱含轉換 (\10.2) 從
X到Y存在,但不是從Y到X,則Y是條件表達式的類型。 - 否則,如果存在從 到
X的隱含列舉轉換(Y),則Y是條件表達式的類型。 - 否則,如果存在從 到
Y的隱含列舉轉換(X),則X是條件表達式的類型。 - 否則,如果隱含轉換 (\10.2) 從
Y到X存在,但不是從X到Y,則X是條件表達式的類型。 - 否則,無法判斷表達式類型,而且會發生編譯時期錯誤。
- 如果 和 之間
- 如果只有其中一個
x和y具有類型,而且x和y都會隱含地轉換成該類型,則為條件表達式的類型。 - 否則,無法判斷表達式類型,而且會發生編譯時期錯誤。
表單的 ref 條件表示式的執行時間處理 b ? ref x : ref y 包含下列步驟:
- 首先,會評估
b,並判斷bool的b值:- 如果存在從
b類型到bool類型的隱含轉換,則會執行此隱含轉換來產生bool值。 - 否則,會叫用
operator true類型所定義的b,以產生bool值。
- 如果存在從
- 如果上述步驟所產生的
bool值是true,則會評估x,而產生的變數參考會成為條件表達式的結果。 - 否則,會評估
y,而產生的變數參考會成為條件表達式的結果。
運行時處理條件運算式的形式 b ? x : y 由以下步驟組成:
- 首先,會評估
b,並判斷bool的b值:- 如果存在從
b類型到bool類型的隱含轉換,則會執行此隱含轉換來產生bool值。 - 否則,會叫用
operator true類型所定義的b,以產生bool值。
- 如果存在從
- 如果上述步驟所產生的
bool值true,則會評估x並轉換成條件表達式的類型,這會成為條件表達式的結果。 - 否則,會評估
y並轉換為條件表達式的類型,這就成為條件表達式的結果。
12.21 匿名函數運算式
12.21.1 一般
匿名函式 是代表「內嵌」方法定義的表達式。 匿名函式本身沒有 值或型別,但可轉換成相容的委派或表達式樹狀結構類型。 匿名函式轉換的評估取決於轉換的目標類型:如果目標類型是委託類型,轉換的結果將評估為參考匿名函式所定義方法的委託值。 如果是表達樹類型,則轉換結果將被評估為一個表達樹,該樹表示方法的結構為對象結構。
附註:基於歷史原因,匿名函式有兩種語法類型,即 lambda_expression和 anonymous_method_expression。 為了幾乎所有的目的,lambda_expression比 anonymous_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 匿名函式是異步函式,並遵循 \15.14 中所述的規則。
匿名函式以 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_expression和 anonymous_method_expression的行為相同,但下列幾點除外:
- anonymous_method_expression允許完全省略參數清單,使其能轉換為任何值參數清單的委派類型。
- lambda_expression允許省略和推斷參數類型,而 anonymous_method_expression需要明確陳述參數類型。
- lambda_expression 的主體可以是表達式或區塊,而 anonymous_method_expression 主體應該是區塊。
- 只有 lambda_expression轉換成相容的表達式樹狀結構類型 ()。
12.21.2 匿名函數簽章
匿名函式 anonymous_function_signature 會定義匿名函式的名稱,並選擇性地定義匿名函式的參數類型。 匿名函式的參數範圍是 anonymous_function_body (§7.7)。 匿名方法主體與參數清單(如果指定)一起構成宣告空間({7.3)。 因此,如果匿名函式的參數名稱與其範圍內的局部變數、局部常數或參數的名稱相同,則會造成編譯時錯誤。這些範圍包括 anonymous_method_expression 或 lambda_expression。
如果匿名函式具有 explicit_anonymous_function_signature,則相容的委派類型和表達式樹狀結構類型集合會限制為具有相同參數類型和修飾詞的相同順序 (\10.7)。 與方法群組轉換 (“”10.8不同,不支援匿名函式參數類型的反變異數。 如果匿名函式沒有 anonymous_function_signature,則相容的委派類型和表達式樹狀目錄類型集合會限制為沒有輸出參數的委派類型。
請注意,anonymous_function_signature 不能包含屬性或參數陣列。 不過,anonymous_function_signature 可能與參數清單包含參數陣列的委派類型相容。
另請注意,即使類型相容,轉換至表達式樹類型在編譯時仍可能會失敗(§8.6)。
12.21.3 匿名函數體
匿名函式的主體(表達式 或 區塊)受限於下列規則:
- 如果匿名函式包含簽章,則本文中會提供簽章中指定的參數。 如果匿名函式沒有簽名,則可以轉換成具有參數的委派類型或運算式類型(§10.7),但無法在函式主體中存取這些參數。
- 除了在最近的封閉匿名函式的簽章中指定的傳址參數(如果有的話)之外,在函式主體中存取傳址參數會導致編譯時期錯誤。
- 除了最接近封入其中的匿名函式的簽名(若有)中指定的參數外,如果函式體嘗試存取
ref struct類型的參數,則會出現編譯時期錯誤。 - 當
this的類型是結構類型時,程式碼存取this會產生編譯時錯誤。 無論存取方式是明確的(如this.x中),還是隱含的(如x中,其中x是該結構的實例成員)。 此規則僅禁止這類存取,不會影響成員查詢是否屬於結構中的成員。 - 本文可以存取匿名函數的外部變數 (§12.21.6)。 外部變數的存取會參考評估 lambda_expression 或 anonymous_method_expression 時作用中的變數實例 (§12.21.7)。
- 若內文包含目標在內文外部或在內文包含的匿名函式的主體內的
goto語句、break語句或continue語句,則會產生編譯時期錯誤。 - 本文中的
return語句會從最接近封入匿名函式的調用傳回控件,而不是從封入函式成員傳回。
尚未明確指定是否有其他方法可執行匿名函式的區塊,除了透過評估及調用 lambda_expression 或 anonymous_method_expression。 特別是,編譯程式可以選擇藉由合成一或多個具名方法或類型來實作匿名函式。 任何這類合成項目的名稱應為保留供編譯程式使用的格式(4.3)。
12.21.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參數,負責從清單項目中擷取要加總的數值。 擷取的值可以是int或double,產生的總和同樣可以是int或double。例如,
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.UnitCount與Func<Detail,int>和Func<Detail,double>兼容。 不過,多載解析會挑選第一個Sum方法,因為轉換為Func<Detail,int>的轉換比轉換成Func<Detail,double>更好。在第二個
orderDetails.Sum調用中,只有第二個Sum方法適用,因為匿名函式d => d.UnitPrice * d.UnitCount會產生類型為double的值。 因此,重載解析會選擇該次呼叫的第二個Sum方法。結束範例
12.21.5 匿名函數和動態綁定
匿名函式不能是動態系結作業的接收者、自變數或操作數。
12.21.6 外部變數
12.21.6.1 一般規定
任何範圍包含
12.21.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結束範例
當匿名函數捕獲局部變數或值參數時,局部變數或參數不再被視為固定變數(§24.4),而是被視為可移動變數。 但是,捕獲的外部變數不能在語句中使用 fixed (§24.7),因此無法獲取捕獲的外部變數的位址。
附注:不同於未擷取的變數,擷取的局部變數可以同時公開至多個執行線程。 結尾註釋
12.21.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.21.7 匿名函數表達式的評估
匿名函式 F 一律會轉換成委派類型 D 或表示式樹狀結構類型 E,直接或透過執行委派建立表達式 new D(F)。 此轉換會決定匿名函數的結果,如 §10.7中所述。
12.21.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.21.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.22 查詢運算式
12.22.1 一般規定
查詢表示式 為類似 SQL 和 XQuery 等關係型和階層式查詢語言的查詢提供語言整合語法。
query_expression
: from_clause query_body
;
from_clause
: 'from' type? identifier 'in' expression
;
query_body
: query_body_clause* select_or_group_clause query_continuation?
;
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 子句開頭,並以 select 或 group 子句結尾。 初始 from 子句之後可以接零個或多個 from、let、where、join 或 orderby 子句。 每個 from 子句都是一個產生器,引入 範圍變數,範圍涵蓋 序列的元素。 每個 let 子句都會引進一個範圍變數,代表以先前範圍變數計算的值。 每個 where 子句都是用來過濾從結果中排除項目的條件。 每個 join 子句都會比較來源序列的指定索引鍵與另一個序列的索引鍵,產生相符的配對。 每個 orderby 子句都會根據指定的準則重新排序專案。最終 select 或 group 子句會以範圍變數來指定結果的形狀。 最後,into 子句可用來「擷取」查詢,方法是將一個查詢的結果視為後續查詢中的產生器。
12.22.2 查詢表達式中的歧義
查詢表達式使用一些內容關鍵詞 ():ascending、by、descending、equals、from、group、into、join、let、on、orderby、select 和 where。
為了避免這些識別符在查詢表達式中作為關鍵字和簡單名稱時產生歧義,除非這些識別符前加上 `@`(§6.4.4),否則它們在任何位置都被視為關鍵字。 針對此目的,查詢表達式是開頭為 “from標識符” 的任何表達式,後面接著 “;”、“=” 或 “,” 以外的任何標記。
12.22.3 查詢表達式翻譯
12.22.3.1 一般規定
C# 語言不會指定查詢表達式的執行語意。 相反地,查詢運算式會轉譯成遵循查詢運算式模式的方法叫用 (§12.22.4) 。 具體而言,查詢表達式會轉譯成名為 Where、Select、SelectMany、Join、GroupJoin、OrderBy、OrderByDescending、ThenBy、ThenByDescending、GroupBy和 Cast的方法調用。 這些方法預期會具有特定的簽章和傳回類型,如 §12.22.4 中所述。 這些方法可能是所查詢之對象的實例方法,或是物件外部的擴充方法。 這些方法會實作查詢的實際執行。
從查詢表達式到方法調用的轉譯是語法對應,會在執行任何類型系結或多載解析之前發生。 在查詢表達式轉譯之後,所產生的方法呼叫會被當作一般的方法呼叫來處理,這可能會進一步發現編譯時錯誤。 這些錯誤狀況包括但不限於不存在的方法、錯誤的型別自變數,以及類型推斷失敗的泛型方法。
查詢表達式會藉由重複套用下列翻譯來處理,直到無法進一步縮減為止。 翻譯將按照應用次序列出:每個區段假定之前各節的翻譯已經完全執行,且一旦某個區段完成,其內容將不會在處理相同查詢表達式時再次被訪問。
查詢表達式中,如果包含對範圍變數的賦值操作,或者將範圍變數作為參考或輸出參數的引數使用,這會在編譯時出錯。
某些翻譯會插入範圍變數,其中包含 透明標識符, 以 *表示。 這些將在 §12.22.3.8 中進一步描述。
12.22.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.22.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.22.3.4 退化查詢運算式
表單的查詢表達式
from «x» in «e» select «x»
已轉譯成
( «e» ) . Select ( «x» => «x» )
範例:此範例
from c in customers select c已轉譯成
(customers).Select(c => c)結束範例
變質的查詢表達式是可簡單選取來源元素的查詢表達式。
附註: 翻譯的後續階段 (§12.22.3.6 和 §12.22.3.7) 會以來源取代其他翻譯步驟所引進的退化查詢,以移除這些查詢。 不過,請務必確保查詢表達式的結果絕不是來源物件本身。 否則,傳回這類查詢的結果可能會不小心將私用數據(例如元素陣列)公開給呼叫端。 因此,此步驟會藉由在來源上明確呼叫
Select,保護直接在原始程式碼中撰寫的變質查詢。 然後,Select和其他查詢運算子的實作者負責確保這些方法永遠不會回傳來源物件本身。 結尾註釋
12.22.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 customers 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 })其中
x和y是編譯程式產生的標識符,否則為看不見且無法存取的標識符。結束範例
orderby 子句及其先前的 from 子句:
from «x» in «e»
orderby «k1» , «k2» , ... , «kn»
...
已轉譯成
from «x» in ( «e» ) .
OrderBy ( «x» => «k1» ) .
ThenBy ( «x» => «k2» ) .
... .
ThenBy ( «x» => «kn» )
...
如果 ordering 子句指定遞減方向指標,則會改為產生 OrderByDescending 或 ThenByDescending 的叫用。
範例:此範例
from o in orders orderby o.Customer.Name, o.Total descending select o具有最終翻譯
(orders) .OrderBy(o => o.Customer.Name) .ThenByDescending(o => o.Total)結束範例
下列翻譯假設每個查詢表達式中沒有任何 let、where、join 或 orderby 子句,而且每個查詢表達式中最多只有一個初始 from 子句。
12.22.3.6 選擇子句
表單的查詢表達式
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.22.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.22.3.8 透明識別碼
某些翻譯會注入範圍變數,其中包含以 表示的s 。 透明標識碼只存在於查詢表達式轉譯程式中做為中繼步驟。
當查詢轉譯插入透明標識符時,進一步的翻譯步驟會將透明標識符傳播至匿名函式和匿名物件初始化表達式。 在這些內容中,透明標識碼具有下列行為:
- 當透明識別符作為匿名函式中的參數出現時,相關匿名類型的成員會自動在匿名函式主體中可用。
- 如果一個包含透明識別碼的成員在範圍內,那麼該成員的所有成員也在範圍內。
- 當透明標識符作為匿名物件初始化器中的成員宣告符時,它會引入一個具有透明標識符的成員。
在上述的翻譯步驟中,透明標識符一律會與匿名類型一起導入,目的是將多個範圍變數擷取為單一對象的成員。 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 })其中
x和y是編譯程式產生的標識符,否則為看不見且無法存取的標識符。 結束範例
12.22.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>之間建議的關聯性,可確保ThenBy和ThenByDescending方法只能在OrderBy或OrderByDescending的結果上使用。 結尾註釋
附注:建議的
GroupBy結果圖形,其中每個內部序列都有額外的Key屬性。 結尾註釋
附註:由於查詢表達式會透過語法對應轉譯為方法調用,因此類型在實作任何或所有查詢表達式模式的方式方面具有相當大的彈性。 例如,模式的方法可以實作為實例方法或擴充方法,因為兩者具有相同的調用語法,而且方法可以要求委派或表達式樹狀結構,因為匿名函式可轉換成兩者。 只實作某些查詢表示式模式的類型僅支持對應至類型所支援方法的查詢表達式翻譯。 結尾註釋
Note:
System.Linq命名空間提供一個查詢運算式模式的實作,適用於任何實作System.Collections.Generic.IEnumerable<T>介面的類型。 結尾註釋
12.23 賦值運算子
12.23.1 一般規定
除了其中一個指派運算符以外,所有運算子都會將新值指派給變數、屬性、事件或索引器元素。 例外狀況 = ref會將變數參考(§9.5)指派給參考變數(§9.7)。
assignment
: unary_expression assignment_operator expression
;
assignment_operator
: '=' 'ref'? | '+=' | '-=' | '*=' | '/=' | '%=' | '&=' | '|=' | '^=' |
'<<=' | '??='
| right_shift_assignment
;
指派的左操作數應該是分類為變數的表達式,或者,除了 = ref、屬性存取、索引器存取、事件存取或 Tuple 之外。 宣告表達式不能直接作為左操作數,但可能會在解構指派的評估過程中出現。
= 運算子稱為 簡單指派運算子。 它會將右操作數的一個或多個值指派給左操作數所指定的變數、屬性、索引器元素或元組元素。 簡單指派運算子的左操作數不應是事件存取器(除非 §15.8.2中另有說明)。 簡單賦值運算子在 §12.23.2 中描述。
運算子 = ref 稱為 ref 指派運算子。 它會讓右操作數成為 variable_reference (),左操作數所指定之參考變數的參考值。 ref 指派運算子在 §12.23.3 中描述。
除了 和 = 運算= ref子之外的賦值運算子稱為複合賦值運算子s。 這些運算子的處理方式如下:
-
??=針對運算符,只有當左作數的值是null時,才會評估右作數,並將結果指派給左作數所指定的變數、屬性或索引器元素。 - 否則,指定的作業會在兩個作數上執行,然後將產生的值指派給左作數所指定的變數、屬性或索引器元素。 複合賦值運算子在 §12.23.4 中描述。
+=具有事件存取運算式作為左運算元的 and -= 運算子稱為事件指派運算子s。 作為左操作數的情況下,事件存取不接受任何其他指派運算符。 事件指派運算子會在 §12.23.5 中說明。
指派運算子是右關聯運算元,這表示作業會從右至左分組。
範例:形如
a = b = c的運算式計算結果為a = (b = c)。 結束範例
12.23.2 簡單分配
= 運算符稱為簡單指派運算符。
如果簡單賦值的左元件是形如 E.P 或 E[Ei],而 E 在編譯時的類型是 dynamic,則該賦值會被動態綁定(§12.3.3)。 在此情況下,指派表達式的編譯時間類型是 dynamic,以下所述的解析過程將會根據執行時期類型 E在執行時期進行。 如果左操作數的格式為 E[Ei],其中至少有一個 Ei 元素的編譯時間類型為 dynamic,且 E 的編譯時間類型不是陣列,則產生的索引器存取會動態繫結,但具有有限的編譯時間檢查(§12.6.5)。
一個將左操作數分類為元組(Tuple)的簡單指派,也稱為 解構賦值。 如果左操作數的任何元組元素具有元素名稱,就會發生編譯時期錯誤。 如果左操作數的任何元組元素是 declaration_expression,而任何其他元素不是 declaration_expression 或簡單丟棄,編譯時會發生錯誤。
簡單賦值的類型 x = y 是對 x 中 y語句的賦值類型,其遞歸決定如下:
- 如果
x是元組表達式(x1, ..., xn),並且y可以解構為具有(y1, ..., yn)元素的元組表達式n(§12.7),則每個對xi的指派yi具有類型Ti,且該指派具有類型(T1, ..., Tn)。 - 否則,如果
x分類為變數,則變數不會readonly、x具有類型T,而且y隱含轉換成T,則指派的類型為T。 - 否則,如果
x分類為隱含型別變數(亦即隱含型別宣告表達式),且y具有類型T,則變數的推斷型別T,且指派具有類型T。 - 否則,如果
x分類為屬性或索引器存取,則屬性或索引器具有可存取的 set 存取子、x具有類型T,且y隱含轉換成T,則指派具有類型T。 - 否則指派作業無效,而且會發生綁定時間錯誤。
執行具有類型 x = y 之表單 T 簡單指派的運行時間處理,會執行指派給具有類型 x的 yT,其中包含下列遞歸步驟:
- 如果尚未評估,則評估
x。 - 如果
x分類為變數,則會評估y,並視需要透過隱含轉換轉換為T(\10.2)。 - 如果
x分類為屬性或索引器存取: - 如果
x被分類為具有元數(x1, ..., xn)的元組n:-
y被n元素解構為元組表示式e。 - 使用隱含元組轉換,將
t轉換成e,以建立結果元組T。 - 針對每個
xi,依左至右的順序進行將xi賦值給t.Itemi的操作,但不會再次評估xi。 -
t會因為指派而產生。
-
注意:如果
x的編譯時期型別是dynamic,且從y的編譯時期型別有隱含轉換到dynamic,則不需要執行時間解析。 結尾註釋
附註:陣列共變數規則(§17.6)允許陣列類型的值
A[]可以是陣列類型B[]的實例參考,前提是從B到A存在隱含參考轉換。 由於這些規則,指派給 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.X、p.Y、r.A和r.B指派,因為p和r是變數。 不過,在範例中Rectangle r = new Rectangle(); r.A.X = 10; r.A.Y = 10; r.B.X = 100; r.B.Y = 100;指派全都無效,因為
r.A和r.B不是變數。結束範例
12.23.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.23.4 複合分配
如果複合指派的左操作數是 E.P 或 E[Ei],而 E 具有編譯時類型 dynamic,則該指派是動態綁定(§12.3.3)。 在此情況下,指派表達式的編譯時間類型是 dynamic,以下所述的解析過程將會根據執行時期類型 E在執行時期進行。 如果左操作數的格式為 E[Ei],其中至少有一個 Ei 元素的編譯時間類型為 dynamic,且 E 的編譯時間類型不是陣列,則產生的索引器存取會動態繫結,但具有有限的編譯時間檢查(§12.6.5)。
a ??= b等於(T) (a ?? (a = b)),除了a只被評估一次;其中,當T的型別是動態型別時,a為b的型別,否則T為a ?? b的型別。
否則,會套用二元運算符多載解析(x «op»= y)來處理形式為 的運算,就像運算寫成 x «op» y 一樣。 然後
- 如果所選的運算子的傳回型別可以隱含轉換為
x的型別,則運算將被評估為x = x «op» y,只是x只會被評估一次。 - 否則,如果選取的運算符是預先定義的運算符,如果所選取運算符的傳回型別明確轉換成
x類型,而且如果y隱含轉換成x類型,或運算符是 shift 運算符,則會將作業評估為x = (T)(x «op» y),其中T是x類型, 不同之處在於x只會評估一次。 - 否則,複合指派無效,而且會發生綁定時錯誤。
「只評估一次」一詞表示,在評估 x «op» y時,會暫時儲存 x 的任何組成表達式結果,然後在執行指派至 x時重複使用。
範例:在工作分派
A()[B()] += C()中,A是傳回int[]的方法,而B和C是傳回int的方法,方法只會依A、B、C的順序叫用一次。 結束範例
當複合指派的左操作數是屬性存取或索引器存取時,屬性或索引器應同時具有 get 存取子和 set 存取子。 如果情況並非如此,則會發生系結時間錯誤。
上述第二個規則允許 x «op»= y 在特定情境中被評估為 x = (T)(x «op» y)。 規則存在,因此當左操作數的類型為 sbyte、byte、short、ushort或 char時,預先定義的運算子就可以當做複合運算符使用。 即使這兩個自變數都是其中一種類型,預先定義的運算符還是會產生類型 int的結果,如 .12.4.7.3中所述。 因此,如果沒有轉換,就無法將結果指派給左操作數。
預先定義運算子的規則的直觀效果就是,若允許x «op»= y和x «op» y,也就允許x = 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» y或x = (T)(x «op» y),因此評估規則隱含地涵蓋提升運算子。 結尾註釋
12.23.5 事件分配
如果 a += or -= 運算子左邊的操作數被分類為事件存取,則該表達式會按如下方式進行評估:
- 評估事件存取的實例表達式,如果有的話。
- 會評估
+=或-=運算子的右運算元,如果需要,會透過隱含轉換轉變成左運算元的類型(§10.2)。 - 事件的存取子會被叫用,其參數清單包含上一個步驟中計算的值。 如果運算子
+=,則會叫用 add 存取子;如果運算子-=,則會叫用 remove 存取子。
事件指派表達式不會產生值。 因此,事件指派表達式只有在 statement_expression 的語境中有效(§13.7)。
12.24 表達式
表示式 是 non_assignment_expression 或 賦值。
expression
: non_assignment_expression
| assignment
;
non_assignment_expression
: declaration_expression
| conditional_expression
| lambda_expression
| query_expression
;
12.25 常數運算式
常數表達式是應在編譯時期完整評估的表達式。
constant_expression
: expression
;
常數表示式應具有值 null 或下列其中一種類型:
-
sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal、bool、string; - 列舉型別;或
- 參考型別的預設值表達式(§12.8.21)。
常數表示式中只允許下列建構:
- 字面值(包括
null字面值)。 - 類別、結構和介面類型成員的
const參考。 - 列舉類型成員的引用。
- 區域常數的引用。
- 括號內的子表達式,其本身是常數表達式。
- 轉換表達式。
-
checked和unchecked表達式。 -
nameof表示式。 - 預先定義的
+、-、!(邏輯否定)和~一元運算符。 - 預先定義的
+、-、*、/、%、<<、>>、&、|、^、&&、||、==、!=、<、>、<=和>=二元運算符。 -
?:條件運算符。 -
!null-forgiving 運算子 (.12.8.9)。 -
sizeof運算式,前提是非受控類型是 §24.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來表示。 在這些內容中,如果無法在編譯時期完全評估表達式,就會發生編譯時期錯誤。
- 常數宣告 (§15.4)
- 列舉成員宣告 (§20.4)
- 參數清單的預設自變數 (•15.6.2)
-
caseswitch語句的標籤 (13.8.3)。 -
goto case陳述 (第13.10.4節) - 包含初始化表達式之陣列建立表達式中的維度長度(~12.8.17.4)。
- 屬性 (§23)
- 在 constant_pattern (§11.2.3)
隱含常數表達式轉換 (10.2.11)允許將類型 int 常數表達式轉換成 sbyte、byte、short、ushort、uint或 ulong,前提是常數表達式的值在目的型別的範圍內。
12.26 布林運算式
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.20) 遵循與 boolean_expression相同的規則,但基於運算子優先順序的原因,被歸類為 null_coalescing_expression。
boolean_expressionE 必須能夠產生類型為 bool的值,如下所示:
- 如果 E 可以隱含轉換成
bool則會在運行時間套用隱含轉換。 - 否則,一元運算符多載解析(§12.4.4)用於在
operator true上尋找唯一的最佳E實作,並在執行時期套用該實作。 - 如果找不到這類運算符,就會發生系結時間錯誤。