共用方式為


重載解析優先順序

注意

本文是功能規格。 規格可作為功能的設計檔。 其中包含建議的規格變更,以及功能設計和開發期間所需的資訊。 這些文章會發佈,直到提議的規格變更完成並併併入目前的ECMA規格為止。

功能規格與已完成實作之間可能有一些差異。 這些差異被記錄在相關的 語言設計會議(LDM)紀錄中。

您可以在關於 規格的文章中深入瞭解將功能規格採納到 C# 語言標準的過程

冠軍問題:https://github.com/dotnet/csharplang/issues/7706

總結

我們引進了新的屬性 System.Runtime.CompilerServices.OverloadResolutionPriority,API 作者可以使用此屬性來調整單一類型內多載的相對優先順序,作為引導 API 取用者使用特定 API 的方法,即使這些 API 通常被視為模棱兩可,或 C# 的多載解析規則不會選擇。

動機

API 作者通常會在成員淘汰后遇到該怎麼做的問題。 基於向後相容性的目的,許多人會保留現有的成員,將 ObsoleteAttribute 設定為持續錯誤,以避免中斷在運行時間升級二進位檔的使用者。 這特別影響外掛系統,其中外掛的作者無法控制外掛執行的環境。 環境的建立者可能會想要保留較舊的方法,但封鎖存取任何新開發的程序代碼。 不過,ObsoleteAttribute 本身是不夠的。 在多載解析中,類型或成員仍然可見,這可能在有一個完全合適的替代方案時,導致不必要的多載解析失敗。這是因為該替代方案要麼與已淘汰的成員之間產生歧義,要麼是因為已淘汰成員的存在使得多載解析提前結束,未能考慮合適的成員。 基於此目的,我們想要讓 API 作者能夠引導多載解析解決模棱兩可的問題,以便他們能夠發展其 API 介面區域,並引導使用者前往高效能的 API,而不需要危害用戶體驗。

基類庫(BCL)團隊提供了幾個範例,證明這一點非常有用。 一些 (假設) 範例如下:

  • 建立一個使用 CallerArgumentExpression 來獲取正在斷言的表達式的 Debug.Assert 多載,以便將其包含在訊息中,並使其優先於現有的多載。
  • string.IndexOf(string, StringComparison = Ordinal) 優先於 string.IndexOf(string)。 這需要作為潛在的破壞性變更來討論,但有一些考量認為這是更好的默認選擇,而且更有可能是使用者的意圖。
  • 此提案和 CallerAssemblyAttribute 的組合可讓隱含呼叫者身份的方法避免昂貴的堆疊遍歷。 Assembly.Load(AssemblyName) 今天這樣做,它可能更有效率。
  • Microsoft.Extensions.Primitives.StringValues 會顯示隱式轉換至 stringstring[]。 這表示當傳遞至具有 params string[]params ReadOnlySpan<string> 多載的方法時,這是模棱兩可的。 這個屬性可用來設定其中一個多載的優先順序,以避免模棱兩可。

詳細設計

重載解析優先順序

我們會定義新的概念,overload_resolution_priority,這個概念會在解析方法群組的過程中使用。 overload_resolution_priority 是32位整數值。 根據預設,所有方法的 overload_resolution_priority 都設為 0,而且可以藉由將 OverloadResolutionPriorityAttribute 套用至方法來變更。 我們更新 C# 規範的區段 §12.6.4.1,如下所示(粗體字的變更):

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

  • 首先,候選函式成員集合會縮減為與指定自變數清單相關的函式成員()。 如果這個縮減的集合是空的,就會發生編譯時期錯誤。
  • 然後,縮減後的候選成員集會根據宣告類型分組。 在每個群組內:
    • 候選函式成員會依據 overload_resolution_priority進行排序。 如果成員是覆寫,則 overload_resolution_priority 來自該成員的最低衍生宣告。
    • 將所有在其宣告類型群組內的成員中,overload_resolution_priority 低於最高值的成員移除。
  • 縮減的群組接著會重新組合到最後一組適用的候選函式成員。
  • 然後,會找到一組適用候選函式成員中的最佳函式成員。 如果集合只包含一個函式成員,則該函式成員是最佳函式成員。 否則,最佳的函式成員是相較於指定參數清單的所有其他函式成員更優越的那個函式成員,前提是每個函式成員都依照 §12.6.4.3的規則與所有其他函式成員進行比較。 如果沒有完全比所有其他函式成員更好的函式成員,則函式成員調用模棱兩可,而且會發生系結時間錯誤。

