ひとりでのアプリ開発 - fineの備忘録 -

ひとりでアプリ開発をするなかで起こったことや学んだことを書き溜めていきます

C# - 共変性(covariance)・反変性(contravariance)

初めに

 C# 4.0 から、ジェネリックなインターフェースまたはデリゲートに関して、共変性(covariance)・反変性(contravariance)を持たせることができるようになりました。本記事では、共変性・反変性の概念とその使い方についてまとめます。

前提

 本記事では、デリゲート、ジェネリックに関する知識が必要になります。
fineworks-fine.hatenablog.com

共変性(covariance)

共変性とは

 共変性とは、ある型から派生した別の型に対して、その型のインスタンスを代入可能な性質のことをいいます。

 Wikipedia の説明では、次の性質を共変と呼んでいます。

共変(covariant)は、派生 < : 基底 とすると、

B < : A ならば I<B> < : I<A>

になる。

C#における共変性

 C#においては、派生クラスを戻り値とするジェネリックまたはインターフェースを、基底クラスの戻り値にするジェネリックまたはインターフェースに代入可能である性質を共変性といいます。 

F をジェネリックまたはインターフェースとします。このとき、

F <Base> x; ←(代入可能) F <Derived> y;

反変性(contravariance)

反変性とは

 型のインスタンスを逆方向に変換する性質を指します。

 Wikipedia の説明では、次の性質を反変と呼んでいます。

反変(contravariant)は、共変のリバースであり、

B < : A ならば I<A> < : I<B>

になる。

C#における反変性

 C#において、基底クラスを引数にもつジェネリックまたはインターフェースを派生クラスを引数にもつジェネリックまたはインターフェースに代入可能である性質を反変性といいます。

F をジェネリックまたはインターフェースとします。このとき、

F <Derived> x; ←(代入可能) F <Base> y;

動機

 共変性・反変性の仕組みがある動機について、書いています。

共変性について

 コードを書く場合、デリゲートやジェネリックなどのように、考えるできるだけ多くの型に対応できるようにしたいと思うことがあります。

 そこで、次のような例を考えます。

IEnumerable<string> strings = new List<string> { "あ", "い" };
IEnumerable<object> objs = strings;

 object 型はすべての型の継承元であるため、object 型の変数には、任意の型の値を代入することができます。そのため、このようにしても問題がないはずです。

 このように、より具体的な型からより一般的な型への代入をすることができる性質を共変性といいます。

 object 型には任意の型の値を代入できるはずなので、つぎのようにしようとしてみます。するとエラーが生じます。

IEnumerable<string> strings = new List<string> { "あ", "い" };
IEnumerable<object> objs = strings;

objs[0] = 1;  //エラー

 エラーの原因の1つは、IEnumerable インターフェースが out 修飾子を持つからです。

interface IEnumerable<out T>

 この仕組みは、不正な書き換えを防ぐためにあるものです。

 out 修飾子は、型を出力(get)にしか使わないようにするものです。

 先ほどは、out 修飾子があるものに入力をしようとしたため、エラーが発生しました。

 もし、次のようにできてしまった場合、不正な書き換えが起こってしまいます。

IEnumerable<string> strings = new string[] { "あ", "い" };
IEnumerable<object> objs = strings;

objs[0] = 1;  //ここでエラーが出なかったら, stringにint型を代入できてしまう
string str = strings[0];  //上の行でint型の1に書き換えてしまった. まずい.

 out 修飾子がある場合のように値の取得(get)しか使えないようにしていれば、不正な書き換えは起こりません。

反変性について

 get しか使わない場合があるならば、逆に、引数(set)でしか使わない場合もあってよいはずです。

 in 修飾子を使うと、引数(set)にしか値を使わないと宣言できます。引数に基底となる型を指定しておくことで、派生先となる型を多く引数にとることができるようになります。

public delegate void Action<in T>(T obj);
//引数をobjectにしておくことで, なんでも引数にとれる
Action<object> objAction = x => { Console.Write(x); };

//もともとの引数がobjectなので, 引数をstringにしても問題ない
Action<string> strAction = objAction;

 このように、引数をより一般的な型にしておくことで、具体的な型への変換が可能になります。このような性質を反変性といいます。

out 修飾子・in 修飾子

out 修飾子

 out 修飾子を使用することで、指定した型パラメーターに対して、出力(関数の戻り値、プロパティの get )のみを行う制約を課すことができます。

(例)
・定義

//T型のデータを出力するインターフェース
public interface IDataProvider<out T>
{
    T GetData();
}

・インターフェースの実装

public class StringDataProvider : IDataProvider<string>  //Tをstringに
{
    private string data;

    //外部からdataを入力するための関数
    public StringDataProvider(string data)
    {
        this.data = data;
    }

    //データを出力する関数
    public string GetData()
    {
        return data;
    }
}
in 修飾子

 in 修飾子を使用することで、ジェネリック型パラメーターが読み取り専用で入力(メソッドの引数、プロパティの set)のみ可能であることを示します。

(例)
・定義

//Tは
public interface IDataConsumer<in T>
{
    void ConsumeData(T data);
}

・インターフェースの実装

public class Logger : IDataConsumer<string>
{
    public void ConsumeData(in string data)
    {
        Console.WriteLine("Logging: " + data);
        // data = "Updated data";  // コンパイルエラー: 'in' パラメーターには代入できません。
    }
}