12 個表達式
12.1 一般
表達式是運算子和操作數序列。 這個子句會定義語法、操作數和運算子的評估順序,以及表達式的意義。
12.2 運算式分類
12.2.1 一般
運算式的結果會分類為下列其中一項:
- 一個值。 每個值都有相關聯的類型。
- 變數。 除非另有指定,否則變數會明確類型化,而且具有關聯類型,也就是變數的宣告類型。 隱含型別變數沒有相關聯的型別。
- 空字面量。 具有此分類的表達式可以隱含地轉換成參考類型或可為 Null 的實值類型。
- 匿名函式。 具有此分類的表達式可以隱含轉換成相容的委派類型或表達式樹狀結構類型。
- 一個元組。 每個元組都有固定數目的元素,每個元素都有運算式和選用的元素名稱。
- 屬性存取。 每個屬性存取都有相關聯的類型,也就是 屬性的類型。 此外,屬性存取可能會有相關聯的實例表達式。 叫用實例屬性存取的存取子時,評估實例表達式的結果會變成以
this
表示的實例(#.12.8.14)。 - 使用索引器進行存取。 每個索引器存取都有相關聯的類型,也就是索引器的項目類型。 此外,索引器存取具有相關聯的實例表達式和相關聯的自變數清單。 當叫用索引器的存取子時,評估實例表達式的結果會轉變為由
this
代表的實例(§12.8.14),而評估引數清單的結果會轉變為調用的參數清單。 - 無。 當表達式是傳回型別為
void
的方法調用時,就會發生這種情況。 分類為 nothing 的運算式只有在 statement_expression 的上下文中才有效(§13.7)或作為 lambda_expression 的主體(§12.19)。
對於作為較大表達式的子表達式出現的表達式,在已指出的限制下,結果也可以分類為以下之一:
- 命名空間。 具有此分類的表達式只能顯示為 member_access 的左側(.12.8.7)。 在任何其他情境中,分類為命名空間的表達式會導致編譯時錯誤。
- 型別。 具有此分類的表達式只能出現在 member_access 的左邊(§12.8.7)。 在任何其他情境下,被分類為類型的表達式會導致編譯時錯誤。
- 方法群組,是由成員查詢所產生的一組重載方法(§12.5)。 方法群組可能會有相關聯的實例表達式和相關聯的類型自變數清單。 當實例方法被調用時,評估實例表達式的結果會成為由
this
代表的實例(§12.8.14)。 允許在 invocation_expression (§12.8.10)或 delegate_creation_expression (§12.8.17.6)中使用方法群組,並且可以隱式轉換成相容的委派類型(§10.8)。 在任何其他情況下,分類為方法群組的表達式會產生編譯時期的錯誤。 - 事件存取。 每個事件存取都有相關聯的類型,也就是事件的型別。 此外,事件存取可能會有相關聯的實例表達式。 事件存取可能會顯示為
+=
和-=
運算子的左操作數(\12.21.5)。 在任何其他情境中,分類為事件存取的表達式會導致編譯時錯誤。 當實例事件存取的存取子被叫用時,評估實例表達式的結果將成為由this
表示的實例(§12.8.14)。 - throw 表達式可以在表達式中用於多種情境來拋出例外狀況。 擲出運算式可能會透過隱含轉換轉為任何類型。
屬性存取或索引器存取一律會藉由執行 get 存取子或 set 存取子的調用,重新分類為值。 特定的存取子是由屬性或索引器存取的上下文所決定:如果存取是作為指派的目標,則會叫用 set 存取子來指派新的值(§12.21.2)。 否則,將調用 get 存取子來獲取當前的值(§12.2.2)。
實例存取子 是實例上的屬性存取、實例上的事件存取或索引器存取。
12.2.2 表達式的值
大部分牽涉到表達式的建構最終都需要表達式來表示 值。 在這種情況下,如果實際表達式代表命名空間、類型、方法群組或什麼都不是,就會發生編譯時錯誤。 不過,如果表達式表示屬性存取、索引器存取或變數,則會隱含取代屬性、索引器或變數的值:
- 變數的值只是變數目前儲存在變數所識別儲存位置的值。 在取得變數值之前,變數必須被賦予明確的初值(§9.4),否則會發生編譯時期錯誤。
- 叫用 屬性的 get 存取子,即可取得屬性存取表達式的值。 如果屬性沒有 get 存取子,就會發生編譯時期錯誤。 否則,會執行函式成員調用 (\12.6.6),而調用的結果會變成屬性存取表達式的值。
- 透過呼叫索引器的 get 存取子,可以取得索引器存取表達式的值。 如果索引器沒有 get 存取子,就會發生編譯時期錯誤。 否則,會使用與索引器存取表達式相關聯的自變數清單執行函式成員調用(•12.6.6),而調用的結果會成為索引器存取表達式的值。
- 元組表達式的值是藉由將隱含的元組轉換(§10.2.13)套用至元組表達式的類型來取得。 取得一個沒有類型的元組運算式的值是一個錯誤。
12.3 靜態和動態系結
12.3.1 一般
綁定 是根據表達式的類型或值(參數、操作數、接收者)來決定操作所涉及的內容的過程。 例如,方法呼叫的系結是根據接收者和自變數的類型來決定。 運算子的系結是根據其操作數的類型來決定。
在 C# 中,作業的系結通常是在編譯階段根據其子表達式的編譯時間類型來決定。 同樣地,如果表達式包含錯誤,則會在編譯時期偵測並報告錯誤。 這種方法稱為靜態系結 。
不過,如果表達式是 動態表達式(亦即,具有類型 dynamic
),這表示任何參與的系結都應該以其運行時間類型為基礎,而不是它在編譯時期擁有的類型。 因此,這類作業的系結會延後,直到程式執行過程中需要執行該作業時。 這稱為 動態系結。
當作業以動態方式系結時,在編譯時幾乎不會或完全不會進行檢查。 相反地,如果執行期間系結失敗,錯誤會在執行期間以例外狀況回報。
C# 中的下列作業受限於系結:
- 成員存取:
e.M
- 方法調用:
e.M(e₁,...,eᵥ)
- 委派調用:
e(e₁,...,eᵥ)
- 元素存取:
e[e₁,...,eᵥ]
- 物件建立:新增的
C(e₁,...,eᵥ)
- 多載一元運算符:
+
、-
、!
(僅限邏輯否定)、~
、++
、--
、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)
中,會使用i
的舊值呼叫 方法F
,然後使用i
的舊值呼叫方法G
,最後會以 i 的新值呼叫方法H
。 這與運算符優先順序無關。 結束範例
某些運算子可以 多載。 運算子多載 (•12.4.3) 允許針對一或兩個操作數屬於使用者定義類別或結構類型的作業指定使用者定義的運算子實作。
12.4.2 運算元優先順序和關聯性
當表達式包含多個運算符時,運算子 優先順序 會控制個別運算符的評估順序。
Note:例如,表達式
x + y * z
會評估為x + (y * z)
,因為*
運算符的優先順序高於二進位+
運算符。 尾注
運算符的優先順序是由其相關聯文法產生式的定義所建立。
附註:例如,additive_expression 是由
+
或-
運算符分隔的 multiplicative_expression序列所組成,因此讓+
和-
運算符的優先順序低於*
、/
和%
運算符。 尾注
附註:下表摘要說明優先等級從最高到最低的所有運算子:
子條款 類別 運算子 §12.8 主要 x.y
x?.y
f(x)
a[x]
a?[x]
x++
x--
x!
new
typeof
default
checked
unchecked
delegate
stackalloc
§12.9 一元 +
-
!x
~
++x
--x
(T)x
await x
§12.10 乘法 *
/
%
§12.10 添加劑 +
-
§12.11 轉變 <<
>>
§12.12 關係型和類型測試 <
>
<=
>=
is
as
§12.12 平等 ==
!=
§12.13 邏輯與運算 &
§12.13 邏輯 XOR ^
§12.13 邏輯或 \|
§12.14 條件式 AND &&
§12.14 條件式 OR \|\|
§12.15 和 §12.16 空合併運算子和拋出表達式 ??
throw x
§12.18 有條件的 ?:
§12.21 和 §12.19 指派和 Lambda 運算式 =
= ref
*=
/=
%=
+=
-=
<<=
>>=
&=
^=
\|=
=>
尾注
當操作數發生在具有相同優先順序的兩個運算符之間時,運算子的 關聯性 會控制執行作業的順序:
- 除了指派運算符和 Null 聯合運算子之外,所有二元運算子都會 左關聯,這表示作業是從左到右執行。
範例:
x + y + z
評估為(x + y) + z
。 結束範例 - 指派運算符、null 聯合運算子和條件運算符 (
?:
) 是 右關聯,這表示作業是從右至左執行。範例:
x = y = z
評估為x = (y = z)
。 結束範例
優先順序和關聯性可以使用括號來控制。
範例:
x + y * z
先將y
乘以z
,然後將結果新增至x
,但(x + y) * z
先新增x
y
,再將結果乘以z
。 結束範例
12.4.3 運算元多載
所有一元和二元運算子都有預先定義的實作。 此外,您可以藉由在類別和結構中包含運算符宣告(§15.10)來引入使用者定義的實作。 使用者定義的運算子實作一律優先於預先定義的運算子實作:只有當不存在任何適用的使用者定義運算子實作時,才會考慮預先定義的運算子實作,如 §12.4.4 和 §12.4.5中所述。
可多載的一元運算符 如下:
+ - !
(僅限邏輯否定) ~ ++ -- true false
附注:雖然
true
和false
不會被明確用於表達式中(因此不會包含在 §12.4.2的優先順序數據表中),但它們在數個表達式上下文中被調用,因此被視為運算符:布爾表達式(§12.24)以及涉及條件運算子的表達式(§12.18)和條件邏輯運算符(§12.14)。 尾注
附註:空值容忍運算子(後置
!
,§12.8.9)是無法多載的運算子。 尾注
可多載的二進位運算子 如下:
+ - * / % & | ^ << >> == != > < <= >=
只有上述運算子可以多載。 特別是,無法多載成員存取、方法調用或 =
、&&
、||
、??
、?:
、=>
、checked
、unchecked
、new
、typeof
、default
、as
和 is
運算符。
當二元運算子多載時,對應的複合指派運算子(如果有的話)也會被隱含多載。
範例:運算子
*
的多載也是運算子*=
的多載。 這會在 §12.21中進一步說明。 結束範例
指派運算符本身 (=)
無法多載。 指派總是將值簡單儲存到變數中(§12.21.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.21 中的個別運算符描述 , 指定運算符的預先定義實作,以及套用至每個運算符的任何其他規則。 這些描述會使用 一元運算子多載解析、二元運算子多載解析、數值升階這些詞彙,以及在下列子章節中找到的提升運算子定義。
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
之間有身份轉換,Y
所提供的運算子«op»Y
的傳回類型與X
所提供的«op»X
相同,且«op»Y
的操作數類型會轉換成對應的操作數類型«op»X
,那麼集合中只有«op»X
發生。
- 如果
- 如果候選使用者定義運算符集合不是空的,則這會成為作業的候選運算元集合。 否則,預先定義的
operator «op»
二元實作及其提升形式,將成為該操作的候選運算子集合。 預先定義的運算子實作會在運算子的描述中說明。 對於預先定義的列舉和委派運算元,唯一考慮的運算符是列舉或委派類型所提供的運算符,該類型是其中一個操作數的系結時間類型。 -
\12.6.4 的多載解析規則會套用至一組候選運算符,以選取自變數清單
(x, y)
的最佳運算符,而這個運算符會成為多載解析程序的結果。 如果多載解析無法選取單一最佳運算符,則會發生系結時間錯誤。
12.4.6 候選用戶定義運算符
假設類型 T
和運算 operator «op»(A)
,其中 «op» 是可多載的運算符,而 A
是參數清單,則由 T
為運算符 «op»(A)
所提供的候選使用者定義運算符集合會依下列方式決定:
- 判斷類型
T₀
。 如果T
是可為 Null 的實值型別,T₀
是其基礎類型;否則,T₀
等於T
。 - 對於
T₀
中的所有operator «op»
宣告,以及這類運算子的所有提升形式,如果至少有一個運算子適用於自變數清單§12.6.4.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
,則增益運算符會產生null
值。 否則,增益運算符會解除包裝操作數、套用基礎運算符,並包裝結果。 - 對於二元運算符
+
、-
、*
、/
、%
、&
、|
、^
、<<
和>>
,如果操作數和結果型別都是不可為 Null 的實值型別,則運算符的增益形式存在。 將單一?
修飾詞新增至每個運算元和結果類型,即可建構提升形式。 如果一或兩個操作數為null
,則提升運算符會產生null
值(例外情況是bool?
類型的&
和|
運算符,如 第12.13.5章所述)。 否則,提升運算符會解包操作數,應用底層運算符,並封裝結果。 - 對於等號運算子
==
和!=
,如果操作數型別都是不可為 Null 的實值型別,而且如果結果型別是bool
,則存在運算子的增益形式。 提升形式是通過在每個運算元類型中添加一個?
修飾詞來構建的。 提昇運算子會將兩個null
值視作相等,且null
值與任何非null
值視作不相等。 如果兩個操作數都是非null
,則提升運算符會解開操作數,並套用基礎運算符來產生bool
結果。 - 對於關係運算子
<
、>
、<=
和>=
,如果操作數的類型都是不可為 Null 的實值型別,並且結果型別是bool
,則該運算子具有提升形式。 提升形式是藉由將單一?
修飾符加入至每個操作數類型來構建。 若其中一個或兩個運算元是null
,則提升運算子會產生值false
。 否則,增益運算符會解除包裝操作數,並套用基礎運算符來產生bool
結果。
12.5 成員查找
12.5.1 一般
成員查找指的是在一個類型的上下文中確定名稱定義的過程。 成員查詢可以作為評估表達式中的simple_name(§12.8.4)或member_access(§12.8.7)的一部分。 如果 simple_name 或 member_access 作為 invocation_expression 的 primary_expression(§12.8.10.2),則成員稱為 調用。
如果成員是方法或事件,或者它是委派類型的常數、欄位或屬性(§20),或類型 dynamic
(§8.2.4),則表示成員 可被調用。
成員查閱不僅會考慮成員的名稱,也會考慮成員擁有的類型參數數目,以及成員是否可存取。 為了進行成員查閱,泛型方法和巢狀泛型型別具有各自宣告中指出的類型參數數目,而所有其他成員都有零個類型參數。
在類型 T
中,名稱 N
具有 K
型別引數的成員查找按以下方式處理:
- 首先,會決定一組名為
N
的可存取成員: - 接下來,如果
K
為零,則會移除宣告包含類型參數的所有巢狀類型。 如果K
不是零,則會移除具有不同類型參數數目的所有成員。 當K
為零時,不會移除具有型別參數的方法,因為型別推斷過程(§12.6.3)可能會推斷型別參數。 - 接下來,如果叫用成員,則會從集合中移除所有不可叫用的成員。
- 接下來,會從集合中移除其他成員隱藏的成員。 對於集合中每個成員
S.M
,其中S
是宣告成員M
的類型,則會套用下列規則:- 如果
M
是常數、欄位、屬性、事件或列舉成員,則會從以S
作為基底類型所宣告的集合中移除所有成員。 - 如果
M
是類型宣告,則會從集合中移除在S
基底類型中宣告的所有非型別成員,並且會移除在S
基底類型中宣告且型別參數數目與M
相同的所有型別宣告。 - 如果
M
是一個方法,那麼在S
的基類型中宣告的所有非方法成員都會從集合中刪除。
- 如果
- 接下來,類別成員所隱藏的介面成員會從集合中移除。 如果
T
是類型參數,而且T
除了object
之外,還有有效的基類,以及非空白的有效介面集(\15.2.5),這個步驟才有效果。 對於集合中每個成員S.M
,其中S
是宣告成員M
的類型,如果S
是object
以外的類別宣告,則會套用下列規則:- 如果
M
是常數、字段、屬性、事件、列舉成員或類型宣告,則介面宣告中的所有成員都會從集合中移除。 - 如果
M
是方法,則會從集合中移除介面宣告中宣告的所有非方法成員,而且所有簽章與介面宣告中宣告M
相同的方法都會從集合中移除。
- 如果
- 最後,移除隱藏成員之後,查找的結果是:
- 如果集合是由不是方法的單一成員所組成,則此成員是查閱的結果。
- 否則,如果集合只包含方法,則這個方法群組就是查閱的結果。
- 否則,查找會出現模糊不清,並且會發生綁定時間錯誤。
在類型參數和介面之外的類型中查詢成員,以及在只允許單一繼承的介面中查詢成員(繼承鏈中的每個介面最多只有一個直接基底介面),查詢規則的效果僅為衍生成員會隱藏具有相同名稱或簽名的基底成員。 這類單一繼承查閱從不會有模糊不清的情況。 多重繼承介面中的成員查閱可能引發的模稜兩可情況,在 §18.4.6中有描述。
附注:此階段僅涉及一種模棱兩可的情況。 如果成員查閱結果在方法群組中,則方法群組的進一步使用可能會因為模棱兩可而失敗,例如,如
和 6.6.2中所述。 尾注!12.
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)
多載解析會套用至 類別或結構中選取最佳方法 F
T
。 如果方法未為static
,就會發生系結時間錯誤。 方法會使用自變數清單(x, y)
叫用。e.F(x, y)
多載解析用於選擇由 e
類型所指定之類別、結構或介面中的最佳方法F
。 如果方法static
,就會發生系結時間錯誤。 使用實例表達式e
和自變數清單(x, y)
叫用 方法。屬性存取 P
屬性 P
的 get 存取子在其包含的類別或結構中被呼叫。 如果P
為僅限寫入,就會發生編譯時期錯誤。 如果P
不是static
,則實例表示式會this
。P = value
包含類別或結構中屬性 P
的 set 存取子會使用自變數清單(value)
叫用。 如果P
是唯讀的,就會發生編譯時錯誤。 如果P
不是static
,則實例表示式會this
。T.P
在類別或結構 T
中,屬性P
的 get 存取子已被呼叫。 如果P
不是static
,或P
為僅限寫入,就會發生編譯時期錯誤。T.P = value
類別或結構 T
中屬性P
的 set 存取子會叫用自變數清單(value)
。 如果P
不是static
,或者P
是唯讀,就會發生編譯時錯誤。e.P
在由 E
型別提供的類別、結構或介面中,由實例表達式e
叫用屬性P
的 get 存取子。 如果P
為static
,或P
為僅限寫入,則會發生系結時間錯誤。e.P = value
實例表達式 P
e
和自變數清單(value)
,叫用類別、結構或介面中E
型別所指定之屬性的 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
調用類別或結構 T
中事件E
的 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 存取子宣告的隱含值參數。
- 針對使用者定義的一元運算元(包括轉換),單一操作數會對應至運算符宣告的單一參數。
- 針對使用者定義的二元運算符,左操作數會對應至第一個參數,而右操作數會對應至運算符宣告的第二個參數。
- 未命名的自變數會在位外命名自變數或對應至參數陣列的具名自變數之後,對應至無參數。
附注:這可防止
M(c: false, valueB);
叫用void M(bool a = true, bool b = true, bool c = true);
。 第一個自變數會用在位置外(自變數用於第一個位置,但名為c
的參數位於第三個位置),因此應該命名下列自變數。 換句話說,只有當名稱和位置指出相同的對應參數時,才允許非結尾的具名參數。 結尾註解
12.6.2.3 自變數清單的運行時間評估
在函式成員調用的執行時處理期間(§12.6.6),參數清單的表達式或變數參考會按照從左到右的順序被評估,如下所示:
如果是 value 自變數,如果參數的傳遞模式為 value
自變數表示式會被評估,並執行對應參數類型的隱含轉換(§10.2)。 產生的值會成為函式成員調用中 value 參數的初始值。
否則,參數的傳遞模式為輸入。 如果引數是變數參考,而且引數的類型與參數類型之間存在識別轉換(§10.2.2),那麼結果的儲存位置將變為函數成員調用中參數所代表的儲存位置。 否則,會使用與對應參數相同的類型來建立儲存位置。 引數表示式會進行評估,並執行對應參數類型的隱含轉換(§10.2)。 產生的值會儲存在該儲存位置內。 該儲存位置是由函式成員調用中的輸入參數表示。
範例:假設有下列宣告和方法呼叫:
static void M1(in int p1) { ... } int i = 10; M1(i); // i is passed as an input argument M1(i + 5); // transformed to a temporary input argument
在
M1(i)
方法呼叫中,i
本身會當做輸入自變數傳遞,因為它分類為變數,且類型與輸入參數相同int
。 在M1(i + 5)
方法呼叫中,會建立一個未命名的int
變數,使用引數的值初始化,然後作為輸入引數進行傳遞。 請參閱 •12.6.4.2 和 #12.6.4.4。結束範例
針對輸入、輸出或參考自變數,會評估變數參考,而產生的儲存位置會成為函式成員調用中 參數所代表的儲存位置。 對於輸入或參考自變數,變數應該在方法呼叫的點上明確指派。 如果變數參考是以輸出參數的形式提供,或是 reference_type的陣列元素,則會執行運行時檢查,以確保陣列的元素類型與參數的類型相同。 如果這項檢查失敗,則會拋出
System.ArrayTypeMismatchException
。
附註:由於陣列共變異(§17.6),因此需要這個運行時檢查。 尾注
範例:在下列程式代碼中
class Test { static void F(ref object x) {...} static void Main() { object[] a = new object[10]; object[] b = new string[10]; F(ref a[0]); // Ok F(ref b[1]); // ArrayTypeMismatchException } }
第二次叫用
F
會導致擲回System.ArrayTypeMismatchException
,因為b
的實際項目類型是string
,而不是object
。結束範例
方法、索引器和實例建構函式可能會將其最右邊的參數宣告為參數陣列(§15.6.2.4)。 這類函式成員會根據適用的情況,以其正常形式或擴充形式來叫用(§12.6.4.2):
- 以一般形式叫用具有參數陣列的函式成員時,為參數陣列指定的引數應該是可隱式轉換成參數陣列類型的單一表達式(第10.2節)。 在此情況下,參數陣列的行為與實值參數類似。
- 當具有參數陣列的函式成員以其擴展形式被調用時,調用時應為參數陣列指定零個或多個位置參數,其中每個參數都是可以隱式轉換為參數陣列元素類型的表達式(§10.2)。 在此情況下,呼叫會建立一個參數陣列類型的實例,其長度對應到自變數的數目,並以給定的自變數值初始化這個陣列實例的元素,然後使用新建立的陣列實例作為實際參數。
參數列表中的表達式一律會依照文本順序被計算。
範例:因此,範例
class Test { static void F(int x, int y = -1, int z = -2) => Console.WriteLine($"x = {x}, y = {y}, z = {z}"); static void Main() { int i = 0; F(i++, i++, i++); F(z: i++, x: i++); } }
會產生輸出
x = 0, y = 1, z = 2 x = 4, y = -1, z = 3
結束範例
當具有參數陣列的函式成員被以至少一個展開參數的形式調用時,其調用處理相當於在展開的參數周圍插入一個具有陣列初始化表達式的陣列建立表達式(§12.8.17.5)。 當參數陣列沒有自變數時,會傳遞空陣列;未指定傳遞的參考是新配置的或現有的空陣列。
範例:給定的宣告
void F(int x, int y, params object[] args);
方法展開格式的下列調用
F(10, 20, 30, 40); F(10, 20, 1, "hello", 3.0);
完全對應至
F(10, 20, new object[] { 30, 40 }); F(10, 20, new object[] { 1, "hello", 3.0 });
結束範例
當自變數從具有對應選擇性參數的函式成員中省略時,會隱含傳遞函式成員宣告的預設自變數。 (這可以涉及建立儲存位置,如上所述。
附注:因為這些一律是常數,因此它們的評估不會影響其餘參數的評估。 尾注
12.6.3 類型推斷
12.6.3.1 一般
在沒有指定類型自變數的情況下呼叫泛型方法時,類型推斷 進程會嘗試推斷呼叫的類型自變數。 型別推斷的存在可讓更方便的語法用於呼叫泛型方法,並允許程式設計人員避免指定多餘的類型資訊。
範例:
class Chooser { static Random rand = new Random(); public static T Choose<T>(T first, T second) => rand.Next(2) == 0 ? first : second; } class A { static void M() { int i = Chooser.Choose(5, 213); // Calls Choose<int> string s = Chooser.Choose("apple", "banana"); // Calls Choose<string> } }
透過類型推斷,型別參數
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.14),以及尋找一組表達式的最佳通用類型(§12.6.3.15)。 尾注
12.6.3.2 第一個階段
針對每個方法自變數 Eᵢ
:
- 如果
是匿名函式, 明確參數類型推斷 (12.6.3.8 ) 會從到 - 否則,如果
具有類型 ,且對應的參數是值參數 ( 15.6.2.2 ),則下限推斷 (%%.12.6.3.10 ) 會從。 - 否則為 如果
具有類型 ,且對應的參數是參考參數( \15.6.2.3.3 ),或輸出參數 (\15.6.2 .3.4 ),然後確切推斷 (12.6.3.9 )從到 。 - 否則為 如果
具有類型 ,且對應的參數是輸入參數( 15.6.2.3.2 ),而是輸入自變數,則 確切推斷 (到 ) 。 - 否則,如果
具有類型 ,且對應的參數是輸入參數( \15.6.2.3.2 ),則下限推斷 (\12.6.3.10 )從。 - 否則,不會針對這個參數進行推斷。
12.6.3.3 第二階段
第二個階段會繼續進行,如下所示:
- 所有
未固定 類型變數不 相依於 (\12.6.3.6 ), 任何都固定 ( .6.3.12 )。 - 如果不存在這類類型變數,則符合下列所有條件的所有 未固定 類型變數
Xᵢ
都會 固定:- 至少有一個類型變數
Xₑ
相依於Xᵢ
-
Xᵢ
有一組非空白的界限
- 至少有一個類型變數
- 如果不存在這類類型變數,但仍 未修正 類型變數,則類型推斷會失敗。
- 否則,如果沒有進一步 未修正 類型變數存在,類型推斷就會成功。
- 否則為 針對具有對應參數類型
的所有自變數 ,其中 輸出類型 (.12.6.3.5 ) 包含未修正 類型變數,但 輸入類型 (.6.3.4 ) 則不會,輸出類型推斷 (12.6.3.7 ), 是從到 進行 。 然後重複第二個階段。
12.6.3.4 輸入類型
如果
12.6.3.5 輸出類型
如果
12.6.3.6 相依性
如果 Xₑ
直接相依於Xᵢ
,或 Xᵢ
直接相依於Xᵥ
,而 Xᵥ
則取決於Xₑ
,則 Xₑ
相依於Xᵢ
。 因此,“取決於”是“直接依賴”的傳遞但不具反射閉包。
12.6.3.7 輸出類型推斷
- 如果
是匿名函式,且具有推斷的傳回型別 ( ),而 是具有傳回型別的委派類型或表達式樹狀結構類型 ,然後 下限推斷 (12.6.3.10 )從到 。 - 否則,如果
是方法群組,而 是具有參數類型 且傳回型別 的委派型別或表達式樹狀結構類型,且具有型別 的重載 解析會產生具有傳回型別 的單一方法,則 下限推斷 會從到 。 - 否則,如果
是類型為 的表達式,則會 從 到 建立 下限推斷。 - 否則,不會進行推斷。
12.6.3.8 明確參數類型推斷
- 如果
是具有參數類型的明確具型別匿名函式, 是具有參數 類型的委派型別或表達式樹狀目錄類型, 則每個 確切推斷 (“”.12.6.3.9 )會從 對應的到 。
12.6.3.9 確切推斷
從 類型
- 如果
V
是其中一個 未固定的Xᵢ
,則會將U
新增至Xᵢ
的確切界限集。 - 否則,將藉由檢查下列任何情況是否適用來判斷
V₁...Vₑ
和U₁...Uₑ
:-
V
是陣列類型V₁[...]
,U
是相同順位的陣列類型U₁[...]
-
V
是類型V₁?
,U
是類型U₁
-
V
是建構的類型C<V₁...Vₑ>
,U
是建構的類型C<U₁...Uₑ>
如果這些情況中的任何一個適用,則會從每個Uᵢ
到對應的Vᵢ
進行 精確推理。
-
- 否則,不會進行推斷。
12.6.3.10 下限推斷
從 類型
- 如果
V
是未固定的Xᵢ
之一,那麼會將U
添加至Xᵢ
的下界集合。 - 否則,如果
V
是類型V₁?
,且U
是類型U₁?
,則會從U₁
到V₁
進行下限推斷。 - 否則,會透過檢查下列任何情況是否適用來判斷
U₁...Uₑ
和V₁...Vₑ
:-
V
是陣列類型V₁[...]
,U
是相同順位的陣列類型U₁[...]
-
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ₑ>
,則推斷取決於C
的i-th
類型參數:- 如果它是共變數,則會建立 下限推斷。
- 如果是反變數,則會進行 上限推斷。
- 如果它是不變的,則會建立 精確的推論。
-
- 否則,不會進行推斷。
12.6.3.11 上限推斷
從 類型
- 如果
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ₑ>
,則推斷取決於C
的i-th
類型參數:- 如果它是共變數,則會建立 上限推斷。
- 如果是反變數,則會進行 下限推斷。
- 如果它是不變的,則會進行 確切的推斷。
-
- 否則,不會進行推斷。
12.6.3.12 修正
具有一組界限的 未固定 類型變數 Xᵢ
固定,如下所示:
- 一組 候選型別
Uₑ
最初是Xᵢ
界限中的所有型別。 - 接著會逐一檢查每個系結
Xᵢ
:針對每個Xᵢ
的確切系結 U,從候選集中移除所有與U
不一致的Uₑ
類型。 對於每個下限U
Xᵢ
Xᵢ
所有類型Uₑ
不會 從候選集移除隱含轉換U
。 對於Xᵢ
的每個上界 U,從候選集合中移除所有類型Uₑ
,這些類型是沒有隱含轉換到U
的 (但不是)。 - 如果在剩餘的候選類型
Uₑ
之中存在一個可以從所有其他候選類型進行隱式轉換的唯一類型V
,則Xᵢ
被固定為V
。 - 否則,類型推斷會失敗。
12.6.3.13 推斷傳回類型
匿名函式推斷出的傳回型別 F
在類型推斷及多載解析過程中會被使用。 推斷的返回類型只能針對已知所有參數類型的匿名函數來確定,可能是因為它們被明確指定,透過匿名函數轉換提供,或在封閉泛型方法調用中的類型推斷過程中推斷。
推斷的有效返回類型,決定如下:
- 如果
F
的主體是具有類型的 表示式,則F
的推斷有效傳回類型是該表示式的類型。 - 如果
F
主體是 區塊,且區塊return
語句中的表達式集合具有最佳常見類型T
(\12.6.3.15),則推斷的有效傳回類型為F
T
。 - 否則,無法針對
F
推斷有效的傳回型別。
推斷的傳回類型,如下所示:
- 如果
F
是異步的,且F
的主體是被歸類為 nothing 的表達式(§12.2),或是沒有任何return
語句中含有表達式的區塊,則推斷的回傳類型為«TaskType»
(§15.15.1)。 - 如果
F
是異步的,並且具有推斷的有效的傳回型別T
,則推斷的傳回型別是«TaskType»<T>»
(15.15.1)。 - 如果
F
為非異步,且具有推斷的有效傳回類型T
,則推斷的傳回類型會T
。 - 否則,無法推斷
F
的傳回型別。
範例:作為涉及匿名函式的類型推斷範例,請考慮在
System.Linq.Enumerable
類別中宣告的Select
擴充方法:namespace System.Linq { public static class Enumerable { public static IEnumerable<TResult> Select<TSource,TResult>( this IEnumerable<TSource> source, Func<TSource,TResult> selector) { foreach (TSource element in source) { yield return selector(element); } } } }
假設
System.Linq
命名空間是使用using namespace
指令匯入,並且類別Customer
具有類型string
的Name
屬性,則Select
方法可以用來選取客戶清單的名稱:List<Customer> customers = GetCustomerList(); IEnumerable<string> names = customers.Select(c => c.Name);
Select
的擴展方法呼叫(§12.8.10.3)是透過將呼叫重寫為靜態方法呼叫來處理:IEnumerable<string> names = Enumerable.Select(customers, c => c.Name);
由於未明確指定類型自變數,因此會使用類型推斷來推斷類型自變數。 首先,客戶的參數與來源參數相關,推斷
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” 與 value 參數相關,推斷
X
為字串類型。 然後,第一個匿名函式的 參數s
會得到推斷的類型string
,而表達式TimeSpan.Parse(s)
與f1
的傳回型別有關,推斷Y
為System.TimeSpan
。 最後,第二個匿名函式t
的參數會得到推斷的類型System.TimeSpan
,而表達式t.TotalHours
與傳回型別f2
相關,推斷Z
為double
。 因此,呼叫的結果的類型為double
。結束範例
12.6.3.14 方法群組轉換的類型推斷
類似於泛型方法的呼叫,當包含泛型方法的方法群組 M
被轉換為指定的委派類型 D
(§10.8)時,也需要應用型別推斷。 給定的方法
Tₑ M<X₁...Xᵥ>(T₁ x₁ ... Tₑ xₑ)
將方法群組 M
指派給委派類型 D
,類型推斷的任務是尋找類型參數 S₁...Sᵥ
,使表達式:
M<S₁...Sᵥ>
會變成與 D
兼容的(§20.2)。
不同於泛型方法呼叫的類型推斷演算法,在此情況下,只有自變數 類型,沒有自變數 表達式。 特別是沒有匿名函式,因此不需要多個推斷階段。
相反地,所有 Xᵢ
的界限,則類型推斷將失敗。 否則,所有 Xᵢ
都會 固定 對應 Sᵢ
,這是類型推斷的結果。
12.6.3.15 尋找一組表達式的最佳常見類型
在某些情況下,必須推斷一組表達式的一般型別。 特別是透過這種方式來確定隱含型別陣列的元素類型,以及具有 區塊 主體的匿名函式的傳回型別。
一組表達式 E₁...Eᵥ
的最佳常見類型取決於如下:
- 引進新的 未固定的 類型變數
X
。 - 針對每個表達式
Ei
執行 輸出類型推斷 (§12.6.3.7),從中推斷到X
。 -
X
固定 (§12.6.3.12),如果可能的話,產生的類型是最好的常見類型。 - 否則推斷會失敗。
附註:直覺上,此推斷相當於呼叫方法
void M<X>(X x₁ ... X xᵥ)
,並將Eᵢ
當做自變數,並推斷X
。 尾注
12.6.4 多載解析
12.6.4.1 一般
多載解析是一種系結時間機制,用於從引數清單和一組候選的函式成員中選取最適合的函式成員來調用。 在 C# 中,多載解析會針對以下不同情境選擇要叫用的函式成員:
- 在 invocation_expression(§12.8.10)中指定的某個方法的調用。
- object_creation_expression 中名為某實例建構函式的調用(§12.8.17.2)。
- 透過 element_access 呼叫索引器存取子(§12.8.12)。
- 在表達式中所參考的預先定義或使用者定義的運算符呼叫(§12.4.4 和 §12.4.5)。
每個上下文都以自己獨特的方式定義候選函式成員集合及其引數清單。 例如,方法調用的候選專案集合不包含標示覆寫的方法(\12.5),如果衍生類別中的任何方法適用,則基類中的方法不是候選專案(\12.8.10.2)。
一旦識別出候選函式成員和自變數清單之後,在所有情況下,最佳函式成員的選取都會相同:
- 首先,候選函式成員集合會縮減為與指定自變數清單相關的函式成員()。 如果這個縮減的集合是空的,就會發生編譯時期錯誤。
- 然後,會找到一組適用候選函式成員中的最佳函式成員。 如果集合只包含一個函式成員,則該函式成員是最佳函式成員。 最佳函式成員是相較於所提供的引數列表,比其他函式成員更優的那一個,前提是依據 §12.6.4.3 和的規則,將每個函式成員與所有其他函式成員進行比較。 如果沒有完全比所有其他函式成員更好的函式成員,則函式成員調用模棱兩可,而且會發生系結時間錯誤。
下列子條款定義字詞 適用函式成員 和 更好的函式成員的確切意義。
12.6.4.2 適用的函式成員
當滿足以下所有條件時,一個函式成員被稱為 適用的函式成員,以參數列表 A
為其依據:
-
A
中的每個自變數都會對應至函式成員宣告中的參數,如 “”12.6.2.2中所述,最多一個自變數對應至每個參數,而沒有自變數對應的任何參數都是選擇性參數。 - 針對
A
中的每個自變數,自變數的參數傳遞模式與對應參數的參數傳遞模式相同,以及
對於包含參數陣列的函式成員,如果函式成員適用於上述規則,則表示其 一般形式適用。 如果包含參數陣列的函式成員不適用於其一般形式,該函式成員可能會改為適用於其 展開的形式:
- 展開形式是透過在函式成員宣告中將參數陣列替換為該陣列元素型別的零或多個值參數,從而使引數清單中的引數數目
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所產生時,才適用實例方法。
- 如果方法群組從 simple_name產生,則只有在 \12.8.14才允許
this
存取時,才適用實例方法。
- 如果方法群組從 simple_name產生,則只有在 \12.8.14才允許
- 當方法群組來自 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ₓ
中的對應參數更好的參數傳遞選擇(§12.6.4.4),而且Mₓ
中沒有任何參數使用比Mᵥ
更好的參數傳遞選擇,Mᵥ
比Mₓ
更好。 - 否則,沒有更好的函式成員。
12.6.4.4 更好的參數傳遞模式
如果兩個重載方法中的對應參數,只在參數傳遞模式上有所不同,且其中一個參數必須是值傳遞模式,則是允許的,如下所示:
public static void M1(int p1) { ... }
public static void M1(in int p1) { ... }
假設 int i = 10;
,根據 .12.6.4.2,呼叫 M1(i)
和 M1(i + 5)
會導致這兩個多載都適用。 在這種情況下,使用值參數傳遞模式的方法是 更好的參數傳遞模式選擇。
附註:輸入、輸出或參考傳遞模式的自變數不需要這類選擇,因為這些自變數只符合完全相同的參數傳遞模式。 尾注
12.6.4.5 更好的運算式轉換
假設隱含轉換 C₁
從表達式 E
轉換為類型 T₁
,還有將表達式 E
轉換為類型 T₂
的隱含轉換 C₂
,若下列條件之一成立,C₁
是 比 C₂
更好的轉換:
-
E
完全符合T₁
,E
與T₂
不完全相符(§12.6.4.6) -
E
完全符合或完全不符合T₁
和T₂
,且T₁
是比T₂
更好的轉換目標(§12.6.4.7) -
E
是方法群組(),T₁
與方法群組中轉換C₁
的單一最佳方法相容(\20.4),T₂
與方法群組中轉換C₂
的單一最佳方法不相容
12.6.4.6 完全匹配的表達式
假設表達式 E
和類型 T
,如果下列其中一項保留,E
完全符合T
:
-
E
的類型是S
,且從S
到T
存在自我轉換 -
E
是匿名函式,T
是委派類型D
或表達式樹狀結構類型Expression<D>
,且下列其中一項保留:- 推斷的傳回型別
X
存在於D
參數列表的內容中E
(##12.6.3.12),且識別轉換存在從X
轉換成D
的傳回型別 -
E
是沒有傳回值的async
lambda 表達式,且D
具有非泛型的«TaskType»
-
E
為非異步,且D
具有傳回類型Y
,或E
為異步,且D
具有傳回類型«TaskType»<Y>
(§15.15.1),且下述條件之一必須成立:-
E
的主體是一個完全符合Y
的表達式 -
E
主體是區塊,其中每個 return 語句都會傳回完全符合Y
的表達式
-
- 推斷的傳回型別
12.6.4.7 更好的轉換目標
假設有兩種類型 T₁
和 T₂
,如果下列其中一項保留,T₁
是 比 T₂
更好的轉換 目標:
- 存在從
T₁
到T₂
的隱含轉換,而且不存在從T₂
到T₁
的隱含轉換 -
T₁
是«TaskType»<S₁>
(§15.15.1),T₂
是«TaskType»<S₂>
,S₁
是比S₂
更好的轉換目標 -
T₁
是«TaskType»<S₁>
(§15.15.1),T₂
是«TaskType»<S₂>
,T₁
比T₂
更專門 -
T₁
是S₁
或S₁?
,其中S₁
是帶正負號整數型別,T₂
是S₂
或S₂?
,其中S₂
是無符號整數型別。 具體說來:-
S₁
是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.3),在編譯時期,接收者中可以使用的索引器集合是已知的。
在這些情況下,對已知函式成員集中的每個成員進行有限的編譯時間檢查,以確定是否能確定它在運行時間絕對不會被叫用。 針對每個函式成員 F
會建構已修改的參數和自變數清單:
- 首先,如果
F
是一個泛型方法,並且提供了型別引數,則將這些引數代入參數清單中的類型參數。 不過,如果未提供型別自變數,則不會發生這類替代。 - 然後,類型為開放的任何參數(亦即包含類型參數;請參閱 §8.4.3)以及其對應的參數將被省略。
若要 F
通過檢查,必須滿足以下所有條件:
-
F
的修改參數列表適用於 第 12.6.4.2 條中修改的自變數清單。 - 修改參數清單中的所有建構型別都滿足其條件約束 (8.4.5)。
- 如果上述步驟中取代了
F
的類型參數,則會滿足其條件約束。 - 如果
F
是靜態方法,則方法群組不應由在編譯時期已知為變數或值的 member_access 產生。 - 如果
F
是實例方法,則不應從接收端在編譯時期已經確定為類型的 member_access 中產生方法群組。
如果沒有候選者通過此測試,就會發生編譯時錯誤。
12.6.6 函式成員調用
12.6.6.1 一般
此子條款描述在執行時(run-time)調用特定函式成員的過程。 假設繫結時間程序已經決定了要調用的特定成員,這可能是通過將多載決議應用到一組候選函式成員上來實現的。
為了描述調用程式,函式成員分成兩個類別:
- 靜態函式成員。 這些是靜態方法、靜態屬性存取子和使用者定義運算符。 靜態函式成員一律為非虛擬。
- 實例函式成員。 這些是實例方法、實例建構函式、實例屬性存取子和索引器存取子。 實例函式成員為非虛擬或虛擬,且一律在特定實例上叫用。 實例是透過實例運算式計算出來的,並在函式成員中以
this
形式變得可存取 (§12.8.14)。 對於構造函數,實例表達式指的是新分配的物件。
函式成員調用的運行時間處理包含下列步驟,其中 M
是函式成員,如果 M
是實例成員,E
是實例表達式:
如果
M
是靜態函式成員:- 引數列表的評估方式,如 §12.6.2中所述。
-
M
被調用。
否則,如果
E
的類型是實值型別V
,且M
在V
中宣告或覆寫:-
E
已被評估。 如果此評估造成例外狀況,則不會執行任何進一步的步驟。 針對實例建構函式,此評估包含為新物件配置記憶體(通常是從執行堆棧)。 在此情況下,E
分類為變數。 - 如果
E
未分類為變數,或V
不是只讀結構類型(“\16.2.2),E
為下列其中一項:- 輸入參數(.15.6.2.3.2),或
-
readonly
欄位(§15.5.3),或 -
readonly
參考變數或返回(§9.7)
然後會建立
E
類型的暫存局部變數,並將E
的值指派給該變數。 然後,E
被重新定義為該暫存局部變數的參考。 暫存變數可以存取為M
內的this
,但無法以任何其他方式存取。 因此,只有在可以寫入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
所參考的物件會成為這個 所參考的物件。
-
實例建構函式呼叫 (§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
: primary_no_array_creation_expression
| array_creation_expression
;
primary_no_array_creation_expression
: literal
| interpolated_string_expression
| simple_name
| parenthesized_expression
| tuple_expression
| member_access
| null_conditional_member_access
| invocation_expression
| element_access
| null_conditional_element_access
| this_access
| base_access
| post_increment_expression
| post_decrement_expression
| null_forgiving_expression
| object_creation_expression
| delegate_creation_expression
| anonymous_object_creation_expression
| typeof_expression
| sizeof_expression
| checked_expression
| unchecked_expression
| default_value_expression
| nameof_expression
| anonymous_method_expression
| pointer_member_access // unsafe code support
| pointer_element_access // unsafe code support
| stackalloc_expression
;
附註:這些文法規則尚未為 ANTLR 準備好,因為它們是一些相互左遞歸規則的一部分(
primary_expression
、primary_no_array_creation_expression
、member_access
、invocation_expression
、element_access
、post_increment_expression
、post_decrement_expression
、null_forgiving_expression
、pointer_member_access
和pointer_element_access
),而 ANTLR 無法處理這些規則。 標準技術可用來轉換文法,以移除相互的左遞歸。 這尚未完成,因為並非所有剖析策略都需要它(例如 LALR 剖析器不會),這樣做會模糊化結構和描述。 尾注
pointer_member_access()和 pointer_element_access(•23.6.4)僅適用於不安全的程式碼(•23)。
主要表達式分為 array_creation_expression和 primary_no_array_creation_expression。 透過這種方式處理 array_creation_expression,而不是將它與其他簡單的表達式形式一起列出,可讓文法不允許容易混淆的代碼,例如
object o = new int[3][1];
否則會解譯為
object o = (new int[3])[1];
12.8.2 字面量
由 常值(6.4.5) 組成的 primary_expression 會分類為值。
12.8.3 插補字串表達式
interpolated_string_expression 包含 $
、$@
或 @$
,後面緊接著 "
字元內的文字。 在引號文字中,有零或多個 插補點, 以 {
和 }
字元分隔,每個插補都會括住 表達式 和選擇性格式規格。
插值字串表示式有兩種形式:一般的(interpolated_regular_string_expression)和逐字的(interpolated_verbatim_string_expression);在語彙上類似,但在語意上則與字串常值的兩種形式(§6.4.5.6)有所不同。
interpolated_string_expression
: interpolated_regular_string_expression
| interpolated_verbatim_string_expression
;
// interpolated regular string expressions
interpolated_regular_string_expression
: Interpolated_Regular_String_Start Interpolated_Regular_String_Mid?
('{' regular_interpolation '}' Interpolated_Regular_String_Mid?)*
Interpolated_Regular_String_End
;
regular_interpolation
: expression (',' interpolation_minimum_width)?
Regular_Interpolation_Format?
;
interpolation_minimum_width
: constant_expression
;
Interpolated_Regular_String_Start
: '$"'
;
// the following three lexical rules are context sensitive, see details below
Interpolated_Regular_String_Mid
: Interpolated_Regular_String_Element+
;
Regular_Interpolation_Format
: ':' Interpolated_Regular_String_Element+
;
Interpolated_Regular_String_End
: '"'
;
fragment Interpolated_Regular_String_Element
: Interpolated_Regular_String_Character
| Simple_Escape_Sequence
| Hexadecimal_Escape_Sequence
| Unicode_Escape_Sequence
| Open_Brace_Escape_Sequence
| Close_Brace_Escape_Sequence
;
fragment Interpolated_Regular_String_Character
// Any character except " (U+0022), \\ (U+005C),
// { (U+007B), } (U+007D), and New_Line_Character.
: ~["\\{}\u000D\u000A\u0085\u2028\u2029]
;
// interpolated verbatim string expressions
interpolated_verbatim_string_expression
: Interpolated_Verbatim_String_Start Interpolated_Verbatim_String_Mid?
('{' verbatim_interpolation '}' Interpolated_Verbatim_String_Mid?)*
Interpolated_Verbatim_String_End
;
verbatim_interpolation
: expression (',' interpolation_minimum_width)?
Verbatim_Interpolation_Format?
;
Interpolated_Verbatim_String_Start
: '$@"'
| '@$"'
;
// the following three lexical rules are context sensitive, see details below
Interpolated_Verbatim_String_Mid
: Interpolated_Verbatim_String_Element+
;
Verbatim_Interpolation_Format
: ':' Interpolated_Verbatim_String_Element+
;
Interpolated_Verbatim_String_End
: '"'
;
fragment Interpolated_Verbatim_String_Element
: Interpolated_Verbatim_String_Character
| Quote_Escape_Sequence
| Open_Brace_Escape_Sequence
| Close_Brace_Escape_Sequence
;
fragment Interpolated_Verbatim_String_Character
: ~["{}] // Any character except " (U+0022), { (U+007B) and } (U+007D)
;
// lexical fragments used by both regular and verbatim interpolated strings
fragment Open_Brace_Escape_Sequence
: '{{'
;
fragment Close_Brace_Escape_Sequence
: '}}'
;
上述所定義的語彙規則中有六個 是上下文敏感的, 如下所示:
規則 | 情境需求 |
---|---|
Interpolated_Regular_String_Mid | 只有在 Interpolated_Regular_String_Start之後、在任何後續的插值之間,以及在對應的 Interpolated_Regular_String_End之前,才會被辨識。 |
Regular_Interpolation_Format | 當只有在一個 regular_interpolation 中且開始的冒號(:)沒有嵌套在任何類型的括號(小括號/大括號/方括號)內時才被識別。 |
Interpolated_Regular_String_End | 只有在 Interpolated_Regular_String_Start 之後才被辨識,而且只有在任何插補令牌都 Interpolated_Regular_String_Mid或可屬於 regular_interpolation的令牌時,才會辨識,包括這類插補中包含的任何 interpolated_regular_string_expression令牌。 |
Interpolated_Verbatim_String_MidVerbatim_Interpolation_FormatInterpolated_Verbatim_String_End | 辨識這三個規則的方式是參照上述對應規則的辨識方式,不同之處在於將每個提及的 一般 文法規則替換為對應的 逐字 規則。 |
附注:上述規則依賴於上下文,因為其定義與語言中其他標記的定義重疊。 尾注
附注:由於內容敏感的語彙規則的原因,上述文法不符合 ANTLR 的要求。 如同其他語匯器產生器,ANTLR 支援內容敏感的語彙規則,例如使用其 語彙模式,但這是實作詳細數據,因此不屬於此規格的一部分。 尾注
interpolated_string_expression 被視為一個值。 如果它立即通過隱式插值字串轉換成 System.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做修改(如果有的話),從而生成要插入到 interpolated_string_expression的最終 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的字元,如果有的話
- 如果對於從
0
到N-1
的每個數字I
N ≥ 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 會參考該局部變數、參數或常數,並分類為變數或值。 - 如果
為零,且 simple_name 出現在泛型方法宣告中,但在其method_declaration 屬性之外,而且如果該宣告包含名稱為 的類型參數,則 simple_name 會參考該類型參數。 - 否則,針對每個實例類型
T
(),從立即封入型別宣告的實例類型開始,並繼續每個封入類別或結構宣告的實例類型(如果有的話):- 如果
e
為零,且T
宣告包含名稱為I
的類型參數,則 simple_name 會參考該類型參數。 - 否則,若在
T
中查找成員I
並使用e
類型參數(§12.5)產生匹配:- 如果
T
是立即封入類別或結構類型的實體類型,而查閱會識別一或多個方法,則結果是具有相關聯實例表示式的方法群組this
。 如果指定了類型自變數清單,則會用於呼叫泛型方法 (\12.8.10.2)。 - 否則,如果
是直接封閉類別或結構類型的實例類型,並且查找識別出了一個實例成員,且該引用發生在實例構造函數、實例方法或實例存取子 的 區塊中,則結果與形式 的成員存取相同( §12.2.1 ;§12.8.7 )。 這隻能在e
為零時發生。 - 否則,結果與
T.I
或T.I<A₁, ..., Aₑ>
類型的成員存取(§12.8.7)相同。
- 如果
- 如果
- 否則,對於每個命名空間
N
,從 simple_name 所在的命名空間開始,然後依序進行每個封閉的命名空間(如果有的話),直到全域命名空間為止,將評估下列步驟,直到找到實體為止:- 如果
e
為零,且I
為N
中的命名空間名稱,則:- 如果 simple_name 發生的位置被
N
的命名空間宣告所包住,且該命名空間宣告包含 extern_alias_directive 或 using_alias_directive,用來將名稱I
與命名空間或類型產生關聯,那麼 simple_name 會變得模棱兩可,並且會發生編譯時錯誤。 - 否則,simple_name 會參考
N
中名為I
的命名空間。
- 如果 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.17)的形式。 - 否則,simple_name 未定義,而且會發生編譯時期錯誤。
12.8.5 括弧表達式
parenthesized_expression 包含以括弧括住的 表示式。
parenthesized_expression
: '(' expression ')'
;
透過評估括號內的 表達式 來評估 括弧表示式。 如果括弧內的 表示式 表示命名空間或類型,則會發生編譯時期錯誤。 否則,括號運算式 的結果就是對內含的 表達式進行評估的結果。
12.8.6 Tuple 運算式
tuple_expression 代表一個元組,由兩個或多個逗號分隔並且可以選擇命名的 運算式組成,這些運算式被括號括住。 deconstruction_expression 是一種包含隱含型別宣告表達式的元組的速記語法。
tuple_expression
: '(' tuple_element (',' tuple_element)+ ')'
| deconstruction_expression
;
tuple_element
: (identifier ':')? expression
;
deconstruction_expression
: 'var' deconstruction_tuple
;
deconstruction_tuple
: '(' deconstruction_element (',' deconstruction_element)+ ')'
;
deconstruction_element
: deconstruction_tuple
| identifier
;
tuple_expression 會分類為元組。
deconstruction_expressionvar (e1, ..., en)
是 tuple_expression(var e1, ..., var en)
的簡寫,並具有相同的行為。 這會以遞歸方式套用至 deconstruction_expression中的任何巢狀 deconstruction_tuple。 因此,每個巢狀於 deconstruction_expression 內的標識符都會導入宣告表達式(§12.17)。 因此,deconstruction_expression 只能出現在簡單賦值的左側。
只有當每個元素表達式 Ei
都具有類型 Ti
時,元組運算式才會具有類型。 此類型應該是與元組運算式具有相同元數的元組類型,其中每個元素都由下列規則指定:
- 如果對應位置中的 Tuple 元素具有名稱
Ni
,則 Tuple 類型元素應為Ti Ni
。 - 否則,如果
Ei
的型態為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.21.2),從而獲得一個元組值。
範例:
(int i, string) t1 = (i: 1, "One"); (long l, string) t2 = (l: 2, null); var t3 = (i: 3, "Three"); // (int i, string) var t4 = (i: 4, null); // Error: no type
在此範例中,這四個 Tuple 運算式都是有效的。 前兩個
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
可視為零。
具有 dynamic
類型的 primary_expression 中的 member_access 會動態系結(§12.3.3)。 在這種情況下,編譯器會將成員存取分類為 dynamic
類型的屬性存取。 下列規則會在執行時以執行時類型來確定 member_access 的意義,而不是使用 primary_expression的編譯時類型。 如果此運行時間分類導致方法群組,則成員存取權應該是 invocation_expression的 primary_expression。
member_access 被評估並被分類如下所示:
- 如果
e
為零,且E
是命名空間,且E
包含名稱為I
的巢狀命名空間,則結果是該命名空間。 - 否則,如果
E
是命名空間,且E
包含具有名稱I
和K
類型參數的可存取類型,則結果就是使用指定型別自變數建構的類型。 - 如果
E
被分類為一種型別,並且E
不是型別參數,並且在具有K
型別參數的E
中對I
的成員查閱(§12.5)會產生匹配項,則E.I
會被評估並分類如下:附注:當這類成員查閱的結果是方法群組,且
K
為零時,方法群組可以包含具有類型參數的方法。 這可讓這類方法被考慮用於類型參數推斷。 尾注- 如果
I
識別類型,則結果就是使用任何指定型別自變數建構的類型。 - 如果
I
識別一或多個方法,則結果會是沒有相關聯實例表達式的方法群組。 - 如果
I
識別靜態屬性,則結果會是沒有相關聯實例表達式的屬性存取。 - 如果
I
識別靜態字段:- 如果欄位是唯讀的,而且參考發生在宣告欄位的類別或結構靜態建構函式之外,則結果為值,也就是
E
中靜態字段的值I
。 - 否則,結果是變數,也就是
E
中的靜態字段I
。
- 如果欄位是唯讀的,而且參考發生在宣告欄位的類別或結構靜態建構函式之外,則結果為值,也就是
- 如果
I
識別靜態事件:- 如果參考發生在宣告事件的類別或結構內,而且宣告事件時沒有 event_accessor_declarations (\15.8.1),則
E.I
會如同I
是靜態字段一樣處理。 - 否則,結果是事件存取,沒有相關聯的實例表達式。
- 如果參考發生在宣告事件的類別或結構內,而且宣告事件時沒有 event_accessor_declarations (\15.8.1),則
- 如果
I
識別常數,則結果為值,也就是該常數的值。 - 如果
I
識別列舉成員,則結果為值,也就是該列舉成員的值。 - 否則,
E.I
是無效的成員參考,而且會發生編譯時期錯誤。
- 如果
- 如果
E
是屬性存取、索引器存取、變數或值,且其類型是T
,並且在T
中,使用K
類型參數對I
進行成員查閱(§12.5)會產生匹配項,則E.I
會被評估並分類如下:- 首先,如果
E
是屬性或索引器存取,則會取得屬性或索引器存取的值(\12.2.2),而 E 會重新分類為值。 - 如果
I
識別出一個或多個方法,那麼結果將是一個方法群組,具有與E
相關聯的實例表示式。 - 如果
I
識別的是一個實例屬性,那麼結果將是一個具有E
相關聯實例表達式的屬性存取,並且它的相關聯型別為屬性類型。 如果T
是類別類型,則會從從T
開始尋找的屬性的第一個宣告或覆寫中挑選相關聯的類型,並搜尋其基類。 - 如果
T
是 class_type,且I
識別該 class_type的實例字段:- 如果
E
的值是null
,則會拋出System.NullReferenceException
。 - 否則,如果欄位是唯讀的,而且參考發生在宣告欄位之類別的實例建構函式之外,則結果為值,也就是
E
所參考之 物件中I
域的值。 - 否則,結果是變數,也就是
E
所參考之物件中的欄位I
。
- 如果
- 如果
T
是 struct_type,且I
識別該 struct_type的實例字段:- 如果
E
為值,或者如果欄位是唯讀的,而且參考發生在宣告欄位之結構的實例建構函式之外,則結果為值,也就是由E
所指定之結構實例中的字段值I
。 - 否則,結果是變數,也就是
E
所指定結構實例中的欄位I
。
- 如果
- 如果
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_accesses,其中任一標記都可以由 null_forgiving_operator前置。
null_conditional_member_access
: primary_expression '?' '.' identifier type_argument_list?
(null_forgiving_operator? dependent_access)*
;
dependent_access
: '.' identifier type_argument_list? // member access
| '[' argument_list ']' // element access
| '(' argument_list? ')' // invocation
;
null_conditional_projection_initializer
: primary_expression '?' '.' identifier type_argument_list?
;
E
null_conditional_member_access 表示式的格式為 P?.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
是類型參數,尚未確定是參考型別或非可空值類型,則會發生編譯時錯誤。如果
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_access 的序列 ,則為 true , 作業null_conditional_element_access 。12.8.13。 尾注
null_conditional_projection_initializer 是 null_conditional_member_access 的限制,並具有相同的語意。 它只會在匿名物件建立表達式中以投影初始化表達式的形式發生(\12.8.17.7)。
12.8.9 Null-forgiving(允許空值)表達式
12.8.9.1 一般
null 表示表達式的值、類型、分類(•12.2)和安全內容(16.4.12)是其 primary_expression的值、類型、分類和安全內容。
null_forgiving_expression
: primary_expression null_forgiving_operator
;
null_forgiving_operator
: '!'
;
附註:後置 null 允許運算符和前置邏輯否定運算符(§12.9.4),雖然都用相同的語彙標記(!
)表示,但它們是不同的。 只有後者可以被覆寫(§15.10),空接受運算子的定義是固定的。
尾注
對相同的運算式多次套用空值寬容運算子,即使有括弧干擾,仍然會造成編譯時錯誤。
範例:下列全都無效:
var p = q!!; // error: applying null_forgiving_operator more than once var s = ( ( m(t) ! ) )! // error: null_forgiving_operator applied twice to m(t)
結束範例
本子句的其餘部分及以下同級子句為附帶條件的規範性。
執行靜態 Null 狀態分析的編譯程式 (8.9.5)必須符合下列規格。
空值寬容運算符是一種編譯時期的伪操作,用來協助編譯器的靜態空值狀態分析。 它有兩個用途:一是覆寫編譯器對一個表達式 可能為 null的判斷;二是覆寫編譯器發出與空值性相關的警告。
將 null 容許運算子套用到編譯器的靜態 null 狀態分析不會產生任何警告的運算式時,這不是錯誤。
12.8.9.2 覆寫「可能為空值」判斷
在某些情況下,編譯程式的靜態 Null 狀態分析可能會判斷表達式具有 null 狀態,可能是 null,並在其他資訊指出表達式不能為 Null 時發出診斷警告。 將 null 釋放運算子應用於這類運算式時,會通知編譯器的靜態 null 狀態分析,說明該 null 狀態實際上處於 而非 null,這樣做不僅能防止診斷警告,也可能對任何正在進行的分析提供信息。
範例:請考慮下列事項:
#nullable enable public static void M() { Person? p = Find("John"); // returns Person? if (IsValid(p)) { Console.WriteLine($"Found {p!.Name}"); // p can't be null } } public static bool IsValid(Person? person) => person != null && person.Name != null;
如果
IsValid
傳回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
傳遞類型為string
的變數s
作為Assign
的輸出參數,使用的編譯器發出警告,因為s
不是可為 Null 的變數。 由於Assign
的第二個參數不可為 null,因此使用空值容忍運算符來取消警告。結束範例
條件規範文本的結尾。
12.8.10 呼叫表達式
12.8.10.1 一般
調用運算式 可用來呼叫一個方法。
invocation_expression
: primary_expression '(' argument_list? ')'
;
僅當存在 delegate_type時,primary_expression 才可能是 null_forgiving_expression。
invocation_expression 會動態綁定(§12.3.3)如果至少有下列其中一項成立:
-
primary_expression 具有編譯時間類型
dynamic
。 - 可選的 參數列表 中至少有一個參數具有編譯時類型
dynamic
。
在此情況下,編譯程式會將 invocation_expression 分類為 類型 dynamic
的值。 接著,會在運行時間使用運行時間類型來套用下列規則,而非使用 primary_expression 和具有編譯時間類型 dynamic
的自變數的編譯時間類型,以判斷 invocation_expression 的意義。 如果 primary_expression 並非編譯時期型別 dynamic
,則方法呼叫會進行有限度的編譯時期檢查,如 §12.6.5中所述。
invocation_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.19) 的主體中,才允許將一個表達式歸類為「無」。 否則,會發生系結時間錯誤。
- 否則,如果 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,包含下列步驟:
- 方法調用的候選方法集合已建立。 針對每個與方法群組
M
相關聯的方法F
:- 如果
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.F
優先於D.F
和C.F
。結束範例
12.8.10.4 委派調用
對於委派調用,invocation_expression 的 primary_expression 應該是 delegate_type的值。 此外,將 delegate_type 視為與 delegate_type相同的參數清單的函式成員,delegate_type 應適用於 invocation_expressionargument_list\12.6.4.2。
針對形式 D(A)
的委派調用之執行時處理,其中 D
是 delegate_type 的 primary_expression,而 A
是選擇性的 argument_list,包括以下步驟:
- 評估
D
。 如果此評估造成例外狀況,則不會執行任何進一步的步驟。 - 參數列表
A
被評估。 如果此評估造成例外狀況,則不會執行任何進一步的步驟。 -
D
的值會被檢查是否有效。 如果D
的值是null
,則會擲回System.NullReferenceException
,而且不會執行任何進一步的步驟。 - 否則,
D
是委派實例的參考。 函數成員呼叫(§12.6.6)在委派呼叫清單中的每個可呼叫實體上進行。 對於由實例和實例方法所組成的可呼叫實體,調用的實例是可呼叫實體中包含的實例。
如需沒有參數的多個調用清單的詳細數據,請參閱 •20.6。
12.8.11 空條件呼叫表達式
null_conditional_invocation_expression 是語法上的 null_conditional_member_access(§12.8.8)或 null_conditional_element_access(§12.8.13),其中最後一個 dependent_access 是调用表达式(§12.8.10)。
null_conditional_invocation_expression 發生在 statement_expression(§13.7)、anonymous_function_body(§12.19.1)或 method_body(§15.6.1)。
與語法上對等的 null_conditional_member_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。
E
null_conditional_invocation_expression 表示式的格式為 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_no_array_creation_expression,然後接著「[
」符號,再接著 argument_list,最後是「]
」符號。
argument_list 包含一或多個 自變數,並以逗號分隔。
element_access
: primary_no_array_creation_expression '[' argument_list ']'
;
不允許 element_access 的 argument_list 包含 out
或 ref
引數。
如果滿足下列其中一項,element_access 就會動態綁定(§12.3.3):
-
primary_no_array_creation_expression 的編譯時間類型是
dynamic
。 -
argument_list 中至少有一個運算式具有編譯時期類型
dynamic
,而 primary_no_array_creation_expression 並非陣列類型。
在此情況下,編譯程式會將 element_access 分類為 類型 dynamic
的值。 接著,將套用以下規則來判斷 element_access 的意義,這些規則在運行時會使用運行時類型,而非 primary_no_array_creation_expression 和 argument_list 運算式的編譯時間類型,這些運算式具有編譯時間類型 dynamic
。 如果 primary_no_array_creation_expression 沒有編譯時類型 dynamic
,則元素存取只會進行有限的編譯時檢查,如 §12.6.5中所述。
如果 element_access 的 primary_no_array_creation_expression 是 array_type的值,則 element_access 是陣列存取權(\12.8.12.2)。 否則,primary_no_array_creation_expression 應該是具有一或多個索引器成員之類別、結構或介面類型的變數或值,在此情況下,element_access 是索引器存取權(.12.8.12.3)。
12.8.12.2 陣組存取
針對陣列存取,primary_no_array_creation_expression 在 element_access 中應該是 array_type的一個值。 此外,陣列存取的 argument_list 不允許包含具名參數。
argument_list 中的表達式數目應與 array_type的等級相同,而且每個表達式的類型應為 int
、uint
、long
或 ulong,
,或可隱含轉換成其中一或多個類型。
評估陣列存取的結果是陣列元素類型的變數,即由 argument_list中表達式的值所選取的陣列元素。
形式 P[A]
的陣列存取在執行期間的處理,其中 P
是 array_type 的 primary_no_array_creation_expression,而 A
是 argument_list,包含以下步驟:
-
P
被評估。 如果此評估造成例外狀況,則不會執行任何進一步的步驟。 -
argument_list 的索引表達式會依左至右的順序進行評估。 在評估每個索引表示式之後,會執行下列其中一種類型的隱含轉換(\10.2:
int
、uint
、long
、ulong
。 在此清單中,選擇具有隱含轉換的第一個類型。 例如,如果索引表達式的類型為short
,則會執行隱含轉換至int
,因為可能會將隱含轉換從short
轉換成int
,以及從short
轉換成long
。 如果評估索引表達式或後續的隱含轉換會造成例外狀況,則不會評估任何進一步的索引表達式,也不會執行任何進一步的步驟。 -
P
的值會被檢查是否有效。 如果P
的值是null
,則會擲回System.NullReferenceException
,而且不會執行任何進一步的步驟。 -
argument_list 中的每個運算式值都會根據
P
所參考之陣列實例的每個維度的實際界限進行檢查。 如果一或多個值超出範圍,則會擲回System.IndexOutOfRangeException
,而且不會執行任何進一步的步驟。 - 計算由索引表達式指定的陣列元素位置,該位置將成為陣列存取的結果。
12.8.12.3 索引器存取
對於索引器存取,element_access 的 primary_no_array_creation_expression 應該是類別、結構或介面類型的變數或值,而此類型應實作一或多個適用於 element_access之 argument_list 的索引器。
P[A]
形式的索引器存取的綁定時間處理,其中,P
是類別、結構或介面類型 T
的 primary_no_array_creation_expression,而 A
是 argument_list,包含以下步驟:
- 建構
T
所提供的索引器集合。 此集合包含所有在T
或其基底類型T
中宣告、且非覆寫宣告的索引器,並且可以在目前上下文中存取(§7.5)。 - 此集合會縮減為適用且未由其他索引器隱藏的索引器。 下列規則會套用至集合中的每個索引器
S.I
,其中S
是宣告索引器I
的類型: - 如果產生的候選索引器集是空的,則沒有任何適用的索引器存在,而且會發生系結時間錯誤。
- 使用 §12.6.4的多載解析規則來識別一組候選索引器中的最佳索引器。 如果無法識別單一最佳索引器,索引器存取就會模棱兩可,而且會發生系結時間錯誤。
-
argument_list 的索引表達式會依左至右的順序進行評估。 處理索引器存取的結果是分類為索引器存取的表達式。 索引器存取表達式會參考在上述步驟中判斷的索引器,並具有
P
的關聯實例表達式和A
的相關自變數清單,以及索引器類型的關聯型別。 如果T
是類別類型,則會從從T
開始並搜尋其基類時找到的第一個宣告或索引器覆寫中挑選相關聯的類型。
根據所使用的內容,索引器存取會導致叫用 get 存取子或索引器的 set 存取子。 如果索引器存取是指派的目標,則會調用 set 存取子來設定新的值(§12.21.2)。 在其他所有情況下,會叫用 get 存取子來取得目前的值(§12.2.2)。
12.8.13 Null條件式元素存取
null_conditional_element_access 包含一個 primary_no_array_creation_expression,後面接著兩個標記“?
”和“[
”,再接著 argument_list,然後是“]
”標記,接著可以有零個或多個 dependent_access,其中每個都可以用 null_forgiving_operator作為前置運算符。
null_conditional_element_access
: primary_no_array_creation_expression '?' '[' argument_list ']'
(null_forgiving_operator? dependent_access)*
;
null_conditional_element_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
的類型是可為空的值型別:讓
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)。
在上述情境之外使用 this
primary_expression 是編譯時錯誤。 特別是,無法在靜態方法、靜態屬性存取子或欄位宣告的 變數初始化器 中參考 this
。
12.8.15 基本存取
base_access 包含關鍵詞基底,後面接著 「.
」 token 和標識碼和選擇性 type_argument_list 或以方括弧括住的 argument_list:
base_access
: 'base' '.' identifier type_argument_list?
| 'base' '[' argument_list ']'
;
base_access 可用來存取目前類別或結構中類似具名成員所隱藏的基類成員。 只有在實例建構函式、實例方法、實例存取器(§12.2.1)或終結器的主體內,才允許 base_access。 當 base.I
出現在類別或結構中時,我將用來表示該類別或結構的基類成員。 同樣地,當 base[E]
發生在類別中時,適用的索引器應該存在於基類中。
在綁定時,base_access 表達式形式 base.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
相關的函式成員的最衍生實作來確定的(§15.6.4),而不是根據 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
的數值成為此操作的結果。
- 實例表達式(如果
++
和 --
運算符也支援前置表示法 (§12.9.6)。 x
本身在作業之後都有相同的值。
您可以使用後置或前置表達法來調用運算子 ++
或 --
的實作。 這兩個表示法不可能有個別的運算符實作。
12.8.17 新運算符
12.8.17.1 一般
new
運算子可用來建立型別的新實例。
新運算式有三種形式:
- 物件建立表達式和匿名物件建立表達式可用來建立類別類型和實值型別的新實例。
- 陣列建立運算式用於創建陣列類型的新實例。
- 委派創建表達式可用來獲得委派類型的實例。
new
運算符意味著創建一個類型的實例,但不一定意味著分配記憶體。 特別是,實值型別的實例不需要超出其所在變數的任何額外記憶體,並且當使用 new
建立實值型別的實例時,不會發生任何記憶體配置。
附注:委派建立表達式不一定會建立新的實例。 當表達式以與方法群組轉換相同的方式處理時(“\10.8)或匿名函式轉換(\10.7),可能會導致重複使用現有的委派實例。 尾注
12.8.17.2 物件建立表達式
object_creation_expression 可用來建立 class_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.3)或集合初始化器(§12.8.17.4)。
如果選擇性 argument_list 中的任何自變數具有編譯時間類型 dynamic
,則 object_creation_expression 會動態系結 (\12.3.3),而且下列規則會在運行時間使用具有編譯時間類型之 argument_list 的運行時間類型套用 dynamic
。 不過,物件建立會經過有限的編譯時間檢查,如 §12.6.5中所述。
新 T(A)
窗體 object_creation_expression 的系結時間處理,其中 T
是 class_type,或 value_type,而 A
是選擇性的 argument_list,包含下列步驟:
- 如果
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
指定任何實值型別條件約束或建構函式條件約束 (\15.2.5),則會發生系結時間錯誤。 - object_creation_expression 的結果是類型參數已繫結至的運行時類型的值,也就是調用該類型的預設建構函式的結果。 運行時間類型可以是參考型別或實值型別。
- 如果沒有針對
- 否則,如果
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)
物件創建表達式 object_creation_expression 的執行時處理,其中 T
是 class_type 或 struct_type,而 A
是可選的 argument_list,包含以下步驟:
- 如果
T
是 class_type: - 如果
T
是 struct_type:-
T
類型的實例是藉由配置暫存局部變數來建立。 由於 struct_type 的實例建構函式必須明確將值指派給所建立實例的每個欄位,因此不需要初始化暫存變數。 - 實例建構函式會根據函式成員呼叫的規則來調用(§12.6.6)。 新配置的實例參考會自動傳遞至實例建構函式,而且實例可以從該建構函式中存取,如下所示。
-
12.8.17.3 物件初始化器
物件初始化表達式 指定物件零個或多個欄位、屬性或索引元素的值。
object_initializer
: '{' member_initializer_list? '}'
| '{' member_initializer_list ',' '}'
;
member_initializer_list
: member_initializer (',' member_initializer)*
;
member_initializer
: initializer_target '=' initializer_value
;
initializer_target
: identifier
| '[' argument_list ']'
;
initializer_value
: expression
| object_or_collection_initializer
;
物件初始化表達式是由成員初始化表達式序列所組成,由 {
和 }
標記括住,並以逗號分隔。 每個 member_initializer 應指定初始化的目標。
識別子 應為要初始化之物件的可存取字段或屬性命名,而以方括弧括住的 argument_list 應指定初始化物件上可存取索引器自變數。 若物件初始化包含超過一個對於相同欄位或屬性的成員初始化,則會出現錯誤。
附註:雖然物件初始化表達式不允許多次設定相同的欄位或屬性,但索引器沒有這類限制。 物件初始化表達式可能包含參考索引器的多個初始化表達式目標,甚至可能多次使用相同的索引器自變數。 尾注
每個 initializer_target 後跟著等號,然後是表達式、物件初始化表達式或集合初始化表達式之一。 物件初始化器中的表達式無法引用正在初始化的新建物件。
"指定在等號後的表達式的成員初始化器,其處理方式與對目標的指派(§12.21.2)相同。"
在等號之後指定物件初始化表達式的成員初始化表達式是 巢狀物件初始化表達式,也就是內嵌物件的初始化。 巢狀物件初始化表達式中的賦值操作被視為欄位或屬性成員的賦值,而不是將新值直接賦予給欄位或屬性。 巢狀物件初始化表達式無法套用至具有實值類型的屬性,或套用至具有實值類型的唯讀字段。
成員初始化器在等號後指定集合初始化器時,就是在初始化內嵌集合。 初始化表達式中指定的元素會新增至目標所參考的集合,而不是將一個新的集合指派至目標欄位、屬性或索引器。 目標應為符合 .12.8.17.4中所指定需求的集合類型。
當初始化表達式目標參考索引器時,索引器的參數應該只評估一次。 因此,即使自變數最終永遠不會被使用(例如,因為空的巢狀初始化表達式),它們也會評估其副作用。
範例:下列類別代表具有兩個座標的點:
public class Point { public int X { get; set; } public int Y { get; set; } }
您可以建立和初始化
Point
實例,如下所示:Point a = new Point { X = 0, Y = 1 };
這具有與...相同的效果
Point __a = new Point(); __a.X = 0; __a.Y = 1; Point a = __a;
其中
__a
為不可見且無法存取的暫存變數。下列類別顯示從兩個點建立的矩形,以及建立和初始化
Rectangle
實例:public class Rectangle { public Point P1 { get; set; } public Point P2 { get; set; } }
您可以建立和初始化
Rectangle
實例,如下所示:Rectangle r = new Rectangle { P1 = new Point { X = 0, Y = 1 }, P2 = new Point { X = 2, Y = 3 } };
這具有相同的效果
Rectangle __r = new Rectangle(); Point __p1 = new Point(); __p1.X = 0; __p1.Y = 1; __r.P1 = __p1; Point __p2 = new Point(); __p2.X = 2; __p2.Y = 3; __r.P2 = __p2; Rectangle r = __r;
其中
__r
、__p1
和__p2
是暫時變數,否則為看不見且無法存取。如果
Rectangle
的建構函式配置兩個內嵌Point
實例,則可以用來初始化內嵌Point
實例,而不是指派新的實例:public class Rectangle { public Point P1 { get; } = new Point(); public Point P2 { get; } = new Point(); }
下列建構可用來初始化內嵌
Point
實例,而不是指派新的實例:Rectangle r = new Rectangle { P1 = { X = 0, Y = 1 }, P2 = { X = 2, Y = 3 } };
這具有相同的效果
Rectangle __r = new Rectangle(); __r.P1.X = 0; __r.P1.Y = 1; __r.P2.X = 2; __r.P2.Y = 3; Rectangle r = __r;
結束範例
12.8.17.4 集合初始化表達式
集合初始化設定式會指定集合的元素。
collection_initializer
: '{' element_initializer_list '}'
| '{' element_initializer_list ',' '}'
;
element_initializer_list
: element_initializer (',' element_initializer)*
;
element_initializer
: non_assignment_expression
| '{' expression_list '}'
;
expression_list
: expression
| expression_list ',' expression
;
集合初始化表達式是由元素初始化表達式序列所組成,由 {
和 }
標記括住,並以逗號分隔。 每個元素初始化器會指定一個要新增到初始化集合物件中的元素,並包含一個由 {
和 }
標記括住的表達式列表,以逗號分隔。 單一表達式初始化器可以不加上大括號來撰寫,但不能接著是指派表達式,以避免與成員初始化器混淆。
non_assignment_expression 產生式定義於 §12.22。
範例:以下是包含集合初始化表達式的物件建立表達式範例:
List<int> digits = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
結束範例
套用集合初始化表達式的集合物件必須是實作 System.Collections.IEnumerable
的型別,否則會發生編譯時錯誤。 針對每個指定元素依左至右的順序,會套用一般成員查找方法來尋找名為 Add
的成員。 如果成員查找的結果不是方法群組,就會發生編譯時期錯誤。 否則,多載解析會套用專案初始化表達式的表達式清單做為自變數清單,而集合初始化表達式會叫用產生的方法。 因此,集合物件應包含一個適用的實例或擴充方法,其方法名稱為 Add
,對應每個元素初始化器。
範例:下列顯示類別,代表具有名稱和電話號碼清單的聯繫人,以及建立和初始化
List<Contact>
:public class Contact { public string Name { get; set; } public List<string> PhoneNumbers { get; } = new List<string>(); } class A { static void M() { var contacts = new List<Contact> { new Contact { Name = "Chris Smith", PhoneNumbers = { "206-555-0101", "425-882-8080" } }, new Contact { Name = "Bob Harris", PhoneNumbers = { "650-555-0199" } } }; } }
其效果與相同
var __clist = new List<Contact>(); Contact __c1 = new Contact(); __c1.Name = "Chris Smith"; __c1.PhoneNumbers.Add("206-555-0101"); __c1.PhoneNumbers.Add("425-882-8080"); __clist.Add(__c1); Contact __c2 = new Contact(); __c2.Name = "Bob Harris"; __c2.PhoneNumbers.Add("650-555-0199"); __clist.Add(__c2); var contacts = __clist;
其中
__clist
、__c1
和__c2
是暫時變數,否則為看不見且無法存取。結束範例
12.8.17.5 陣列建立表達式
array_creation_expression 用來創建 array_type的新的實例。
array_creation_expression
: 'new' non_array_type '[' expression_list ']' rank_specifier*
array_initializer?
| 'new' array_type array_initializer
| 'new' rank_specifier array_initializer
;
第一種形式的陣列創建表達式會配置一個該型別的陣列實例,該型別來自於假設刪除表達式清單中每個個別運算式後得到的型別。
範例:陣列建立表達式
new int[10,20]
會產生類型為int[,]
的陣列實例,而陣列建立表達式新int[10][,]
會產生類型為int[][,]
的陣列實例。 結束範例
表達式清單中的每個運算式都必須是類型 int
、uint
、long
或 ulong
,或隱式可轉換為其中一或多個類型。 每個表達式的值都會決定新分配的陣列實例中對應維度的長度。 由於陣列維度的長度應該是非負值,所以在表達式清單中具有負值的常數表達式是編譯時期錯誤。
除了不安全的情境(§23.2),陣列的排列未指定。
如果第一種形式的陣列建立運算式包含一個陣列初始化子句,則表示式清單中的每個運算式應該是常數,且表示式清單所指定的階數和維度長度應符合陣列初始化子句中的設置。
在第二或第三種格式的陣列建立運算式中,指定的陣列類型或秩規範的秩必須與陣列初始化器的秩一致。 個別維度長度是從數位初始化表達式的每個對應巢狀層級中的元素數目推斷而來。 因此,下列宣告中的初始化表達式
var a = new int[,] {{0, 1}, {2, 3}, {4, 5}};
完全符合
var a = new int[3, 2] {{0, 1}, {2, 3}, {4, 5}};
第三種形式的陣列建立表示式稱為 隱含型別陣列建立表示式。 它類似於第二種形式,除了未明確指定陣列的元素類型,而是判斷為陣列初始化中運算式的最佳通用類型(§12.6.3.15)。 對於多維度陣列,也就是其中一個 rank_specifier 至少包含一個逗號的陣列,此集合包含巢狀 array_initializers 中找到的所有 表示式。
陣列初始化器會在 第17.7節中進一步說明。
運算陣列生成表達式的結果會被分類為一個值,即新分配的陣列實例的引用。 陣列建立表示式的執行時間處理包含下列步驟:
-
expression_list 的維度長度表達式會依左至右的順序進行評估。 在評估每個表達式之後,會執行其中一種類型的隱含轉換(§10.2):
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.7)結合,以建立匿名型別的資料結構。
範例:
var contacts = new[] { new { Name = "Chris Smith", PhoneNumbers = new[] { "206-555-0101", "425-882-8080" } }, new { Name = "Bob Harris", PhoneNumbers = new[] { "650-555-0199" } } };
結束範例
12.8.17.6 委派建立表達式
delegate_creation_expression 可用來取得 delegate_type的實例。
delegate_creation_expression
: 'new' delegate_type '(' expression ')'
;
委派創建表達式的自變數應該是方法群組、匿名函數,或編譯時間類型 dynamic
或 delegate_type的值。 如果自變數是方法群組,它會識別 方法,而針對實例方法,則會識別要為其建立委派的物件。 如果自變數是匿名函式,它會直接定義委派目標的參數和方法主體。 如果參數是值,它會識別出要建立複本的委派實例。
如果 表達式 具有編譯時類型 dynamic
,則會動態綁定 delegate_creation_expression (12.8.17.6),而下列規則會使用 表達式的執行時類型在執行時套用。 否則,規則會在編譯階段套用。
新 D(E)
表單 delegate_creation_expression 的系結時間處理,其中 D
是 delegate_type,E
是 表示式,包含下列步驟:
如果
E
是方法群組,則委派建立表達式會以與方法群組轉換相同的方式(§10.8)從E
轉換成D
來處理。如果
E
是匿名函式,委派建立表達式會以從E
到D
的匿名函式轉換(§10.7)相同的方式來處理。如果
E
是一個值,則E
應該與D
相容(§20.2),而結果是參考一個新建立的委派,該委派具有調用E
的單一項目調用清單。
新 D(E)
表單 delegate_creation_expression 的運行時間處理,其中 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.17.7 匿名物件建立表達式
anonymous_object_creation_expression 用來建立匿名型別的物件。
anonymous_object_creation_expression
: 'new' anonymous_object_initializer
;
anonymous_object_initializer
: '{' member_declarator_list? '}'
| '{' member_declarator_list ',' '}'
;
member_declarator_list
: member_declarator (',' member_declarator)*
;
member_declarator
: simple_name
| member_access
| null_conditional_projection_initializer
| base_access
| identifier '=' expression
;
匿名物件初始化表達式會宣告匿名型別,並傳回該類型的實例。 匿名類型是直接繼承自 object
的無名稱類別類型。 匿名型別的成員是從用來建立型別實例的匿名物件初始化表達式推斷的只讀屬性序列。 具體而言,表單的匿名物件初始化表達式
new {
p₁=
e₁,
p₂=
e₂,
...
pv=
ev}
宣告表單的匿名類型
class __Anonymous1
{
private readonly «T1» «f1»;
private readonly «T2» «f2»;
...
private readonly «Tn» «fn»;
public __Anonymous1(«T1» «a1», «T2» «a2»,..., «Tn» «an»)
{
«f1» = «a1»;
«f2» = «a2»;
...
«fn» = «an»;
}
public «T1» «p1» { get { return «f1»; } }
public «T2» «p2» { get { return «f2»; } }
...
public «Tn» «pn» { get { return «fn»; } }
public override bool Equals(object __o) { ... }
public override int GetHashCode() { ... }
}
其中每個 «Tx» 都是對應表達式 «ex»的類型。
member_declarator 中使用的運算式應具有型別。 因此,在 member_declarator 中,若表達式為 null
或匿名函式,則會產生編譯期錯誤。
編譯程式會自動產生匿名型別及其 Equals
方法的參數名稱,而且無法在程式文字中參考。
在同一個程式中,兩個匿名物件初始化表達式會以相同順序指定相同名稱和編譯時間類型的屬性序列,將產生相同匿名類型的實例。
範例:在範例中
var p1 = new { Name = "Lawnmower", Price = 495.00 }; var p2 = new { Name = "Shovel", Price = 26.95 }; p1 = p2;
允許最後一行的指派,因為
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»
分別精確等同於以下內容:
«identifer» = «identifier»
、«identifier» = «expr» . «identifier»
和 «identifier» = «expr» ? . «identifier»
因此,在投影初始設定中,標識符會同時選擇要指派的值以及要指派值的欄位或屬性。 以直覺方式,投影初始化表達式不僅會投影值,也會投影值的名稱。
12.8.18 typeof 運算符
typeof
運算子用於取得某類型的 System.Type
物件。
typeof_expression
: 'typeof' '(' type ')'
| 'typeof' '(' unbound_type_name ')'
| 'typeof' '(' 'void' ')'
;
unbound_type_name
: identifier generic_dimension_specifier?
| identifier '::' identifier generic_dimension_specifier?
| unbound_type_name '.' identifier generic_dimension_specifier?
;
generic_dimension_specifier
: '<' comma* '>'
;
comma
: ','
;
第一種 typeof_expression 形式由 typeof
關鍵詞組成,後面接著括號中的類型。 這個表單表達式的結果是所指示型別的 System.Type
物件。 任何指定型別只有一個 System.Type
物件。 這表示對於類型 T
而言,typeof(T) == typeof(T)
一律為 true。 類型不能是 dynamic
。
第二種形式的 typeof_expression 包含 typeof
關鍵詞,後面接著括號 unbound_type_name。
附註:unbound_type_name 與 type_name(§7.8)非常類似,但不同之處在於 unbound_type_name 包含 generic_dimension_specifier,而 type_name 則包含 type_argument_list。 尾注
當 typeof_expression 的操作數是一個同時滿足 unbound_type_name 和 type_name文法的標記序列時,亦即它不包含 generic_dimension_specifier 或 type_argument_list,此序列即被視為 type_name。 unbound_type_name 的意義如下所示:
- 將標記序列轉換成 type_name,方法是將每個 generic_dimension_specifier 取代為具有相同逗號數目的 type_argument_list 和每個 type_argument的關鍵詞
object
。 - 對產生的 type_name進行評估,並忽略所有類型參數限制。
- unbound_type_name 解析為與生成的構造類型相關聯的未綁定的泛型類型(§8.4)。
當類型名稱是可為 Null 的參考型別時,會發生錯誤。
typeof_expression 的結果是所產生未系結泛型類型的 System.Type
物件。
第三種形式的 typeof_expression 包含 typeof
關鍵詞,後面接著括弧化 void
關鍵詞。 這種形式的運算式的結果是 System.Type
對象,其代表不存在的類型。
typeof(void)
傳回的類型對象與針對任何類型傳回的類型物件不同。
附註:這個特殊的
System.Type
對象在語言中允許方法反射的類別庫中很有用,這些方法希望可以用一個System.Type
實例來表示任何方法的返回類型,包括void
方法的返回類型。 尾注
typeof
運算子可用於類型參數。 如果已知類型名稱為可為 Null 的參考型別,則為編譯時間錯誤。 結果是系結至類型參數之執行時類型的 System.Type
物件。 如果執行時類型是可為 Null 的參考型別,結果就是對應的非 Null 參考型別。
typeof
運算子也可以用於建構型別或未系結的泛型型別(§8.4.4)。 未系結泛型類型的 System.Type
對象與實例類型的 System.Type
物件不同(\15.3.2)。 實例類型一律是運行時間的封閉式建構型別,因此其 System.Type
物件相依於使用中的運行時間類型自變數。 另一方面,未系結的泛型型別沒有類型自變數,而且不論運行時間類型自變數為何,都會產生相同的 System.Type
物件。
範例:此範例
class X<T> { public static void PrintTypes() { Type[] t = { typeof(int), typeof(System.Int32), typeof(string), typeof(double[]), typeof(void), typeof(T), typeof(X<T>), typeof(X<X<T>>), typeof(X<>) }; for (int i = 0; i < t.Length; i++) { Console.WriteLine(t[i]); } } } class Test { static void Main() { X<int>.PrintTypes(); } }
會產生下列輸出:
System.Int32 System.Int32 System.String System.Double[] System.Void System.Int32 X`1[System.Int32] X`1[X`1[System.Int32]] X`1[T]
請注意,
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
運算子會在 .23.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)。
下列作業會受到已核取和未核取運算符和語句所建立的溢位檢查內容所影響:
- 當操作數是整數或列舉型別時,預先定義的
++
和--
運算符 (12.8.16 和 12.9.6)。 - 預先定義的
-
一元運算符(§12.9.3),當操作數是整數型別時。 - 當兩個操作數都是整數或列舉型別時,預定義的
+
、-
、*
和/
二元運算符(§12.10)。 - 從一個整數或列舉型別到另一個整數或列舉型別,或從
float
或double
轉換成整數或列舉型別的明確數值轉換(10.3.2)。
當上述其中一個作業產生的結果過大以至無法適應於目的類型時,執行的上下文將控制產生的行為。
- 在
checked
內容中,如果操作是常數運算式(§12.23),則會有一個編譯時錯誤。 否則,在執行時執行作業時,會拋出System.OverflowException
。 - 在
unchecked
環境中,會捨棄不符合目標類型的任何高階位元,以截斷結果。
對於非常數表達式(•12.23)(在運行時間評估的表達式),不會由任何 checked
或 unchecked
運算符或語句所括住,除非外部因素(例如編譯程式參數和執行環境組態)呼叫已檢查的評估,否則預設溢位檢查內容會取消核取。
對於常數表達式 (•12.23)(可在編譯階段完整評估的表達式),一律會檢查預設溢位檢查內容。 除非明確將常數表示式放在 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
不會影響Multiply
中x * y
的評估,因此會在預設溢位檢查內容中評估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
: explictly_typed_default
| default_literal
;
explictly_typed_default
: 'default' '(' type ')'
;
default_literal
: 'default'
;
default_literal 代表預設值(§9.3)。 它沒有類型,但可以透過預設字面值轉換成任何類型(§10.2.16)。
default_value_expression 的結果是 explitly_typed_default中明確類型的預設值(§9.3),或作為 default_value_expression轉換的目標類型。
如果類型為下列其中一項,則 default_value_expression 是常數表達式(•12.23)。
- 參考型別
- 已知為參考型別的類型參數 (§8.2) 。
- 下列其中一個實值類型:
sbyte
、byte
、short
、ushort
、int
、uint
、long
、ulong
、char
、float
、double
、decimal
、bool,
;或 - 任何列舉類型。
12.8.22 堆棧配置
堆疊分配表達式會從執行堆疊分配一個記憶體區塊。 執行堆疊 是儲存局部變數的記憶體區域。 執行堆疊不是受控堆積的一部分。 當目前函式傳回時,用於儲存局部變數的記憶體會被自動回收。
堆疊配置表達式的安全內容規則會在 •16.4.12.7中描述。
stackalloc_expression
: 'stackalloc' unmanaged_type '[' expression ']'
| 'stackalloc' unmanaged_type? '[' constant_expression? ']' stackalloc_initializer
;
stackalloc_initializer
: '{' stackalloc_initializer_element_list '}'
;
stackalloc_initializer_element_list
: stackalloc_element_initializer (',' stackalloc_element_initializer)* ','?
;
stackalloc_element_initializer
: expression
;
unmanaged_type(§8.8)表示將儲存在新配置位置的項目類型,而 表達式 表示這些項目的數量。 結合在一起,這些說明了所需的分配大小。
表示式 的類型應可以隱式地轉換到類型 int
。
由於堆疊配置的大小不可以是負數,因此將項目數目指定為計算為負值的 constant_expression 是編譯時錯誤。
在運行時間,如果要配置的項目數目是負值,則行為未定義。 如果它是零,則不會進行配置,而且傳回的值是實作定義。 如果沒有足夠的記憶體可用來配置項目,則會拋出 System.StackOverflowException
。
當 stackalloc_initializer 存在時:
- 如果省略 unmanaged_type,則會根據 stackalloc_element_initializers 集合的最佳常見類型規則(\12.6.3.15)來推斷。
- 如果省略 constant_expression,則會推斷為 stackalloc_element_initializer的數量。
- 如果 constant_expression 存在,它應等於 stackalloc_element_initializer的數目。
每個 stackalloc_element_initializer 都必須具有可轉換成 unmanaged_type 的隱式轉換(§10.2)。 stackalloc_element_initializer會以遞增順序初始化配置記憶體中的元素,從索引為零的元素開始。 如果沒有 stackalloc_initializer,新配置的記憶體內容就會未定義。
如果 stackalloc_expression 直接作為 local_variable_declaration 的初始化表達式(§13.6.2),其中 local_variable_type 是指針類型(§23.3)或推斷(var
),則 stackalloc_expression 的結果是類型 T*
的指標(§23.9)。 在此情況下,stackalloc_expression 必須出現在不安全的程式代碼中。 否則,stackalloc_expression 的結果是 Span<T>
類型的實例,其中 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
成功,則表達式會視為 invocation_expression,而不論調用是否有效。 否則,它是 nameof_expression。
簡單名稱和成員存取查閱會在編譯時期針對 named_entity 進行,遵循 §12.8.4 和 §12.8.7中所述的規則。 不過,在
這是 named_entity 指定方法群組具有 type_argument_list的編譯時間錯誤。 具有類型 dynamic
的 named_entity_target 是編譯時間錯誤。
nameof_expression 是類型為 string
的常數運算式,而且在執行期間沒有影響。 具體來說,其 named_entity 不會進行評估,且在進行明確指派分析(§9.4.4.22)時會被忽略。 其值是選擇性最終 type_argument_list之前 named_entity 的最後一個標識符,會以下列方式轉換:
- 若使用此「
@
」字首,則將移除。 - 每個 unicode_escape_sequence 都會轉換成其對應的 Unicode 字元。
- 移除任何 formatting_characters。
這些是測試標識符相等性時應用在 §6.4.3 的相同轉換。
範例:下列範例說明各種
nameof
表達式的結果,假設System.Collections.Generic
命名空間內宣告的泛型型別List<T>
:using TestAlias = System.String; class Program { static void Main() { var point = (x: 3, y: 4); string n1 = nameof(System); // "System" string n2 = nameof(System.Collections.Generic); // "Generic" string n3 = nameof(point); // "point" string n4 = nameof(point.x); // "x" string n5 = nameof(Program); // "Program" string n6 = nameof(System.Int32); // "Int32" string n7 = nameof(TestAlias); // "TestAlias" string n8 = nameof(List<int>); // "List" string n9 = nameof(Program.InstanceMethod); // "InstanceMethod" string n10 = nameof(Program.GenericMethod); // "GenericMethod" string n11 = nameof(Program.NestedClass); // "NestedClass" // Invalid // string x1 = nameof(List<>); // Empty type argument list // string x2 = nameof(List<T>); // T is not in scope // string x3 = nameof(GenericMethod<>); // Empty type argument list // string x4 = nameof(GenericMethod<T>); // T is not in scope // string x5 = nameof(int); // Keywords not permitted // Type arguments not permitted for method group // string x6 = nameof(GenericMethod<Program>); } void InstanceMethod() { } void GenericMethod<T>() { string n1 = nameof(List<T>); // "List" string n2 = nameof(T); // "T" } class NestedClass { } }
此範例中可能令人驚訝的部分是將
nameof(System.Collections.Generic)
解析為 「泛型」,而不是完整命名空間,而nameof(TestAlias)
為 “TestAlias” 而非 “String”。 結束範例
12.8.24 匿名方法表達式
anonymous_method_expression 是定義匿名函式的兩種方式之一。 這些會進一步說明於 #.12.19。
12.9 一元運算符
12.9.1 一般
+
、-
、!
(僅針對邏輯否定 §12.9.4)、~
、++
、--
、轉換和 await
運算符稱為一元運算符。
附注:後置 null-forgiving 運算符 (12.8.9),
!
,因為其編譯時間和不可多載的本質,會排除在上述清單中。 尾注
unary_expression
: primary_expression
| '+' unary_expression
| '-' unary_expression
| logical_negation_operator unary_expression
| '~' unary_expression
| pre_increment_expression
| pre_decrement_expression
| cast_expression
| await_expression
| pointer_indirection_expression // unsafe code support
| addressof_expression // unsafe code support
;
pointer_indirection_expression(•23.6.2)和 addressof_expression(•23.6.5)僅適用於不安全的程式代碼(•23)。
如果 unary_expression 的操作數在編譯時具有類型 dynamic
,則會進行動態繫結(§12.3.3)。 在此情況下,unary_expression 的編譯時類型是 dynamic
,而以下表示的解析度將會使用操作數的運行時類型在運行時進行。
12.9.2 一元加號運算子
針對形式為 +x
的運算,一元運算子重載解析(§12.4.4)會被應用來選擇特定的運算符實作。 操作數會轉換成所選取運算子的參數類型,而結果的類型是運算元的傳回類型。 預先定義的一元加運算符如下:
int operator +(int x);
uint operator +(uint x);
long operator +(long x);
ulong operator +(ulong x);
float operator +(float x);
double operator +(double x);
decimal operator +(decimal x);
針對這些運算符,結果只是操作數的值。
此外,上述未提升預先定義一元加號運算符的提升後的(§12.4.8)形式也會被預先定義。
12.9.3 一元減號運算符
對於形如 –x
的運算,會套用一元運算子多載解析(§12.4.4)來選擇特定運算子的實作。 操作數會轉換成所選取運算子的參數類型,而結果的類型是運算元的傳回類型。 預先定義的一元減號運算符如下:
整數否定:
int operator –(int x); long operator –(long x);
結果是透過從零減去
X
計算出來的。 如果X
的值是操作數類型的最小可表示值(針對int
是 −2³¹ 或針對long
是 −2⁶³),則操作數類型的X
數學互補無法在操作數類型內表示。 如果這種情況發生在checked
內容中,則會擲回System.OverflowException
;如果在unchecked
內容中發生,結果就是操作數的值,而且不會報告溢位。如果負運算子的操作數的類型為
uint
,則會轉換成類型long
,且結果的類型為long
。 例外是允許將int
值−2147483648
(−2³¹)寫成十進位整數字面值的規則(§6.4.5.3)。如果負運算子的操作數類型為
ulong
,就會發生編譯時期錯誤。 例外狀況是允許將long
值−9223372036854775808
(−2⁶³) 寫入為十進位整數常值的規則 (§6.4.5.3)浮點否定:
float operator –(float x); double operator –(double x);
結果是將
X
的值的正負號反轉。 如果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
是具有基礎類型 U
的列舉型別 E
表達式,與評估 (E)(~(U)x)
的結果完全一致,唯一的不同在於轉換至 E
總是如同在 unchecked
環境中執行(§12.8.20)。
此外,上述未提升的預先定義位補碼運算符的提升形式(§12.4.8)也已預先定義。
12.9.6 前置式遞增和遞減運算子
pre_increment_expression
: '++' unary_expression
;
pre_decrement_expression
: '--' unary_expression
;
前置遞增或遞減運算的操作數應該是分類為變數、屬性存取或索引器存取的表達式。 作業的結果是與操作數相同的類型值。
如果前置詞遞增或遞減作業的操作數是屬性或索引器存取,則屬性或索引器應同時具有 get 和 set 存取子。 如果情況並非如此,則會發生系結時間錯誤。
一元運算子多載決議(§12.4.4)用於選定特定的運算子實作。 下列類型存在預先定義的 ++
和 --
運算符: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.7 轉換表達式
cast_expression 可用來將表達式明確轉換成指定的型別。
cast_expression
: '(' type ')' unary_expression
;
形式為 (T)E
的 型態轉換運算式,其中 T
是一種型別,E
是 一元運算式,對 E
的值執行明確轉換 (§10.3)為型別 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.8 Await 運算式
12.9.8.1 一般
await
運算符用來暫停執行封閉的非同步函式,直到操作數表示的非同步操作完成為止。
await_expression
: 'await' unary_expression
;
在異步函式的主體中,只允許使用 await_expression(§15.15)。 在最接近的封入異步函式中,await_expression 不應發生在下列位置:
- 在嵌套的(非異步)匿名函式內
- 在 鎖定語句 的區塊內
- 在匿名函式轉換為表達式樹類型時(§10.7.3)
- 在不安全的情境中
附注:await_expression 不能出現在 query_expression的大部分位置,因為這些語法會被轉換為使用非同步 Lambda 表達式。 尾注
在異步函式內,await
不得作為 available_identifier 使用,雖然可以使用逐字標識碼 @await
。 因此,await_expression與涉及標識符的各種表達式之間沒有語法模棱兩可。 在異步函式之外,await
做為一般標識符。
await_expression 的運算元稱為 工作。 它表示在評估 await_expression 時,異步操作可能已完成或尚未完成。
await
運算子的目的是暫停封入異步函式的執行,直到等候的工作完成,然後取得其結果。
12.9.8.2 可等待的運算式
await_expression 的工作必須 可等候。 如果下列條件之一成立,則表達式 t
可等待:
-
t
是編譯時間類型dynamic
-
t
有一個可存取的實例或擴充方法,稱為GetAwaiter
,沒有參數和類型參數,且其回傳型別A
滿足下列所有條件:-
A
實作介面System.Runtime.CompilerServices.INotifyCompletion
(以下簡稱INotifyCompletion
) -
A
具有可存取且可讀取的實例屬性IsCompleted
類型bool
-
A
具有可存取的實例方法,GetResult
沒有參數,也沒有類型參數
-
GetAwaiter
方法的目的是為了取得任務的「awaiter」。
A
類型稱為針對 await 表達式的 awaiter 類型。
IsCompleted
屬性的目的是判斷工作是否已完成。 如果是,就不需要暫停評估。
INotifyCompletion.OnCompleted
方法的目的是註冊工作的「接續」;亦即,工作完成後,將會叫用的委派(類型為 System.Action
)。
GetResult
方法的目的是在工作完成之後取得工作的結果。 此結果可能成功完成,可能附帶一個結果值,或可能是由 GetResult
方法擲回的例外。
12.9.8.3 await 表達式分類
表達式 await t
分類的方式與表達式 (t).GetAwaiter().GetResult()
相同。 因此,如果 GetResult
的傳回型別是 void
,則 await_expression 會分類為 nothing。 如果它的傳回值不是void
類型 T
,則 await_expression 會被歸類為類型 T
的值。
12.9.8.4 await 表達式的運行時間評估
在執行時,表達式 await t
會被計算如下。
- 評估表達式
(t).GetAwaiter()
,以獲得 awaitera
。 - 通過評估表達式
(a).IsCompleted
來獲得bool
b
。 - 如果
b
是false
,那麼取決於a
是否實作介面System.Runtime.CompilerServices.ICriticalNotifyCompletion
(以下簡稱ICriticalNotifyCompletion
)。 在綁定時完成此檢查;亦即,如果a
具有編譯時類型dynamic
,則在運行時檢查,否則在編譯時檢查。 讓r
表示恢復委派(§15.15):- 如果
a
未實作ICriticalNotifyCompletion
,則會評估表達式((a) as INotifyCompletion).OnCompleted(r)
。 - 如果
a
實作ICriticalNotifyCompletion
,則會評估表達式((a) as ICriticalNotifyCompletion).UnsafeOnCompleted(r)
。 - 評估接著會暫停,並將控制權傳回至異步函式的當前呼叫端。
- 如果
- 緊接在之後(如果
b
true
),或在稍後叫用恢復委派時(如果b
已false
),就會評估表達式(a).GetResult()
。 如果傳回值,該值就是 await_expression的結果。 否則,結果是空的。
實作 awaiter 的介面方法 INotifyCompletion.OnCompleted
和 ICriticalNotifyCompletion.UnsafeOnCompleted
,應該導致委派 r
被叫用最多一次。 否則,封入異步函式的行為是未定義的。
12.10 算術運算元
12.10.1 一般
*
、/
、%
、+
和 -
運算符稱為算術運算符。
multiplicative_expression
: unary_expression
| multiplicative_expression '*' unary_expression
| multiplicative_expression '/' unary_expression
| multiplicative_expression '%' unary_expression
;
additive_expression
: multiplicative_expression
| additive_expression '+' multiplicative_expression
| additive_expression '-' multiplicative_expression
;
如果算術運算子的操作數具有編譯時間類型 dynamic
,則表達式會動態系結(\12.3.3)。 在這種情況下,表達式的編譯時類型是 dynamic
,而以下所述的解析將會在執行時,使用具有編譯時類型的操作數的執行時類型 dynamic
。
12.10.2 乘法運算符
針對格式 x * y
的操作,會套用二元運算子重載解析(§12.4.5)用來選擇特定的運算子實作。 操作數會轉換成所選取運算子的參數類型,而結果的類型是運算元的傳回類型。
預先定義的乘法運算符如下所列。 運算子都會計算 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
為無限大。 由於四捨五入,即使x
或y
都不是零,z
也可能為零。+y
-y
+0
-0
+∞
-∞
NaN
+x
+z
-z
+0
-0
+∞
-∞
NaN
-x
-z
+z
-0
+0
-∞
+∞
NaN
+0
+0
-0
+0
-0
NaN
NaN
NaN
-0
-0
+0
-0
+0
NaN
NaN
NaN
+∞
+∞
-∞
NaN
NaN
+∞
-∞
NaN
-∞
-∞
+∞
NaN
NaN
-∞
+∞
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
(除非另有說明,否則在 §12.10.2–§12.10.6 浮點數據表中,使用 “
+
” 表示值為正數;使用 “-
” 表示值為負數;而缺少正負號表示值可能是正數或負數,或無標識符號(NaN)。)十進位乘法:
decimal operator *(decimal x, decimal y);
如果產生的值大小太大而無法以十進位格式表示,則會擲回
System.OverflowException
。 由於四捨五入,即使兩個操作數都不是零,結果也可能為零。 結果的尺度,在任何四捨五入之前,為兩個操作數的尺度之和。 十進位乘法相當於使用類型為System.Decimal
的乘法運算符。
上述未提升的預先定義乘法運算符的提升形式(§12.4.8)也已經預先定義。
12.10.3 除法運算符
針對形式 x / y
的運算,會套用二元運算子重載解析(§12.4.5)來選取特定的運算子實作。 操作數會轉換成所選取運算子的參數類型,而結果的類型是運算元的傳回類型。
預先定義的除法運算符如下所列。 運算子都會計算 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
-0
NaN
-x
-z
+z
-∞
+∞
-0
+0
NaN
+0
+0
-0
NaN
NaN
+0
-0
NaN
-0
-0
+0
NaN
NaN
-0
+0
NaN
+∞
+∞
-∞
+∞
-∞
NaN
NaN
NaN
-∞
-∞
+∞
-∞
+∞
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
十進制除法:
decimal operator /(decimal x, decimal y);
如果右操作數的值為零,則會拋出
System.DivideByZeroException
。 如果產生的值大小太大而無法以十進位格式表示,則會擲回System.OverflowException
。 由於四捨五入,即使第一個操作數不是零,結果也可能為零。 在進行任何捨入之前,結果的縮放比例是最接近所需比例的小數位設置,以保證結果等於精確結果。 所選擇的比例是x
的比例減去y
的比例。十進位除法相當於使用類型為
System.Decimal
的除法運算符。
此外,已預先定義上述未提升之預定義除法運算符的提升形式(§12.4.8)。
12.10.4 餘數運算符
形式為 x % y
的運算中,會套用二元運算子多載解析(§12.4.5)以選擇特定的運算符實作。 操作數會轉換成所選取運算子的參數類型,而結果的類型是運算元的傳回類型。
預先定義的餘數運算符如下所列。 運算子都會計算 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
,則只有在x / y
擲回例外狀況時,才會擲回System.OverflowException
。浮點餘數:
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
+z
NaN
NaN
+x
+x
NaN
-x
-z
-z
NaN
NaN
-x
-x
NaN
+0
+0
+0
NaN
NaN
+0
+0
NaN
-0
-0
-0
NaN
NaN
-0
-0
NaN
+∞
NaN
NaN
NaN
NaN
NaN
NaN
NaN
-∞
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
十進位餘數:
decimal operator %(decimal x, decimal y);
如果右操作數的值為零,則會拋出
System.DivideByZeroException
。 當擲回System.ArithmeticException
或子類別時,即會定義實作。 如果x / y
不會拋出例外狀況,那麼符合規範的實作就不應該拋出x % y
的例外狀況。 在任何四捨五入之前,結果的小數位數是兩個操作數中較大的小數位數。如果結果的符號非零,則結果的符號與x
相同。小數餘數相當於使用 類型為
System.Decimal
的餘數運算符。附注:這些規則確保針對所有類型,結果永遠不會具有與左操作數相反的符號。 尾注
提升形式(§12.4.8)的上述未提升的預定義餘數運算子也已預先定義。
12.10.5 加法運算符
針對型態 x + y
的運算,會套用二元運算子重載解析(§12.4.5)以選擇特定的運算符實作。 操作數會轉換成所選取運算子的參數類型,而結果的類型是運算元的傳回類型。
預先定義的加法運算符如下所列。 針對數值和列舉型別,預先定義的加法運算符會計算兩個操作數的總和。 當一或兩個操作數的類型為 string
時,預先定義的加法運算符會串連操作數的字串表示。
整數加法:
int operator +(int x, int y); uint operator +(uint x, uint y); long operator +(long x, long y); ulong operator +(ulong x, ulong y
在
checked
內容中,如果總和超出結果類型的範圍,則會引發System.OverflowException
。 在unchecked
內容中,不會報告溢位,並且會捨棄超出結果類型範圍的所有高位元。浮點加法:
float operator +(float x, float y); double operator +(double x, double y);
總和是根據 IEC 60559 算術規則計算。 下表列出非零有限值、零、無限和 NaN 的所有可能組合結果。 在數據表中,
x
和y
是非零的有限值,z
是x + y
的結果。 如果x
和y
具有相同的大小,但相反的跡象,z
為正零。 如果x + y
太大而無法表示在目標類型中,z
是和x + y
擁有相同符號的無限值。y
+0
-0
+∞
-∞
NaN
x
z
x
x
+∞
-∞
NaN
+0
y
+0
+0
+∞
–∞
NaN
-0
y
+0
-0
+∞
-∞
NaN
+∞
+∞
+∞
+∞
+∞
NaN
NaN
-∞
-∞
-∞
-∞
NaN
-∞
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
十進位加法:
decimal operator +(decimal x, decimal y);
如果產生的值大小太大而無法以十進位格式表示,則會擲回
System.OverflowException
。 結果的縮放比例,在任何四捨五入之前,都是兩個操作數的較大比例。十進位加法相當於使用
System.Decimal
類型的加法運算符。列舉加法。 每個列舉型別都會隱含地提供下列預先定義的運算符,其中
E
是列舉類型,而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
操作數都會藉由叫用繼承自類型object
的虛擬ToString
方法,轉換為其字串表示法。 如果ToString
傳回null
,則會取代空字串。範例:
class Test { static void Main() { string s = null; Console.WriteLine("s = >" + s + "<"); // Displays s = >< int i = 1; Console.WriteLine("i = " + i); // Displays i = 1 float f = 1.2300E+15F; Console.WriteLine("f = " + f); // Displays f = 1.23E+15 decimal d = 2.900m; Console.WriteLine("d = " + d); // Displays d = 2.900 } }
批注中顯示的輸出是 US-English 系統上的典型結果。 精確的輸出可能取決於執行環境的地區設定。 字串串連運算符本身在每個案例中的行為都相同,但在執行期間隱含呼叫的
ToString
方法可能會受到區域設定的影響。結束範例
字串串連運算符的結果是
string
,其中包含左操作數的字元,後面接著右操作數的字元。 字串串連運算符永遠不會傳回null
值。 如果沒有足夠的記憶體可供配置產生的字串,可能會擲回System.OutOfMemoryException
。委託合併。 每個委派類型都會隱含地提供下列預先定義的運算符,其中
D
是委派類型:D operator +(D x, D y);
如果第一個操作數是
null
,則作業的結果是第二個操作數的值(即使這也是null
)。 否則,如果第二個操作數是null
,則作業的結果就是第一個操作數的值。 否則,作業的結果是新的委派實例,其調用清單是由第一個操作數調用清單中的元素所組成,後面接著第二個操作數調用清單中的元素。 也就是說,產生的委派調用清單是兩個操作數的調用清單串連。附註 :如需委派組合的範例,請參閱< 12.10.6 和\20.6 。 因為System.Delegate
不是委派類型,所以不會為其定義運算子 + 。尾注
已增強形式的上述未增強預先定義加法運算符(§12.4.8)也已預先定義。
12.10.6 減法運算符
針對 x – y
形式的運算,會運用二元操作符重載解析(§12.4.5)以選擇特定的運算符實作。 操作數會轉換成所選取運算子的參數類型,而結果的類型是運算元的傳回類型。
預先定義的減法運算符如下所列。 運算子都會從 x
減去 y
。
整數減法:
int operator –(int x, int y); uint operator –(uint x, uint y); long operator –(long x, long y); ulong operator –(ulong x, ulong y
在
checked
的情境中,如果差異超出了結果類型的範圍,則會拋出System.OverflowException
。 在unchecked
環境中,不會報告溢位,而且會捨棄結果類型範圍以外的所有重要的高階位元。浮點減法:
float operator –(float x, float y); double operator –(double x, double y);
差異是根據 IEC 60559 算術規則來計算。 下表列出非零有限值、零、無限和 NaN 的所有可能組合結果。 在數據表中,
x
和y
是非零的有限值,z
是x – y
的結果。 如果x
和y
相等,z
為正零。 如果x – y
太大而無法表示在目的地類型中,z
是具有與x – y
相同符號的無限大。y
+0
-0
+∞
-∞
NaN
x
z
x
x
-∞
+∞
NaN
+0
-y
+0
+0
-∞
+∞
NaN
-0
-y
-0
+0
-∞
+∞
NaN
+∞
+∞
+∞
+∞
NaN
+∞
NaN
-∞
-∞
-∞
-∞
-∞
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
(在上表中,
-y
專案表示 否定y
的,而不是值為負值。十進位減法:
decimal operator –(decimal x, decimal y);
如果產生的值大小太大而無法以十進位格式表示,則會擲回
System.OverflowException
。 在任何四捨五入之前,結果的精度是兩個操作數中較大者的精度。十進位減法相當於使用 類型為
System.Decimal
的減法運算符。列舉減法。 每個列舉型別都會隱含地提供下列預先定義的運算符,其中
E
是列舉型別,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
,則作業的結果就是第一個操作數的值。 - 否則,這兩個運算元都代表非空的調用清單(§20.2)。
- 如果清單比較相等,由委派相等運算符(§12.12.9)判斷,則操作的結果是
null
。 - 否則,操作的結果是新的調用列表,其中包含第一個操作數的項目列表,並移除了第二個操作數的項目,如果第二個操作數的項目列表是第一個操作數的子列表。 (若要判斷子清單相等,相對應的專案會與委派相等運算符比較。如果第二個操作數的清單符合第一個操作數清單中連續專案的多個子清單,則會移除連續項目的最後一個相符子清單。
- 否則,作業的結果就是左操作數的值。
- 如果清單比較相等,由委派相等運算符(§12.12.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.11 位移運算符
<<
和 >>
運算子可用來執行位元移位作業。
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.12 關係型和型別測試運算符
12.12.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
運算子的右操作數必須先測試為 類型,然後作為可能跨越多個標記的 表達式。 如果運算元是 表達式,則模式表達式的優先順序必須至少等於或高於 位移表達式。 尾注
is
運算子在 §12.12.12 中描述,as
運算子在 §12.12.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 |
如果 x 等於 y ,則 true ,否則 false |
x != y |
true 如果 x 不等於 y ,否則為 false |
x < y |
true 如果 x 小於 y ,則為 ,否則為 false |
x > y |
如果 x 大於 y ,則使用 true ,否則使用 false |
x <= y |
如果 x 小於或等於 y ,則 true 否則為 false |
x >= y |
如果 x 大於或等於 y ,則 true 否則為 false |
12.12.2 整數比較運算符
預先定義的整數比較運算符如下:
bool operator ==(int x, int y);
bool operator ==(uint x, uint y);
bool operator ==(long x, long y);
bool operator ==(ulong x, ulong y);
bool operator !=(int x, int y);
bool operator !=(uint x, uint y);
bool operator !=(long x, long y);
bool operator !=(ulong x, ulong y);
bool operator <(int x, int y);
bool operator <(uint x, uint y);
bool operator <(long x, long y);
bool operator <(ulong x, ulong y);
bool operator >(int x, int y);
bool operator >(uint x, uint y);
bool operator >(long x, long y);
bool operator >(ulong x, ulong y);
bool operator <=(int x, int y);
bool operator <=(uint x, uint y);
bool operator <=(long x, long y);
bool operator <=(ulong x, ulong y);
bool operator >=(int x, int y);
bool operator >=(uint x, uint y);
bool operator >=(long x, long y);
bool operator >=(ulong x, ulong y);
每一個運算符都會比較兩個整數操作數的數值,並傳回 bool
值,指出特定關聯性是 true
還是 false
。
上述未提升的預定義整數比較運算子的提升(§12.4.8)形式也預先定義。
12.12.3 浮點比較運算符
預先定義的浮點比較運算符如下:
bool operator ==(float x, float y);
bool operator ==(double x, double y);
bool operator !=(float x, float y);
bool operator !=(double x, double y);
bool operator <(float x, float y);
bool operator <(double x, double y);
bool operator >(float x, float y);
bool operator >(double x, double y);
bool operator <=(float x, float y);
bool operator <=(double x, double y);
bool operator >=(float x, float y);
bool operator >=(double x, double y);
運算子會根據 IEC 60559 標準的規則來比較操作數:
如果任一操作數為 NaN,則在所有運算子中,其結果為 false
,除非是運算子 !=
,在此情況下,結果為 true
。 對於任兩個操作數,x != y
一律會產生與 !(x == y)
相同的結果。 不過,當一或兩個操作數都是 NaN 時,<
、>
、<=
和 >=
運算符不會產生與相反運算符邏輯否定相同的結果 。
範例:如果任一
x
和y
為 NaN,則x < y
為false
,但!(x >= y)
為true
。 結束範例
當兩個操作數都不是 NaN 時,運算子會比較兩個浮點操作數與排序相關的值
–∞ < –max < ... < –min < –0.0 == +0.0 < +min < ... < +max < +∞
其中 min
和 max
是最小且最大的正有限值,可以用指定的浮點格式表示。 此排序的顯著影響如下:
- 負數和正零視為相等。
- 負無限大被認為小於所有其他值,但等於另一個負無限大。
- 正無限大被視為大於所有其他值,但與另一個正無限大相等。
提升形式的浮點比較運算符(§12.4.8)和未提升的預先定義運算符一樣,都是預先定義的。
12.12.4 十進位比較運算符
預先定義的十進位比較運算符如下:
bool operator ==(decimal x, decimal y);
bool operator !=(decimal x, decimal y);
bool operator <(decimal x, decimal y);
bool operator >(decimal x, decimal y);
bool operator <=(decimal x, decimal y);
bool operator >=(decimal x, decimal y);
每一個運算子都會比較兩個十進位操作數的數值,並傳回 bool
值,指出特定關聯性是 true
還是 false
。 每個十進位比較都相當於使用類型 System.Decimal
相對應的關係或等式運算子。
預先定義的提升形式的小數比較運算符(§12.4.8)也同樣已預先定義。
12.12.5 布爾值相等運算符
預先定義的布爾等號比較運算符如下:
bool operator ==(bool x, bool y);
bool operator !=(bool x, bool y);
如果 x
和 y
都是 true
,或是 x
和 y
都 false
,則 ==
的結果為 true
。 否則,結果會是 false
。
如果 x
和 y
都是 true
,或是 x
和 y
都 false
,則 !=
的結果為 false
。 否則,結果是 true
。 當操作數的類型為 bool
時,!=
運算符會產生與 ^
運算符相同的結果。
上述未提升的預定義布爾等式運算符的提升形式(§12.4.8)也已預先定義。
12.12.6 列舉比較運算子
每個列舉型別都會隱含地提供下列預先定義的比較運算符
bool operator ==(E x, E y);
bool operator !=(E x, E y);
bool operator <(E x, E y);
bool operator >(E x, E y);
bool operator <=(E x, E y);
bool operator >=(E x, E y);
評估 x «op» y
的結果,其中 x 和 y 是具有基礎類型 U
之列舉型別 E
表達式,而 «op» 是其中一個比較運算符,與評估 ((U)x) «op» ((U)y)
完全相同。 換句話說,列舉型別比較運算符只會比較兩個操作數的基礎整數值。
上述未提升的預先定義列舉比較運算符的提升(§12.4.8)形式也已預先定義。
12.12.7 參考型別相等運算符
每個類別類型 C
隱含地提供下列預先定義的參考型別相等運算子:
bool operator ==(C x, C y);
bool operator !=(C x, C y);
除非已存在預先定義的等號運算符,對於 C
而言(例如,當 C
是 string
或 System.Delegate
時)。
運算符會傳回比較兩個參考是否相等或不相等的結果。 只有在 x
和 y
參考相同的實例或同時 null
時,operator ==
才會傳回 true
;而只有在具有相同操作數的 operator ==
傳回 false
時,operator !=
才會傳回 true
。
除了一般適用性規則 (•12.6.4.2),預先定義的參考類型相等運算符還需要下列其中一項才能適用:
- 這兩個操作數都是已知為 reference_type 或常值
null
的類型值。 此外,存在從任一操作數到另一個操作數類型的身份或明確引用轉換(§10.3.5)。 - 其中一個操作數是字面值
null
,另一個操作數是類型T
的值,其中T
是一個 類型參數,已知它不是一個值類型,且不具備值類型的條件約束。- 如果在執行階段
T
是不可為 Null 的實值類型,那麼==
的結果是false
,而!=
的結果是true
。 - 如果在執行期間
T
是可為 Null 的實值型別,則結果是從運算元的HasValue
屬性計算得出的,如 (§12.12.10) 中所述。 - 如果在執行時
T
是參考型別,當操作數是null
時結果為true
,否則為false
。
- 如果在執行階段
除非其中一個條件成立,否則會發生系結時間錯誤。
附注:這些規則的顯著影響如下:
- 使用預先定義的參考型別平等運算符來比較在系結時間已知不同的兩個參考,屬於系結時間的錯誤。 例如,如果操作數的系結時間類型是兩個類別類型,而且兩者都不是衍生自另一個,則兩個操作數不可能參考相同的物件。 因此,作業會被視為系結時間錯誤。
- 預先定義的參考型別相等運算符不允許比較實值型別操作數(除非類型參數與
null
進行比較,這是特別處理的)。- 預先定義的參考型別相等運算子的操作數絕不會進行裝箱。 執行這類 Boxing 作業是毫無意義的,因為新配置的 Boxed 實例參考必然與所有其他參考不同。
對於形式
x == y
或x != y
的運算操作,如果存在任何適用的使用者定義operator ==
或operator !=
,則運算子重載解析規則(§12.4.5)將選擇該運算子,而非預先定義的參考型別相等運算子。 一律可以藉由明確將一或兩個操作數轉換成類型object
,來選取預先定義的參考型別相等運算符。尾注
範例:下列範例會檢查未限制類型參數類型的自變數是否
null
。class C<T> { void F(T x) { if (x == null) { throw new ArgumentNullException(); } ... } }
即使
T
可能代表不可為 Null 的實值型別,x == null
建構還是允許使用,而且當T
為不可為 Null 的實值型別時,結果只會定義為false
。結束範例
對於形式 x == 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
時,會選擇使用預先定義的字串等號運算符(§12.12.8)。 所有其餘的比較輸出False
,因為當任一操作數的系結時間類型為object
時,string
類型的operator ==
多載函數不適用。請注意,上述技術對實值型別沒有意義。 範例
class Test { static void Main() { int i = 123; int j = 123; Console.WriteLine((object)i == (object)j); } }
輸出
False
,因為轉換會建立兩個不同的boxedint
值實例的參考。結束範例
12.12.8 字串相等運算符
預先定義的字串相等運算符如下:
bool operator ==(string x, string y);
bool operator !=(string x, string y);
當下列其中一個值成立時,會將兩個 string
值視為相等:
- 這兩個值都是
null
。 - 兩個值都是對非
null
字串實例的參考,這些字串實例具有相同的長度,且每個字元位置的字元也相同。
字串相等運算符會比較字串值,而不是字串參考。 當兩個不同的字串實例包含完全相同的字元序列時,字串的值會相等,但參考不同。
附註:如 •12.12.7中所述,參考型別相等運算符可用來比較字元串參考,而不是字符串值。 尾注
12.12.9 委派相等運算符
預先定義的委派相等運算符如下:
bool operator ==(System.Delegate x, System.Delegate y);
bool operator !=(System.Delegate x, System.Delegate y);
兩個委派實例會視為相等,如下所示:
- 如果任一個委派實例是
null
,那麼只有當兩者都是null
時,它們才相等。 - 如果委派對象具有不同的執行時類型,則是永遠不相等的。
- 如果這兩個委派實例都有叫用清單(),則只有在它們的叫用清單長度相同,且其中一個叫用清單中的每個項目依序等於另外一個叫用清單中的對應項目時,這些實例才會相等。
下列規則會控管調用清單項目的相等性:
- 如果兩個調用清單條目都指向相同的靜態方法,那麼這些條目是相等的。
- 如果兩個叫用清單條目都參考同一目標物件上的同一個非靜態方法(由參考相等運算子定義),則這些條目是相等的。
- 允許(但不要求)從語意上相同的匿名函式(§12.19)評估所產生的調用列表條目中,相同(可能為空)的擷取外部變數實例集合是相等的。
如果運算子多載解析解析為委派相等運算符,且兩個操作數的系結時間類型都是 §20 中所述的委派類型而非 System.Delegate
中所述,且系結類型操作數類型之間沒有識別轉換,則會發生系結時間錯誤。
附註:此規則可防止因為參考不同類型的委派實例而無法將非
null
值視為相等的比較。 尾注
12.12.10 可為 Null 的實值型別與 null 常值之間的相等運算符
==
和 !=
運算符允許一個操作數是可為 Null 的實值型別值,而另一個操作數則為 null
常值,即使作業中沒有預先定義或使用者定義的運算符(以未提升或已解除的形式)存在也一樣。
針對其中一個形式的操作
x == null null == x x != null null != x
其中 x
是可為 Null 實值型別的運算式,如果運算子多載解析(§12.4.5)找不到適用的運算子,則會改為從 x
的 HasValue
屬性計算結果。 具體來說,前兩個窗體會轉譯為 !x.HasValue
,最後兩個窗體會轉譯為 x.HasValue
。
12.12.11 Tuple 相等運算符
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.12.12 The is 運算符
is
運算子有兩種形式。 其中一個是 類型運算子,其右側有類型。 另一個是 is-pattern 運算子,其右側有一個模式。
12.12.12.1 類型判斷運算符
is-type 運算子 用來檢查物件的運行時間類型是否與指定的類型相容。 檢查在執行時進行。 作業 E is T
的結果,其中 E
是表達式,T
是非 dynamic
類型,是一個布林值,指出 E
是否為非 null,且可透過參考轉換、裝箱轉換、拆箱轉換、包裝轉換或解包轉換成功轉換成類型 T
。
作業的評估方式如下:
- 如果
E
是匿名函式或方法群組,則會發生編譯時期錯誤 - 如果
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
的箱式轉換。
或者:D
是實值型別,T
是由D
實作的介面類型。
-
- 如果
T
是可為 Null 的實值型別,而D
是T
的基礎類型,那麼結果是true
。 - 如果
T
是不可為 Null 的實值型別,則如果D
和T
是相同的類型,則結果會true
。 - 否則,結果是
false
。
is
運算子不會考慮使用者定義的轉換。
附註:由於在執行時評估
is
運算符,所有型別參數已經替換完畢,且沒有開放型別(§8.4.3)。 尾注
附注:可以根據編譯時期類型和轉換來理解
is
運算符,其中C
是E
的編譯時期類型。
- 如果
e
的編譯時類型與T
相同,或存在從編譯時類型E
到T
的隱含參考轉換(§10.2.8)、裝箱轉換(§10.2.9)、封裝轉換(§10.6),或明確的解除封裝轉換(§10.6):
- 如果
C
為不可為 Null 的實值型別,則作業的結果會為true
。- 否則,作業的結果相當於評估
E != null
。- 否則,如果從
C
到T
存在明確的參考轉換(§10.3.5)或拆箱轉換(§10.3.7),或者如果C
或T
是開放類型(§8.4.3),則應進行如上所述的運行時檢查。- 否則,不可能將
E
的引用、拳套化、包裝或拆裝轉換為類型T
,作業的結果是false
。 編譯程式可以根據編譯時間類型實作優化。尾注
12.12.12.2 is-模式運算子
is-pattern 運算子 用來檢查表達式 計算的值是否符合給定的模式(§11)。 檢查在執行時進行。 如果值符合模式,則is-pattern 運算子的結果為 true;否則為 false。
形如 E is P
的運算式,其中 E
是類型 T
的關係表達式,而 P
是模式,如果下列任一項成立,則為編譯時期錯誤:
-
E
未指定值或沒有型別。 - 模式
P
不適用於類型T
(§11.2)。
12.12.13 as 型態轉換運算符
as
運算子可用來明確將值轉換成指定的參考型別或可為 Null 的實值型別。 不同於轉換表達式(§12.9.7),as
運算符永遠不會拋出例外。 相反地,如果無法進行指定的轉換,產生的值會 null
。
在形式 E as T
的操作中,E
應為表達式,T
應為引用型別、已知為引用型別的類型參數或是可為 Null 的實值型別。 此外,下列條件至少應滿足其中一項,否則會發生編譯時期錯誤:
- 識別(§10.2.2)、隱含可為 Null(§10.2.6)、隱含參考(§10.2.8)、封箱(§10.2.9)、明確可為 Null(§10.3.4)、明確參考(§10.3.5),或包裝(§8.3.12)的轉換存在從
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 } }
G
的類型參數T
已知為參考型別,因為它具有類別條件約束。 不過,H
的類型參數U
不是;因此不允許在H
中使用as
運算符。結束範例
12.13 邏輯運算符
12.13.1 一般
&,
^
和 |
運算符稱為邏輯運算符。
and_expression
: equality_expression
| and_expression '&' equality_expression
;
exclusive_or_expression
: and_expression
| exclusive_or_expression '^' and_expression
;
inclusive_or_expression
: exclusive_or_expression
| inclusive_or_expression '|' exclusive_or_expression
;
如果邏輯運算子的操作數具有編譯時間類型 dynamic
,則表示式會動態系結 ()。 在這種情況下,表示式的編譯時期類型是 dynamic
,而下文所述的解析將會在執行時使用那些具有編譯時期類型 dynamic
的運算元的執行時期類型。
對於形式 x «op» y
的操作,其中「op」是某個邏輯運算子,多載解析(第12.4.5節)會被用來選擇特定的運算子實作。 操作數會轉換成所選取運算子的參數類型,而結果的類型是運算元的傳回類型。
預先定義的邏輯運算符號會在下列子條款中描述。
12.13.2 整數邏輯運算符
預先定義的整數邏輯運算符如下:
int operator &(int x, int y);
uint operator &(uint x, uint y);
long operator &(long x, long y);
ulong operator &(ulong x, ulong y);
int operator |(int x, int y);
uint operator |(uint x, uint y);
long operator |(long x, long y);
ulong operator |(ulong x, ulong y);
int operator ^(int x, int y);
uint operator ^(uint x, uint y);
long operator ^(long x, long y);
ulong operator ^(ulong x, ulong y);
&
運算子計算兩個操作數的位元邏輯 AND,|
運算子計算兩個操作數的位元邏輯 OR,^
運算子計算兩個操作數的位元邏輯互斥 OR。 這些作業不會發生溢位問題。
提升形式的上述未提升的整數邏輯運算符也預先定義(§12.4.8)。
12.13.3 列舉邏輯運算符
每個列舉類型 E
隱含地提供下列預先定義的邏輯運算子:
E operator &(E x, E y);
E operator |(E x, E y);
E operator ^(E x, E y);
評估 x «op» y
的結果,其中 x
和 y
是具有基礎類型 U
之列舉型別 E
表達式,而 «op» 是其中一個邏輯運算符,與評估 (E)((U)x «op» (U)y)
完全相同。 換句話說,列舉型別邏輯運算符只會在兩個操作數的基礎類型上執行邏輯運算。
此外,上述未提升之列舉邏輯運算符的提升(§12.4.8)形式也已預先定義。
12.13.4 布爾邏輯運算符
預先定義的布林邏輯運算符如下:
bool operator &(bool x, bool y);
bool operator |(bool x, bool y);
bool operator ^(bool x, bool y);
如果 x
和 y
都 true
,則 x & y
的結果為 true
。 否則,結果會是 false
。
x | y
的結果為 true
,如果 x
或 y
是 true
。 否則,結果是 false
。
如果 x
是 true
且 y
是 false
,或 x
是 false
且 y
是 true
,則 x ^ y
的結果為 true
。 否則,結果會是 false
。 當操作數的類型為 bool
時,^
運算符會計算與 !=
運算符相同的結果。
12.13.5 可為 Null 的布爾值 & 和 |運算子
可空的布林類型 bool?
可以代表三個值,true
、false
和 null
。
與其他二元運算符一樣,邏輯運算子 &
和 |
(12.13.4)的提升形式也已預先定義:
bool? operator &(bool? x, bool? y);
bool? operator |(bool? x, bool? y);
下表定義了提升 &
和 |
運算符的語意:
x |
y |
x & y |
x \| y |
---|---|---|---|
true |
true |
true |
true |
true |
false |
false |
true |
true |
null |
null |
true |
false |
true |
false |
true |
false |
false |
false |
false |
false |
null |
false |
null |
null |
true |
null |
true |
null |
false |
false |
null |
null |
null |
null |
null |
附注:
bool?
類型在概念上類似於 SQL 中布爾運算式所使用的三個值型別。 上表遵循與 SQL 相同的語意,而將 §12.4.8 的規則套用至&
和|
運算符則不會。 12.4.8 的規則已經為提升^
運算符提供類似 SQL 的語意。 尾注
12.14 條件式邏輯運算元
12.14.1 一般
&&
和 ||
運算符稱為條件式邏輯運算符。 它們也稱為「短路」邏輯運算符。
conditional_and_expression
: inclusive_or_expression
| conditional_and_expression '&&' inclusive_or_expression
;
conditional_or_expression
: conditional_and_expression
| conditional_or_expression '||' conditional_and_expression
;
&&
和 ||
運算子是 &
和 |
運算子的條件式版本:
- 作業
x && y
對應至作業x & y
,但只有當x
不是false
時,才會評估y
。 - 作業
x || y
對應至作業x | y
,但只有當x
不為true
時,才會評估y
。
附注:短路使用「非真」和「非假」條件的原因是讓使用者可以定義條件運算子來決定短路何時應用。 用戶定義型別可能處於
operator true
傳回false
且operator false
傳回false
的狀態。 在這些情況下,&&
和||
都不會短路。 尾注
如果條件式邏輯運算子的操作數具有編譯時間類型 dynamic
,則表示式會動態系結 (#\12.3.3)。 在這種情況下,表達式的編譯時期類型是 dynamic
,而以下所述的解析將於執行時期進行,使用具有編譯時期類型 dynamic
的操作數的執行時期類型。
形式 x && y
或 x || y
的操作通過套用多載解析(§12.4.5)來處理,就好像該操作是寫成 x & y
或 x | y
。 然後
- 如果多載解析找不到單一最佳運算符,或者多載解析會選取其中一個預先定義的整數邏輯運算符或可為 Null 的布爾邏輯運算元(\12.13.5),則會發生系結時間錯誤。
- 否則,如果選取的運算符是其中一個預先定義的布爾邏輯運算元(
&12.13.4 ),則會依照.14.2 所述處理作業。 - 否則,選取的運算符是使用者定義的運算符,而且作業會依照 \12.14.3中所述處理。
無法直接多載條件式邏輯運算元。 不過,由於條件邏輯運算子是基於一般邏輯運算子來進行運算,因此一般邏輯運算子的多載(在某些限制下)也被視為條件邏輯運算子的多載。 這會在 •12.14.3中進一步說明。
12.14.2 布爾條件邏輯運算符
當 &&
或 ||
的操作數屬於類型 bool
,或操作數不是定義適用 operator &
或 operator |
的類型時,但會定義對 bool
的隱含轉換,作業會如下所示處理:
- 作業
x && y
被評估為x ? y : false
。 換句話說,x
會先評估並轉換成類型bool
。 然後,如果x
是true
,則會評估y
並轉換成類型bool
,這會成為作業的結果。 否則,作業的結果為false
。 - 操作
x || y
被評估為x ? true : y
。 換句話說,x
會先評估並轉換成類型bool
。 然後,如果x
是true
,則作業的結果會true
。 否則,y
會評估並轉換成類型bool
,這會成為作業的結果。
12.14.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)
是T
中宣告的operator false
調用,而T.&(x, y)
是選取operator &
的調用。 換句話說,系統會先評估x
,並在結果上叫用operator false
,以判斷x
是否絕對為 false。 然後,如果x
絕對為 false,作業的結果就是先前針對x
計算的值。 否則,會評估y
,並在先前針對x
計算的值和針對y
計算的值上叫用選取的operator &
,以產生作業的結果。 - 作業
x || y
被評估為T.true(x) ? x : T.|(x, y)
,其中T.true(x)
調用的是在T
宣告的operator true
,而T.|(x, y)
調用的是選定的operator |
。 換句話說,系統會先評估x
,並在結果上叫用operator true
,以判斷x
是否絕對正確。 然後,如果x
絕對成立,作業的結果就是先前針對x
計算的值。 否則,會評估y
,然後將選定的operator |
套用於先前計算出的x
值和y
值,以產生作業結果。
在這些運算中的任一個中,由 x
指定的運算式只會被評估一次,而由 y
指定的運算式要麼不被評估,要麼只被評估一次。
12.15 空合併運算子
??
運算符稱為 null 聯合運算符。
null_coalescing_expression
: conditional_or_expression
| conditional_or_expression '??' null_coalescing_expression
| throw_expression
;
在 a ?? b
的 null 合併運算式中,如果 a
非null
,則結果為 a
;否則,結果為 b
。 當 a
是 null
時,作業才會評估 b
。
Null 聯合運算子是右關聯運算元,表示作業會從右至左分組。
範例:
a ?? b ?? c
的運算式會評估為?? (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
存在且不是可為 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
不相容,並且會在編譯時發生錯誤 a
。
12.16 擲回表達式運算子
throw_expression
: 'throw' null_coalescing_expression
;
throw_expression 會擲回評估 null_coalescing_expression所產生的值。 表達式應可隱含轉換成 System.Exception
,而且評估表達式的結果會在擲回之前轉換為 System.Exception
。 在運行時間評估 throw 表達式 的行為,與 throw 語句 所指定的行為相同(§13.10.6)。
throw_expression 沒有類型。 throw_expression 可透過 隱式拋出轉換轉換為每種類型。
拋出表達式 只能出現在下列語法情境中:
- 做為三元條件運算符的第二或第三個操作數(
?:
)。 - 做為 空合併運算子的第二個操作數(
??
)。 - 做為具表達式主體的 Lambda 或成員。
12.17 宣告表達式
宣告表達式會宣告局部變數。
declaration_expression
: local_variable_type identifier
;
local_variable_type
: type
| 'var'
;
如果簡單名稱查閱找不到相關的宣告,則 simple_name_
也被視為宣告表達式(§12.8.4)。 當作宣告表示式使用時,_
稱為 簡單捨棄。 其語意相當於 var _
,但允許在更多地方使用。
宣告表達式應該只會發生在下列語法內容中:
- 做為 argument_list中的
out
argument_value。 - 作為包含在簡單指派左側的簡單捨棄
_
(12.21.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
的宣告顯示嘗試在巢狀呼叫M
時使用i2
,但這是不允許的,因為該參考出現在宣告i2
的參數列表中。 另一方面,允許在最終參數中引用b2
,因為它是在b2
宣告的巢狀參數列表結束之後出現的。
s3
的宣告顯示了使用隱含和明確型別的捨棄宣告表達式。 因為丟棄不會宣告具名變數,所以允許多次出現_
這個識別碼。(int i1, int _, (var i2, var _), _) = (1, 2, (3, 4), 5);
這個範例示範在解構賦值中對變數和捨棄符使用隱式和顯式型別的宣告表達式。 當找不到
_
宣告時,simple_name_
相當於var _
。void M1(out int i) { ... } void M2(string _) { M1(out _); // Error: `_` is a string M1(out var _); }
此範例示範如何使用
var _
,在無法使用_
時提供隱含型別的捨棄,因為它會在封入範圍中指定變數。結束範例
12.18 條件運算符
?:
運算符稱為條件運算元。 有時也稱為三元運算符。
conditional_expression
: null_coalescing_expression
| null_coalescing_expression '?' expression ':' expression
| null_coalescing_expression '?' 'ref' variable_reference ':'
'ref' variable_reference
;
如果 ref
存在,則在條件運算符中不允許拋出表達式(§12.16)。
表單的條件表示式 b ? x : y
會先評估條件 b
。 然後,如果 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
之間存在識別轉換,則結果是一組運算式的最佳共有類型(§12.6.3.15)。 如果任一類型為dynamic
,則類型推斷偏好dynamic
(§8.7)。 如果其中一個類型是元組類型(§8.3.11),則當兩個元組中相同序數位置的元素名稱相符時,類型推斷會包括這些元素名稱。 - 否則,如果隱含轉換 (\10.2) 從
X
到Y
存在,但不是從Y
到X
,則Y
是條件表達式的類型。 - 否則,如果存在從
X
到Y
的隱含列舉轉換(§10.2.4),則Y
是條件表達式的類型。 - 否則,如果從
Y
到X
存在隱含的列舉轉換(§10.2.4),則X
是條件表達式的類型。 - 否則,如果隱含轉換 (\10.2) 從
Y
到X
存在,但不是從X
到Y
,則X
是條件表達式的類型。 - 否則,無法判斷表達式類型,而且會發生編譯時期錯誤。
- 如果
- 如果只有其中一個
x
和y
具有類型,而且x
和y
都會隱含地轉換成該類型,則為條件表達式的類型。 - 否則,無法判斷表達式類型,而且會發生編譯時期錯誤。
表單的 ref 條件表示式的執行時間處理 b ? ref x : ref y
包含下列步驟:
- 首先,會評估
b
,並判斷b
的bool
值:- 如果存在從
b
類型到bool
類型的隱含轉換,則會執行此隱含轉換來產生bool
值。 - 否則,會叫用
b
類型所定義的operator true
,以產生bool
值。
- 如果存在從
- 如果上述步驟所產生的
bool
值是true
,則會評估x
,而產生的變數參考會成為條件表達式的結果。 - 否則,會評估
y
,而產生的變數參考會成為條件表達式的結果。
運行時處理條件運算式的形式 b ? x : y
由以下步驟組成:
- 首先,會評估
b
,並判斷b
的bool
值:- 如果存在從
b
到bool
類型的隱含轉換,則會執行該隱含轉換以產生bool
值。 - 否則,會調用
b
類型所定義的operator true
,以產生bool
值。
- 如果存在從
- 如果上述步驟所產生的
bool
值true
,則會評估x
並轉換成條件表達式的類型,這會成為條件表達式的結果。 - 否則,會評估
y
並轉換為條件表達式的類型,這就成為條件表達式的結果。
12.19 匿名函式表達式
12.19.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
修飾詞的匿名函式是異步函式,並遵循 中所述的規則。
匿名函式以 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.19.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.19.3 匿名函式主體
匿名函式的主體(表達式 或 區塊)受限於下列規則:
- 如果匿名函式包含簽章,則本文中會提供簽章中指定的參數。 如果匿名函式沒有簽名,則可以轉換成具有參數的委派類型或運算式類型(§10.7),但無法在函式主體中存取這些參數。
- 除了在最近的封閉匿名函式的簽章中指定的傳址參數(如果有的話)之外,在函式主體中存取傳址參數會導致編譯時期錯誤。
- 除了最接近封入其中的匿名函式的簽名(若有)中指定的參數外,如果函式體嘗試存取
ref struct
類型的參數,則會出現編譯時期錯誤。 - 當
this
的類型是結構類型時,程式碼存取this
會產生編譯時錯誤。 無論存取方式是明確的(如this.x
中),還是隱含的(如x
中,其中x
是該結構的實例成員)。 此規則僅禁止這類存取,不會影響成員查詢是否屬於結構中的成員。 - 函數體可以存取匿名函式的外部變數(§12.19.6)。 外部變數的存取會參考在評估 lambda 表達式 或 匿名方法表達式 時,當下作用的變數實例(§12.19.7)。
- 若內文包含目標在內文外部或在內文包含的匿名函式的主體內的
goto
語句、break
語句或continue
語句,則會產生編譯時期錯誤。 - 本文中的
return
語句會從最接近封入匿名函式的調用傳回控件,而不是從封入函式成員傳回。
尚未明確指定是否有其他方法可執行匿名函式的區塊,除了透過評估及調用 lambda_expression 或 anonymous_method_expression。 特別是,編譯程式可以選擇藉由合成一或多個具名方法或類型來實作匿名函式。 任何這類合成項目的名稱應為保留供編譯程式使用的格式(4.3)。
12.19.4 多載解析
自變數清單中的匿名函式會參與類型推斷和多載解析。 如需確切的規則,請參閱 •12.6.3 和 #12.6.4。
範例:下列範例說明匿名函式對多載解析的影響。
class ItemList<T> : List<T> { public int Sum(Func<T, int> selector) { int sum = 0; foreach (T item in this) { sum += selector(item); } return sum; } public double Sum(Func<T, double> selector) { double sum = 0; foreach (T item in this) { sum += selector(item); } return sum; } }
ItemList<T>
類別有兩個Sum
方法。 每個函式都接受一個selector
參數,負責從清單項目中擷取要加總的數值。 擷取的值可以是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.19.5 匿名函式和動態系結
匿名函式不能是動態系結作業的接收者、自變數或操作數。
12.19.6 外部變數
12.19.6.1 一般
任何範圍包含
12.19.6.2 擷取的外部變數
當匿名函式參考外部變數時,據說外部變數已被匿名函式 擷取。 一般而言,局部變數的存留期僅限於執行與之相關的區塊或語句(§9.2.9.1)。 不過,擷取的外部變數的生命周期至少延伸至從匿名函式建立的委派或表達式樹狀結構符合垃圾收集資格為止。
範例:在範例中
delegate int D(); class Test { static D F() { int x = 0; D result = () => ++x; return result; } static void Main() { D d = F(); Console.WriteLine(d()); Console.WriteLine(d()); Console.WriteLine(d()); } }
匿名函式會堲取局部變數
x
,而且x
的存留期至少會延伸,直到從F
傳回的委派才有資格進行垃圾收集。 由於匿名函式的每個調用都會在相同的x
實例上運作,因此範例的輸出為:1 2 3
結束範例
當匿名函式擷取局部變數或值參數時,局部變數或參數不再被視為固定變數(),而是被視為可移動的變數。 不過,擷取的外部變數不能用於 fixed
語句中(§23.7),因此無法取得擷取外部變數的位址。
附注:不同於未擷取的變數,擷取的局部變數可以同時公開至多個執行線程。 尾注
12.19.6.3 局部變數的具現化
當執行進入變數範圍時,局部變數會被視為 實例化。
範例:例如,當呼叫下列方法時,局部變數
x
將針對迴圈的每次迭代具現化並初始化三次。static void F() { for (int i = 0; i < 3; i++) { int x = i * 2 + 1; ... } }
將
x
的宣告移到迴圈外會只生成一個x
:static void F() { int x; for (int i = 0; i < 3; i++) { x = i * 2 + 1; ... } }
結束範例
未擷取時,很難準確觀察到局部變數被實例化的頻率,因為實例化的生命週期互不重疊,因此每次實例化都可能使用相同的儲存位置。 不過,當匿名函式擷取局部變數時,具現化的效果就會變得明顯。
範例:此範例
delegate void D(); class Test { static D[] F() { D[] result = new D[3]; for (int i = 0; i < 3; i++) { int x = i * 2 + 1; result[i] = () => Console.WriteLine(x); } return result; } static void Main() { foreach (D d in F()) { d(); } } }
會產生輸出:
1 3 5
不過,當宣告
x
移動到迴圈外時:delegate void D(); class Test { static D[] F() { D[] result = new D[3]; int x; for (int i = 0; i < 3; i++) { x = i * 2 + 1; result[i] = () => Console.WriteLine(x); } return result; } static void Main() { foreach (D d in F()) { d(); } } }
輸出為:
5 5 5
請注意,允許編譯器(但不要求)將三個實例優化為單一委派實例(§10.7.2)。
結束範例
如果 for 迴圈宣告了一個迭代變數,那麼該變數本身會被視為在迴圈之外宣告。
範例:因此,如果範例改為直接擷取迭代變數本身:
delegate void D(); class Test { static D[] F() { D[] result = new D[3]; for (int i = 0; i < 3; i++) { result[i] = () => Console.WriteLine(i); } return result; } static void Main() { foreach (D d in F()) { d(); } } }
只會擷取一個迭代變數的實例,這會產生輸出:
3 3 3
結束範例
匿名函式委派可以共用一些已擷取的變數,但也可以擁有其他各自的實例。
範例:例如,如果
F
變更為static D[] F() { D[] result = new D[3]; int x = 0; for (int i = 0; i < 3; i++) { int y = 0; result[i] = () => Console.WriteLine($"{++x} {++y}"); } return result; }
這三個代理捕捉到相同的
x
實例,但y
的個別實例,並且其輸出為:1 1 2 1 3 1
結束範例
個別的匿名函式可以擷取外部變數的相同實例。
範例:在範例中:
delegate void Setter(int value); delegate int Getter(); class Test { static void Main() { int x = 0; Setter s = (int value) => x = value; Getter g = () => x; s(5); Console.WriteLine(g()); s(10); Console.WriteLine(g()); } }
這兩個匿名函式會擷取本機變數的相同實例
x
,因此可以透過該變數「通訊」。 此範例的輸出為:5 10
結束範例
12.19.7 匿名函式表達式的評估
匿名函式 F
一律會轉換成委派類型 D
或表示式樹狀結構類型 E
,直接或透過執行委派建立表達式 new D(F)
。 此轉換會決定匿名函數的結果,如 §10.7中所述。
12.19.8 實作範例
這個子章節具有資訊性。
此子段描述其他 C# 建構中匿名函式轉換的可能的實作。 此處所述的實作是以商業 C# 編譯程式所使用的相同原則為基礎,但絕不是授權實作,也不是唯一可能的實作。 它只會簡要提及轉換成表達式樹,因為其確切語意不在此規格的範圍之內。
這個子程式代碼的其餘部分提供數個程式代碼範例,其中包含具有不同特性的匿名函式。 針對每個範例,會提供只使用其他 C# 建構之程式代碼的對應轉譯。 在範例中,會假設標識碼 D
代表下列委派類型:
public delegate void D();
匿名函式最簡單的形式是不擷取任何外部變數的函式。
delegate void D();
class Test
{
static void F()
{
D d = () => Console.WriteLine("test");
}
}
這可以翻譯成委派實例化,參考編譯器生成的靜態方法,其中放置匿名函數的代碼:
delegate void D();
class Test
{
static void F()
{
D d = new D(__Method1);
}
static void __Method1()
{
Console.WriteLine("test");
}
}
在下列範例中,匿名函式會參考 this
的實例成員:
delegate void D();
class Test
{
int x;
void F()
{
D d = () => Console.WriteLine(x);
}
}
這可以轉譯成編譯程式產生的實例方法,其中包含匿名函式的程式代碼:
delegate void D();
class Test
{
int x;
void F()
{
D d = new D(__Method1);
}
void __Method1()
{
Console.WriteLine(x);
}
}
在此範例中,匿名函式會擷取局部變數:
delegate void D();
class Test
{
void F()
{
int y = 123;
D d = () => Console.WriteLine(y);
}
}
局部變數的存留期現在必須延伸至至少匿名函式委派的存留期。 這可藉由將局部變數「吊接」到編譯程式產生類別的欄位來達成。 局部變數的具現化(•12.19.6.3),然後對應至建立編譯程式產生的類別實例,而存取局部變數會對應至存取編譯程式產生的類別實例中的字段。 此外,匿名函式會成為編譯程式產生類別的實例方法:
delegate void D();
class Test
{
void F()
{
__Locals1 __locals1 = new __Locals1();
__locals1.y = 123;
D d = new D(__locals1.__Method1);
}
class __Locals1
{
public int y;
public void __Method1()
{
Console.WriteLine(y);
}
}
}
最後,下列匿名函式會擷取 this
,以及具有不同存留期的兩個局部變數:
delegate void D();
class Test
{
int x;
void F()
{
int y = 123;
for (int i = 0; i < 10; i++)
{
int z = i * 2;
D d = () => Console.WriteLine(x + y + z);
}
}
}
在這裡,會針對每個區塊建立編譯程式產生的類別,其中會擷取局部變數,讓不同區塊中的局部變數可以有獨立的存留期。
__Locals2
的實例,編譯程式為內部區塊產生的類別,包含局部變數 z
,以及參考 __Locals1
實例的欄位。
__Locals1
的實例,編譯程式為外部區塊產生的類別,包含局部變數 y
,以及參考封入函式成員 this
的欄位。 透過這些數據結構,您可以透過 __Local2
的實例連線到所有擷取的外部變數,因此匿名函式的程式代碼可以實作為該類別的實例方法。
delegate void D();
class Test
{
int x;
void F()
{
__Locals1 __locals1 = new __Locals1();
__locals1.__this = this;
__locals1.y = 123;
for (int i = 0; i < 10; i++)
{
__Locals2 __locals2 = new __Locals2();
__locals2.__locals1 = __locals1;
__locals2.z = i * 2;
D d = new D(__locals2.__Method1);
}
}
class __Locals1
{
public Test __this;
public int y;
}
class __Locals2
{
public __Locals1 __locals1;
public int z;
public void __Method1()
{
Console.WriteLine(__locals1.__this.x + __locals1.y + z);
}
}
}
將匿名函式轉換成表達式樹狀結構時,也可以使用此處所套用的相同方法來擷取局部變數:編譯程式產生的對象的參考可以儲存在表達式樹狀結構中,而局部變數的存取可以表示為這些物件的欄位存取。 這種方法的優點在於,它允許委派和運算式樹共享提升的局部變數。
資訊文本結束。
12.20 查詢表達式
12.20.1 一般
查詢表示式 為類似 SQL 和 XQuery 等關係型和階層式查詢語言的查詢提供語言整合語法。
query_expression
: from_clause query_body
;
from_clause
: 'from' type? identifier 'in' expression
;
query_body
: query_body_clauses? select_or_group_clause query_continuation?
;
query_body_clauses
: query_body_clause
| query_body_clauses query_body_clause
;
query_body_clause
: from_clause
| let_clause
| where_clause
| join_clause
| join_into_clause
| orderby_clause
;
let_clause
: 'let' identifier '=' expression
;
where_clause
: 'where' boolean_expression
;
join_clause
: 'join' type? identifier 'in' expression 'on' expression
'equals' expression
;
join_into_clause
: 'join' type? identifier 'in' expression 'on' expression
'equals' expression 'into' identifier
;
orderby_clause
: 'orderby' orderings
;
orderings
: ordering (',' ordering)*
;
ordering
: expression ordering_direction?
;
ordering_direction
: 'ascending'
| 'descending'
;
select_or_group_clause
: select_clause
| group_clause
;
select_clause
: 'select' expression
;
group_clause
: 'group' expression 'by' expression
;
query_continuation
: 'into' identifier query_body
;
查詢表達式會以 from
子句開頭,並以 select
或 group
子句結尾。 初始 from
子句之後可以接零個或多個 from
、let
、where
、join
或 orderby
子句。 每個 from
子句都是一個產生器,引入 範圍變數,範圍涵蓋 序列的元素。 每個 let
子句都會引進一個範圍變數,代表以先前範圍變數計算的值。 每個 where
子句都是用來過濾從結果中排除項目的條件。 每個 join
子句都會比較來源序列的指定索引鍵與另一個序列的索引鍵,產生相符的配對。 每個 orderby
子句都會根據指定的準則重新排序專案。最終 select
或 group
子句會以範圍變數來指定結果的形狀。 最後,into
子句可用來「擷取」查詢,方法是將一個查詢的結果視為後續查詢中的產生器。
12.20.2 查詢表達式中的模棱兩可
查詢表達式使用一些內容關鍵詞 ():ascending
、by
、descending
、equals
、from
、group
、into
、join
、let
、on
、orderby
、select
和 where
。
為了避免這些識別符在查詢表達式中作為關鍵字和簡單名稱時產生歧義,除非這些識別符前加上 `@
`(§6.4.4),否則它們在任何位置都被視為關鍵字。 針對此目的,查詢表達式是開頭為 “from
標識符” 的任何表達式,後面接著 “;
”、“=
” 或 “,
” 以外的任何標記。
12.20.3 查詢表達式翻譯
12.20.3.1 一般
C# 語言不會指定查詢表達式的執行語意。 相反地,查詢表達式會被轉譯成遵循查詢表達式模式的方法的調用(§12.20.4)。 具體而言,查詢表達式會轉譯成名為 Where
、Select
、SelectMany
、Join
、GroupJoin
、OrderBy
、OrderByDescending
、ThenBy
、ThenByDescending
、GroupBy
和 Cast
的方法調用。 這些方法預期具有特定的特徵和傳回型別,如 第12.20.4節中所述。 這些方法可能是所查詢之對象的實例方法,或是物件外部的擴充方法。 這些方法會實作查詢的實際執行。
從查詢表達式到方法調用的轉譯是語法對應,會在執行任何類型系結或多載解析之前發生。 在查詢表達式轉譯之後,所產生的方法呼叫會被當作一般的方法呼叫來處理,這可能會進一步發現編譯時錯誤。 這些錯誤狀況包括但不限於不存在的方法、錯誤的型別自變數,以及類型推斷失敗的泛型方法。
查詢表達式會藉由重複套用下列翻譯來處理,直到無法進一步縮減為止。 翻譯將按照應用次序列出:每個區段假定之前各節的翻譯已經完全執行,且一旦某個區段完成,其內容將不會在處理相同查詢表達式時再次被訪問。
查詢表達式中,如果包含對範圍變數的賦值操作,或者將範圍變數作為參考或輸出參數的引數使用,這會在編譯時出錯。
某些翻譯會插入範圍變數,其中包含 透明標識符, 以 *表示。 這些會在 §12.20.3.8中進一步說明。
12.20.3.2 含接續的查詢表達式
查詢表達式,在其查詢主體之後有一個延續部分
from «x1» in «e1» «b1» into «x2» «b2»
已轉譯成
from «x2» in ( from «x1» in «e1» «b1» ) «b2»
下列各節中的翻譯假設查詢沒有接續。
範例:範例:
from c in customers group c by c.Country into g select new { Country = g.Key, CustCount = g.Count() }
被翻譯成:
from g in (from c in customers group c by c.Country) select new { Country = g.Key, CustCount = g.Count() }
最後的翻譯如下:
customers. GroupBy(c => c.Country). Select(g => new { Country = g.Key, CustCount = g.Count() })
結束範例
12.20.3.3 明確範圍變數類型
明確指定範圍變數類型的 from
子句
from «T» «x» in «e»
已轉譯成
from «x» in ( «e» ) . Cast < «T» > ( )
明確指定範圍變數類型的 join
子句
join «T» «x» in «e» on «k1» equals «k2»
已轉譯成
join «x» in ( «e» ) . Cast < «T» > ( ) on «k1» equals «k2»
下列各節中的翻譯假設查詢沒有明確的範圍變數類型。
範例:此範例
from Customer c in customers where c.City == "London" select c
已轉譯成
from c in (customers).Cast<Customer>() where c.City == "London" select c
其最終翻譯為
customers. Cast<Customer>(). Where(c => c.City == "London")
結束範例
附註:明確範圍變數類型適用於查詢實作非泛型
IEnumerable
介面的集合,但不適用於泛型IEnumerable<T>
介面。 在上述範例中,如果客戶的類型為ArrayList
,則會出現這種情況。 尾注
12.20.3.4 變質查詢表達式
表單的查詢表達式
from «x» in «e» select «x»
已轉譯成
( «e» ) . Select ( «x» => «x» )
範例:此範例
from c in customers select c
已轉譯成
(customers).Select(c => c)
結束範例
變質的查詢表達式是可簡單選取來源元素的查詢表達式。
附注:翻譯的後續階段(§12.20.3.6 和 §12.20.3.7)通過將其替換為其來源來移除其他翻譯步驟所引進的退化查詢。 不過,請務必確保查詢表達式的結果絕不是來源物件本身。 否則,傳回這類查詢的結果可能會不小心將私用數據(例如元素陣列)公開給呼叫端。 因此,此步驟會藉由在來源上明確呼叫
Select
,保護直接在原始程式碼中撰寫的變質查詢。 然後,Select
和其他查詢運算子的實作者負責確保這些方法永遠不會回傳來源物件本身。 尾注
12.20.3.5 From、let、where、join 和 orderby 子句
具有第二個 from
子句的查詢表達式,後面接著 select
子句
from «x1» in «e1»
from «x2» in «e2»
select «v»
已轉譯成
( «e1» ) . SelectMany( «x1» => «e2» , ( «x1» , «x2» ) => «v» )
範例:此範例
from c in customers from o in c.Orders select new { c.Name, o.OrderID, o.Total }
已轉譯成
(customers). SelectMany(c => c.Orders, (c,o) => new { c.Name, o.OrderID, o.Total } )
結束範例
具有第二個 from
子句的查詢表達式,隨後接著的查詢主體 Q
包含一組非空的查詢主體子句:
from «x1» in «e1»
from «x2» in «e2»
Q
已轉譯成
from * in («e1») . SelectMany( «x1» => «e2» ,
( «x1» , «x2» ) => new { «x1» , «x2» } )
Q
範例:此範例
from c in customers from o in c.Orders orderby o.Total descending select new { c.Name, o.OrderID, o.Total }
已轉譯成
from * in (customers). SelectMany(c => c.Orders, (c,o) => new { c, o }) orderby o.Total descending select new { c.Name, o.OrderID, o.Total }
其最終翻譯為
customers. SelectMany(c => c.Orders, (c,o) => new { c, o }). OrderByDescending(x => x.o.Total). Select(x => new { x.c.Name, x.o.OrderID, x.o.Total })
其中
x
是編譯程式產生的標識符,是本質上看不見且無法存取的。結束範例
一個 let
表示式及其前面的 from
子句:
from «x» in «e»
let «y» = «f»
...
已轉譯成
from * in ( «e» ) . Select ( «x» => new { «x» , «y» = «f» } )
...
範例:此範例
from o in orders let t = o.Details.Sum(d => d.UnitPrice * d.Quantity) where t >= 1000 select new { o.OrderID, Total = t }
已轉譯成
from * in (orders).Select( o => new { o, t = o.Details.Sum(d => d.UnitPrice * d.Quantity) }) where t >= 1000 select new { o.OrderID, Total = t }
其最終翻譯為
orders .Select(o => new { o, t = o.Details.Sum(d => d.UnitPrice * d.Quantity) }) .Where(x => x.t >= 1000) .Select(x => new { x.o.OrderID, Total = x.t })
其中
x
是編譯程式產生的識別符號,否則為隱藏且不可存取。結束範例
where
表达式及其之前的 from
子句:
from «x» in «e»
where «f»
...
已轉譯成
from «x» in ( «e» ) . Where ( «x» => «f» )
...
join
子句緊接在 select
子句後面
from «x1» in «e1»
join «x2» in «e2» on «k1» equals «k2»
select «v»
已轉譯成
( «e1» ) . Join( «e2» , «x1» => «k1» , «x2» => «k2» , ( «x1» , «x2» ) => «v» )
範例:此範例
from c in customersh join o in orders on c.CustomerID equals o.CustomerID select new { c.Name, o.OrderDate, o.Total }
已轉譯成
(customers).Join( orders, c => c.CustomerID, o => o.CustomerID, (c, o) => new { c.Name, o.OrderDate, o.Total })
結束範例
join
子句後面接著一個查詢內容子句:
from «x1» in «e1»
join «x2» in «e2» on «k1» equals «k2»
...
已轉譯成
from * in ( «e1» ) . Join(
«e2» , «x1» => «k1» , «x2» => «k2» ,
( «x1» , «x2» ) => new { «x1» , «x2» })
...
一個 join
-into
子句後面緊跟著一個 select
子句
from «x1» in «e1»
join «x2» in «e2» on «k1» equals «k2» into «g»
select «v»
已轉譯成
( «e1» ) . GroupJoin( «e2» , «x1» => «k1» , «x2» => «k2» ,
( «x1» , «g» ) => «v» )
join into
子句後面接著查詢主體子句
from «x1» in «e1»
join «x2» in «e2» on «k1» equals «k2» into *g»
...
已轉譯成
from * in ( «e1» ) . GroupJoin(
«e2» , «x1» => «k1» , «x2» => «k2» , ( «x1» , «g» ) => new { «x1» , «g» })
...
範例:此範例
from c in customers join o in orders on c.CustomerID equals o.CustomerID into co let n = co.Count() where n >= 10 select new { c.Name, OrderCount = n }
已轉譯成
from * in (customers).GroupJoin( orders, c => c.CustomerID, o => o.CustomerID, (c, co) => new { c, co }) let n = co.Count() where n >= 10 select new { c.Name, OrderCount = n }
其最終翻譯為
customers .GroupJoin( orders, c => c.CustomerID, o => o.CustomerID, (c, co) => new { c, co }) .Select(x => new { x, n = x.co.Count() }) .Where(y => y.n >= 10) .Select(y => new { y.x.c.Name, OrderCount = y.n })
其中
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.20.3.6 Select 子句
表單的查詢表達式
from «x» in «e» select «v»
已轉譯成
( «e» ) . Select ( «x» => «v» )
除了 «v»
是標識碼 «x»
以外,翻譯只是簡單的
( «e» )
範例:此範例
from c in customers.Where(c => c.City == "London") select c
只是轉譯成
(customers).Where(c => c.City == "London")
結束範例
12.20.3.7 群組子句
group
子句
from «x» in «e» group «v» by «k»
已轉譯成
( «e» ) . GroupBy ( «x» => «k» , «x» => «v» )
除非 «v»
是標識碼 «x»
時,則翻譯為
( «e» ) . GroupBy ( «x» => «k» )
範例:此範例
from c in customers group c.Name by c.Country
已轉譯成
(customers).GroupBy(c => c.Country, c => c.Name)
結束範例
12.20.3.8 透明標識符
某些翻譯會將範圍變數插入 透明標識符, 以 *
表示。 透明標識碼只存在於查詢表達式轉譯程式中做為中繼步驟。
當查詢轉譯插入透明標識符時,進一步的翻譯步驟會將透明標識符傳播至匿名函式和匿名物件初始化表達式。 在這些內容中,透明標識碼具有下列行為:
- 當透明識別符作為匿名函式中的參數出現時,相關匿名類型的成員會自動在匿名函式主體中可用。
- 如果一個包含透明識別碼的成員在範圍內,那麼該成員的所有成員也在範圍內。
- 當透明標識符作為匿名物件初始化器中的成員宣告符時,它會引入一個具有透明標識符的成員。
在上述的翻譯步驟中,透明標識符一律會與匿名類型一起導入,目的是將多個範圍變數擷取為單一對象的成員。 C# 的實作可以使用與匿名類型不同的機制,將多個範圍變數分組在一起。 下列翻譯範例假設使用匿名類型,並顯示一個可能的透明標識符轉譯。
範例:此範例
from c in customers from o in c.Orders orderby o.Total descending select new { c.Name, o.Total }
已轉譯成
from * in (customers).SelectMany(c => c.Orders, (c,o) => new { c, o }) orderby o.Total descending select new { c.Name, o.Total }
進一步轉譯成
customers .SelectMany(c => c.Orders, (c,o) => new { c, o }) .OrderByDescending(* => o.Total) .Select(\* => new { c.Name, o.Total })
這相當於清除透明標識時的結果
customers .SelectMany(c => c.Orders, (c,o) => new { c, o }) .OrderByDescending(x => x.o.Total) .Select(x => new { x.c.Name, x.o.Total })
其中
x
是由編譯器生成的識別符,並且本身是不可見且無法存取的。範例
from c in customers join o in orders on c.CustomerID equals o.CustomerID join d in details on o.OrderID equals d.OrderID join p in products on d.ProductID equals p.ProductID select new { c.Name, o.OrderDate, p.ProductName }
已轉譯成
from * in (customers).Join( orders, c => c.CustomerID, o => o.CustomerID, (c, o) => new { c, o }) join d in details on o.OrderID equals d.OrderID join p in products on d.ProductID equals p.ProductID select new { c.Name, o.OrderDate, p.ProductName }
進一步縮減為
customers .Join(orders, c => c.CustomerID, o => o.CustomerID, (c, o) => new { c, o }) .Join(details, * => o.OrderID, d => d.OrderID, (*, d) => new { *, d }) .Join(products, * => d.ProductID, p => p.ProductID, (*, p) => new { c.Name, o.OrderDate, p.ProductName })
其最終翻譯為
customers .Join(orders, c => c.CustomerID, o => o.CustomerID, (c, o) => new { c, o }) .Join(details, x => x.o.OrderID, d => d.OrderID, (x, d) => new { x, d }) .Join(products, y => y.d.ProductID, p => p.ProductID, (y, p) => new { y.x.c.Name, y.x.o.OrderDate, p.ProductName })
其中
x
和y
是編譯程式產生的標識符,否則為看不見且無法存取的標識符。 結束範例
12.20.4 查詢表達式模式
查詢表達式的模式 建立了一種方法模式,類型可以實作這種方法以支持查詢表達式。
如果泛型型別 C<T>
的公共成員方法和可公開存取的擴充方法可以用下列類別定義取代,則其支援查詢表達式模式。 成員和可存取的擴展方法被稱為泛型類型的「形狀」,C<T>
。 泛型型別可用來說明參數和傳回型別之間的適當關聯性,但也可以實作非泛型型別的模式。
delegate R Func<T1,R>(T1 arg1);
delegate R Func<T1,T2,R>(T1 arg1, T2 arg2);
class C
{
public C<T> Cast<T>() { ... }
}
class C<T> : C
{
public C<T> Where(Func<T,bool> predicate) { ... }
public C<U> Select<U>(Func<T,U> selector) { ... }
public C<V> SelectMany<U,V>(Func<T,C<U>> selector,
Func<T,U,V> resultSelector) { ... }
public C<V> Join<U,K,V>(C<U> inner, Func<T,K> outerKeySelector,
Func<U,K> innerKeySelector, Func<T,U,V> resultSelector) { ... }
public C<V> GroupJoin<U,K,V>(C<U> inner, Func<T,K> outerKeySelector,
Func<U,K> innerKeySelector, Func<T,C<U>,V> resultSelector) { ... }
public O<T> OrderBy<K>(Func<T,K> keySelector) { ... }
public O<T> OrderByDescending<K>(Func<T,K> keySelector) { ... }
public C<G<K,T>> GroupBy<K>(Func<T,K> keySelector) { ... }
public C<G<K,E>> GroupBy<K,E>(Func<T,K> keySelector,
Func<T,E> elementSelector) { ... }
}
class O<T> : C<T>
{
public O<T> ThenBy<K>(Func<T,K> keySelector) { ... }
public O<T> ThenByDescending<K>(Func<T,K> keySelector) { ... }
}
class G<K,T> : C<T>
{
public K Key { get; }
}
上述方法會使用泛型委派類型 Func<T1, R>
和 Func<T1, T2, R>
,但是它們同樣可以使用參數和傳回型別中具有相同關聯性的其他委派或表達式樹狀結構類型。
附注:
C<T>
與O<T>
之間建議的關聯性,可確保ThenBy
和ThenByDescending
方法只能在OrderBy
或OrderByDescending
的結果上使用。 尾注
附注:建議的
GroupBy
結果圖形,其中每個內部序列都有額外的Key
屬性。 尾注
附註:由於查詢表達式會透過語法對應轉譯為方法調用,因此類型在實作任何或所有查詢表達式模式的方式方面具有相當大的彈性。 例如,模式的方法可以實作為實例方法或擴充方法,因為兩者具有相同的調用語法,而且方法可以要求委派或表達式樹狀結構,因為匿名函式可轉換成兩者。 只實作某些查詢表示式模式的類型僅支持對應至類型所支援方法的查詢表達式翻譯。 尾注
Note:
System.Linq
命名空間提供一個查詢運算式模式的實作,適用於任何實作System.Collections.Generic.IEnumerable<T>
介面的類型。 尾注
12.21 指派運算符
12.21.1 一般
除了其中一個指派運算符以外,所有運算子都會將新值指派給變數、屬性、事件或索引器元素。 例外狀況 = ref
會將變數參考(§9.5)指派給參考變數(§9.7)。
assignment
: unary_expression assignment_operator expression
;
assignment_operator
: '=' 'ref'? | '+=' | '-=' | '*=' | '/=' | '%=' | '&=' | '|=' | '^=' | '<<='
| right_shift_assignment
;
指派的左操作數應該是分類為變數的表達式,或者,除了 = ref
、屬性存取、索引器存取、事件存取或 Tuple 之外。 宣告表達式不能直接作為左操作數,但可能會在解構指派的評估過程中出現。
=
運算子稱為 簡單指派運算子。 它會將右操作數的一個或多個值指派給左操作數所指定的變數、屬性、索引器元素或元組元素。 簡單指派運算子的左操作數不應是事件存取器(除非 §15.8.2中另有說明)。 簡單指派運算子描述於 .12.21.2。
運算子 = ref
稱為 ref 指派運算子。 它會讓右操作數成為 variable_reference (),左操作數所指定之參考變數的參考值。 ref 指派運算符於 §12.21.3中描述。
=
與 = ref
運算子以外的指派運算元稱為 複合指派運算子。 這些運算子會在兩個操作數上執行指示的作業,然後將產生的值指派給左操作數所指定的變數、屬性或索引器元素。 複合指派運算子在 §12.21.4中描述。
+=
和 -=
運算符,當左操作數是事件存取表達式時,稱為 事件指派運算子。 作為左操作數的情況下,事件存取不接受任何其他指派運算符。 事件指派運算符描述於 .12.21.5。
指派運算子是右關聯運算元,這表示作業會從右至左分組。
範例:將表達式
a = b = c
計算為a = (b = c)
。 結束範例
12.21.2 簡單指派
=
運算符稱為簡單指派運算符。
如果簡單賦值的左元件是形如 E.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
可以解構為具有n
元素的元組表達式(y1, ..., yn)
(§12.7),則每個對yi
的指派xi
具有類型Ti
,且該指派具有類型(T1, ..., Tn)
。 - 否則,如果
x
分類為變數,則變數不會readonly
、x
具有類型T
,而且y
隱含轉換成T
,則指派的類型為T
。 - 否則,如果
x
分類為隱含型別變數(亦即隱含型別宣告表達式),且y
具有類型T
,則變數的推斷型別T
,且指派具有類型T
。 - 否則,如果
x
分類為屬性或索引器存取,則屬性或索引器具有可存取的 set 存取子、x
具有類型T
,且y
隱含轉換成T
,則指派具有類型T
。 - 否則指派作業無效,而且會發生綁定時間錯誤。
執行具有類型 T
之表單 x = y
簡單指派的運行時間處理,會執行指派給具有類型 T
的 y
x
,其中包含下列遞歸步驟:
- 如果尚未評估,則評估
x
。 - 如果
x
分類為變數,則會評估y
,並視需要透過隱含轉換轉換為T
(\10.2)。- 如果
x
指定的變數是 reference_type的陣列元素,則會執行執行時檢查,以確保為y
計算的值與x
所屬的陣列實例相容。 檢查會成功的條件是:如果y
是null
,或如果存在從y
所參考實例的類型到包含x
的陣列實例的實際元素類型的隱含參考轉換(§10.2.8)。 否則,會拋出System.ArrayTypeMismatchException
。 - 評估和轉換
y
所產生的值會儲存到由評估x
所給出的位置,並作為指派的結果返回。
- 如果
- 如果
x
分類為屬性或索引器存取:- 會評估
y
,並在需要時透過隱含轉換(§10.2)轉換為T
。 -
x
的 set 存取子被叫用,其值參數是由對y
的評估與轉換所得出。 -
y
的評估與轉換所得之值,被作為賦值的結果。
- 會評估
- 如果
x
被分類為具有元數n
的元組(x1, ..., xn)
:-
y
被n
元素解構為元組表示式e
。 - 使用隱含元組轉換,將
e
轉換成T
,以建立結果元組t
。 - 針對每個
xi
,依左至右的順序進行將t.Itemi
賦值給xi
的操作,但不會再次評估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.21.3 參考指派
= ref
運算符稱為 ref 賦值 運算符。
左操作數必須是綁定至參考變數的表達式(§9.7)、參考參數(除了this
)、輸出參數或輸入參數。 右操作數應該是一個運算結果,其中生成 variable_reference 並指定一個與左操作數相同類型的值。
如果左操作數的 ref-safe-context (\9.7.2)比右操作數的 ref-safe-context 寬,則為編譯時間錯誤。
右操作數應該在 ref 指派的點上明確指派。
當左操作數係結至輸出參數時,如果在 ref 指派運算子的開頭未明確指派該輸出參數,就會發生錯誤。
如果左操作數是一個可寫的引用(亦即,它指定的不是 ref readonly
本地或輸入參數),那麼右操作數必須是可寫的 變數引用。 如果右操作數變數是可寫的,則左操作數可能是可寫的或只讀的參考 (ref)。
此作業會使左操作數成為右操作數變數的別名。 即使變數的右運算元是可寫的,別名也可能是唯讀的。
ref 指派運算子會產生一個屬於指派類型的 variable_reference。 如果左操作數可寫入,則此項也為可寫入。
ref 指派運算符不得讀取右操作數所參考的儲存位置。
範例:以下是使用
= ref
的一些範例:public static int M1() { ... } public static ref int M2() { ... } public static ref uint M2u() { ... } public static ref readonly int M3() { ... } public static void Test() { int v = 42; ref int r1 = ref v; // OK, r1 refers to v, which has value 42 r1 = ref M1(); // Error; M1 returns a value, not a reference r1 = ref M2(); // OK; makes an alias r1 = ref M2u(); // Error; lhs and rhs have different types r1 = ref M3(); // error; M3 returns a ref readonly, which r1 cannot honor ref readonly int r2 = ref v; // OK; make readonly alias to ref r2 = ref M2(); // OK; makes an alias, adding read-only protection r2 = ref M3(); // OK; makes an alias and honors the read-only r2 = ref (r1 = ref M2()); // OK; r1 is an alias to a writable variable, // r2 is an alias (with read-only access) to the same variable }
結束範例
附註:在使用
= ref
運算符閱讀程式碼時,可能很容易錯誤地將ref
部分視為操作數的一部分。 當操作數是條件式?:
表示式時,這特別令人困惑。 例如,在讀取ref int a = ref b ? ref x : ref y;
時,務必要理解為= ref
是運算子,而b ? ref x : ref y
是右操作數:ref int a = ref (b ? ref x : ref y);
。 重要的是,表達式ref b
是 而不是 該語句的一部分,即使乍看之下似乎是如此。 尾注
12.21.4 複合指派
如果複合指派的左操作數是 E.P
或 E[Ei]
,而 E
具有編譯時類型 dynamic
,則該指派是動態綁定(§12.3.3)。 在此情況下,指派表達式的編譯期間類型是 dynamic
,而所述的解析將根據 E
的運行期間類型在運行時進行。 如果左操作數的格式為 E[Ei]
,其中至少有一個 Ei
元素具有編譯時類型 dynamic
,且 E
的編譯時類型不是陣列,則產生的索引器存取會動態繫結,但具有限的編譯時檢查(§12.6.5)。
形式 x «op»= y
的運算會通過套用二元運算子的多載解析(§12.4.5)來進行處理,如同該運算被寫作 x «op» y
。 然後
- 如果所選的運算子的傳回型別可以隱含轉換為
x
的型別,則運算將被評估為x = x «op» y
,只是x
只會被評估一次。 - 否則,如果選取的運算符是預先定義的運算符,如果所選取運算符的傳回型別明確轉換成
x
類型,而且如果y
隱含轉換成x
類型,或運算符是 shift 運算符,則會將作業評估為x = (T)(x «op» y)
,其中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 = y
,也就允許x «op»= y
。
範例:在下列程式代碼中
byte b = 0; char ch = '\0'; int i = 0; b += 1; // OK b += 1000; // Error, b = 1000 not permitted b += i; // Error, b = i not permitted b += (byte)i; // OK ch += 1; // Error, ch = 1 not permitted ch += (char)1; // OK
每個錯誤的直覺原因是對應的簡單指派也會是錯誤。
結束範例
附註:這也表示複合賦值運算支援提升的運算子。 由於複合指派
x «op»= y
評估為x = x «op» y
或x = (T)(x «op» y)
,因此評估規則隱含地涵蓋提升運算子。 尾注
12.21.5 事件指派
如果 a += or -=
運算子左邊的操作數被分類為事件存取,則該表達式會按如下方式進行評估:
- 評估事件存取的實例表達式,如果有的話。
- 會評估
+=
或-=
運算子的右運算元,如果需要,會透過隱含轉換轉變成左運算元的類型(§10.2)。 - 事件的存取子會被叫用,其參數清單包含上一個步驟中計算的值。 如果運算子
+=
,則會叫用 add 存取子;如果運算子-=
,則會叫用 remove 存取子。
事件指派表達式不會產生值。 因此,事件指派表達式只有在 statement_expression 的語境中有效(§13.7)。
12.22 表達式
表示式 是 non_assignment_expression 或 賦值。
expression
: non_assignment_expression
| assignment
;
non_assignment_expression
: declaration_expression
| conditional_expression
| lambda_expression
| query_expression
;
12.23 常數表達式
常數表達式是應在編譯時期完整評估的表達式。
constant_expression
: expression
;
常數表示式應具有值 null
或下列其中一種類型:
-
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
表達式,前提是 unmanaged-type 是 §23.6.9 中指定的類型之一,且sizeof
會傳回一個常數值。 - 默認值表達式,前提是類型是上述其中一種類型。
常數表示式允許下列轉換:
- 身分識別轉換
- 數值轉換
- 列舉轉換
- 常數表達式轉換
- 隱含和明確的參考轉換,前提是轉換的來源是評估為
null
值的常數表達式。
附註:常數運算式中不允許其他轉換,包括非
null
值的裝箱、拆箱和隱含參考轉換。 尾注
範例:在下列程式代碼中
class C { const object i = 5; // error: boxing conversion not permitted const object str = "hello"; // error: implicit reference conversion }
i
的初始化是錯誤,因為需要 Boxing 轉換。str
初始化是錯誤,因為需要來自非null
值的隱含參考轉換。結束範例
每當表達式符合上述需求時,表達式就會在編譯階段進行評估。 即使表達式是包含非常數建構之較大表達式的子表達式,也是如此。
常數表達式的編譯時期評估會使用與非常數的表達式在執行時期評估相同的規則,但在執行時期評估會拋出異常的情況下,編譯時期評估則會導致編譯時期錯誤發生。
除非在 unchecked
內容中明確放置常數表達式,否則在執行編譯時期評估時,發生於整數類型算術運算和轉換中的溢位,將一律導致編譯時錯誤(§12.8.20)。
在下列情境中需要常數表達式,這在文法中以使用 constant_expression來表示。 在這些內容中,如果無法在編譯時期完全評估表達式,就會發生編譯時期錯誤。
- 常數宣告 (§15.4)
- 列舉成員宣告 (第19.4條)
- 參數清單的預設自變數 (•15.6.2)
-
case
switch
語句的標籤 (13.8.3)。 -
goto case
陳述 (第13.10.4節) - 陣列創建表達式中的維度長度(12.8.17.5 中包含初始化器的表達式)。
- 屬性 (§22)
- 在 constant_pattern (§11.2.3)
隱含常數表達式轉換 (10.2.11)允許將類型 int
常數表達式轉換成 sbyte
、byte
、short
、ushort
、uint
或 ulong
,前提是常數表達式的值在目的型別的範圍內。
12.24 布爾表達式
boolean_expression 是產生類型 bool
結果的表達式;直接或透過在特定內容中套用 operator true
,如下所述:
boolean_expression
: expression
;
if_statement 的控制條件表達式(§13.8.2),while_statement(§13.9.2),do_statement(§13.9.3),或 for_statement(§13.9.4)是 boolean_expression。
?:
運算子的控制用的條件表達式(§12.18)遵循與 boolean_expression相同的規則,但因為運算符優先順序的原因,被分類為 null_coalescing_expression。
boolean_expressionE
必須能夠產生類型為 bool
的值,如下所示:
- 如果 E 可以隱含轉換成
bool
則會在運行時間套用隱含轉換。 - 否則,一元運算符多載解析(§12.4.4)用於在
E
上尋找唯一的最佳operator true
實作,並在執行時期套用該實作。 - 如果找不到這類運算符,就會發生系結時間錯誤。