例如,這項功能會導致下列代碼段列印 「Span」,而不是 「Array」:

using System.Runtime.CompilerServices;

var d = new C1();
int[] arr = [1, 2, 3];
d.M(arr); // Prints "Span"

class C1
{
    [OverloadResolutionPriority(1)]
    public void M(ReadOnlySpan<int> s) => Console.WriteLine("Span");
    // Default overload resolution priority
    public void M(int[] a) => Console.WriteLine("Array");
}

這項變更的效果是,就像對大部分衍生類型進行修剪一樣,我們會針對重載解析的優先權新增最終修剪。 由於這個剪除發生在過載解析過程的結尾,所以它確實表示基底類型無法使其成員優先於任何衍生類型。 這是刻意的,並防止發生軍備競賽,其中基底類型可能會試圖總是要比派生類型更好。 例如:

using System.Runtime.CompilerServices;

var d = new Derived();
d.M([1, 2, 3]); // Prints "Derived", because members from Base are not considered due to finding an applicable member in Derived

class Base
{
    [OverloadResolutionPriority(1)]
    public void M(ReadOnlySpan<int> s) => Console.WriteLine("Base");
}

class Derived : Base
{
    public void M(int[] a) => Console.WriteLine("Derived");
}

允許使用負數,而且可用來將特定多載標示為比其他所有預設多載更差。

成員的 overload_resolution_priority 來自該成員的衍生最低宣告。 overload_resolution_priority 不會從任何類型成員可能實作的介面成員繼承或推斷,假設有一個成員 Mx 實作介面成員 Mi,如果 MxMi 有不同的 overload_resolution_priorities,則不會發出任何警告。

NB:此規則的意圖是複製 params 修飾詞的行為。

System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute

我們引入以下屬性到 BCL:

namespace System.Runtime.CompilerServices;

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public sealed class OverloadResolutionPriorityAttribute(int priority) : Attribute
{
    public int Priority => priority;
}

C# 中的所有方法都有預設的 overload_resolution_priority 為 0,除非它們被賦予 OverloadResolutionPriorityAttribute屬性。 如果它們具備該屬性,則其 overload_resolution_priority 是賦予該屬性的第一個參數的整數值。

OverloadResolutionPriorityAttribute 套用至下列位置時發生錯誤:

  • 非索引器屬性
  • 屬性、索引器或事件存取子
  • 轉換運算元
  • Lambda
  • 區域函式
  • 終結器
  • 靜態建構函式

C# 會忽略元數據中這些位置上遇到的屬性。

OverloadResolutionPriorityAttribute 套用至會被忽略的位置時發生錯誤,例如在覆寫基底方法時,因為優先順序是從成員的最不衍生的宣告中讀取。

NB:這刻意不同於 params 修飾詞的作用,後者在被忽略時允許重新指定或新增。

成員的可呼叫性

OverloadResolutionPriorityAttribute 的一個重要注意事項是,這可能導致某些成員無法從源頭進行調用。 例如:

using System.Runtime.CompilerServices;

int i = 1;
var c = new C3();
c.M1(i); // Will call C3.M1(long), even though there's an identity conversion for M1(int)
c.M2(i); // Will call C3.M2(int, string), even though C3.M1(int) has less default parameters

class C3
{
    public void M1(int i) {}
    [OverloadResolutionPriority(1)]
    public void M1(long l) {}

    [Conditional("DEBUG")]
    public void M2(int i) {}
    [OverloadResolutionPriority(1), Conditional("DEBUG")]
    public void M2(int i, [CallerArgumentExpression(nameof(i))] string s = "") {}

