質問

.NET/CLR での API のバージョン管理、特に API の変更によってクライアント アプリケーションがどのように影響を受けるか、または影響を受けないかについて、できる限り多くの情報を収集したいと考えています。まず、いくつかの用語を定義しましょう。

APIの変更 - パブリック メンバーを含む、型の一般公開されている定義の変更。これには、型とメンバー名の変更、型の基本型の変更、型の実装されたインターフェイスのリストからのインターフェイスの追加/削除、メンバー (オーバーロードを含む) の追加/削除、メンバーの可視性の変更、メソッドと型パラメーターの名前変更、デフォルト値の追加が含まれます。メソッド パラメーター、型とメンバーの属性の追加/削除、型とメンバーのジェネリック型パラメーターの追加/削除 (何か見落としていましたか?)。これには、メンバー団体の変更やプライベートメンバー (つまり、プライベートメンバー) の変更は含まれません。反射は考慮していません)。

バイナリレベルのブレーク - API の変更により、古いバージョンの API に対してコンパイルされたクライアント アセンブリが新しいバージョンで読み込まれない可能性があります。例:以前と同じ方法で呼び出すことができる場合でも、メソッドのシグネチャを変更します (例:void を使用して型/パラメータのデフォルト値を返すオーバーロード)。

ソースレベルブレーク - API の変更により、古いバージョンの API に対してコンパイルするように作成された既存のコードが、新しいバージョンではコンパイルされなくなる可能性があります。ただし、すでにコンパイルされたクライアント アセンブリは以前と同様に動作します。例:新しいオーバーロードを追加すると、以前は明確であったメソッド呼び出しが曖昧になる可能性があります。

ソースレベルのクワイエットセマンティクスの変更 - API の変更により、古いバージョンの API に対してコンパイルするように作成された既存のコードがそのセマンティクスを静かに変更します。別のメソッドを呼び出すことによって。ただし、コードは警告やエラーなしでコンパイルを続行し、以前にコンパイルされたアセンブリは以前と同様に動作するはずです。例:既存のクラスに新しいインターフェイスを実装すると、オーバーロードの解決中に別のオーバーロードが選択されます。

最終的な目標は、できる限り多くの破壊的なセマンティクス API の変更と静かなセマンティクス API の変更をカタログ化し、破壊の正確な影響と、その影響を受ける言語と影響を受けない言語を説明することです。後者を拡張するには:一方、一部の変更はすべての言語に普遍的に影響します (例:インターフェイスに新しいメンバーを追加すると、どの言語でもそのインターフェイスの実装が中断されます)。一部の言語では、中断するために非常に特殊な言語セマンティクスが必要になります。これには最も一般的にメソッドのオーバーロードが含まれ、一般的には暗黙的な型変換に関係するあらゆるものが含まれます。CLS 準拠の言語であっても、ここで「最小公倍数」を定義する方法はないようです。少なくとも CLI 仕様で定義されている「CLS コンシューマ」のルールに準拠しているもの) - ただし、誰かがここで間違っていると訂正してくれれば幸いです - したがって、これは言語ごとに行う必要があります。最も興味深いものは、当然のことながら、すぐに .NET に付属しているものです。C#、VB、F#。ただし、IronPython、IronRuby、Delphi Prism などの他のものも関連します。特殊なケースであればあるほど、より興味深いものになります。メンバーの削除などは非常に自明のことですが、たとえばメンバー間の微妙な相互作用があります。メソッドのオーバーロード、オプション/デフォルトのパラメーター、ラムダ型推論、変換演算子は、非常に驚​​くべきものになることがあります。

これを開始するためのいくつかの例:

新しいメソッドのオーバーロードの追加

親切:ソースレベルのブレーク

影響を受ける言語:C#、VB、F#

変更前のAPI:

public class Foo
{
    public void Bar(IEnumerable x);
}

変更後のAPI:

public class Foo
{
    public void Bar(IEnumerable x);
    public void Bar(ICloneable x);
}

サンプル クライアント コードは変更前は動作していましたが、変更後には機能しませんでした。

new Foo().Bar(new int[0]);

新しい暗黙的な変換演算子のオーバーロードの追加

親切:ソースレベルのブレーク。

影響を受ける言語:C#、VB

