.NET は、その歴史を通じて、バージョンからバージョン、および .NET の実装全体にわたる高レベルの互換性を維持しようとしました。 .NET 5 (および .NET Core) 以降のバージョンは、.NET Framework と比較して新しいテクノロジと見なすことができますが、.NET Framework からのこの実装の機能が制限される主な要因は 2 つあります。
多数の開発者が、当初は .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 インターフェイスです。
❓ 判断が必要:新しい基底クラスの導入
新しい 抽象 メンバーを導入したり、既存の型のセマンティクスや動作を変更したりしない場合は、2 つの既存の型間の階層に型を導入できます。 たとえば、.NET Framework 2.0 では、DbConnection クラスは、以前にSqlConnectionから直接派生したComponentの新しい基底クラスになりました。
✔️ 許可: あるアセンブリから別のアセンブリに型を移動する
古いアセンブリは、新しいアセンブリを指すTypeForwardedToAttributeでマークする必要があります。
✔️ 許可: 構造体型を
readonly struct
型に変更するreadonly struct
型をstruct
型に変更することはできません。✔️ 許可: アクセス可能な (パブリックまたは保護された) コンストラクターがない場合に、sealed キーワードまたは abstract キーワードを型に追加する
✔️ 型の可視性を拡張することを許可する
❌ 未許可: 型の名前空間または名前を変更する
❌ 許可されていません: 「public type」の名前変更または削除
これにより、名前が変更または削除された型を使用するすべてのコードが中断されます。
注
まれに、.NET によってパブリック API が削除される場合があります。 詳細については、「 .NET での API の削除」を参照してください。 に関する情報を参照してください。NET のサポート ポリシーについては、「 .NET サポート ポリシー」を参照してください。
❌ 未許可: 列挙型の基になる型を変更する
これは、コンパイル時と動作の破壊的変更だけでなく、属性引数を解析不可能にする可能性があるバイナリ破壊的変更です。
❌ 未許可: 以前はアンシールドだった型をシールする
❌ 許可されていません: インターフェイスの基本型のセットにインターフェイスを追加する
インターフェイスが以前に実装していなかったインターフェイスを実装している場合、元のバージョンのインターフェイスを実装したすべての型が壊れます。
❓ 判断が必要: 実装されている一連のインターフェイスから基底クラスまたはインターフェイスのセットからクラスを削除する
インターフェイスの削除規則には例外が 1 つあります。削除されたインターフェイスから派生するインターフェイスの実装を追加できます。 たとえば、型またはインターフェイスがIDisposableを実装するIComponentを実装している場合は、IDisposableを削除できます。
❌ 未許可:
readonly struct
型を struct 型に変更するただし、
struct
型をreadonly struct
型に変更することはできます。❌ 禁止: 構造体 型を
ref struct
型に変更すること、およびその逆も許されない❌ 未許可: 型の可視性を下げる
ただし、型の可視性を高めることは許可されます。
メンバー
✔️ 許可: 仮想ではないメンバーの可視性を拡張する
✔️ 許可: アクセス可能な (パブリックまたは保護された) コンストラクターがないパブリック型に抽象メンバーを追加する、または型がシールされている
ただし、アクセス可能な (パブリックまたは保護された) コンストラクターがあり、
sealed
されていない型に抽象メンバーを追加することはできません。✔️ 許可: 型にアクセス可能な (パブリックまたは保護された) コンストラクターがない場合、または型がシールされている場合に、保護されたメンバーの可視性を制限する
✔️ 許可: メンバーを削除された型よりも階層内の上位クラスに移動する
✔️ 許可: オーバーライドの追加または削除
オーバーライドを導入すると、以前のコンシューマーが ベースを呼び出すときにオーバーライドをスキップする可能性があります。
✔️ ALLOWED: 以前にクラスにコンストラクターがない場合は、パラメーターなしのコンストラクターと共に、クラスにコンストラクターを追加する
ただし、パラメーターなしのコンストラクターを追加 せずに 、以前にコンストラクターが存在しなかったクラスにコンストラクターを追加することは許可されません。
✔️ 許可:
ref readonly
からref
戻り値への変更 (仮想メソッドまたはインターフェイスを除く)✔️ 許可:フィールドの静的な型が可変値型ではない場合に、フィールドから readonly を削除する
✔️ 許可: 以前に定義されていない新しいイベントを呼び出す
❓ 判断が必要: 新しいインスタンス フィールドを型に追加する
この変更はシリアル化に影響します。
❌ 未許可: パブリック メンバーまたはパラメーターの名前変更または削除
これにより、名前が変更または削除されたメンバーまたはパラメーターを使用するすべてのコードが中断されます。
これには、プロパティからのゲッターまたはセッターの削除または名前変更、および列挙メンバーの名前変更または削除が含まれます。
❌ 禁止: インターフェイスへのメンバーの追加
実装を指定した場合、既存のインターフェイスに新しいメンバーを追加しても、ダウンストリーム アセンブリでコンパイルエラーが発生するとは限りません。 ただし、すべての言語で既定のインターフェイス メンバー (DIM) がサポートされているわけではありません。 また、一部のシナリオでは、ランタイムは呼び出す既定のインターフェイス メンバーを決定できません。 このような理由から、既存のインターフェイスにメンバーを追加することは重大な変更と見なされます。
❌ 禁止: パブリック定数または列挙メンバーの値の変更
❌ DISALLOWED: プロパティ、フィールド、パラメーター、または戻り値の型を変更する
❌ 許可されていません: パラメーターの追加、削除、または順序の変更
❌ DISALLOWED: パラメーターの名前変更 (大文字と小文字の変更を含む)
これは、次の 2 つの理由で中断と見なされます。
❌ 禁止:
ref
の戻り値からref readonly
の戻り値への変更❌️ 許可されていない: 仮想メソッドまたはインターフェイスの戻り値を
ref readonly
からref
に変更すること❌ 未許可: メンバーから abstract を追加または削除する
❌ 禁止: メンバーから 仮想 キーワードを削除すること
❌ 未許可: メンバーに virtual キーワードを追加する
C# コンパイラが非仮想メソッドを呼び出すために callvirt Intermediate Language (IL) 命令を出力する傾向があるので、これは多くの場合、重大な変更ではありません (
callvirt
は null チェックを実行しますが、通常の呼び出しでは実行されません)。ただし、いくつかの理由からこの動作は不変ではありません。.NET がターゲットとする言語は C# だけではありません。
C# コンパイラは、ターゲット メソッドが非仮想であり、おそらく null でない場合 (
callvirt
を介してアクセスされるメソッドなど) に対して、通常の呼び出しに対するの最適化をますます試みます。
メソッドを仮想にすると、多くの場合、コンシューマー コードは非仮想的に呼び出されます。
❌ 未許可: virtual メンバーを abstract にする
仮想メンバーは、派生クラスによってオーバーライドできるメソッド実装を提供します。 抽象メンバーには実装がないのでオーバーライドする "必要があります"。
❌ 未許可: sealed キーワードをインターフェイス メンバーに追加する
既定のインターフェイス メンバーに
sealed
を追加すると、そのメンバーの派生型の実装が呼び出されないように、非仮想になります。❌ DISALLOWED: アクセス可能な (パブリックまたはプロテクト) コンストラクターを持ち、シールドされていないパブリック型に抽象メンバーを追加することは許可されていません
❌ 許可されていません: メンバーに対する 静的 キーワードを追加したり削除したりすること
❌ DISALLOWED: 既存のオーバーロードを排除し、別の動作を定義するオーバーロードを追加する
これにより、前のオーバーロードにバインドされた既存のクライアントが中断されます。 たとえば、クラスに、 UInt32を受け入れるメソッドの単一バージョンがある場合、既存のコンシューマーは、 Int32 値を渡すときに、そのオーバーロードに正常にバインドされます。 ただし、 Int32を受け入れるオーバーロードを追加すると、再コンパイル時または遅延バインディングの使用時に、コンパイラが新しいオーバーロードにバインドされるようになりました。 異なる動作結果になる場合、これは破壊的変更です。
❌ DISALLOWED: パラメーターなしのコンストラクターを追加せずに、以前にコンストラクターが存在しなかったクラスにコンストラクターを追加する
❌️ 未許可: フィールドに readonly を追加する
❌ 許可されていません: メンバーの可視性を低下させる
これには、アクセス可能な (または
public
) コンストラクターが存在し、型がprotected
場合に、保護されたメンバーの可視性を低下させることが含まれます。 そうでない場合は、保護されたメンバーの可視性を下げることが許可されます。メンバーの可視性を高めることは許可されます。
❌ 許可されていません: メンバーの型を変更する
メソッドの戻り値、またはプロパティまたはフィールドの型は変更できません。 たとえば、 Object を返すメソッドのシグネチャは、 Stringを返すように変更することはできません。その逆も同様です。
❌ DISALLOWED: 非パブリック フィールドを持たない構造体にインスタンス フィールドを追加する
構造体にパブリック フィールドしかない場合、またはフィールドがまったくない場合、呼び出し元は、最初に使用する前にすべてのパブリック フィールドが構造体に設定されている限り、構造体のコンストラクターを呼び出したり、ローカルを
default(T)
に初期化したりせずに、その構造体型のローカルを宣言できます。 このような構造体に新しいフィールド (パブリックまたは非パブリック) を追加することは、これらの呼び出し元のソース破壊的変更です。コンパイラでは、追加のフィールドを初期化する必要が出るようになったためです。さらに、新しいフィールド (パブリックまたは非パブリック) をフィールドのない構造体に追加するか、パブリック フィールドのみを持つ構造体に追加することは、コードに
[SkipLocalsInit]
を適用した呼び出し元に対するバイナリ破壊的変更です。 コンパイラはコンパイル時にこれらのフィールドを認識していなかったため、構造体を完全に初期化しない IL を出力し、初期化されていないスタック データから構造体が作成される可能性があります。構造体にパブリックでないフィールドがある場合、コンパイラは既にコンストラクターまたは
default(T)
を使用して初期化を強制しており、新しいインスタンス フィールドの追加は破壊的変更ではありません。❌ 未許可: 以前に発生したことのない既存のイベントを発生させる
動作の変更
アセンブリ
✔️ 許可: 同じプラットフォームがまだサポートされている場合にアセンブリを移植可能にする
❌ 許可されていません: アセンブリの名前を変更する
❌ 許可されていません: アセンブリの公開キーの変更
プロパティ、フィールド、パラメーター、および戻り値
✔️ 許可: プロパティ、フィールド、戻り値、または out パラメーターの値をより派生型に変更する
たとえば、 Object の型を返すメソッドは、 String インスタンスを返すことができます。 (ただし、メソッドシグネチャは変更できません)。
✔️ 許可: メンバーが仮想でない場合に、プロパティまたはパラメーターに対して受け入れられる値の範囲を増やす
メソッドに渡すことができる値またはメンバーから返される値の範囲は拡張できますが、パラメーターまたはメンバー型は展開できません。 たとえば、メソッドに渡される値は 0 から 124 から 0 - 255 に拡張できますが、パラメーターの型を Byte から Int32 に変更することはできません。
❌ DISALLOWED: メンバーが仮想である場合に、プロパティまたはパラメーターで受け入れられる値の範囲を増やす
この変更により、既存のオーバーライドされたメンバーが壊れ、拡張された値の範囲で正しく機能しなくなります。
❌ 許可されていません: プロパティまたはパラメーターに対して受け入れられる値の範囲を減らす
❌ 禁止事項: プロパティ、フィールド、戻り値、または out パラメーターで返される値の範囲を広げること
❌ DISALLOWED: プロパティ、フィールド、メソッドの戻り値、または out パラメーターの戻り値を変更する
❌ DISALLOWED: プロパティ、フィールド、またはパラメーターの既定値を変更する
パラメーターの既定値を変更または削除することは、バイナリ区切りではありません。 パラメーターの既定値を削除するとソースの中断が発生し、パラメーターの既定値を変更すると、再コンパイル後に動作が中断される可能性があります。
このため、パラメーターの既定値を削除することは、あいまいさを排除するために、これらの既定値を新しいメソッド オーバーロードに "移動" する場合に許容されます。 たとえば、既存のメソッド
MyMethod(int a = 1)
を考えてみましょう。MyMethod
とa
の 2 つの省略可能なパラメーターを持つb
のオーバーロードを導入する場合は、a
の既定値を新しいオーバーロードに移動することで互換性を維持できます。 2 つのオーバーロードはMyMethod(int a)
とMyMethod(int a = 1, int b = 2)
です。 このパターンを使用すると、MyMethod()
をコンパイルできます。❌ 未許可: 数値の戻り値の有効桁数を変更する
❓ 判断が必要:入力の解析と新しい例外のスローを変更する (解析の動作がドキュメントで指定されていない場合でも)
例外
✔️ 許可:既存の例外ではなく派生した例外をスローする
新しい例外は既存の例外のサブクラスであるため、以前の例外処理コードは引き続き例外を処理します。 たとえば、.NET Framework 4 では、カルチャの作成と取得のメソッドは、カルチャが見つからない場合にCultureNotFoundExceptionではなく、ArgumentExceptionをスローし始めました。 CultureNotFoundExceptionはArgumentExceptionから派生しているため、これは許容される変更です。
✔️ 許可: NotSupportedException、NotImplementedException、NullReferenceExceptionよりも具体的な例外を投げる
✔️ 許可:回復不能と見なされる例外をスローする
回復不能な例外をキャッチするのではなく、高レベルの catch-all ハンドラーで処理する必要があります。 したがって、ユーザーには、これらの明示的な例外をキャッチするコードは必要ありません。 回復不可能な例外は次のとおりです。
✔️ 許可:新しいコード パスで新しい例外をスローする
例外は、新しいパラメーター値または状態で実行され、以前のバージョンを対象とする既存のコードでは実行できない新しいコード パスにのみ適用する必要があります。
✔️ 許可: より堅牢な動作または新しいシナリオを有効にするために例外を削除する
たとえば、以前は正の値のみを処理し、それ以外の場合は
Divide
をスローしたArgumentOutOfRangeExceptionメソッドは、例外をスローせずに負の値と正の値の両方をサポートするように変更できます。✔️ 許可: エラー メッセージのテキストを変更する
開発者は、ユーザーのカルチャに基づいて変更されるエラー メッセージのテキストに依存しないでください。
❌ 未許可: 上記以外の場合に例外をスローする
❌ 許可されていません: 上記以外の場合の例外の削除
属性
✔️ 許可されること: 観測対象ではない属性の値を変更する
❌ 禁止: 観察可能な属性の値を変更すること
❓ 判断が必要: 属性の削除
ほとんどの場合、属性 ( NonSerializedAttribute など) の削除は重大な変更です。
プラットフォームのサポート
✔️ 許可: 以前はサポートされなかったプラットフォームでの操作のサポート
❌ 禁止されています: 以前はプラットフォームでサポートされていた操作が、現在はサポートされていないか、特定のサービスパックが必要になりました
内部実装の変更
❓ 判断が必要:内部タイプの表面積を変更する
このような変更は一般に許可されていますが、非公開のリフレクションは互換性がなくなります。 一般的なサード パーティ製ライブラリや多数の開発者が内部 API に依存している場合、このような変更が許可されない場合があります。
❓ 判断が必要: メンバーの内部実装を変更する
これらの変更は一般に許可されますが、プライベートなリフレクションには支障をきたす可能性があります。 顧客コードがプライベートリフレクションに頻繁に依存している場合や、変更によって意図しない副作用が生じる場合、これらの変更が許可されない場合があります。
✔️ 許可:操作のパフォーマンスを向上させる
操作のパフォーマンスを変更する機能は不可欠ですが、このような変更により、操作の現在の速度に依存するコードが中断される可能性があります。 これは、非同期操作のタイミングに依存するコードに特に当てはまります。 パフォーマンスの変更は、問題の API の他の動作には影響しません。それ以外の場合、変更は中断されます。
✔️ 許可: 操作の性能を間接的に変更すること (多くの場合、それが悪影響を及ぼすことがあります)
問題の変更が何らかの理由で破壊的変更として分類されていない場合、これは許容されます。 多くの場合、追加の操作を含めたり、新しい機能を追加したりするアクションを実行する必要があります。 これはほとんどの場合、パフォーマンスに影響しますが、問題の API を期待どおりに機能させるために不可欠な場合があります。
❌ DISALLOWED: 同期 API を非同期 (およびその逆) に変更する
コード変更
✔️ 許可: パラメーターにparamsを追加する
❌ 禁止: コード ブロックにcheckedステートメントを追加すること
この変更により、以前に実行されたコードから OverflowException がスローされることがあるため、許容されません。
❌ DISALLOWED: パラメーターからparamsを削除することは許可されていません
❌ 禁止: イベントの発生順序を変更すること
開発者は、イベントが同じ順序で発生することを合理的に期待できます。開発者コードは、イベントが発生する順序によって頻繁に異なります。
❌ 許可されていません: 特定のアクションに対するイベントの発生を削除する
❌ DISALLOWED: 指定されたイベントが呼び出される回数を変更する
❌ 許可されていません: 列挙型にFlagsAttributeを追加することは
こちらも参照ください
.NET