    public void M3(string s) {}
    [OverloadResolutionPriority(1)]
    public void M3(object o) {}
}

針對這些範例,預設優先順序多載實際上變得幾乎沒有作用,需要額外的步驟和努力才能被呼叫:

  • 將 方法轉換成委派,然後使用該委派。
    • 對於某些參考型別差異案例,例如優先於 M3(string)M3(object),此策略將會失敗。
    • 條件式方法,例如 M2,也無法使用此策略呼叫,因為條件式方法無法轉換成委派。
  • 使用 UnsafeAccessor 運行時間功能,透過比對簽章呼叫它。
  • 手動使用反射來取得方法的引用,然後調用它。
  • 未重新編譯的程式代碼會繼續呼叫舊方法。
  • 手寫的 IL 可以指定它所選擇的任何內容。

未解決的問題

擴充方法分組(已回答)

按照目前的描述,擴充方法僅在其自身的類型中依照優先順序進行排序。 例如:

new C2().M([1, 2, 3]); // Will print Ext2 ReadOnlySpan

static class Ext1
{
    [OverloadResolutionPriority(1)]
    public static void M(this C2 c, Span<int> s) => Console.WriteLine("Ext1 Span");
    [OverloadResolutionPriority(0)]
    public static void M(this C2 c, ReadOnlySpan<int> s) => Console.WriteLine("Ext1 ReadOnlySpan");
}

static class Ext2
{
    [OverloadResolutionPriority(0)]
    public static void M(this C2 c, ReadOnlySpan<int> s) => Console.WriteLine("Ext2 ReadOnlySpan");
}

class C2 {}

針對擴充成員執行多載解析時,我們不應該依宣告類型排序,而是考慮相同範疇內的所有擴充成員?

我們一律會分組。 上述範例會列印 Ext2 ReadOnlySpan

屬性的繼承與覆寫(已回答)

應該繼承屬性嗎? 如果沒有,覆寫成員的優先順序為何?
如果在虛擬成員上指定了屬性,那麼在覆寫該成員時是否也必須重複該屬性?

屬性不會標示為繼承。 我們將查看成員的最小衍生宣告,以判斷其多載解析優先順序。

覆寫時的應用程式錯誤或警告(已回答)

class Base
{
    [OverloadResolutionPriority(1)] public virtual void M() {}
}
class Derived
{
    [OverloadResolutionPriority(2)] public override void M() {} // Warn or error for the useless and ignored attribute?
}

在忽略 OverloadResolutionPriorityAttribute 的情境中,例如覆蓋時,我們應該怎麼做:

  1. 不執行任何動作,讓它靜靜地被忽略。
  2. 發出警告,表示該屬性將被忽略。
  3. 屬性不被允許時發出錯誤。

3 是最謹慎的方法,如果我們認為未來可能會有一個能夠允許覆寫以指定這個屬性的空間。

我們將選擇 3,並在應用程式會被忽略的地點加以封鎖。

隱式介面實作(已回答)

隱含介面實作的行為應該是什麼? 是否必須指定 OverloadResolutionPriority? 當編譯程式遇到沒有優先順序的隱含實作時,編譯程序的行為應該是什麼? 這幾乎肯定會發生,因為介面連結庫可能會更新,但不會更新實作。 既有技術中的params不應該被指定,也不會延續其值:

using System;

var c = new C();
c.M(1, 2, 3); // error CS1501: No overload for method 'M' takes 3 arguments
((I)c).M(1, 2, 3);

interface I
{
    void M(params int[] ints);
}

class C : I
{
    public void M(int[] ints) { Console.WriteLine("params"); }
}

我們的選項如下:

  1. 請遵循 paramsOverloadResolutionPriorityAttribute 不會被隱式帶入或要求必須指定。
  2. 隱含地傳遞 屬性。
  3. 請勿隱式傳遞屬性,要求在呼叫位置指定該屬性。
    1. 這帶來了額外的問題:當編譯程式遇到具有已編譯參考的案例時,行為應該是什麼?