影響を受けない言語:F#

変更前のAPI:

public class Foo
{
    public static implicit operator int ();
}

変更後のAPI:

public class Foo
{
    public static implicit operator int ();
    public static implicit operator float ();
}

サンプル クライアント コードは変更前は動作していましたが、変更後には機能しませんでした。

void Bar(int x);
void Bar(float x);
Bar(new Foo());

ノート:F# はオーバーロードされた演算子を言語レベルでサポートしておらず、明示的にも暗黙的にもサポートしていないため、壊れていません。両方とも次のように直接呼び出す必要があります。 op_Explicit そして op_Implicit 方法。

新しいインスタンスメソッドの追加

親切:ソースレベルのクワイエットセマンティクスが変更されます。

影響を受ける言語:C#、VB

影響を受けない言語:F#

変更前のAPI:

public class Foo
{
}

変更後のAPI:

public class Foo
{
    public void Bar();
}

静かなセマンティクスの変更を受けるクライアント コードのサンプル:

public static class FooExtensions
{
    public void Bar(this Foo foo);
}

new Foo().Bar();

ノート:F# は言語レベルでサポートされていないため、壊れていません。 ExtensionMethodAttribute, であり、CLS 拡張メソッドを静的メソッドとして呼び出す必要があります。

役に立ちましたか?

解決

メソッドのシグネチャを変更する

種類:バイナリ・レベルのブレーク

の影響を受ける言語:C#の(VBとF#の最も可能性が高いが、テストされていない)

変更前のAPI

public static class Foo
{
    public static void bar(int i);
}

変更後のAPI

public static class Foo
{
    public static bool bar(int i);
}

変更前の作業のサンプルクライアントコード

Foo.bar(13);

他のヒント

デフォルト値を持つパラメータを追加します。

休憩の種類:バイナリレベルのブレーク

呼び出し元のソース コードを変更する必要がない場合でも、再コンパイルする必要があります (通常のパラメーターを追加する場合と同様)。

これは、C# がパラメーターの既定値を呼び出し側アセンブリに直接コンパイルするためです。これは、古いアセンブリが引数の少ないメソッドを呼び出そうとするため、再コンパイルしないと MissingMethodException が発生することを意味します。

変更前のAPI

public void Foo(int a) { }

変更後のAPI

public void Foo(int a, string b = null) { }

後で壊れるサンプルクライアントコード

Foo(5);

