プログラミング言語理論における共変性と反変性の概念を誰かが説明できますか?
4 に答える
共分散は非常に単純で、いくつかのコレクションクラスの観点から最もよく考えられていList
ます。いくつかのタイプパラメータを使用してクラスをパラメータ化できます。つまり、リストにはいくつかのタイプの要素が含まれています。次の場合、リストは共変になりますList
T
T
T
SはTのサブタイプです。ただし、List[S]はList[T]のサブタイプです。
(ここで、数学的な定義iffを使用している場合は、その場合に限ります。)
つまり、aList[Apple]
は List[Fruit]
です。List[Fruit]
パラメータとしてaを受け入れるルーチンがあり、私がを持っているList[Apple]
場合、これを有効なパラメータとして渡すことができます。
def something(l: List[Fruit]) {
l.add(new Pear())
}
コレクションクラスList
が変更可能である場合、上記のようにルーチンが他のフルーツ(リンゴではない)を追加できると想定する可能性があるため、共分散は意味がありません。したがって、不変のコレクションクラスのみが共変であるようにする必要があります。
共分散と反分散の間には違いがあります。
大まかに言えば、操作は型の順序を保持する場合は共変であり、この順序を逆にする場合は反変です。
順序付け自体は、より一般的なタイプをより具体的なタイプよりも大きく表すことを意図しています。
C# が共分散をサポートする状況の一例を次に示します。まず、これはオブジェクトの配列です:
object[] objects=new object[3];
objects[0]=new object();
objects[1]="Just a string";
objects[2]=10;
もちろん、配列に異なる値を挿入することは可能です。最終的にはすべてSystem.Object
.Net フレームワークから派生するからです。つまり、System.Object
非常に一般的または大きな型です。共分散がサポートされている場所は次のとおりです:
小さい型の値を大きい型の変数に代入する
string[] strings=new string[] { "one", "two", "three" };
objects=strings;
type の変数オブジェクトobject[]
は、実際には type の値を格納できますstring[]
。
考えてみてください — ある意味では、それはあなたが期待するものですが、そうではありません。結局のところ、string
は から派生していますがobject
、は から派生していstring[]
ませんobject[]
。この例の共分散の言語サポートにより、いずれにせよ代入が可能になります。これは、多くの場合に見られるものです。バリアンスは、言語をより直感的に機能させる機能です。
これらのトピックに関する考慮事項は非常に複雑です。たとえば、前のコードに基づいて、エラーが発生する 2 つのシナリオを次に示します。
// Runtime exception here - the array is still of type string[],
// ints can't be inserted
objects[2]=10;
// Compiler error here - covariance support in this scenario only
// covers reference types, and int is a value type
int[] ints=new int[] { 1, 2, 3 };
objects=ints;
反変性の仕組みの例はもう少し複雑です。次の 2 つのクラスを想像してください。
public partial class Person: IPerson {
public Person() {
}
}
public partial class Woman: Person {
public Woman() {
}
}
Woman
Person
明らかにから派生しています。次の 2 つの関数があるとします。
static void WorkWithPerson(Person person) {
}
static void WorkWithWoman(Woman woman) {
}
関数の 1 つは で何かを実行し (何をしても構いません) Woman
、もう 1 つはより一般的で、 から派生した任意の型で動作しますPerson
。物事のWoman
側面では、次のものもあります。
delegate void AcceptWomanDelegate(Woman person);
static void DoWork(Woman woman, AcceptWomanDelegate acceptWoman) {
acceptWoman(woman);
}
DoWork
は、および も受け取る関数Woman
への参照を受け取ることができる関数でありWoman
、 のインスタンスをWoman
デリゲートに渡します。ここにある要素のポリモーフィズムを考慮してください。Person
はよりも大きくWoman
、WorkWithPerson
はよりも大きいですWorkWithWoman
。
分散の目的よりも大きいWorkWithPerson
と見なされます。AcceptWomanDelegate
最後に、次の 3 行のコードがあります。
Woman woman=new Woman();
DoWork(woman, WorkWithWoman);
DoWork(woman, WorkWithPerson);
Woman
インスタンスが作成されます。次に DoWork が呼び出され、Woman
インスタンスとWorkWithWoman
メソッドへの参照が渡されます。後者は明らかにデリゲート型と互換性がありAcceptWomanDelegate
ます — type の 1 つのパラメーターでありWoman
、戻り値の型はありません。ただし、3行目は少し奇妙です。このメソッドWorkWithPerson
は、で必要なPerson
ではなく、 をパラメーターとして受け取ります。ただし、デリゲート型と互換性があります。反変性によりそれが可能になるため、デリゲートの場合、より大きな型をより小さな型の変数に格納できます。繰り返しになりますが、これは直感的なことです。Woman
AcceptWomanDelegate
WorkWithPerson
WorkWithPerson
AcceptWomanDelegate
WorkWithPerson
Person
Woman
ここまでで、これらすべてがジェネリックにどのように関係するのか疑問に思われるかもしれません。答えは、バリアンスはジェネリックにも適用できるということです。前の例では配列を使用object
しています。string
ここでは、コードは配列の代わりに一般的なリストを使用しています:
List<object> objectList=new List<object>();
List<string> stringList=new List<string>();
objectList=stringList;
これを試してみると、C# でサポートされているシナリオではないことがわかります。C# バージョン 4.0 および .Net Framework 4.0 では、ジェネリックのバリアンス サポートがクリーンアップされ、新しいキーワードをジェネリック型パラメーターで使用できるようになりました。特定の型パラメーターのデータ フローの方向を定義および制限して、バリアンスを機能させることができます。しかし、 の場合、型のデータは両方向に流れます。型には値を返すメソッドと、そのような値を受け取るメソッドがあります。List<T>
T
List<T>
T
これらの方向制限のポイントは、意味のある場所での分散を許可することですが、前の配列の例の 1 つで言及された実行時エラーのような問題を防ぐことです。型パラメーターがinまたはoutで正しく修飾されている場合、コンパイラーはコンパイル時にその差異をチェックし、許可または禁止することができます。Microsoft は、次のような .Net フレームワークの多くの標準インターフェイスにこれらのキーワードを追加する努力をしてきましたIEnumerable<T>
。
public interface IEnumerable<out T>: IEnumerable {
// ...
}
このインターフェイスの場合、型T
オブジェクトのデータ フローは明確です。これらは、このインターフェイスでサポートされているメソッドからのみ取得でき、メソッドに渡すことはできません。その結果、List<T>
以前に説明した試みと同様の例を構築することが可能ですが、以下を使用しIEnumerable<T>
ます。
IEnumerable<object> objectSequence=new List<object>();
IEnumerable<string> stringSequence=new List<string>();
objectSequence=stringSequence;
このコードは、バージョン 4.0 以降の C# コンパイラで受け入れられます。これは、型パラメーターのout指定子IEnumerable<T>
により共変であるためです。T
ジェネリック型を扱うときは、コードが期待どおりに動作するようにするために、分散と、コンパイラがさまざまな種類の策略を適用する方法に注意することが重要です。
バリアンスについては、この章で説明した以外にも知っておくべきことがありますが、それ以降のすべてのコードを理解するには、これで十分です。
参照:
C# と CLR はどちらも、メソッドをデリゲートにバインドするときに、参照型の共分散と反分散を許可します。共分散とは、メソッドがデリゲートの戻り値の型から派生した型を返すことができることを意味します。反分散とは、メソッドがデリゲートのパラメーター型のベースであるパラメーターを受け取ることができることを意味します。たとえば、次のように定義されたデリゲートがあるとします。
delegate Object MyCallback(FileStream s);
プロトタイプ化されたメソッドにバインドされたこのデリゲート型のインスタンスを構築することが可能です
このような:
String SomeMethod(Stream s);
ここで、SomeMethod の戻り値の型 (String) は、デリゲートの戻り値の型 (Object) から派生した型です。この共分散は許容されます。SomeMethod のパラメーターの型 (Stream) は、デリゲートのパラメーターの型 (FileStream) の基本クラスである型です。この反分散は許容されます。
共分散と反分散は参照型でのみサポートされ、値型や void ではサポートされないことに注意してください。したがって、たとえば、次のメソッドを MyCallback デリゲートにバインドできません。
Int32 SomeOtherMethod(Stream s);
SomeOtherMethod の戻り値の型 (Int32) は MyCallback の戻り値の型 (Object) から派生していますが、Int32 は値型であるため、この形式の共分散は許可されません。
明らかに、共分散と反分散に値型と void を使用できない理由は、参照型のメモリ構造は常にポインターであるのに対し、これらのメモリ構造はさまざまであるためです。幸いなことに、サポートされていないことをしようとすると、C# コンパイラはエラーを生成します。