備註
本文是功能規格。 規格可作為功能的設計檔。 其中包含建議的規格變更,以及功能設計和開發期間所需的資訊。 這些文章會發佈,直到提議的規格變更完成並併併入目前的ECMA規格為止。
功能規格與已完成實作之間可能有一些差異。 這些差異已記錄在相關的 語言設計會議(LDM)備忘錄中。
您可以在 規範的文章中深入瞭解將功能規範納入 C# 語言標準的過程。
Champion 期數:https://github.com/dotnet/csharplang/issues/8697
聲明
語法
class_body
: '{' class_member_declaration* '}' ';'?
| ';'
;
class_member_declaration
: constant_declaration
| field_declaration
| method_declaration
| property_declaration
| event_declaration
| indexer_declaration
| operator_declaration
| constructor_declaration
| finalizer_declaration
| static_constructor_declaration
| type_declaration
| extension_declaration // add
;
extension_declaration // add
: 'extension' type_parameter_list? '(' receiver_parameter ')' type_parameter_constraints_clause* extension_body
;
extension_body // add
: '{' extension_member_declaration* '}' ';'?
;
extension_member_declaration // add
: method_declaration
| property_declaration
| operator_declaration
;
receiver_parameter // add
: attributes? parameter_modifiers? type identifier?
;
延伸宣告只能在非泛型、非巢狀靜態類別中宣告。
類型命名 extension為 是錯誤。
範圍規則
延伸宣告的類型參數和接收者參數位於延伸宣告主體內的範圍內。 從靜態成員內參照接收者參數是錯誤,但運算式內 nameof 除外。 成員宣告類型參數或參數 (以及直接在成員本文內的局部變數和局部函式) 與延伸模組宣告的類型參數或接收者參數名稱相同的錯誤。
public static class E
{
extension<T>(T[] ts)
{
public bool M1(T t) => ts.Contains(t); // `T` and `ts` are in scope
public static bool M2(T t) => ts.Contains(t); // Error: Cannot refer to `ts` from static context
public void M3(int T, string ts) { } // Error: Cannot reuse names `T` and `ts`
public void M4<T, ts>(string s) { } // Error: Cannot reuse names `T` and `ts`
}
}
成員本身與封閉延伸宣告的類型參數或接收器參數具有相同的名稱,這不是錯誤。 成員名稱不會直接在延伸宣告內的簡單名稱查閱中找到;因此,查閱會尋找該名稱的類型參數或接收者參數,而不是成員。
成員確實會產生直接在封閉的靜態類別上宣告的靜態方法,而這些方法可以透過簡單的名稱查閱找到;不過,會先找到相同名稱的延伸宣告類型參數或接收者參數。
public static class E
{
extension<T>(T[] ts)
{
public void T() { M(ts); } // Generated static method M<T>(T[]) is found
public void M() { T(ts); } // Error: T is a type parameter
}
}
靜態類別作為擴充容器
延伸模組是在頂層非泛型靜態類別內宣告,就像現在的擴充方法一樣,因此可以與傳統延伸方法和非延伸模組靜態成員共存:
public static class Enumerable
{
// New extension declaration
extension(IEnumerable source) { ... }
// Classic extension method
public static IEnumerable<TResult> Cast<TResult>(this IEnumerable source) { ... }
// Non-extension member
public static IEnumerable<int> Range(int start, int count) { ... }
}
延伸模組宣告
延伸宣告是匿名的,並提供具有任何相關聯類型參數和條件約束的 接收者規格 ,後面接著一組延伸成員宣告。 接收器規格可以是參數的形式,或者 - 如果只宣告靜態延伸成員 - 類型:
public static class Enumerable
{
extension(IEnumerable source) // extension members for IEnumerable
{
public bool IsEmpty { get { ... } }
}
extension<TSource>(IEnumerable<TSource> source) // extension members for IEnumerable<TSource>
{
public IEnumerable<T> Where(Func<TSource, bool> predicate) { ... }
public IEnumerable<TResult> Select<TResult>(Func<TSource, TResult> selector) { ... }
}
extension<TElement>(IEnumerable<TElement>) // static extension members for IEnumerable<TElement>
where TElement : INumber<TElement>
{
public static IEnumerable<TElement> operator +(IEnumerable<TElement> first, IEnumerable<TElement> second) { ... }
}
}
接收器規格中的類型稱為 接收器類型 ,參數名稱 (如果存在) 稱為 接收器參數。
如果已命名 接收器參數 ,則 接收器類型 可能不是靜態的。
如果未命名,則不允許 接收器參數 具有修飾符,並且僅允許具有下面列出的參考修飾符, scoped 否則。
receiver 參數與傳統延伸方法的第一個參數具有相同的限制。
如果屬性 [EnumeratorCancellation] 放置在 接收器參數上,則會忽略該屬性。
擴充功能成員
延伸成員宣告在語法上與類別和結構宣告中的對應實例和靜態成員相同 (建構函式除外) 。 實例成員會參考具有接收器參數名稱的接收器:
public static class Enumerable
{
extension(IEnumerable source)
{
// 'source' refers to receiver
public bool IsEmpty => !source.GetEnumerator().MoveNext();
}
}
如果封閉的延伸宣告未指定接收者參數,則指定實例延伸成員是錯誤:
public static class Enumerable
{
extension(IEnumerable) // No parameter name
{
public bool IsEmpty => true; // Error: instance extension member not allowed
}
}
在延伸模組宣告abstract的成員上指定下列修飾元是錯誤:、virtual、override、newsealedpartial和 protected (和相關的協助工具修飾元)。
在延伸模組宣告的成員上指定 readonly 修飾元是錯誤。
延伸模組宣告中的屬性可能沒有 init 存取子。
如果 接收器參數 未命名,則不容許實例成員。
所有成員的名稱都應與靜態封閉類別的名稱不同,以及擴充類型的名稱 (如果有的話)。
使用屬性裝飾 [ModuleInitializer] 延伸模組成員是錯誤。
參考
根據預設,接收者會依值傳遞至實例延伸模組成員,就像其他參數一樣。
不過,參數形式的延伸宣告接收器可以指定 ref和 ref readonly ,只要 in已知接收器類型是值類型即可。
可空性和屬性
接收器類型可以是或包含可為 Null 的參考類型,而參數形式的接收器規格可以指定屬性:
public static class NullableExtensions
{
extension(string? text)
{
public string AsNotNull => text is null ? "" : text;
}
extension([NotNullWhen(false)] string? text)
{
public bool IsNullOrEmpty => text is null or [];
}
extension<T> ([NotNull] T t) where T : class?
{
public void ThrowIfNull() => ArgumentNullException.ThrowIfNull(t);
}
}
與經典擴展方法的兼容性
實例延伸方法會產生符合傳統延伸方法所產生的成品。
具體來說,產生的靜態方法具有宣告的擴充方法的屬性、修飾符和名稱,以及從擴充宣告和方法宣告按該順序串連的類型參數清單、參數清單和約束清單:
public static class Enumerable
{
extension<TSource>(IEnumerable<TSource> source) // Generate compatible extension methods
{
public IEnumerable<TSource> Where(Func<TSource, bool> predicate) { ... }
public IEnumerable<TSource> Select<TResult>(Func<TSource, TResult> selector) { ... }
}
}
產生:
[Extension]
public static class Enumerable
{
[Extension]
public static IEnumerable<TSource> Where<TSource>(IEnumerable<TSource> source, Func<TSource, bool> predicate) { ... }
[Extension]
public static IEnumerable<TSource> Select<TSource, TResult>(IEnumerable<TSource> source, Func<TSource, TResult> selector) { ... }
}
運營商
雖然延伸運算子具有明確的運算元類型,但仍需要在延伸宣告內宣告:
public static class Enumerable
{
extension<TElement>(IEnumerable<TElement>) where TElement : INumber<TElement>
{
public static IEnumerable<TElement> operator *(IEnumerable<TElement> vector, TElement scalar) { ... }
public static IEnumerable<TElement> operator *(TElement scalar, IEnumerable<TElement> vector) { ... }
}
}
這允許宣告和推斷類型參數,類似於必須在其其中一個運算元類型內宣告一般使用者定義運算子的方式。
檢查
推斷性: 針對每個非方法延伸模組成員,其延伸模組區塊的所有類型參數都必須用於延伸模組和成員的合併參數集。
獨特性: 在指定的封閉靜態類別中,具有相同接收器類型的擴充成員宣告集 (模身分轉換和類型參數名稱取代) 會被視為單一宣告空間,類似於類別或結構宣告內的成員,並受限於唯一 性的相同規則。
public static class MyExtensions
{
extension<T1>(IEnumerable<int>) // Error! T1 not inferrable
{
...
}
extension<T2>(IEnumerable<T2>)
{
public bool IsEmpty { get ... }
}
extension<T3>(IEnumerable<T3>?)
{
public bool IsEmpty { get ... } // Error! Duplicate declaration
}
}
此唯一性規則的應用包括同一靜態類別內的經典擴充方法。
為了與延伸宣告內的方法進行比較,參數 this 會被視為接收器規格,以及該接收器類型中提及的任何類型參數,而其餘的類型參數和方法參數會用於方法簽章:
public static class Enumerable
{
public static IEnumerable<TResult> Cast<TResult>(this IEnumerable source) { ... }
extension(IEnumerable source)
{
IEnumerable<TResult> Cast<TResult>() { ... } // Error! Duplicate declaration
}
}
使用量
嘗試延伸成員查閱時,不論接收者類型為何,靜態類別 using內的所有延伸宣告都會將其成員提供為候選。 只有作為解決方案的一部分,接收器類型不相容的候選才會被丟棄。
會在引數類型 (包括實際接收器) 與任何類型參數之間嘗試完整的泛型類型推斷 (結合延伸模組宣告和延伸模組成員宣告) 中的參數。
提供明確類型引數時,它們會用來取代延伸宣告和延伸成員宣告的類型參數。
string[] strings = ...;
var query = strings.Select(s => s.Length); // extension invocation
var query2 = strings.Select<string, int>(s => s.Length); // ... with explicit full set of type arguments
var query3 = Enumerable.Select(strings, s => s.Length); // static method invocation
var query4 = Enumerable.Where<string, int>(strings, s => s.Length); // ... with explicit full set of type arguments
public static class Enumerable
{
extension<TSource>(IEnumerable<TSource> source)
{
public IEnumerable<TResult> Select<TResult>(Func<T, TResult> predicate) { ... }
}
}
與傳統擴充方法類似,發出的實作方法可以靜態叫用。
這可讓編譯器在具有相同名稱和 arity 的延伸模組成員之間消除歧義。
object.M(); // ambiguous
E1.M();
new object().M2(); // ambiguous
E1.M2(new object());
_ = _new object().P; // ambiguous
_ = E1.get_P(new object());
static class E1
{
extension(object)
{
public static void M() { }
public void M2() { }
public int P => 42;
}
}
static class E2
{
extension(object)
{
public static void M() { }
public void M2() { }
public int P => 42;
}
}
靜態擴充方法將像實例擴充方法一樣解析(我們將考慮接收器類型的額外引數)。
擴充屬性將像擴充方法一樣解析,具有單一參數 (接收者參數) 和單一引數 (實際接收者值)。
using static 指示詞
using_static_directive會讓類型宣告中延伸模組區塊的成員可供延伸模組存取。
using static N.E;
new object().M();
object.M2();
_ = new object().Property;
_ = object.Property2;
C c = null;
_ = c + c;
c += 1;
namespace N
{
static class E
{
extension(object o)
{
public void M() { }
public static void M2() { }
public int Property => 0;
public static int Property2 => 0;
}
extension(C c)
{
public static C operator +(C c1, C c2) => throw null;
public void operator +=(int i) => throw null;
}
}
}
class C { }
如同先前,可以直接參考指定類型宣告中直接包含的可存取靜態成員 (擴充方法除外) 。
這意味著實作方法(除了那些是擴充方法的)可以直接用作靜態方法:
using static E;
M();
System.Console.Write(get_P());
set_P(43);
_ = op_Addition(0, 0);
_ = new object() + new object();
static class E
{
extension(object)
{
public static void M() { }
public static int P { get => 42; set { } }
public static object operator +(object o1, object o2) { return o1; }
}
}
using_static_directive仍然不會直接匯入擴充方法作為靜態方法,因此無法直接以靜態方法的形式呼叫非靜態擴充方法的實作方法。
using static E;
M(1); // error: The name 'M' does not exist in the current context
static class E
{
extension(int i)
{
public void M() { }
}
}
OverloadResolutionPriorityAttribute
封閉靜態類別內的延伸成員會根據 ORPA 值進行優先順序。 封閉的靜態類別被視為 ORPA 規則所考慮的「包含類型」。
延伸模組屬性上存在的任何 ORPA 屬性都會複製到屬性存取子的實作方法上,以便在透過消除歧義語法使用這些存取子時遵守優先順序。
切入點
擴充區塊的方法不符合進入點候選的條件(請參閱「7.1 應用程式啟動」)。 注意:實作方法可能仍為候選。
降低
延伸聲明的降低策略不是語言層級的決定。 不過,除了實作語言語意之外,它必須滿足某些需求:
- 在所有情況下都應明確指定產生的類型、成員和元資料的格式,以便其他編譯器可以使用和產生它。
- 產生的成品應該是穩定的,因為合理的後期修改不應破壞針對舊版編譯的取用者。
隨著實施的進展,這些要求需要進一步完善,並且可能需要在極端情況下進行妥協,以便採用合理的實施方法。
宣告的中繼資料
目標
以下設計允許:
- 透過中繼資料來回延伸宣告符號 (完整和參考元件),
- 擴充功能成員的穩定參考 (XML 文件),
- 發出名稱的本地確定(有助於 EnC),
- 公共 API 追蹤。
對於 xml 文件,延伸成員的 docID 是中繼資料中延伸成員的 docID。 例如,is cref="Extension.extension(object).M(int)" 中使用的 M:Extension.<>E__ExtensionGroupingTypeNameForObject.M(System.Int32) docID 和該 docID 在擴展塊的重新編譯和重新排序中是穩定的。 理想情況下,當擴展塊的約束發生變化時,它也會保持穩定,但我們沒有找到一種設計可以實現這一點,而不會對成員衝突的語言設計產生不利影響。
針對 EnC,在本機知道 (只要查看修改的延伸模組成員) 很有用,更新的延伸模組成員會在中繼資料中發出的位置。
對於公共 API 追蹤,更穩定的名稱可減少雜訊。 但從技術上講,在這類案例中,延伸模組分組類型名稱不應該發揮作用。 在查看擴充功能成員 M時,擴充功能分組類型的名稱是什麼並不重要,重要的是它所屬的擴充區塊的簽章。 公用 API 簽章不應被視為 ,而應視為 Extension.<>E__ExtensionGroupingTypeNameForObject.M(System.Int32)Extension.extension(object).M(int)。 換句話說,擴充功能成員應該被視為具有兩組類型參數和兩組參數。
概觀
延伸模組區塊會依其 CLR 層級簽章分組。 每個 CLR 對等群組都會發出為具有內容型名稱的 延伸模組群組類型 。 CLR 對等群組內的延伸模組區塊接著會依 C# 對等進行子分組。 每個 C# 對等群組都會發出為具有內容型名稱的 延伸模組標記類型 ,巢狀在其對應的延伸模組群組類型中。 延伸模組標記類型包含編碼延伸模組參數的單一 延伸模組標記方法 。 延伸模組標記方法及其包含延伸模組標記類型會以完整逼真度編碼延伸模組區塊的簽章。 每個延伸模組成員的宣告都會在正確的延伸模組群組類型中發出,透過屬性依其名稱參考回延伸模組標記類型,並伴隨具有已修改簽章的最上層靜態 實作方法 。
以下是中繼資料編碼的架構化概觀:
[Extension]
static class EnclosingStaticClass
{
[Extension]
public sealed class ExtensionGroupingType1 // has type parameters with minimal constraints sufficient to keep extension member declarations below valid
{
public static class ExtensionMarkerType1 // has re-declared type parameters with full fidelity of C# constraints
{
public static void <Extension>$(... extension parameter ...) // extension marker method
}
... ExtensionMarkerType2, etc ...
... extension members for ExtensionGroupingType1, each points to its corresponding extension marker type ...
}
... ExtensionGroupingType2, etc ...
... implementation methods ...
}
封閉的靜態類別會發出屬性 [Extension] 。
CLR 層級簽章與 C# 層級簽章
延伸模組區塊的 CLR 層級簽章會產生下列項目:
- 將類型參數名稱正規化為
T0、 等T1... - 移除屬性
- 清除參數名稱
- 擦除參數修飾符 (例如
ref、 、inscoped、 ... - 清除元組名稱
- 清除可空性註釋
- 清除
notnull限制
註: 會保留其他約束,例如 new()、 structclassallows ref structunmanaged和類型約束。
延伸模組分組類型
延伸模組群組類型會發出至來源中每一組延伸模組區塊的中繼資料,並具有相同的 CLR 層級簽章。
- 其名稱是無法言說的,並根據 CLR 層級簽章的內容來決定。 更多詳情如下。
- 其類型參數具有正規化名稱 (
T0,T1, ...) 且沒有屬性。 - 它是公開的,是密封的。
- 它標有
specialname旗標和[Extension]屬性。
延伸模組群組類型的內容型名稱是以 CLR 層級簽章為基礎,並包含下列項目:
- 延伸模組參數類型的完整 CLR 名稱。
- 引用的類型參數名稱將被規範化為
T0、 、T1等 ...根據它們在類型聲明中出現的順序。 - 完整名稱將不包含包含元件。 類型通常會在元件之間移動,而且不應中斷 xml 文件參考。
- 引用的類型參數名稱將被規範化為
- 類型參數的條件約束將被包含並排序,以便在原始程式碼中重新排序它們不會改變名稱。 具體來說:
- 類型參數條件約束將按宣告順序列出。 第 N 個類型參數的約束將發生在第 N 個+1 類型參數之前。
- 類型限制將透過按順序比較完整名稱來排序。
- 非類型條件約束會以決定性方式排序,並進行處理,以避免任何歧義或與類型條件約束衝突。
- 由於這不包含屬性,因此它故意忽略 C# 主義,例如元組名稱、可空性等...
注意:保證名稱在重新編譯、重新排序和 C# 主義的變更中保持穩定 (亦即不會影響 CLR 層級簽章) 。
延伸標記類型
標記類型會重新宣告其包含分組類型 (延伸模組分組類型) 的類型參數,以取得延伸模組區塊 C# 檢視的完整逼真度。
延伸模組標記類型會發出至來源中具有相同 C# 層級簽章的每組延伸模組區塊的中繼資料。
- 其名稱是不可言說的,是根據擴充區塊的 C# 層級簽章的內容來決定的。 更多詳情如下。
- 它會重新宣告其包含分組類型的類型參數,以成為來源 (包括名稱和屬性) 中宣告的參數。
- 它是公開的和靜態的。
- 它標
specialname有旗幟。
延伸模組標記類型的內容型名稱是根據下列項目:
- 類型參數的名稱將按照它們在擴展聲明中的出現順序包含在內
- 類型參數的屬性將被包含並排序,以便在原始程式碼中重新排序它們不會變更名稱。
- 類型參數的條件約束將被包含並排序,以便在原始程式碼中重新排序它們不會改變名稱。
- 擴充類型的完整 C# 名稱
- 這將包括可為空的註釋、元組名稱等項目......
- 完整名稱將不包含包含元件
- 擴充參數的名稱
- 延伸參數 ( ,
ref,ref readonly, ...) 的修飾元 (scoped以決定性順序排列) - 以決定性順序套用至延伸參數之任何屬性的完整名稱及屬性引數
注意:保證名稱在重新編譯和重新排序時保持穩定。
附註: 延伸標記類型和延伸標記方法會發出為參考組合的一部分。
擴充標記方法
marker方法的目的是對擴充區塊的擴充參數進行編碼。 因為它是延伸標記類型的成員,所以它可以參考延伸標記類型的重新宣告類型參數。
每個延伸標記類型都包含單一方法,即延伸標記方法。
- 它是靜態的、非泛型的、空隙返回的,稱為
<Extension>$。 - 它的單一參數具有延伸參數中的屬性、參考、類型和名稱。
如果擴充參數未指定名稱,則參數名稱是空的。 - 它標
specialname有旗幟。
標記方法的協助工具會是對應宣告延伸模組成員中限制最少的協助工具, private 如果未宣告任何延伸模組成員,則會使用。
擴充功能成員
來源中擴充區塊中的方法/屬性宣告會表示為中繼資料中擴充分組類型的成員。
- 原始方法的簽章會保留 (包括屬性),但其主體會取代為
throw NotImplementedException()。 - 這些不應該在 IL 中引用。
- 方法、屬性及其存取子會標示為
[ExtensionMarkerName("...")]參考對應至該成員擴充區塊的延伸標記類型名稱。
實作方法
來源中擴充區塊中方法/屬性宣告的方法主體會發出為最上層靜態類別中的靜態實作方法。
- 實作方法與原始方法具有相同的名稱。
- 它具有衍生自擴充區塊的類型參數,這些參數會附加到原始方法的類型參數 (包括屬性) 的前面。
- 它具有與原始方法相同的可訪問性和屬性。
- 如果它實作靜態方法,則具有相同的參數和傳回類型。
- 如果實作實例方法,則會在原始方法的簽章中加上參數。 此參數的屬性、參考度、類型和名稱衍生自相關擴充區塊中宣告的擴充參數。
- 實作方法中的參數是指實作方法所擁有的類型參數,而不是擴充區塊的參數。
- 如果原始成員是實例普通方法,則實作方法會標示屬性
[Extension]。
ExtensionMarkerName 屬性
該 ExtensionMarkerNameAttribute 類型僅供編譯器使用 - 源代碼中不允許它。 如果編譯中尚未包含類型宣告,則由編譯器合成。
namespace System.Runtime.CompilerServices;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Interface | AttributeTargets.Delegate, Inherited = false)]
public sealed class ExtensionMarkerNameAttribute : Attribute
{
public ExtensionMarkerNameAttribute(string name)
=> Name = name;
public string Name { get; }
}
附註: 雖然包含某些屬性目標以備將來 (延伸巢狀類型、延伸欄位、延伸事件),但 AttributeTargets.Constructor 不會包含在內,因為延伸建構函式不會是建構函式。
Example
注意:為了可讀性,我們使用簡化的內容名稱。 注意:由於 C# 無法表示類型參數重新宣告,因此表示元資料的程式碼不是有效的 C# 程式碼。
以下範例說明了在沒有成員的情況下分組的運作方式:
class E
{
extension<T>(IEnumerable<T> source)
{
... member in extension<T>(IEnumerable<T> source)
}
extension<U>(ref IEnumerable<U?> p)
{
... member in extension<U>(ref IEnumerable<U?> p)
}
extension<T>(IEnumerable<U> source)
where T : IEquatable<U>
{
... member in extension<T>(IEnumerable<U> source) where T : IEquatable<U>
}
}
會發出為
[Extension]
class E
{
[Extension, SpecialName]
public sealed class <>E__ContentName_For_IEnumerable_T<T0>
{
[SpecialName]
public static class <>E__ContentName1 // note: re-declares type parameter T0 as T
{
[SpecialName]
public static void <Extension>$(IEnumerable<T> source) { }
}
[SpecialName]
public static class <>E__ContentName2 // note: re-declares type parameter T0 as U
{
[SpecialName]
public static void <Extension>$(ref IEnumerable<U?> p) { }
}
[ExtensionMarkerName("<>E__ContentName1")]
... member in extension<T>(IEnumerable<T> source)
[ExtensionMarkerName("<>E__ContentName2")]
... member in extension<U>(ref IEnumerable<U?> p)
}
[Extension, SpecialName]
public sealed class <>ContentName_For_IEnumerable_T_With_Constraint<T0>
where T0 : IEquatable<T0>
{
[SpecialName]
public static class <>E__ContentName3 // note: re-declares type parameter T0 as U
{
[SpecialName]
public static void <Extension>$(IEnumerable<U> source) { }
}
[ExtensionMarkerName("ContentName3")]
public static bool IsPresent(U value) => throw null!;
}
... implementation methods
}
以下是說明如何發出成員的範例:
static class IEnumerableExtensions
{
extension<T>(IEnumerable<T> source) where T : notnull
{
public void Method() { ... }
internal static int Property { get => ...; set => ...; }
public int Property2 { get => ...; set => ...; }
}
extension(IAsyncEnumerable<int> values)
{
public async Task<int> SumAsync() { ... }
}
public static void Method2() { ... }
}
會發出為
[Extension]
static class IEnumerableExtensions
{
[Extension, SpecialName]
public sealed class <>E__ContentName_For_IEnumerable_T<T0>
{
// Extension marker type is emitted as a nested type and re-declares its type parameters to include C#-isms
// In this example, the type parameter `T0` is re-declared as `T` with a `notnull` constraint:
// .class <>E__IEnumerableOfT<T>.<>E__ContentName_For_IEnumerable_T_Source
// .typeparam T
// .custom instance void NullableAttribute::.ctor(uint8) = (...)
[SpecialName]
public static class <>E__ContentName_For_IEnumerable_T_Source
{
[SpecialName]
public static <Extension>$(IEnumerable<T> source) => throw null;
}
[ExtensionMarkerName("<>E__ContentName_For_IEnumerable_T_Source")]
public void Method() => throw null;
[ExtensionMarkerName("<>E__ContentName_For_IEnumerable_T_Source")]
internal static int Property
{
[ExtensionMarkerName("<>E__ContentName_For_IEnumerable_T_Source")]
get => throw null;
[ExtensionMarkerName("<>E__ContentName_For_IEnumerable_T_Source")]
set => throw null;
}
[ExtensionMarkerName("<>E__ContentName_For_IEnumerable_T_Source")]
public int Property2
{
[ExtensionMarkerName("<>E__ContentName_For_IEnumerable_T_Source")]
get => throw null;
[ExtensionMarkerName("<>E__ContentName_For_IEnumerable_T_Source")]
set => throw null;
}
}
[Extension, SpecialName]
public sealed class <>E__ContentName_For_IAsyncEnumerable_Int
{
[SpecialName]
public static class <>E__ContentName_For_IAsyncEnumerable_Int_Values
{
[SpecialName]
public static <Extension>$(IAsyncEnumerable<int> values) => throw null;
}
[ExtensionMarkerName("<>E__ContentName_For_IAsyncEnumerable_Int_Values")]
public Task<int> SumAsync() => throw null;
}
// Implementation for Method
[Extension]
public static void Method<T>(IEnumerable<T> source) { ... }
// Implementation for Property
internal static int get_Property<T>() { ... }
internal static void set_Property<T>(int value) { ... }
// Implementation for Property2
public static int get_Property2<T>(IEnumerable<T> source) { ... }
public static void set_Property2<T>(IEnumerable<T> source, int value) { ... }
// Implementation for SumAsync
[Extension]
public static int SumAsync(IAsyncEnumerable<int> values) { ... }
public static void Method2() { ... }
}
每當在源代碼中使用擴展成員時,我們都會發出這些成員作為實作方法的參考。
例如:的 enumerableOfInt.Method() 呼叫會以靜態呼叫 IEnumerableExtensions.Method<int>(enumerableOfInt)的形式發出。
XML 文件
擴充區塊上的 doc 註解會針對標記類型發出 (擴充區塊的 DocID 位於 E.<>E__MarkerContentName_For_ExtensionOfT'1 下列範例中)。
允許它們分別使用 和 <paramref> 引用<typeparamref>擴展參數和類型參數)。
註: 您不得在延伸構件上記載延伸參數或類型參數 (with <param> 和 <typeparam>)。
如果兩個延伸區塊發出為一種標記類型,則它們的文件註解也會合併。
使用 xml 文件的工具負責 <param> 視需要將 和 <typeparam> 從擴充區塊複製到擴充功能成員上 (亦即,參數資訊應該只複製實例成員) 。
在實作方法上發出, <inheritdoc> 它會 cref以 . 例如,getter 的實作方法是指擴充屬性的文件。
如果延伸模組成員沒有文件註解,則會省略 。<inheritdoc>
對於擴充區塊和擴充功能成員,如果出現以下情況,我們目前不會發出警告:
- 已記錄延伸模組參數,但延伸模組成員上的參數不會
- 反之亦然
- 或在具有未記載類型參數的對等案例中
例如,以下文檔註釋:
/// <summary>Summary for E</summary>
static class E
{
/// <summary>Summary for extension block</summary>
/// <typeparam name="T">Description for T</typeparam>
/// <param name="t">Description for t</param>
extension<T>(T t)
{
/// <summary>Summary for M, which may refer to <paramref name="t"/> and <typeparamref name="T"/></summary>
/// <typeparam name="U">Description for U</typeparam>
/// <param name="u">Description for u</param>
public void M<U>(U u) => throw null!;
/// <summary>Summary for P</summary>
public int P => 0;
}
}
產生下列 xml:
<?xml version="1.0"?>
<doc>
<assembly>
<name>Test</name>
</assembly>
<members>
<member name="T:E">
<summary>Summary for E</summary>
</member>
<member name="T:E.<>E__MarkerContentName_For_ExtensionOfT`1">
<summary>Summary for extension block</summary>
<typeparam name="T">Description for T</typeparam>
<param name="t">Description for t</param>
</member>
<member name="M:E.<>E__MarkerContentName_For_ExtensionOfT`1.M``1(``0)">
<summary>Summary for M, which may refer to <paramref name="t"/> and <typeparamref name="T"/></summary>
<typeparam name="U">Description for U</typeparam>
<param name="u">Description for u</param>
</member>
<member name="P:E.<>E__MarkerContentName_For_ExtensionOfT`1.P">
<summary>Summary for P</summary>
</member>
<member name="M:E.M``2(``0,``1)">
<inheritdoc cref="M:E.<>E__MarkerContentName_For_ExtensionOfT`1.M``1(``0)"/>
</member>
<member name="M:E.get_P``1(``0)">
<inheritdoc cref="P:E.<>E__MarkerContentName_For_ExtensionOfT`1.P"/>
</member>
</members>
</doc>
CREF 參考
我們可以將擴充區塊視為巢狀類型,可以透過其簽章來尋址(就好像它是具有單一擴充參數的方法一樣)。
範例: E.extension(ref int).M().
但 cref 無法解決擴展塊本身。
E.extension(int) 可以參考類型 E中名為「extension」的方法。
static class E
{
extension(ref int i)
{
void M() { } // can be addressed by cref="E.extension(ref int).M()" or cref="extension(ref int).M()" within E, but not cref="M()"
}
extension(ref int i)
{
void M(int i2) { } // can be addressed by cref="E.extension(ref int).M(int)" or cref="extension(ref int).M(int)" within E
}
}
已知查閱會尋找所有相符的延伸區塊。
由於我們不允許對擴展成員進行無條件的引用,因此 cref 也會不允許它們。
語法會是:
member_cref
: conversion_operator_member_cref
| extension_member_cref // added
| indexer_member_cref
| name_member_cref
| operator_member_cref
;
extension_member_cref // added
: 'extension' type_argument_list? cref_parameter_list '.' member_cref
;
qualified_cref
: type '.' member_cref
;
cref
: member_cref
| qualified_cref
| type_cref
;
在頂層 (extension_member_cref) 使用或巢狀在另一個擴充功能 (extension(int).M) 中是錯誤E.extension(int).extension(string).M。
重大突破性變更
類型和別名不得命名為「延伸」。
未解決的問題
文件中與未決問題相關的臨時部分,包括對未最終語法和替代設計的討論
- 存取擴充功能成員時,我們是否應該調整接收者需求? (評論)
-
確認(答案:extensionvs.extensions作為關鍵字extension,LDM 2025-03-24) -
確認我們要禁止(答案:是,不允許,LDM 2025-06-11)[ModuleInitializer] -
確認我們可以丟棄擴展塊作為進入點候選(答案:是的,丟棄,LDM 2025-06-11) -
確認 LangVer 邏輯(跳過新擴展,而不是在選擇時考慮並報告它們)(答案:無條件綁定並報告 LangVer 錯誤,實例擴展方法除外,LDM 2025-06-11) (答:區塊合併時doc註解會靜默合併,不需要partial合併並合併其文檔註釋的擴展塊是否應該需要?partial,郵件確認 2025-09-03)確認成員不應該以包含或擴充類型命名。(答:是,通過電子郵件確認 2025-09-03)
根據可攜性問題重新審視分組/衝突規則: https://github.com/dotnet/roslyn/issues/79043
(答案:此案例已解決為具有內容型類型名稱的新中繼資料設計的一部分,允許)
目前的邏輯是將具有相同接收器類型的擴充區塊分組。 這不考慮限制。 這會導致此案例出現可攜性問題:
static class E
{
extension<T>(ref T) where T : struct
void M()
extension<T>(T) where T : class
void M()
}
建議是使用我們針對延伸模組分組類型設計所規劃的相同分組邏輯,也就是考慮 CLR 層級條件約束 (,即忽略 notnull、元組名稱、可 Null 性註解) 。
refness 應該編碼在分組類型名稱中嗎?
- 未
包含在擴展分組類型名稱中的審查提案答案:通過電子郵件確認 2025-09-03)ref(需要在工作小組重新審視分組/衝突規則後進一步討論,LDM 2025-06-23)(
public static class E
{
extension(ref int)
{
public static void M()
}
}
它發出為:
public static class E
{
public static class <>ExtensionTypeXYZ
{
.. marker method ...
void M()
}
}
而第三方 CREF 引用的發出為 E.extension(ref int).MM:E.<>ExtensionGroupingTypeXYZ.M() If ref 被刪除或添加到擴展參數中,我們可能不希望 CREF 中斷。
我們不太關心這種情況,因為任何作為擴展的用法都會是歧義:
public static class E
{
extension(ref int)
static void M()
extension(int)
static void M()
}
但我們關心這種情況(為了可移植性和實用性),這應該適用於我們調整衝突規則後建議的元數據設計:
static class E
{
extension<T>(ref T) where T : struct
void M()
extension<T>(T) where T : class
void M()
}
不考慮引用性有一個缺點,因為在這種情況下我們失去了可移植性:
static class E
{
extension<T>(ref T)
void M()
extension<T>(T)
void M()
}
// portability issue: since we're grouping without accounting for refness, the emitted extension members conflict (not implementation members). Mitigation: keep as classic extensions or split to another static class
name的
我們是否應該像傳統和新的擴展方法一樣禁止 nameof 中的擴展屬性?(答案:我們想要使用 'nameof(EnclosingStaticClass.ExtensionMember)。 需要設計,可能是來自 .NET 10 的平底船。 LDM 2025-06-11)
型樣型建構
Methods
新的擴展方法應該在哪裡發揮作用?(答案:經典擴展方法發揮作用的地方相同,LDM 2025-05-05)
這包括:
-
GetEnumerator/GetAsyncEnumerator在foreach -
Deconstruct在解構中,在位置模式中和 foreach -
Addin 集合初始化運算式 -
GetPinnableReference中的fixed -
GetAwaiter中的await
這不包括:
-
Dispose/DisposeAsync在using和foreach -
MoveNext/MoveNextAsync在foreach -
Slice隱式索引器中的索引器(int可能還有清單模式? -
GetResult中的await
屬性和索引子
延伸模組屬性和索引子應該在哪裡發揮作用?(答:先從四開始吧,LDM 2025-05-05)
我們將包括:
- 物件初始化運算式:
new C() { ExtensionProperty = ... } - Dictionary Intializer:
new C() { [0] = ... } -
with:x with { ExtensionProperty = ... } - 屬性模式:
x is { ExtensionProperty: ... }
我們會排除:
-
Current中的foreach -
IsCompleted中的await -
Count/Lengthlist-pattern 中的屬性和索引子 -
Count/Length隱含索引子中的屬性和索引子
委派傳回屬性
確認此圖形的延伸屬性應該只在 LINQ 查詢中發揮作用,以符合實例屬性的作用。(答案:有道理,LDM 2025-04-06)
清單和傳播模式
- 確認延伸模組
Index/Range索引子應該在清單模式中播放 (答案:與 C# 14 無關)
重新審視延伸模組屬性發揮作用的地方Count/Length
集合運算式
- 擴展
Add工作 - 擴展
GetEnumerator適用於傳播 - 延伸不
GetEnumerator會影響元素類型的判斷(必須是實例) - 靜態
Create擴充方法不應算作祝福 的建立 方法 - 擴充可數屬性應該影響集合運算式嗎?
params 收藏
- 延伸模組
Add不會影響允許的類型params
字典運算式
- 確認延伸模組索引子不會在字典運算式中發揮作用,因為索引子的存在是定義字典類型的不可或缺的一部分。 (答案:與 C# 14 無關)
extern
-
我們計劃允許(答案:已批准,LDM 2025-06-23)extern可移植性: https://github.com/dotnet/roslyn/issues/78572
延伸類型的命名/編號配置
Issue
目前的編號系統會導致 公用 API 的驗證 發生問題,以確保公用 API 在僅參考元件和實作元件之間相符。
我們應該進行以下更改之一嗎? (答案:我們正在採用基於內容的命名方案來提高公共 API 的穩定性,並且工具仍需要更新以考慮標記方法)
- 調整工具
- 使用一些基於內容的命名方案 (TBD)
- 讓名稱透過一些語法來控制
新的泛型延伸模組 Cast 方法仍然無法在 LINQ 中運作
Issue
在角色/延伸模組的早期設計中,只能明確指定方法的類型引數。
但現在我們專注於從傳統擴展方法的無形轉換,因此必須明確給出所有類型參數。
這無法解決 LINQ 中延伸模組 Cast 方法使用的問題。
我們是否應該更改擴展功能以適應這種情況? (答:不,這不會導致我們重新審視擴展解析設計,LDM 2025-05-05)
限制延伸功能成員上的延伸參數
我們應該允許以下內容嗎? (答:不可以,這個可以稍後添加)
static class E
{
extension<T>(T t)
{
public void M<U>(U u) where T : C<U> { } // error: 'E.extension<T>(T).M<U>(U)' does not define type parameter 'T'
}
}
public class C<T> { }
空值可設定
-
確認當前設計,即最大的便攜性/兼容性(答案:是,LDM 2025-04-17)
extension([System.Diagnostics.CodeAnalysis.DoesNotReturnIf(false)] bool b)
{
public void AssertTrue() => throw null!;
}
extension([System.Diagnostics.CodeAnalysis.NotNullIfNotNull("o")] ref int? i)
{
public void M(object? o) => throw null!;
}
後設資料
骨架方法應該拋出(答案:是,LDM 2025-04-17)NotSupportedException還是其他一些標準異常(現在我們這樣做throw null;)?我們是否應該在元數據中的標記方法中接受多個參數(以防新版本添加更多信息)?(答:我們可以保持嚴格,LDM 2025-04-17)擴展標記或可說的實現方法是否應該用特殊名稱標記?(答:標記方法要標註特殊名稱,我們應該檢查,但不能檢查實現方法,LDM 2025-04-17)即使裡面沒有實例擴展方法,我們是否應該在靜態類上添加(答案:是,LDM 2025-03-10)[Extension]屬性?確認我們也應該將屬性新增至(答案:否,LDM 2025-03-10)[Extension]實作 getter 和 setter。-
確認延伸模組類型應該標示為特殊名稱,且編譯器會在中繼資料中需要此旗標 (這是預覽版的重大變更)(答案:已核准,LDM 2025-06-23)
靜態工廠案例
靜態方法的衝突規則是什麼?(答案:包含靜態類型使用現有的C#規則,不鬆弛,LDM 2025-03-17)
查詢
既然我們有了可說出的實作名稱,如何解決實例方法調用?我們更喜歡骨架方法而不是其相應的實現方法。如何解決靜態擴展方法?(答:就像實例擴展方法一樣,LDM 2025-03-03)如何解析屬性?(以粗略的方式回答 LDM 2025-03-03,但需要跟進以改善)-
擴充參數和類型參數的範圍和陰影規則(答案:在擴充區塊範圍內,不允許陰影,LDM 2025-03-10) ORPA 應如何應用於新的擴展方法?(答:將擴充區塊視為透明,ORPA的「包含類型」是封閉的靜態類,LDM 2025-04-17)
public static class Extensions
{
extension(Type1)
{
[OverloadResolutionPriority(1)]
public void Overload(...)
}
extension(Type2)
{
public void Overload(...)
}
}
ORPA 是否應該適用於新的擴建房產?(答案:是的,ORPA 應該複製到實施方法上,LDM 2025-04-23)
public static class Extensions
{
extension(int[] i)
{
public P { get => }
}
extension(ReadOnlySpan<int> r)
{
[OverloadResolutionPriority(1)]
public P { get => }
}
}
- 如何重構經典的擴展解析規則? 我們做
- 更新傳統擴充方法的標準,並使用它來描述新的擴充方法,
- 保留經典擴展方法的現有語言,也用它來描述新的擴展方法,但兩者都有已知的規格偏差,
- 經典擴展方法保留現有語言,但新擴展方法使用不同的語言,並且只有經典擴展方法有已知的規格偏差?
-
確認我們要禁止屬性存取上的明確類型參數(答案:沒有具有明確類型參數的屬性訪問,在 WG 中討論)
string s = "ran";
_ = s.P<object>; // error
static class E
{
extension<T>(T t)
{
public int P => 0;
}
}
-
確認我們希望即使接收者是類型,也要套用更好度規則(答案:解析靜態擴充功能成員時應考慮僅類型擴充參數,LDM 2025-06-23)
int.M();
static class E1
{
extension(int)
{
public static void M() { }
}
}
static class E2
{
extension(in int i)
{
public static void M() => throw null;
}
}
-
確認當方法和屬性都適用時,我們可以接受模稜兩可(答案:我們應該設計一個比現狀更好的提案,從 .NET 10 中踢出,LDM 2025-06-23) - 在
判斷獲勝成員類型之前,確認我們不想要所有成員的改善(答案:從 .NET 10 中移出,WG 2025-07-02)
string s = null;
s.M(); // error
static class E
{
extension(string s)
{
public System.Action M => throw null;
}
extension(object o)
{
public string M() => throw null;
}
}
我們在擴展聲明中是否有隱含接收器?(答:沒有,之前在LDM討論過)
static class E
{
extension(object o)
{
public void M()
{
M2();
}
public void M2() { }
}
}
我們應該允許查找類型參數嗎?(討論)(答案:不,我們要等待反饋,LDM 2025-04-16)
可及性
延伸聲明中協助工具的意義為何?(答案:擴充宣告不算作協助工具範圍,LDM 2025-03-17)我們是否應該對接收器參數應用“不一致的可訪問性”檢查,即使是靜態成員?(答案:是,LDM 2025-04-17)
public static class Extensions
{
extension(PrivateType p)
{
// We report inconsistent accessibility error,
// because we generate a `public static void M(PrivateType p)` implementation in enclosing type
public void M() { }
public static void M2() { } // should we also report here, even though not technically necessary?
}
private class PrivateType { }
}
延伸聲明驗證
我們是否應該放寬只有方法的類型參數驗證(可推斷性:所有類型參數都必須出現在擴展參數的類型中)?(答案:是,LDM 2025-04-06)這將允許移植 100% 的經典擴展方法。
如果您有TResult M<TResult, TSource>(this TSource source),您可以將其移植為extension<TResult, TSource>(TSource source) { TResult M() ... }。確認擴充功能是否應該允許僅初始化存取器(答案:暫時不允許可以,LDM 2025-04-17)是否應該允許(答案:否,保持規範規則,LDM 2025-03-24)extension(int receiver) { public void M2() {} }extension(ref int receiver) { public void M2() {} }接收器引用性的唯一差異?我們應該抱怨這樣的(答案:是的,保持規範規則,LDM 2025-03-24)extension(object receiver) { public int P1 => 1; }extension(object receiver) { public int P1 {set{}} }衝突嗎?我們是否應該抱怨骨架方法之間的衝突,而這些衝突不是實作方法之間的衝突?(答案:是的,保持規範規則,LDM 2025-03-24)
static class E
{
extension(object)
{
public void Method() { }
public static void Method() { }
}
}
目前的衝突規則是: 1. 使用類別/結構規則檢查類似擴充功能內沒有衝突,2. 檢查各種延伸模組宣告的實作方法之間沒有衝突。
我們仍然需要規則的第一部分嗎?(答案:是的,我們保留此結構,因為它有助於使用 API,LDM 2025-03-24)
XML 文件
(答案:是,擴充功能成員允許將 paramref 轉換為擴充參數,LDM 2025-05-05)paramref擴充功能成員是否支援接收器參數? 即使是靜態的? 它在輸出中是如何編碼的? 標準方法<paramref name="..."/>可能適用於人類,但存在一些現有工具不樂意在 API 的參數中找到它的風險。我們應該將 doc 註釋複製到具有可說出名稱的實現方法中嗎?(答案:禁止複製,LDM 2025-05-05)是否應該(答案:禁止複製,LDM 2025-05-05)<param>從實例方法的擴展容器複製與接收器參數相對應的元素? 還有什麼東西應該從容器複製到實現方法(<typeparam>等)?是否應該(答案:沒有,暫時,LDM 2025-05-05)<param>允許在延伸模組成員上允許 for 延伸模組參數作為覆寫?- 擴展塊的摘要會出現在任何地方嗎?
CREF
-
確認語法(答案:提案不錯,LDM 2025-06-09) 是否應該可以引用擴展塊 ((答案:否,LDM 2025-06-09)E.extension(int))?是否應該可以使用非限定語法來引用成員:(答案:是,LDM 2025-06-09)extension(int).Member?- 我們是否應該為難以言喻的名稱使用不同的字符,以避免 XML 轉義? (答:遵照工作小組,LDM 2025-06-09)
確認對骨架和實作方法的引用都是可能的:(答案:是,LDM 2025-06-09)E.Mvs。E.extension(int).M兩者似乎都是必要的(擴展屬性和經典擴展方法的可移植性)。擴充功能中繼資料名稱是否對版本控制文件有問題?(答案:是的,我們將放棄序數,使用基於內容的穩定命名方案)
新增對更多成員類型的支援
我們不需要一次實現所有這些設計,但可以一次處理一種或幾種成員類型。 根據我們核心函式庫中的已知場景,我們應該按照以下順序進行工作:
- 屬性和方法 (實例和靜態)
- 運營商
- 索引器(實例和靜態,可以在較早的時間點機會主義地完成)
- 其他任何
我們想為其他類型的成員提前加載設計嗎?
extension_member_declaration // add
: constant_declaration
| field_declaration
| method_declaration
| property_declaration
| event_declaration
| indexer_declaration
| operator_declaration
| constructor_declaration
| finalizer_declaration
| static_constructor_declaration
| type_declaration
;
巢狀類型
如果我們確實選擇繼續使用擴展嵌套類型,以下是先前討論中的一些註釋:
- 如果兩個延伸模組宣告宣告具有相同名稱和 arity 的巢狀延伸模組類型,就會發生衝突。 我們沒有在元數據中表示這一點的解決方案。
- 我們討論的元資料粗略方法:
- 我們會發出具有原始類型參數且沒有成員的骨架巢狀類型
- 我們會發出實作巢狀類型,其中包含來自延伸模組宣告的前置類型參數,以及所有成員實作,因為它們出現在來源中 (類型參數的取模參考)
建構函數
建構函式通常在 C# 中描述為實例成員,因為它們的主體可以透過關鍵字存取 this 新建立的值。
不過,這不適用於實例延伸模組成員的參數型方法,因為沒有要傳入為參數的先前值。
相反地,延伸建構函式的工作方式更像靜態工廠方法。
它們被視為靜態成員,因為它們不相依於接收器參數名稱。
他們的主體需要明確創建並返回構建結果。
成員本身仍會使用建構函式語法宣告,但不能有 this 或 base 初始設定式,而且不依賴具有可存取建構函式的接收器類型。
這也表示可以針對沒有自己的建構函式的類型宣告延伸建構函式,例如介面和列舉類型:
public static class Enumerable
{
extension(IEnumerable<int>)
{
public static IEnumerable(int start, int count) => Range(start, count);
}
public static IEnumerable<int> Range(int start, int count) { ... }
}
允許:
var range = new IEnumerable<int>(1, 100);
較短的表格
建議的設計避免了接收器規範的每個成員重複,但最終確實會使擴展成員嵌套在靜態類 和 擴展聲明中兩個深度。 靜態類別只包含一個擴充宣告,或延伸宣告只包含一個成員,這似乎很常見,而且我們允許這些案例的語法縮寫似乎是合理的。
合併靜態類別和擴充宣告:
public static class EmptyExtensions : extension(IEnumerable source)
{
public bool IsEmpty => !source.GetEnumerator().MoveNext();
}
這最終看起來更像我們所說的「基於類型」的方法,其中擴展成員的容器本身是命名的。
合併擴充宣告和擴充成員:
public static class Bits
{
extension(ref ulong bits) public bool this[int index]
{
get => (bits & Mask(index)) != 0;
set => bits = value ? bits | Mask(index) : bits & ~Mask(index);
}
static ulong Mask(int index) => 1ul << index;
}
public static class Enumerable
{
extension<TSource>(IEnumerable<TSource> source) public IEnumerable<TSource> Where(Func<TSource, bool> predicate) { ... }
}
這最終看起來更像我們所說的「基於成員」的方法,其中每個擴展成員都包含自己的接收器規範。