我們選1。

進一步的應用程式錯誤(已回答)

還有一些位置,例如 需要確認。 其中包括:

  • 轉換運算符 - 規格從未指出轉換運算符會經歷多載解析,因此實作會阻止應用到這些成員。 應該確認嗎?
  • Lambda - 同樣地,Lambda 永遠不會受限於多載解析,因此實作會封鎖它們。 應該確認嗎?
  • 解構函式 - 目前已封鎖。
  • 靜態建構函式,目前仍被封鎖。
  • 本機函式 - 這些函式目前不會遭到封鎖,因為它們 進行多載解析,所以您無法多載這些函式。 這類似於當屬性套用至未多載的類型成員時,我們不會出現錯誤。 是否應該確認此行為?

上述所有位置都會遭到封鎖。

Langversion 行為 (已回答)

實作目前只會在套用 OverloadResolutionPriorityAttribute 時發出 langversion 錯誤,在實際影響任何專案時不會。 之所以做出此決定,是因為 BCL 將在現在和未來新增會使用此屬性的 API;如果使用者手動將語言版本重設為 C# 12 或更早的版本,他們可能會看到這些成員,並且依照我們的 langversion 行為,會有不同的顯示結果:

  • 如果我們忽略 C# <13 中的屬性,則發生模棱兩可的錯誤,因為 API 在沒有 屬性的情況下確實模棱兩可,或;
  • 如果我們在屬性影響結果時發生錯誤,就會遇到 API 無法運作的錯誤。 這會特別糟糕,因為 Debug.Assert(bool) 在 .NET 9 中已取消優先順序,或;
  • 如果我們在沒有通知的情況下變更解析度,當一個編譯器版本理解該屬性而另一個不理解時,不同的編譯器版本之間可能會出現不同的行為。

最後一個行為是選擇的,因為它會產生最向前相容性,但變更的結果可能會令某些使用者感到意外。 我們應該確認這一點,還是應該選擇其他選項之一?

我們將選擇選項 1,悄悄忽略舊語言版本中的屬性。

替代方案

先前的一項 提案試圖指定一種 BinaryCompatOnlyAttribute 方法,而這種方法在消除可見物方面顯得非常嚴格。 不過,這有許多難以實現的問題,要麼導致提案過於嚴格而不切實際(例如防止測試舊的 API),要麼太過鬆散,以至於錯失了一些原本的目標(例如本應被視為模糊的 API 反而能夠調用一個新 API)。 該版本會復寫如下。

BinaryCompatOnlyAttribute 提案 [過時]

BinaryCompatOnlyAttribute

詳細設計

System.BinaryCompatOnlyAttribute

我們引進了新的保留屬性:

namespace System;

// Excludes Assembly, GenericParameter, Module, Parameter, ReturnValue
[AttributeUsage(AttributeTargets.Class
                | AttributeTargets.Constructor
                | AttributeTargets.Delegate
                | AttributeTargets.Enum
                | AttributeTargets.Event
                | AttributeTargets.Field
                | AttributeTargets.Interface
                | AttributeTargets.Method
                | AttributeTargets.Property
                | AttributeTargets.Struct,
                AllowMultiple = false,
                Inherited = false)]
public class BinaryCompatOnlyAttribute : Attribute {}

當套用至某個類型的成員時,編譯器會將該成員視為在所有位置都無法存取,這表示它不會參與成員查找、多載決策或任何其他類似的過程。

無障礙領域

我們會更新 §7.5.3 無障礙領域 作為如下所示

成員的 存取域 包含允許存取成員的程式碼的區段(可能不連續)。 為了定義成員的存取範圍定義域,如果成員未在類型內宣告,則表示成員 最上層,而且如果成員宣告在另一種類型內,則會 巢狀。 此外,程式 程式文字 被定義為所有程式的編譯單位中包含的所有文字,而型別的程式文字則被定義為該型別的 type_declaration中包含的所有文字(也包括可能在該型別內的巢狀類型)。

