共用方式為


ref readonly 參數

注意

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

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

您可以在 規格一文中深入瞭解採納功能規範至 C# 語言標準的過程

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

總結

允許參數宣告位置修飾詞 ref readonly 並變更調用位置規則,如下所示:

呼叫站點註釋 ref 參數 ref readonly 參數 in 參數 out 參數
ref 允許 允許的 警告 錯誤
in 錯誤 允許的 允許 錯誤
out 錯誤 錯誤 錯誤 允許
沒有註釋 錯誤 警告 允許 錯誤

請注意,現有規則有一項變更:具有 ref 呼叫端註釋的 in 參數會產生警告,而不是錯誤。)

變更自變數值規則,如下所示:

實值種類 ref 參數 ref readonly 參數 in 參數 out 參數
rvalue 錯誤 警告 允許 錯誤
lvalue 允許 允許的 允許 允許

其中,lvalue 表示一個變數(即具有位置的值,不一定是可寫入/可指派的),而 rvalue 則表示任何類型的值。

賦予動機

C# 7.2 引進了 in 參數, 作為傳遞唯讀引用的方法。 in 參數同時允許 lvalues 和 rvalues,且可在呼叫位置不使用任何註解即可使用。 不過,從其參數擷取或傳回參考的 API 會想要不允許右值,也會在呼叫端強制執行一些指示,指出正在擷取參考。 ref readonly 參數在這類情況下非常理想,因為當它們與 rvalue 或在呼叫端缺乏註釋一起使用時會發出警告。

此外,還有一些 API 只需要唯讀參考,但仍在使用中。

  • ref 參數,因為它們是在 in 推出之前引進的,而改為 in 將會是打破原碼和二進位相容性的更改,例如 QueryInterface
  • in 參數接受唯讀參考,儘管將右值傳遞給它們其實不太合理,例如 ReadOnlySpan<T>..ctor(in T value)
  • ref 參數禁止右值,即使它們不會改變傳遞的參考,例如 Unsafe.IsNullRef

這些 API 可以移轉至 ref readonly 參數,而不會中斷使用者。 如需二進位相容性的詳細資訊,請參閱建議的 元數據編碼。 具體來說,變更

  • refref readonly 只會對虛擬方法造成二進位相容性的破壞,
  • refin 也是虛擬方法的二進位中斷性變更,但不是來源中斷性變更(因為規則變更為只針對傳遞至 in 參數的 ref 自變數發出警告),
  • inref readonly 不會是重大變更(但沒有呼叫端註釋或右值會導致警告),
    • 請注意,對於使用舊版本編譯器的用戶來說,這將是一個改變編譯行為的重要變更(因為舊版本編譯器將 ref readonly 參數解釋為 ref 參數,這使得在呼叫位置不允許使用 in 或不加註解),以及具有 LangVersion <= 11 的新編譯器版本(為了與舊版本編譯器保持一致,如果沒有使用 ref 修飾詞傳遞相應的參數,將會發出錯誤,ref readonly 參數將不被支援)。

往相反方向改變

  • ref readonlyref 可能是源代碼中斷性變更(除非只使用 ref 呼叫點註釋,而且只使用唯讀參考作為參數),以及虛擬方法的二進位相容性中斷性變更,
  • ref readonlyin 不會是重大變更(但 ref 呼叫站點註解會導致警告)。

請注意,上述規則適用於方法簽章,但不適用於委派簽章。 例如,將委派簽名中的 ref 變更為 in 可能會造成源代碼不兼容的更改(如果使用者將帶有 ref 參數的方法指派給該委派類型,在 API 變更後就會出錯)。

詳細設計

一般而言,ref readonly 參數的規則與 提案in 參數所指定的規則相同,但在此提案中明確變更的位置除外。

參數宣告

不需要變更文法。 允許參數使用修飾詞 ref readonly。 除了一般方法之外,索引器參數也允許 ref readonly(例如 in 但不同於 ref),但不允許運算符參數(例如 ref 但與 in不同)。

預設參數值將在發出警告的情況下允許用於 ref readonly 參數,因為它們相當於傳遞右值。 這可讓 API 作者將具有預設值 in 參數變更為 ref readonly 參數,而不引進來源重大變更。

數值類型檢查