クライアント コードを再コンパイルする必要がある Foo(5, null) バイトコードレベルで。呼び出されたアセンブリには、 Foo(int, string), 、 ない Foo(int). 。これは、デフォルトのパラメーター値は純粋に言語機能であり、.Net ランタイムはそれらについて何も知らないためです。(これは、C# でデフォルト値がコンパイル時の定数でなければならない理由も説明しています)。

私は特にインターフェイスに対して同じ状況との違いを考慮して、それを発見したときに

この1は非常に非自明でした。これは、すべてのブレークではないが、それは私がそれを含めることを決めたことを十分に驚きだ。

基底クラスにリファクタリングクラスのメンバー

種類:!ないブレーク

言語影響:なし(すなわち、何も破壊されていない)

変更前のAPIます:

class Foo
{
    public virtual void Bar() {}
    public virtual void Baz() {}
}

変更後のAPIます:

class FooBase
{
    public virtual void Bar() {}
}

class Foo : FooBase
{
    public virtual void Baz() {}
}

の変化を通じて作業し続けるサンプルコード(私はそれを破ることが期待にもかかわらず):

// C++/CLI
ref class Derived : Foo
{
   public virtual void Baz() {{

   // Explicit override    
   public virtual void BarOverride() = Foo::Bar {}
};

注:

「明示的なオーバーライド」 -

C ++ / CLIは、仮想基本クラスのメンバの明示的なインターフェイスの実装に類似した構造を持っている唯一の.NET言語です。 Iは、完全(IL明示的なオーバーライドのために生成するので、明示的なインプリメンテーションの場合と同じである)は、ベース・インターフェースにインターフェース部材を移動させるときと破損の同じ種類をもたらすことが期待しました。驚いたことに、このケースではありません - 生成されたILはまだBarOverrideオーバーライドではなくFoo::BarよりFooBase::Barように指定していても、アセンブリローダーは、苦情なしで正常に別のものを代用するのに十分なスマートです - どうやら、Fooがクラスであるという事実は何ですか違いになります。行くフィギュア...

この1は、「追加/削除するインターフェイスメンバー」の、おそらくそれほど明白ではない特殊なケースである、と私はそれが私が次投稿するつもりです他の例に照らして、独自のエントリに値する考え出し。だから、ます:

ベースインターフェイスにリファクタリングインターフェース部材

種類:ソースとバイナリレベルの両方で改

の影響を受ける言語:のC#、VB、C ++ / CLI、F#(ソース・ブレークのため、バイナリ1が自然に任意の言語に影響を与える)

変更前のAPIます:

interface IFoo
{
    void Bar();
    void Baz();
}

変更後のAPIます:

interface IFooBase 
{
    void Bar();
}

interface IFoo : IFooBase
{
    void Baz();
}

ソースレベルでの変化によって破壊されたサンプルクライアントコード:

class Foo : IFoo
{
   void IFoo.Bar() { ... }
   void IFoo.Baz() { ... }
}
バイナリレベルでの変化によって破壊される

サンプルクライアントコードと、

(new Foo()).Bar();

注:

ソース・レベル・ブレークの場合、問題は、C#、VBおよびC ++ / CLIはすべてインターフェース部材の実装の宣言内の正確のインタフェース名を必要とすることです。部材は、ベースインタフェースに移動入った場合このように、コードはもはやコンパイルありません。

バイナリブレークは、インターフェイスメソッドは、明示的な実装のために生成されたILで完全修飾され、およびインタフェース名も正確な存在でなければならないという事実のためである。

(すなわち、C#およびC ++ / CLIはなく、VB)利用可能な

暗黙の実装では、ソースとバイナリレベルの両方で正常に動作します。メソッドの呼び出しはどちらか壊れません。

列挙値の並べ替え

休憩の種類: ソースレベル/バイナリレベルのクワイエットセマンティクスの変更

影響を受ける言語:全て

列挙値を並べ替えると、リテラルが同じ名前を持つため、ソース レベルの互換性が維持されますが、順序インデックスが更新されるため、ある種のサイレント ソース レベルの中断が発生する可能性があります。

さらに悪いことに、クライアント コードが新しい API バージョンに対して再コンパイルされない場合にサイレント バイナリ レベルのブレークが発生する可能性があります。Enum 値はコンパイル時の定数であるため、それらの使用はクライアント アセンブリの IL に組み込まれます。このケースは、場合によっては特に発見するのが難しい場合があります。

変更前のAPI

public enum Foo
{
   Bar,
   Baz
}

変更後のAPI

public enum Foo
{
   Baz,
   Bar
}

動作するがその後壊れるサンプル クライアント コード:

Foo.Bar < Foo.Baz

この1は本当に実際には非常にまれなことですが、それにもかかわらず、驚くべき1それが起こるときます。

新しい非オーバーロードされたメンバーは、

を追加します

種類:ソースレベルのブレークまたは静かなセマンティクスの変更

の影響を受ける言語:C#の、VB

言語の影響を受けない:F#の、C ++ / CLI

変更前のAPIます:

public class Foo
{
}

変更後のAPIます:

public class Foo
{
    public void Frob() {}
}

の変化によって破壊されたサンプルクライアントコード:

class Bar
{
    public void Frob() {}
}

class Program
{
    static void Qux(Action<Foo> a)
    {
    }

    static void Qux(Action<Bar> a)
    {
    }

    static void Main()
    {
        Qux(x => x.Frob());        
    }
}

注:

ここでの問題は、オーバーロード解決の存在下でのC#とVBでラムダ型推論によって引き起こされます。ダックタイピングの限定された形は、ラムダの本体は、指定された型のセンス行うか否かをチェックすることによって、複数のタイプが一致した関係を破壊するためにここで使用されている - 。コンパイル体に一種類のみの結果は、1つが選択された場合に

ここで危険なのは、クライアントコードは、いくつかの方法が自分の型の引数を取り、他の人があなたのライブラリーによって公開された型の引数を取るオーバーロードされたメソッド基を有していてもよいということです。彼のコードのいずれかが、その後、単にメンバーの有無に基づいて、正しい方法を決定するために型推論アルゴリズムに依存している場合は、クライアントのタイプのいずれかと同じ名前で、あなたのタイプのいずれかに新しいメンバーを追加すると、潜在的に推論を投げることができますオフ、オーバーロード解決の間に曖昧さが生じる。

この例ではタイプFooBarはない継承によってもそうでない場合は、何らかの方法で関連していないことに留意されたいです。単一のメソッド群では、それらの単なる使用は、これを引き起こすのに十分であり、これは、クライアントコードで発生した場合、あなたはそれを制御することはできません。

上記のサンプルコードは、これは、ソース・レベル・ブレーク(すなわち、コンパイラエラーの結果)である単純な状況を示しています。推論を経て選ばれた過負荷がそれ以外の場合は、暗黙的に必要な宣言と実際の引数の間にデフォルト値、または型の不一致と例えばオプションの引数(下のランク付けさせるような他の引数を持っていた場合は、これはまた、サイレントセマンティクスを変更することができ変換)。このようなシナリオでは、オーバーロードの解決は、もはや失敗しますが、異なる過負荷は静かに、コンパイラによって選択されます。しかし、実際には、慎重に慎重にそれを引き起こすためにメソッドのシグネチャを構築することなく、この場合に実行することは非常に困難である。

明示一つに暗黙的なインタフェースの実装に変換します。

ブレークの種類:ソースとバイナリ

影響を受ける

言語:すべての

これは本当にメソッドのアクセシビリティを変更するだけの変化である - そのちょうどもう少し微妙な、それはインターフェースのメソッドにないすべてのアクセスは、インターフェイスのタイプを参照することにより、必然であるという事実を見落とすことは簡単ですので、

変更前のAPIます:

public class Foo : IEnumerable
{
    public IEnumerator GetEnumerator();
}

変更後のAPIます:

public class Foo : IEnumerable
{
    IEnumerator IEnumerable.GetEnumerator();
}
変更前の作品と、その後切断された

サンプルクライアントコード:

new Foo().GetEnumerator(); // fails because GetEnumerator() is no longer public

暗黙一つに明示的なインターフェイスのインプリメンテーションに変換します。

ブレークの種類:ソース

影響を受ける

言語:すべての

暗黙の一つに、明示的なインターフェイスの実装のリファクタリングは、それがAPIを破ることができる方法で、より微妙です。表面には、継承と組み合わせたとき、それは問題を引き起こす可能性があります、しかし、これは比較的安全であることを思わます。

変更前のAPIます:

public class Foo : IEnumerable
{
    IEnumerator IEnumerable.GetEnumerator() { yield return "Foo"; }
}

変更後のAPIます:

public class Foo : IEnumerable
{
    public IEnumerator GetEnumerator() { yield return "Foo"; }
}
変更前の作品と、その後切断された

サンプルクライアントコード:

class Bar : Foo, IEnumerable
{
    IEnumerator IEnumerable.GetEnumerator() // silently hides base instance
    { yield return "Bar"; }
}

foreach( var x in new Bar() )
    Console.WriteLine(x);    // originally output "Bar", now outputs "Foo"

プロパティにフィールドを変更する

ブレークの種類:API

の影響を受ける言語:Visual BasicとC#*

インフォメーション:あなたはVisual Basicでプロパティに通常のフィールドまたは変数を変更すると、どのような方法でそのメンバーを参照する任意の外部のコードを再コンパイルする必要があります。

変更前の API:

Public Class Foo    
    Public Shared Bar As String = ""    
End Class

変更後の API:

Public Class Foo
    Private Shared _Bar As String = ""
    Public Shared Property Bar As String
        Get
            Return _Bar
        End Get
        Set(value As String)
            _Bar = value
        End Set
    End Property
End Class    

を動作しますが、その後切断されたサンプルクライアントコード:

Foo.Bar = "foobar"

名前空間の追加

ソースレベルのブレーク / ソースレベルのクワイエットセマンティクスの変更

vb.Net での名前空間解決の仕組みが原因で、ライブラリに名前空間を追加すると、以前のバージョンの API でコンパイルされた Visual Basic コードが新しいバージョンでコンパイルされなくなる可能性があります。

サンプルクライアントコード:

Imports System
Imports Api.SomeNamespace

Public Class Foo
    Public Sub Bar()
        Dim dr As Data.DataRow
    End Sub
End Class

新しいバージョンの API で名前空間が追加された場合 Api.SomeNamespace.Data, の場合、上記のコードはコンパイルされません。

プロジェクトレベルの名前空間インポートでは、さらに複雑になります。もし Imports System 上記のコードでは省略されていますが、 System 名前空間がプロジェクト レベルでインポートされた場合でも、コードでエラーが発生する可能性があります。

ただし、API にクラスが含まれている場合は、 DataRow その中で Api.SomeNamespace.Data 名前空間の場合、コードはコンパイルされますが、 dr の例になります System.Data.DataRow 古いバージョンの API でコンパイルした場合、 Api.SomeNamespace.Data.DataRow 新しいバージョンの API でコンパイルした場合。

引数の名前変更

ソースレベルブレーク

引数の名前の変更は、vb.net ではバージョン 7(?) (.Net バージョン 1?) から、c#.net ではバージョン 4 (.Net バージョン 4) からは重大な変更です。

変更前のAPI:

namespace SomeNamespace {
    public class Foo {
        public static void Bar(string x) {
           ...
        }
    }
}

変更後のAPI:

namespace SomeNamespace {
    public class Foo {
        public static void Bar(string y) {
           ...
        }
    }
}

サンプルクライアントコード:

Api.SomeNamespace.Foo.Bar(x:"hi"); //C#
Api.SomeNamespace.Foo.Bar(x:="hi") 'VB

参照パラメータ

ソースレベルブレーク

1 つのパラメーターが値ではなく参照によって渡されることを除き、同じシグネチャを持つメソッド オーバーライドを追加すると、API を参照する vb ソースが関数を解決できなくなります。Visual Basic には、引数名が異なる場合を除き、呼び出し時点でこれらのメソッドを区別する方法 (?) がないため、このような変更により両方のメンバーが vb コードから使用できなくなる可能性があります。

変更前のAPI:

namespace SomeNamespace {
    public class Foo {
        public static void Bar(string x) {
           ...
        }
    }
}

変更後のAPI:

namespace SomeNamespace {
    public class Foo {
        public static void Bar(string x) {
           ...
        }
        public static void Bar(ref string x) {
           ...
        }
    }
}

サンプルクライアントコード:

Api.SomeNamespace.Foo.Bar(str)

フィールドからプロパティへの変更

バイナリレベルブレーク/ソースレベルブレーク

明らかなバイナリ レベルのブレークに加えて、メンバーが参照によってメソッドに渡された場合、ソース レベルのブレークが発生する可能性があります。

変更前のAPI:

namespace SomeNamespace {
    public class Foo {
        public int Bar;
    }
}

変更後のAPI:

namespace SomeNamespace {
    public class Foo {
        public int Bar { get; set; }
    }
}

サンプルクライアントコード:

FooBar(ref Api.SomeNamespace.Foo.Bar);

APIの変更:

  1. [Obsolete] 属性の追加 (属性について言及することでこれをカバーしました。ただし、警告をエラーとして使用する場合、これは重大な変更になる可能性があります)。

バイナリレベルのブレーク:

  1. 型をあるアセンブリから別のアセンブリに移動する
  2. 型の名前空間の変更
  3. 別のアセンブリから基本クラス型を追加します。
  4. 別のアセンブリ (Class2) の型をテンプレート引数制約として使用する新しいメンバー (イベント保護) を追加します。

    protected void Something<T>() where T : Class2 { }
    
  5. 子クラス (Class3) がこのクラスのテンプレート引数として使用される場合、別のアセンブリの型から派生するように変更します。

    protected class Class3 : Class2 { }
    protected void Something<T>() where T : Class3 { }
    

ソースレベルのクワイエットセマンティクスの変更:

  1. Equals()、GetHashCode()、または ToString() のオーバーライドの追加/削除/変更

(これらがどこに当てはまるかわかりません)

デプロイメントの変更:

  1. 依存関係/参照の追加/削除
  2. 依存関係を新しいバージョンに更新する
  3. 「ターゲット プラットフォーム」を x86、Itanium、x64、または anycpu の間で変更する
  4. 別のフレームワーク インストールでのビルド/テスト (例:.Net 2.0 ボックスに 3.5 をインストールすると、.Net 2.0 SP2 を必要とする API 呼び出しが可能になります)

ブートストラップ/構成の変更:

  1. カスタム構成オプションの追加/削除/変更 (例:App.config 設定)
  2. 今日のアプリケーションでは IoC/DI が頻繁に使用されているため、DI に依存するコードのブートストラップ コードを再構成および/または変更する必要があります。

アップデート:

申し訳ありませんが、これが私にとって問題となる唯一の理由は、テンプレート制約でそれらを使用したことであることに気づきませんでした。

オーバーロード メソッドを追加してデフォルトのパラメーターの使用を廃止する

休憩の種類: ソースレベルのクワイエットセマンティクスの変更

コンパイラーは、デフォルトのパラメーター値が欠落しているメソッド呼び出しを呼び出し側のデフォルト値を持つ明示的な呼び出しに変換するため、既存のコンパイル済みコードとの互換性が確保されます。正しいシグネチャを持つメソッドは、以前にコンパイルされたすべてのコードに対して見つかります。

一方、オプションのパラメーターを使用しない呼び出しは、オプションのパラメーターが欠落している新しいメソッドへの呼び出しとしてコンパイルされるようになりました。すべては引き続き正常に動作しますが、呼び出されたコードが別のアセンブリに存在する場合、それを呼び出す新しくコンパイルされたコードは、このアセンブリの新しいバージョンに依存するようになります。リファクタリングされたコードが存在するアセンブリをデプロイせずに、リファクタリングされたコードを呼び出すアセンブリをデプロイすると、「メソッドが見つかりません」という例外が発生します。

変更前のAPI

  public int MyMethod(int mandatoryParameter, int optionalParameter = 0)
  {
     return mandatoryParameter + optionalParameter;
  }    

変更後のAPI

  public int MyMethod(int mandatoryParameter, int optionalParameter)
  {
     return mandatoryParameter + optionalParameter;
  }

  public int MyMethod(int mandatoryParameter)
  {
     return MyMethod(mandatoryParameter, 0);
  }

まだ動作するサンプルコード

  public int CodeNotDependentToNewVersion()
  {
     return MyMethod(5, 6); 
  }

コンパイル時に新しいバージョンに依存するようになったサンプル コード

  public int CodeDependentToNewVersion()
  {
     return MyMethod(5); 
  }

インターフェイスの名前を変更

ちょっと休憩の:ソースとのバイナリ

の影響を受ける言語:ほとんどの場合、すべての、C#でテストした。

変更前の API:

public interface IFoo
{
    void Test();
}

public class Bar
{
    IFoo GetFoo() { return new Foo(); }
}

変更後の API:

public interface IFooNew // Of the exact same definition as the (old) IFoo
{
    void Test();
}

public class Bar
{
    IFooNew GetFoo() { return new Foo(); }
}

を動作しますが、その後切断されたサンプルクライアントコード:

new Bar().GetFoo().Test(); // Binary only break
IFoo foo = new Bar().GetFoo(); // Source and binary break

null 許容型のパラメータを使用したメソッドのオーバーロード

親切: ソースレベルブレーク

影響を受ける言語: C#、VB

変更前のAPI:

public class Foo
{
    public void Bar(string param);
}

変更後のAPI:

public class Foo
{
    public void Bar(string param);
    public void Bar(int? param);
}

変更前は機能し、変更後は壊れたサンプル クライアント コード:

new Foo().Bar(null);

例外:次のメソッドまたはプロパティ間の呼び出しがあいまいです。

拡張メソッドへ昇格

種類:ソースレベルのブレーク

の影響を受ける言語:C#のV6と高い(?多分他)

変更前のAPIます:

public static class Foo
{
    public static void Bar(string x);
}

変更後のAPIます:

public static class Foo
{
    public void Bar(this string x);
}

サンプルクライアントコードが変更前の作業とそれの後に壊れます:

using static Foo;

class Program
{
    static void Main() => Bar("hello");
}

詳細: https://github.com/dotnet/csharplang/issues/665

scroll top