預定義類型(例如 objectintdouble)的存取範圍是無限制的。

在程式 P 中宣告的最上層無綁定類型 T 的存取範圍(§8.4.4)的定義如下:

  • 如果 T 標示為 BinaryCompatOnlyAttribute,則 T 的存取範圍對 P 的程式碼以及任何參考 P的程式完全不可存取。
  • 如果 T 的可見性是公開的,則 T 的可見性範圍是 P 的程式代碼以及任何參考 P的程式。
  • 如果 T 的存取權限被宣告為內部,則 T 的存取範圍是在 P的程式文本內。

Note:從這些定義可以得出,最上層未系結類型的可存取範疇至少包含宣告該類型之程式的程式碼。 尾注

建構型別 T<A₁, ..., Aₑ> 的可存取性領域是未系結泛型型別 T 的可存取性領域與型別參數 A₁, ..., Aₑ的可存取性領域的交集。

在程式 P中的類型 T 宣告的巢狀成員 M 的存取範圍定義如下(注意 M 本身可能是類型):

  • 如果 M 標示為 BinaryCompatOnlyAttribute,則 M 的存取範圍對 P 的程式碼以及任何參考 P的程式是完全無法存取的。
  • 如果宣告的 M 存取範圍是 public,則 M 的輔助功能網域是 T的輔助功能領域。
  • 宣告 M 的可訪問性如果是 protected internal,則讓 D 成為 P 的程式文字與在 P外部宣告的衍生自 T型別的程式文字的聯集。 M 的輔助功能網域是 T 的輔助功能網域與 D的交集。
  • 如果宣告的 M 存取範圍是 private protected,則讓 D 成為 PT 的程式文字的交集,以及任何從 T衍生出來的類型。 M 的可及性域是 T 的可及性域與 D的交集。
  • 如果宣告的 M 存取範圍是 protected,則讓 D 成為 T的程式碼和衍生自 T的任何類型的程式碼的聯集。 M 的輔助功能域是 T 的輔助功能域與 D的交集。
  • 如果宣告的 M 存取範圍是 internal,則 M 的存取範圍是 T 的存取範圍與程式文字 P的交集。
  • 如果宣告的 M 可存取性是 private,則 M 的存取範圍是 T的程序範圍。

這些新增項目的目標是讓標示為 BinaryCompatOnlyAttribute 的成員對任何位置完全不可存取,它們不會參與成員檢索,而且不會影響程式的其他部分。 因此,這表示它們無法實作介面成員、無法彼此呼叫,而且無法覆寫(虛擬方法)、隱藏或實作(介面成員)。 這是否太嚴格,是以下幾個公開問題的主題。

未解決的問題

虛擬方法與重載

當虛擬方法標示為 BinaryCompatOnly時,我們該怎麼辦? 衍生類別中的覆寫甚至可能不在目前的組件中,並且可能是使用者想要引入方法的新版本,例如,僅僅是傳回型別不同,而 C# 通常不允許基於此進行多載。 在重新編譯時,該方法的任何覆寫會發生什麼事? 如果他們也標示為 BinaryCompatOnly,是否允許覆寫 BinaryCompatOnly 成員?

在相同的 DLL 內使用

此提案指出,BinaryCompatOnly 成員在任何地方都不可見,即便是在當前正在編譯的組件中也是如此。 太嚴格了,還是 BinaryCompatAttribute 成員需要彼此鏈結?

隱含實作介面成員

BinaryCompatOnly 成員是否能夠實作介面成員? 或者,應該防止它們這樣做。 這需要當使用者想要將隱含介面實作轉換成 BinaryCompatOnly時,他們還需要提供明確的介面實作,可能會複製與 BinaryCompatOnly 成員相同的主體,因為明確介面實作將無法再看到原始成員。

實作標示為 BinaryCompatOnly 的界面成員

當介面成員標示為 BinaryCompatOnly時,我們該怎麼辦? 類型仍然需要為該成員提供實現;我們可能需要直接說明,介面成員不能被標記為 BinaryCompatOnly