請注意,儘管允許對 ref readonly 參數使用 ref 引數修飾詞,但這並不會改變關於值種類檢查的情況,亦即,

  • ref 只能搭配可指派的值使用;
  • 若要傳遞只讀參考,必須改用 in 參數修飾詞。
  • 若要傳遞右值,必須不使用修飾詞(如 提案摘要所述,這會對 ref readonly 參數產生警告)。

多載解析

多載解析將允許混合 ref/ref readonly/in/no callsite 批注和參數修飾詞,如 此提案摘要,也就是在多載解析期間,所有允許的 警告 案例都會被視為可能的候選專案。 具體來說,現有行為有所變更,其中具有 in 參數的方法會比對呼叫與標示為 ref的對應自變數 ,此變更將會在 LangVersion 上進行閘道處理。

不過,如果參數符合特定條件,則不會顯示將不含呼叫點修飾詞的引數傳遞至 ref readonly 參數的警告。

  • 擴充函式調用中的接收者,
  • 隱含使用做為自定義集合初始化表達式或插補字串處理程式的一部分。

如果沒有任何引數修飾詞,則優先使用 by-value 多載而非 ref readonly 多載。(in 參數具有相同行為)。

方法轉換

同樣地,為了匿名函式 [•10.7] 和方法群組 [•10.8] 轉換,這些修飾詞會被視為相容 (但不同修飾詞之間允許的轉換都會導致警告):

  • 允許目標方法的 ref readonly 參數對應於委派的 inref 參數,
  • 允許目標方法的 in 參數匹配 ref readonly 或者,根據 LangVersion,匹配委派的 ref 參數。
  • 注意:目標方法的 ref 參數 不允許 比對委派的 inref readonly 參數。

例如:

DIn dIn = (ref int p) => { }; // error: cannot match `ref` to `in`
DRef dRef = (in int p) => { }; // warning: mismatch between `in` and `ref`
DRR dRR = (ref int p) => { }; // error: cannot match `ref` to `ref readonly`
dRR = (in int p) => { }; // warning: mismatch between `in` and `ref readonly`
dIn = (ref readonly int p) => { }; // warning: mismatch between `ref readonly` and `in`
dRef = (ref readonly int p) => { }; // warning: mismatch between `ref readonly` and `ref`
delegate void DIn(in int p);
delegate void DRef(ref int p);
delegate void DRR(ref readonly int p);

請注意,函式指標轉換的行為沒有任何變更。 提醒您,如果參考類型修飾符不一致,則隱式函數指標轉換不允許,而且總是允許顯式轉換,而不會有任何警告。

簽章比對

單一型別中宣告的成員在簽章中不能僅因 ref/out/in/ref readonly而有所不同。 針對其他簽章比對的用途(例如隱藏或覆寫),ref readonly 可以與 in 修飾詞互換,但這會在宣告位置引發警告 [§7.6]。 這不適用於匹配 partial 宣告與其實作,以及匹配攔截器簽名與被攔截簽名時。 請注意,ref/inref readonly/ref 修飾符的覆寫沒有變化,這些修飾符對無法互換,因為它們的簽章與二進位不相容。 為了一致性,其他簽章比對目的也是如此(例如隱藏)。

元數據編碼

提醒您:

  • ref 參數會以純 byref 類型發出(IL 中的T&),
  • in 參數就像 ref 加上批註 System.Runtime.CompilerServices.IsReadOnlyAttribute。 在 C# 7.3 和更高版本中,它們也會被以 [in] 發出,如果是虛擬的,則是 modreq(System.Runtime.InteropServices.InAttribute)

ref readonly 參數將會以 [in] T&形式發出,並加上下列屬性的批註:

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
    public sealed class RequiresLocationAttribute : Attribute
    {
    }
}

