自其歷史開始以來,.NET 一直嘗試保持從版本到版本以及跨 .NET 的各種實作的高相容性。 雖然 .NET 5 (和 .NET Core) 和更新版本可以視為與 .NET Framework 相比的新技術,但兩個主要因素會限制 .NET 實作從 .NET Framework 分離的能力:
- 許多開發人員原本開發或繼續開發 .NET Framework 應用程式。 他們預期 .NET 各種實作之間的行為保持一致性。
- .NET Standard 連結庫專案可讓開發人員建立連結庫,以 .NET Framework 和 .NET 5 和 .NET Core 和更新版本共用的通用 API 為目標。 開發人員預期 .NET 應用程式中使用的程式庫的行為應該與 .NET Framework 應用程式中使用的相同程式庫相同。
除了 .NET 實作的相容性之外,開發人員預期特定 .NET 實作的版本之間具有較高的相容性。 特別是,針對舊版 .NET Core 撰寫的程式代碼應該在 .NET 5 或更新版本上順暢地執行。 事實上,許多開發人員預期在新發行的 .NET 版本中找到的新 API 也應該與引進這些 API 的發行前版本相容。
本文概述會影響相容性的變更,以及 .NET 小組評估每種變更類型的方式。 了解 .NET 團隊如何處理可能導致行為變更的問題,對於提交修改現有 .NET API 行為的合併請求的開發人員特別有幫助。
下列各節說明對 .NET API 所做的變更類別,以及其對應用程式相容性的影響。 變更是允許的()、不允許的✔️(❌),或者需要判斷和評估先前的行為是否可預測、明顯和一致(❓)。
備註
- 除了做為如何評估 .NET 連結庫變更的指南,連結庫開發人員也可以使用這些準則來評估以多個 .NET 實作和版本為目標的連結庫變更。
- 如需相容性類別的相關信息,例如向前和回溯相容性,請參閱 程式代碼變更如何影響相容性。
修改公共契約
此類別中的變更會修改類型的公用界面範圍。 不允許此類別中的大部分變更,因為它們違反 回溯相容性 (使用舊版 API 開發的應用程式能夠在更新版本上不需重新編譯即可執行)。
型別
✔️ ALLOWED:可以從類型中移除介面實作,當該介面已由基底類型實作時
❓ 需要判定:將新的介面實現新增至類型
這是可接受的變更,因為它不會對現有的用戶端造成負面影響。 類型的任何變更都必須在此處定義的可接受變更界限內運作,才能讓新實作保持可接受。 當新增會直接影響設計者或序列化工具產生無法被下層使用的程式代碼或數據的能力時,必須特別小心。 例如,ISerializable 介面。
❓ 需要判斷:引入新的基類
如果類型未導入任何新的 抽象 成員,或變更現有類型的語意或行為,則可以將類型引入兩個現有型別之間的階層。 例如,在 .NET Framework 2.0 中,類別 DbConnection 會成為 的新基類,而這個基類 SqlConnection先前是直接衍生自 Component。
✔️ 允許:將類型從某個元件移到另一個元件
舊組件必須標示為指向TypeForwardedToAttribute新組件的標記。
✔️ 允許:將 結構 類型變更為
readonly struct類型不允許將
readonly struct類型變更為struct類型。✔️ 允許:展開類型的可見度
❌ 不允許:變更類型的命名空間或名稱
❌ 不允許:重新命名或移除公用類型
這會中斷所有使用已重新命名或移除類型的程序代碼。
備註
在罕見的情況下,.NET 可能會移除公用 API。 如需詳細資訊,請參閱 .NET 中的 API 移除。 如需了解 .NET 支援政策相關資訊,請參閱 .NET 支援政策。
❌ 不允許:變更列舉的基礎類型
這是編譯時間和行為中斷性變更,以及可讓屬性自變數無法剖析的二進位中斷性變更。
❌ 不允許:對先前已解封的類型進行密封
❌ 不允許:將介面新增至介面的基底類型集合
如果一個介面實作另一個先前未實作的介面,那麼所有實作原始版本介面的類型的功能都會受到影響。
❓ 需要判斷:移除基類集合中的一個類別或已實現的介面集合中的一個介面
移除介面的規則有一個例外:您可以新增來自已移除介面的衍生介面實作。 例如,如果型別或介面現在實作 IDisposable,則您可以移除 IComponent ,這會實作 IDisposable。
❌ 禁止:將
readonly struct類型更改為 結構 類型然而,可以將
struct類型變更為readonly struct。❌ 禁止:將 struct 類型變更為
ref struct類型,及反向操作❌ 不允許:減少類型的可見度
不過,允許增加類型的可見度。
成員
✔️ 允許:展開非虛擬成員的可見度
✔️ ALLOWED:將抽象成員新增至沒有 可存取 (公用或受保護)建構函式的公用類型,或 類型已密封
不過,將抽象成員新增至具有可存取(公用或受保護)建構函式的型別時是不被允許的,且該型別不能是
sealed。✔️ 允許:將成員移至階層中比移除的類型更高的類別
✔️ 允許:新增或移除覆寫
引入覆寫可能導致先前的使用者在呼叫 基底時忽略覆寫。
✔️ ALLOWED:將建構函式新增至類別,並且如果該類別先前沒有建構函式,則新增一個無參數建構函式
不過,不允許在未加入無參數建構函式 的情況下 ,將建構函式新增至先前沒有建構函式的類別。
✔️ 允許:從
ref readonly變更為ref傳回值(虛擬方法或介面除外)✔️ ALLOWED:將唯讀屬性從欄位中移除,除非該欄位的靜態類型是可變值類型
✔️ 允許:呼叫先前未定義的新事件
❓ 需要謹慎判斷:將新的實例欄位新增至類別
這項變更會影響串行化。
❌ 不允許:重新命名或移除公用成員或參數
這會中斷所有使用已重新命名或移除成員或參數的程序代碼。
這包括從屬性移除或重新命名 getter 或 setter,以及重新命名或移除列舉成員。
❓ 需要判斷:將成員新增至介面
雖然這是重大變更,因為它會將您的最低 .NET 版本提高到 .NET Core 3.0 (C# 8.0) ,也就是引進 預設介面成員 (DIM) 的時候,但允許將靜態、非抽象、非虛擬成員新增至介面。
如果您 提供實作,將新成員新增至現有介面不一定會導致下游元件編譯失敗。 不過,並非所有語言都支援 DIM。 此外,在某些情況下,運行時間無法決定要叫用的預設介面成員。 在某些情況下,介面是由
ref struct類型實現的。 因為類型無法裝箱,所以ref struct無法轉換成介面類型。 因此,ref struct類型必須為每個介面成員提供隱含實作。 他們無法使用介面提供的預設實作。 基於這些原因,請在將成員新增至現有介面時使用判斷。❌ 不允許:變更公用常數或列舉成員的值
❌ DISALLOWED:變更屬性、欄位、參數或傳回值的類型
❌ 不允許:新增、移除或變更參數的順序
❌ 不允許:重新命名參數(包括變更其大小寫)
這被認為是中斷的原因有兩個:
❌ 不允許:從
ref傳回值變更為ref readonly傳回值❌️ 不允許:在虛擬方法或介面中將傳回值從
ref readonly變更為ref❌ 不允許:對成員新增或移除abstract
❌ 禁止:從成員中移除虛擬關鍵詞
❌ 不允許:將 虛擬 關鍵詞新增至成員
雖然這通常不是重大變更,因為 C# 編譯器傾向於發出呼叫非虛擬方法的 callvirt 中介語言(IL)指令(
callvirt執行 Null 檢查,但正常呼叫不會),但由於多個原因,這種行為並非總是不變:- C# 不是 .NET 目標的唯一語言。
- 每當目標方法為非虛擬且可能不是 Null 時,C# 編譯程式就會嘗試優化
callvirt為一般呼叫(例如透過 ?. null 傳播運算符存取的方法)。
使方法成為虛擬意味著使用者程式碼通常會以非虛擬方式呼叫它。
❌ 不允許:將虛擬成員抽象化
❌ 不允許:將 sealed 關鍵詞新增至介面成員
新增
sealed至預設介面成員會使它成為非虛擬的,以防止呼叫該成員的衍生型別實作。❌ 不允許:將抽象成員新增至具有可存取(公用或受保護)建構函式且未密封的公用類型
❌ 不允許:從成員新增或移除 靜態 關鍵詞
❌ 不允許:新增多載,排除現有多載並定義不同行為的多載
這會破壞已綁定到先前過載的現有客戶端。 例如,如果類別具有接受 UInt32的單一方法版本,則現有的取用者會在傳遞 Int32 值時成功系結至該多載。 不過,如果您新增接受 Int32的多載,在重新編譯或使用晚期系結時,編譯程式現在會系結至新的多載。 如果導致不同的行為結果,這是突破性改變。
❌ DISALLOWED:將建構函式新增至先前沒有建構函式的類別,而不需新增無參數建構函式
❌️ 不允許:將 只讀 新增至欄位
❌ 不允許:減少成員的可見度
這包括在有可存取的建構函式(或)而類型未密封的情況下,減少受保護成員的可見度。 如果情況並非如此,則允許減少受保護成員的可見度。
允許增加成員的可見度。
❌ 不允許:變更成員的類型
無法修改方法的傳回值或屬性或欄位的類型。 例如,傳回 Object 之方法的簽章無法變更為傳回 String,反之亦然。
❌ 不允許:將實例欄位新增到沒有任何非公用欄位的結構中
如果結構只有公用欄位或完全沒有欄位,則呼叫方可以宣告該結構類型的本地變數,而不需要呼叫結構的建構函式或先初始化本地變數為
default(T),只要在首次使用之前將所有公用欄位都設置在結構上即可。 將任何新的欄位 - 公用或非公用 - 新增至這類結構是這些呼叫端的來源中斷性變更,因為編譯程式現在會要求初始化其他欄位。此外,將任何新的欄位-- 無論是公用或非公用 -- 新增至沒有欄位或只有公用欄位的結構體,對已套用
[SkipLocalsInit]至其程式碼的呼叫端而言,將造成二進位相容性中斷。 由於編譯器在編譯時期不知道這些欄位,因此可能會產生未完全初始化結構體的中間語言 (IL),導致結構體從未初始化的堆疊數據中建立。如果結構有任何非公用字段,編譯程式已經透過建構函式 或
default(T)強制執行初始化,而且新增實例字段不是重大變更。❌ 不允許:觸發以前從未觸發過的現有事件
行為變更
組件
✔️ 允許:在仍支援相同平臺時,使組件具備可攜性
❌ 不允許:變更元件的名稱
❌ 不允許:修改組件的公鑰
屬性、欄位、參數和傳回值
✔️ ALLOWED:將屬性、字段、傳回值或 out 參數的值變更為更衍生的類型
✔️ ALLOWED:如果成員不是虛擬,請增加屬性或參數可接受的值範圍
雖然可以傳遞至方法或成員傳回的值範圍可以展開,但參數或成員類型無法展開。 例如,雖然傳遞至方法的值可以從 0-124 展開至 0-255,但參數類型無法從 Byte 變更為 Int32。
❌ DISALLOWED:如果成員為虛擬,請增加屬性或參數的已接受值範圍
這項變更會中斷現有的覆寫成員,這些成員在擴展的數值範圍內將無法正常運作。
❌ DISALLOWED:縮減屬性或參數的接受值範圍
❌ 不允許:增加屬性、欄位、傳回值或 out 參數的傳回值範圍
❌ 禁止:變更屬性、欄位、方法回傳值或 out 參數
❌ 不允許:變更屬性、欄位或參數的預設值
更改或移除 參數 的預設值不會造成二進位相容性問題。 拿掉參數預設值是來源中斷,而變更參數預設值可能會導致重新編譯后的行為中斷。
基於這個理由,移除參數預設值在特定案例中是可接受的,即「將」這些預設值移至新的方法多載,以消除模棱兩可。 例如,請考慮現有的方法
MyMethod(int a = 1)。 如果您引入帶有兩個選擇性參數MyMethod和a的重載,您可以將b的預設值移至新的重載,以保留相容性。 現在兩個重載是MyMethod(int a)和MyMethod(int a = 1, int b = 2)。 此模式允許MyMethod()編譯。❌ 不允許:變更數值傳回值的精度
❓ 需要判斷:輸入解析和拋出新例外狀況的變更(即使文件中未指定解析行為)
例外狀況
✔️ 允許:擲回比現有例外狀況更多的衍生例外狀況
因為新的例外狀況是現有例外狀況的子類別,因此先前的例外狀況處理程式碼會繼續處理例外狀況。 例如,在 .NET Framework 4 中,如果找不到文化特性,文化特性建立和擷取方法就會開始擲回 CultureNotFoundException ,而不是 ArgumentException 。 由於 CultureNotFoundException 衍生自 ArgumentException,因此這是可接受的變更。
✔️ 允許:丟出比NotSupportedException、NotImplementedException、NullReferenceException更具體的例外
✔️ 允許:拋出被視為無法復原的例外
無法復原的例外狀況不應攔截,而應由高階的全局處理程序負責處理。 因此,使用者不應該有攔截這些明確例外狀況的程序代碼。 無法復原的例外狀況如下:
✔️ 允許:在新程式碼路徑中拋出新的例外
例外狀況只能套用至以新參數值或狀態執行的新程式代碼路徑,而且無法由以舊版為目標的現有程式代碼執行。
✔️ 允許:移除例外狀況以啟用更健全的行為或新案例
例如,
Divide方法先前只處理正值,並在其他情況下擲回一個 ArgumentOutOfRangeException,可以更改為同時支援負值和正值,而不擲回例外。✔️ 允許:變更錯誤訊息的文字
開發人員不應依賴錯誤訊息的文字,這也會根據使用者的文化特性而變更。
❌ 不允許:在上述未列出的任何其他情況下拋出例外
❌ 不允許:在未列出的任何其他情況中移除例外
屬性
✔️ ALLOWED:變更 非可觀察的 屬性值
❌ 不允許:變更 可觀察的屬性 值
❓ 需要判斷:移除屬性
在大部分情況下,移除屬性 (例如 NonSerializedAttribute) 是重大變更。
平台支援
✔️ 允許:在先前不支援的平臺上支持作業
❌ 不允許:不支援或現在要求特定 Service Pack 執行先前在平台上支援的作業
內部實作變更
❓ REQUIRES 判斷:變更內部類型的表面積
雖然這類變更通常被允許,但它們會中斷私有反射。 在某些情況下,當熱門的第三方程式庫或大量開發人員依賴內部 API 時,可能無法進行這類變更。
❓ 需要判斷:變更成員的內部實作
雖然這些變更會中斷私人反映,但通常允許這些變更。 在某些情況下,客戶程式代碼經常相依於私人反映,或變更引入非預期的副作用時,可能不允許這些變更。
✔️ 允許:改善作業的效能
修改作業效能的能力很重要,但這類變更可能會中斷依賴作業目前速度的程序代碼。 這特別適用於相依於異步作時間的程序代碼。 性能改變應不會影響所提及的 API 的其他行為;否則,此變更將會破壞。
✔️ 允許:間接變更作業效能(且通常是不利的)
如果所提到的變更未因其他原因被分類為重大改動,那麼這是可以接受的。 通常,需要採取可能包含額外作業或新增新功能的動作。 這幾乎總是會影響效能,但使該API能如預期運作可能是必要的。
❌ 不允許:將同步 API 變更為異步 (反之亦然)
程式碼變更
✔️ 允許:將 params 新增為參數的一部分
❌ 不允許:將 checked 語句新增至程式代碼區塊
這項變更可能會導致先前執行的程式碼拋出 OverflowException ,而這是不可接受的。
❌ 禁用:從參數移除屬性
❌ 不允許:更改觸發事件的順序
開發人員可以合理預期事件會以相同順序引發,而開發人員程式代碼經常取決於引發事件的順序。
❌ 不允許:移除指定動作的事件引發
❌ 不允許:變更呼叫指定事件的次數
❌ 不允許:將 FlagsAttribute 加入列舉型別