變更相容性的規則
縱觀其歷程記錄,.NET 一直嘗試維護 .NET 各版本之間以及各種實作之間的相容性。 雖然相較於 .NET Framework,.NET 5 (和 .NET Core) 和更新版本可視為新技術,但有兩個主要因素會限制此 .NET 實作與 .NET Framework 有所不同的能力:
最初開發或是繼續開發 .NET Framework 應用程式的大量開發人員。 他們預期跨 .NET 實作有一致的行為。
.NET Standard 程式庫專案可讓開發人員建立以 .NET Framework 和 .NET 5 (和 .NET Core) 和更新版本共用的常見 API 為目標的程式庫。 開發人員預期 .NET 5 應用程式中使用之程式庫的行為,應與 .NET Framework 應用程式中所使用的程式庫相同。
除了 .NET 實作之間的相容性之外,開發人員還需要特定 .NET 實作版本之間的高階相容性。 特別是,針對較早版本的 .NET Core 所撰寫的程式碼應該在 .NET 5 或更新版本上順暢地執行。 實際上,許多開發人員預期在新發行的 .NET 版本中找到的新 API,也應該與引入那些 API 的發行前版本相容。
本文會概述會影響相容性的變更,以及 .NET 小組評估每種變更型別的方式。 了解 .NET 小組如何處理可能的中斷性變更,對於建立提取要求以修改現有 .NET API 行為的開發人員特別有幫助。
以下各節描述對 .NET API 所做的變更類別,以及其對應用程式相容性的影響。 變更可為允許 (✔️)、禁止 (❌),或需要判斷並評估先前行為可預測、明顯及一致的程度 (❓)。
注意
- 除了作為如何評估 .NET 程式庫變更的指南之外,程式庫開發人員也可以使用這些準則來評估其程式庫 (以多個 .NET 實作和版本為目標) 的變更。
- 如需相容性類別的相關資訊 (例如向前和回溯相容性),請參閱程式碼變更如何影響相容性。
公用合約的修改
此類別中的變更會修改某個類型的公用介面區。 此類別中的大多數變更都是不允許的,因為它們違反了回溯相容性 (可讓使用舊版 API 開發的應用程式能夠在更新版本上執行而不必重新編譯的能力)。
類型
✔️ 允許:當介面已由基底型別實作時,從型別中移除介面實作
❓ 需要判斷:將新的介面實作加入型別
這是一個可接受的變更,因為它不會對現有的用戶端產生負面影響。 對類型的任何變更都必須在此處定義的可接受變更的界限內運作,以使新實作維持可接受。 如果您要加入的介面會直接影響設計工具或序列化程式產生無法在低層級取用之程式碼或資料的能力,請格外小心。 其中一個範例是 ISerializable 介面。
❓ 需要判斷:引進新的基底類別
如果型別不引進任何新的 abstract 成員或變更現有型別的語意或行為,則可以將型別引進兩個現有型別之間的階層中。 例如,在 .NET Framework 2.0 中,DbConnection 類別成為 SqlConnection 的新基底類別,它先前直接衍生自 Component。
✔️ 允許:將型別從一個組件移動到另一個組件
必須使用指向新組件的 標記舊TypeForwardedToAttribute組件。
✔️ 允許:將 struct 型別變更為
readonly struct
型別不允許將
readonly struct
型別變更為struct
型別。✔️ 允許:當沒有可存取 (public 或 protected) 的建構函式時,將 sealed 或 abstract 關鍵字新增到型別
✔️ 允許:延伸型別的可見性
❌禁止:變更型別的命名空間或名稱
❌禁止:重新命名或移除公用型別
這會中斷使用已重新命名或已移除之型別的所有程式碼。
注意
在罕見的情況下,.NET 可能會移除公用 API。 如需詳細資訊,請參閱 .NET 中的 API 移除。 如需 .NET 支援原則的相關資訊,請參閱 .NET 支援原則。
❌禁止:變更列舉的底層型別
這是編譯時期和行為的中斷性變更,以及可以使屬性引數無法進行剖析的二進位中斷性變更。
❌禁止:密封先前未密封的型別
❌禁止:將介面加入至介面的基底型別集合
如果介面實作先前未實作的介面,則實作介面原始版本的所有型別都會中斷。
❓ 需要判斷:從基底類別集合移除類別,或從已實作介面集合移除介面
介面移除規則有一個例外:您可以新增衍生自已移除之介面的介面實作。 例如,如果型別或介面現在實作 IComponent (它會實作 IDisposable),則可以移除 IDisposable。
❌禁止:將
readonly struct
型別變更為 struct 型別但會允許將
struct
型別變更為readonly struct
型別。❌禁止:將 struct 型別變更為
ref struct
型別,反之亦然❌禁止:降低型別的可見性
但是,允許增加型別的可見性。
成員
✔️ 允許:展開非 virtual 成員的可見性
✔️ 允許:將抽象成員新增至沒有可存取 (public 或 protected) 建構函式的公用型別,否則型別將會是 sealed
但是,不允許將抽象成員新增至具有可存取 (公用或受保護) 建構函式且不是
sealed
的型別。✔️ 允許:當型別沒有可存取 (public 或 protected) 建構函式或型別為 sealed 時,限制 protected 成員的可見性
✔️ 允許:將成員移動到階層中比移除它的型別更高的類別
✔️ 允許:新增或移除覆寫
引進覆寫可能會導致先前的取用者在呼叫基礎映像時跳過覆寫。
✔️ 允許:如果類別先前沒有建構函式,則將建構函式與無參數建構函式一起新增至類別中
但是,不允許將建構函式新增至先前沒有建構函式且沒有新增無參數建構函式的類別。
✔️ 允許:從
ref readonly
變更為ref
傳回值 (虛擬方法或介面除外)✔️ 允許:除非欄位的靜態型別是可變動的實值型別,否則從欄位中移除 readonly
✔️ 允許:呼叫先前未定義的新事件
❓ 需要判斷:將新的執行個體欄位新增至型別
此變更會影響序列化。
❌禁止:重新命名或移除公用成員或參數
這會中斷使用已重新命名或已移除之成員或參數的所有程式碼。
這包括從屬性中移除或重新命名 getter 或 setter,以及重新命名或移除列舉成員。
❌禁止:將成員新增至介面
如果您提供實作,將新的成員新增至現有的介面不一定會導致下游組件發生編譯失敗。 不過,並非所有語言都支援預設介面成員 (DIM)。 此外,在某些情況下,執行階段無法決定要叫用的預設介面成員。 基於這些原因,將成員新增至現有介面會被視為中斷性變更。
❌禁止:變更公用常數或列舉成員的值
❌禁止:變更屬性、欄位、參數或傳回值的型別
❌禁止:新增、移除或變更參數的順序
❌禁止:重新命名參數 (包括變更其大小寫)
這會視為中斷的原因有二:
❌禁止:從
ref
傳回值變更為ref readonly
傳回值❌️禁止:在虛擬方法或介面上從
ref readonly
變更為ref
傳回值❌禁止:在成員中新增或移除 abstract
❌禁止:從成員中移除 virtual 關鍵字
❌禁止:將 virtual 關鍵字新增至成員
雖然這通常不是一個中斷性變更,因為 C# 編譯器傾向於發出 callvirt 中繼語言 (IL) 指令以呼叫非虛擬方法 (
callvirt
會執行 Null 檢查,而正常呼叫不會),由於以下幾個原因,此行為並不是不變的:C# 不是 .NET 以其為目標的唯一語言。
只要目標方法為非虛擬且可能不是 Null 時 (例如透過 ?. null 傳播運算子存取的方法),C# 編譯器就會漸漸地嘗試將
callvirt
最佳化為正常呼叫。
使方法成為虛擬表示取用者程式碼通常最後會以非虛擬方式呼叫它。
❌禁止:將虛擬成員設為抽象
❌禁止:將 sealed 關鍵字新增至介面成員
新增
sealed
至預設介面成員會使它成為非虛擬,以防止呼叫該成員的衍生型別實作。❌禁止:將抽象成員新增至具有可存取 (public 或 protected) 建構函式且不是 sealed 的公用型別
❌禁止:從成員中新增或移除 static 關鍵字
❌禁止:新增一個防止現有多載的多載,並定義不同的行為
這會中斷繫結到先前多載的現有用戶端。 例如,如果某個類別具有接受 UInt32 的方法的單一版本,則現有的取用者在傳遞 Int32 值時將成功繫結至該多載。 但是,如果新增接受 Int32 的多載,則在重新編譯或使用晚期繫結時,編譯器現在會繫結至新的多載。 如果產生不同的行為,這是一個中斷性變更。
❌禁止:將建構函式新增至先前沒有建構函式且沒有新增無參數建構函式的類別
❌禁止:將 readonly 新增至欄位
❌禁止:降低成員的可見性
這包括在有「可存取」(
public
或protected
) 的建構函式存在且型別「不是」sealed 的情況下,降低 protected 成員的可見性。 如果不是這種情況,則允許降低受保護成員的可見性。這允許增加成員的可見性。
❌禁止:變更成員的型別
無法修改方法的傳回值,或屬性或欄位的型別。 例如,傳回 Object 之方法的簽章不能變更為傳回 String,反之亦然。
❌禁止:將執行個體欄位新增至沒有非公用欄位的結構
如果結構只有公用欄位或完全沒有欄位,則呼叫者可以宣告該結構型別的區域變數,而不需要呼叫結構的建構函式,或先將區域變數初始化為
default(T)
,只要第一次使用之前在結構上設定所有公用欄位即可。 對這些呼叫者而言,將任何新的欄位 (公用或非公用) 新增至這種結構都是來源中斷性變更,因為編譯器現在需要初始化其他欄位。此外,將任何新的欄位 (公用或非公用) 新增至沒有欄位或只有公用欄位的結構,對已將
[SkipLocalsInit]
套用至其程式碼的呼叫者而言,就是二進位中斷性變更。 由於編譯器在編譯時間未察覺這些欄位,因此可能會發出未完全將結構初始化的 IL,導致從未初始化的堆疊資料建立結構。如果結構有任何非公用欄位,且編譯器已經透過建構函式或
default(T)
強制執行初始化,則新增執行個體欄位不是中斷性變更。❌禁止:引發先前從未引發的現有事件
行為變更
組件
✔️ 允許:在仍支援相同平台的情況下,讓組件可移植
❌禁止:變更組件的名稱
❌禁止:變更組件的公開金鑰
屬性、欄位、參數與傳回值
✔️ 允許:將屬性、欄位、傳回值或 out 參數的值變更為衍生程度較大的型別
✔️ 允許:如果成員不是 virtual,則增加屬性或參數的可接受值範圍
雖然可以傳遞給方法或由成員傳回之值的範圍可以擴展,但參數或成員型別不能。 例如,雖然傳遞至方法的值可以從 0-124 擴充至 0-255,但參數類型無法從 Byte 變更為 Int32。
❌禁止:如果成員為 virtual,則增加屬性或參數的可接受值範圍
此變更會中斷現有的覆寫成員,這些成員將無法在擴充的值範圍內正常執行。
❌禁止:縮減屬性或參數的可接受值範圍
❌禁止:增加屬性、欄位、傳回值或 out 參數的傳回值範圍
❌禁止:變更屬性、欄位、方法傳回值或 out 參數的傳回值
❌禁止:變更屬性、欄位或參數的預設值
變更或移除參數預設值不是二進位中斷。 移除參數預設值是來源中斷,而變更參數預設值可能會導致重新編譯後的行為中斷。
基於此理由,移除參數預設值在將這些預設值移至新的方法多載,以消除不明確性的特定情況下是可接受的。 例如,請考量現有的方法
MyMethod(int a = 1)
。 如果引進MyMethod
的多載具有兩個選用參數a
和b
,您可以將a
的預設值移至新的多載,以保留相容性。 現在,這兩個多載是MyMethod(int a)
和MyMethod(int a = 1, int b = 2)
。 此模式會允許MyMethod()
編譯。❌禁止:變更傳回數值的有效位數
❓ 需要判斷:剖析輸入和擲回新的例外狀況中的變更 (即使文件中未指定剖析行為)
例外狀況
✔️ 允許:擲回衍生程度比現有例外狀況更大的例外狀況
因為新的例外狀況是現有例外狀況的子類別,所以先前的例外狀況處理程式碼會繼續處理例外狀況。 例如,在 .NET Framework 4 中,文化特性建立和擷取方法已開始擲回 CultureNotFoundException 而不是 ArgumentException (如果找不到文化特性)。 因為 CultureNotFoundException 衍生自 ArgumentException,所以這是一個可接受的變更。
✔️ 允許:擲回比 NotSupportedException、NotImplementedException、NullReferenceException 更具體的例外情況
✔️ 允許:擲回被視為無法復原的例外狀況
無法復原的例外狀況不應該攔截,而應該由高層級追補處理常式處理。 因此,使用者不應該擁有攔截這些明確例外狀況的程式碼。 無法復原的例外狀況有:
✔️ 允許:在新的程式碼路徑中擲回新的例外狀況
該例外狀況必須僅套用到使用新參數值或狀態執行的新程式碼路徑,而且無法由以舊版為目標的現有程式碼執行。
✔️ 允許:移除例外狀況以啟用更穩定的行為或新的案例
例如,之前只處理正值並擲回 ArgumentOutOfRangeException 的
Divide
方法可以變更為支援負值和正值,而不擲回例外狀況。✔️ 允許:變更錯誤訊息的文字
開發人員不應該依賴錯誤訊息的文字,這也會根據使用者的文化特性而變更。
❌禁止:擲回以上未列出之任何其他情況中的例外狀況
❌禁止:移除以上未列出之任何其他情況中的例外狀況
屬性
✔️ 允許:變更不可觀察的屬性值
❌禁止:變更可觀察的屬性值
❓ 需要判斷:移除屬性
在大部分情況下,移除屬性 (例如NonSerializedAttribute) 是一個中斷性變更。
平台支援
✔️ 允許:支援先前不支援的平台上的作業
❌禁止:不支援或現在針對先前平台上支援的作業需要特定 Service Pack
內部實作的變更
❓ 需要判斷:變更內部型別的介面區
通常允許此類變更,儘管它們可能會中斷私人反映。 在一些熱門的協力廠商程式庫或大量開發人員依賴內部 API 的案例中,可能不允許此類變更。
❓ 需要判斷:變更成員的內部實作
通常允許這些變更,儘管它們可能會中斷私人反映。 在一些客戶程式碼經常依賴私人反映或變更引進非預期之副作用的情況下,可能不允許這些變更。
✔️ 允許:提升作業效能
修改作業效能的能力是很重要的,但此類變更可能會中斷仰賴目前作業速度的程式碼。 對於仰賴非同步作業時間的程式碼尤其如此。 效能變更應該不會影響相關 API 的其他行為;否則,變更將會中斷。
✔️ 允許:間接 (通常是不利) 變更作業的效能
如果由於某些其他原因而未將相關變更歸類為中斷,則這是可以接受的。 通常,需要採取可能包括額外的作業或新增新功能的動作。 這幾乎一律會影響效能,但對於使相關 API 如預期般運作至關重要。
❌禁止:將同步 API 變更為非同步 (反之亦然)
程式碼變更
✔️ 允許:將 params 新增至參數
❌禁止:將 checked 關鍵字新增至程式碼區塊
此變更可能會導致先前執行的程式碼擲回 OverflowException 且無法接受。
❌禁止:從參數中移除 params
❌禁止:變更引發事件的順序
開發人員可以合理地預期事件以相同的順序引發,而開發人員程式碼經常會取決於事件的引發順序。
❌禁止:移除在指定動作上的事件引發
❌禁止:變更指定事件的呼叫次數
❌禁止:將 FlagsAttribute 新增至列舉型別