此外,如果是虛擬的,則會使用 modreq(System.Runtime.InteropServices.InAttribute) 來發出,以確保與 in 參數的二進位相容性。 請注意,與 in 參數不同,不會針對 ref readonly 參數發出任何 [IsReadOnly],以避免增加元數據大小,也讓較舊的編譯程式版本將 ref readonly 參數解譯為 ref 參數(因此,即使不同編譯程式版本之間 refref readonly 也不會是重大變更的來源。

如果編譯中尚未包含,則編譯器會以具命名空間限定的名稱來比對 RequiresLocationAttribute,並進行合成。

如果屬性套用至參數,則在來源中指定屬性會錯誤,如同 ParamArrayAttribute

函式指標

在函式指標中,in 參數會與 modreq(System.Runtime.InteropServices.InAttribute) 一起使用(請參閱 函式指標提案)。 ref readonly 參數將會在沒有該 modreq的情況下發出,但改為使用 modopt(System.Runtime.CompilerServices.RequiresLocationAttribute)。 較舊的編譯程式版本會忽略 modopt,因此將 ref readonly 參數解譯為 ref 參數(這與上述對於有 ref readonly 參數的普通方法的較舊編譯器行為一致),而新版本的編譯器會認識 modopt,並利用它來辨識 ref readonly 參數,以在 轉換期間發出警告,並在調用時使用。 為了與舊版編譯程式版本一致,具有 LangVersion <= 11 的新編譯程式版本會報告不支援 ref readonly 參數的錯誤,除非對應的自變數是使用 ref 修飾詞傳遞。

請注意,如果它們屬於公用 API 的一部分,則變更函式指標簽章中的修飾詞是二進位中斷,因此將 refin 變更為 ref readonly時,這將是二進位中斷。 不過,只有在變更 inref readonly 時,具有 LangVersion <= 11 的呼叫端才會發生來源中斷(如果叫用具有 in 呼叫月臺修飾詞的指標),與一般方法一致。

重大變更

在多載解析中,ref/in 不匹配放鬆會導致如下範例中展示的行為中斷變更:

class C
{
    string M(in int i) => "C";
    static void Main()
    {
        int i = 5;
        System.Console.Write(new C().M(ref i));
    }
}
static class E
{
    public static string M(this C c, ref int i) => "E";
}

在 C# 11 中,呼叫會系結至 E.M,因此會列印 "E"。 在 C# 12 中,C.M 可以進行綁定(但會出現警告),並且不會搜尋任何擴展範圍,因為我們有一個適用的候選項目,因此會列印 "C"

也有來源中斷性變更,原因相同。 下列範例會在 C# 11 中輸出 "1",但會因模糊不清的錯誤訊息而無法在 C# 12 中編譯。

var i = 5;
System.Console.Write(C.M(null, ref i));

interface I1 { }
interface I2 { }
static class C
{
    public static string M(I1 o, ref int x) => "1";
    public static string M(I2 o, in int x) => "2";
}

上述範例展示了方法調用中斷的情況,但由於這些中斷是由多載解析變更所引起,因此也會因方法轉換而觸發類似情況。

替代方案

參數宣告

API 作者可以標註 in 參數,其設計目的是只接受具有自定義屬性的 lvalue,並提供分析器來標幟不正確的使用方式。 這將不允許 API 作者更改現有 API 的簽章,這些 API 選擇使用 ref 參數來禁止右值。 如果這類 API 的呼叫者只能存取 ref readonly 變數,仍然需要執行額外的工作,才能取得 ref。 將這些 API 從 ref 變更為 [RequiresLocation] in 將是源代碼中斷性變更(在虛擬方法的情況下,也將是二進位中斷性變更)。

編譯程式不會允許修飾詞 ref readonly,而是可以在將特殊屬性(例如 [RequiresLocation])套用至參數時辨識。 這是在 LDM 2022-04-25中討論的,決定這是語言功能,而不是分析器,因此看起來應該像這樣。

實值類型檢查

不需任何修飾詞傳遞 lvalue 給 ref readonly 參數,可能會允許沒有任何警告,類似於C++的隱含 byref 參數。 這在 LDM 2022-05-11中討論,指出 ref readonly 參數的主要動機是從這些參數擷取或傳回參考的 API,因此某種標記是件好事。

傳遞右值至 ref readonly 可能會是錯誤,而非只是警告。 最初在 LDM 2022-04-25中被接受,但隨後的電子郵件討論放寬了這一規定,因為不這樣做我們將失去更改現有 API 的能力而不影響使用者。

in 可能是 ref readonly 參數的「自然」呼叫網站修飾詞,而使用 ref 可能會導致警告。 這可確保一致的程式代碼樣式,並在呼叫端清楚指出參考是唯讀的(不像 ref)。 它最初被接受於 LDM 2022-04-25。 不過,警告可能是 API 作者從 ref 移至 ref readonly的摩擦點。 此外,in 已重新定義為 ref readonly + 便利性功能,因此在 LDM 2022-05-11中遭到拒絕。

擱置LDM檢閱

C# 12 中未實作下列任何選項。 它們仍然是潛在的建議。

參數宣告

可以允許反向排序修飾詞(readonly ref 而不是 ref readonly)。 這樣做與 readonly ref 的傳回值和欄位的行為不一致(不允許反向順序或反向順序表示不同的意義),而且如果將來實作的話,可能會與只讀參數發生衝突。

預設參數值可能會導致 ref readonly 參數的錯誤。

實值類型檢查

當將右值傳遞給 ref readonly 參數,或當呼叫點註釋與參數修飾詞不匹配時,可能會產生錯誤而非警告。 同樣地,可以使用特殊 modreq,而不是 屬性,以確保 ref readonly 參數與二進位層級上的 in 參數不同。 這將提供更強的保證,因此對於新的 API 來說很適合,但仍不能在現有的執行時 API 中採用,因為這些 API 不能引入破壞性更改。

值種類檢查可能會放寬,以允許透過 ref 將只讀參考傳遞至 in/ref readonly 參數。 這類似於 ref 指定和 ref 傳回的現行運作方式,它們也允許透過來源運算式上的 ref 修飾詞,將參考以唯讀方式傳遞。 不過,ref 通常會靠近目標宣告為 ref readonly的位置,因此很明顯,我們是以唯讀的方式傳遞引用,不像函數調用,其引數和參數修飾詞通常相距甚遠。 此外,他們只允許 作為ref 修飾詞,而不允許像參數那樣也使用 in,因此 inref 對於參數來說會變得可以互換,或者,如果使用者想讓程式碼保持一致,in 實際上會變得過時(因為 ref 是唯一允許用於 ref 指派和 ref 傳回的修飾詞,他們可能會到處使用 ref)。

多載解析

重載解析、覆寫和轉換可能會不允許 ref readonlyin 修飾符的可互換性。

現有 in 參數的多載解析變更可以無條件取得(不考慮 LangVersion),但這將會是重大變更。

呼叫具有 ref readonly 接收器的擴充方法可能會導致警告「參數 1 應以 refin 關鍵詞傳遞」,這種情況會在呼叫非擴充方法且沒有呼叫現場修飾詞時發生。使用者可以藉由將擴充方法的呼叫轉換成靜態方法的呼叫來修正此類警告。 使用自定義集合初始化表達式或內插字串處理程式搭配 ref readonly 參數時,可能會報告相同的警告,不過使用者無法解決此問題。

當沒有呼叫點修飾詞或可能發生不明確錯誤時,應優先選擇 ref readonly 多載而非傳值多載。

方法轉換

我們可以允許目標方法的 ref 參數符合委派的 inref readonly 參數。 這將使 API 作者能夠在不影響使用者的情況下,將委派簽章中的 ref 變更為 in,這與允許更改一般方法簽章的規則一致。 不過,它也會導致下列違反 readonly 保證的情況,只不過會有警告:

class Program
{
    static readonly int f = 123;
    static void Main()
    {
        var d = (in int x) => { };
        d = (ref int x) => { x = 42; }; // warning: mismatch between `ref` and `in`
        d(f); // changes value of `f` even though it is `readonly`!
        System.Console.WriteLine(f); // prints 42
    }
}

函式指標轉換可能會在 ref readonly/ref/in 不相符時發出警告,但如果我們想要在 LangVersion 上對這項轉換進行閘道,則需要大量實作投資,因為目前類型轉換不需要存取編譯。 此外,目前不相符雖然被視為錯誤,但使用者可以輕鬆新增型別轉換來允許這種不符,如果他們需要的話。

元數據編碼

可以允許在來源中指定 RequiresLocationAttribute,類似於 InOut 屬性。 或者,在除了參數之外的其他情境中適用時,它可能會出錯,與 IsReadOnly 屬性類似。用以保留更多的設計空間。

函式指標 ref readonly 參數可以使用不同的 modopt/modreq 組合發出(請注意,此數據表中的「來源中斷」表示適用於具有 LangVersion <= 11的呼叫端):

修飾 符 可以在跨編譯中識別 舊的編譯程式會將它們視為 refref readonly inref readonly
modreq(In) modopt(RequiresLocation) 是的 in 二進制、源代碼中斷 二進位分割
modreq(In) in 二進位、原始碼中斷 還行
modreq(RequiresLocation) 是的 不支援 二進位、原始碼中斷 binary、源中斷
modopt(RequiresLocation) 是的 ref 二進制斷點 二進位制、來源中斷

我們可以針對 ref readonly 參數發出 [RequiresLocation][IsReadOnly] 屬性。 然後,即使舊版編譯程式版本,inref readonly 也不會是重大變更,但 refref readonly 會成為舊版編譯程式版本的來源中斷性變更(因為它們會將 ref readonly 解譯為 in、不允許 ref 修飾詞),以及具有 LangVersion <= 11 的新編譯程式版本(一致性)。

我們可以讓 LangVersion <= 11 的行為與舊版編譯程序的行為不同。 例如,每當呼叫 ref readonly 參數時(即便在呼叫點使用 ref 修飾詞時),都可能會發生錯誤;或者也可能一律允許而不會發生任何錯誤。

重大變更

此提案建議接受一項會造成行為中斷的變更,因為此情況應該很罕見,並且受到 LangVersion 的限制,而使用者可以透過明確呼叫擴充方法來解決此問題。 相反地,我們可以透過

  • 不允許 ref/in 之間的錯誤匹配(這僅會阻止先前因為尚未提供 in 而使用 ref 的舊 API 移轉到 in),
  • 修改多載解析規則,以便在出現此提案引入的 ref 類型不匹配時,能繼續尋找更好的匹配(依據下文指定的優化規則)。
    • 或只繼續處理 refin 之間的不符,而不處理其他(ref readonlyref/in的/by-value 情況)。
進步為王

下例中,由於三次調用 M,目前會產生三個含糊不清的錯誤。 我們可以新增更好的規則來解決模棱兩可的情況。 這也會解決稍早所述的來源中斷性變更。 其中一種方式是將範例列印 221(其中 ref readonly 參數與 in 自變數相符,因為呼叫時沒有修飾詞,而允許 in 參數則為警告)。

interface I1 { }
interface I2 { }
class C
{
    static string M(I1 o, in int i) => "1";
    static string M(I2 o, ref readonly int i) => "2";
    static void Main()
    {
        int i = 5;
        System.Console.Write(M(null, ref i));
        System.Console.Write(M(null, in i));
        System.Console.Write(M(null, i));
    }
}

新的較佳規則可能會標示為更糟的參數,其自變數可能已使用不同的自變數修飾詞傳遞,使其變得更好。 換句話說,用戶應該一律能夠藉由變更其對應的自變數修飾詞,將更差的參數轉換成更好的參數。 例如,當自變數由 in傳遞時,ref readonly 參數優先於 in 參數,因為使用者可以傳遞自變數 by-value 來選擇 in 參數。 此規則只是目前生效的依值/in 偏好規則的延伸(這是多載解析規則中最後的一個規則,如果有任何參數比另一個多載的對應參數更好而且沒有更差,那麼整個多載會被視為更好)。

論點 更好的參數 更糟的參數
ref/in ref readonly in
ref ref ref readonly/in
值傳遞 by-value/in ref readonly
in in ref

我們應該以類似的方式處理方法轉換。 下列範例目前會產生兩個委派指派的模棱兩可錯誤。 新的更好的規則可能偏好方法參數,其 refness 修飾詞符合對應的目標委派參數 refness 修飾詞,而不是不相符的方法參數。 因此,下列範例會列印 12

class C
{
    void M(I1 o, ref readonly int x) => System.Console.Write("1");
    void M(I2 o, ref int x) => System.Console.Write("2");
    void Run()
    {
        D1 m1 = this.M;
        D2 m2 = this.M; // currently ambiguous

        var i = 5;
        m1(null, in i);
        m2(null, ref i);
    }
    static void Main() => new C().Run();
}
interface I1 { }
interface I2 { }
class X : I1, I2 { }
delegate void D1(X s, ref readonly int x);
delegate void D2(X s, ref int x);

設計會議