trsing’s diary

勉強、読んだ本、仕事で調べたこととかのメモ。

EFFECTIVE C# 6.0/7.0 読書メモ 項目22

項目22 ジェネリックの共変性と反変性をサポートする

独自にインターフェイスやデリゲートを作成する場合、できる限り反変性inあるいは共変性outを設定する。

共変性と反変性をサポートするメリット

作成したインターフェイスやデリゲートの変性*1が誤用されたとしても、コンパイラが検出してくれる。

ジェネリック型C<T>における共変性と反変性

型Xが型Yに変換できる(Y:基底、X:派生)ことがわかっている場合

共変性

C<X>をC<Y>に変換できる(C<Y>←C<X>)

反変性

C<Y>をC<X>に変換できる(C<X>←C<Y>)

配列の共変性にある問題

基底クラスの配列に派生クラスの配列を割り当てることができるが、その要素に別の派生クラスを入れようとすると実行時エラーになる。

static void Main(string[] args)
{
    var planets = new CelestialBody[] { new Planet { Name = "mar", Mass = 1000 } };
    CoVariantArray(planets);
    UnsafeVariantArray(planets);
    CelestialBody[] spaceJunk = new Moon[2] { new Moon(), new Moon { Name="ast",Mass=10000} };
    UnsafeVariantArray(spaceJunk);//System.ArrayTypeMismatchException: '配列と互換性のない型の要素にアクセスしようとしました'
    spaceJunk[0] = new Planet();//System.ArrayTypeMismatchException: '配列と互換性のない型の要素にアクセスしようとしました'
}
abstract public class CelestialBody  :IComparable<CelestialBody>
{
    public double Mass { get; set; }
    public string Name { get; set; }
    public int CompareTo(CelestialBody other)
    { throw new NotImplementedException();  }
}
public class Planet : CelestialBody { }
public class Moon : CelestialBody { }
public class Asteroid : CelestialBody { }
//これはタイプセーフ
public static void CoVariantArray(CelestialBody[] baseItems)
{
    foreach (var thing in baseItems)
        Console.WriteLine($"{thing.Name}の質量は{thing.Mass}Kgです");
}
//これはタイプセーフではない
public static void UnsafeVariantArray(CelestialBody[] baseItems)
{
    baseItems[0] = new Asteroid { Name = "Hygiea", Mass = 8.85e19 };
}

※基底クラスの配列の要素に派生クラスの配列を入れるのはエラー出ない。

    CelestialBody[] spaceJunk = new CelestialBody[2] { new Moon(), new Moon { Name="ast",Mass=10000} };
    spaceJunk[0] = new Planet();

C#における共変性、反変性

共変性:out修飾子がつく
出力位置((限定された場所のこと。関数の戻り値、プロパティのgetアクセサ、デリゲートのシグネチャの一部))にある型がTに制限されるようコンパイラに強制させることができる。

反変性:in修飾子がつく
Tが入力位置にのみ表れるようコンパイラに強制させることができる。

デリゲートでは共変性と反変性が逆転してしまうことがあることに注意

DerivedクラスをBaseクラスからの派生とした場合、

ICovariantDelegate<out T>に対して*2

ICovariantDelegate<Base> ib = new CovariantDelegate<Derived>();
  • Func<T> GetAnItemLater();

使用者:返り値がFunc<Base>であり、このデリゲートを使用すると、Baseが返ってくると期待する。
実際:Func<Derived>が返り値で、このデリゲートを使用するとDerivedが返ってくる。しかしBaseに変換できるので問題なし。

Func<Base> fb = ib.GetAnItemLater();
Base b = fb();
> fb.GetType()
[System.Func`1[Submission#0+Derived]]
> b.GetType()
[Submission#0+Derived]
  • void GiveAnItemLater(Action<T> WhatToDo);

使用者:Action<Base>を設定する。
実際:GiveAnItemLaterは引数がAction<Derived>と考えているのでAction<Base>Derivedを入れる。しかしBaseに変換できるので問題なし。

> ib.GiveAnItemLater((Base b) => { Console.WriteLine(b.GetType().Name); })
Derived

IContravariantDelegate<in T>に対して

IContravariantDelegate<Derived> id = new ContravariantDelegate<Base>();
  • void GetAnItemLater(Func<T> item);

使用者:Func<Derived>を設定する。
実際:GetAnItemLaterFuncからの返り値がBaseであるとして処理をする。Derivedが返ってくるがBaseに変換できるので問題なし

> id.GetAnItemLater(() =>  new Derived() );
Derived
  • Action<T> ActOnAnItemLater();

使用者:返り値がAction<Derived>であることを期待し、このデリゲートにDerivedを入れる。
実際:Action<Base>が返り値で、このデリゲートにDerivedを入れる。しかしBaseに変換できるので問題なし。

> Action<Derived> ad = id.ActOnAnItemLater();
> ad.GetType()
[System.Action`1[Submission#0+Base]]
> ad(new Derived());
Derived

結局どういうことなの

派生クラスは基底クラスに変換できるということ。

  • 引数として基底クラスを期待してるメソッドには派生クラスを入れることができる。

Action<Derived>Action<Base>を代入(反変性)。 ab(new Derived())が可能であるからadに代入しても問題ない。

> Action<Base> ab = (target) => Console.WriteLine(target.GetType().Name);
> Action<Derived> ad = ab;
> ad(new Derived());
Derived

Action<Derived>の引数は中身がAction<Base>でもDerivedじゃないとだめ。

> ad(new Base());
(1,4): error CS1503: 引数 1: は 'Base' から 'Derived' へ変換することはできません。
  • 返り値として基底クラスを期待している型には派生クラスを返すことができる。

Func<Base>Func<Derived>を代入(共変性)。

> Func<Derived> fd = () => new Derived();
> Func<Base> fb = fd;
> Base br = fb();
> br.GetType().Name

返り値はBase(に変換されたDerived)なのでDerivedで受けるには型変換が必要。

> Derived dr = fb();
(1,14): error CS0266: 型 'Base''Derived' に暗黙的に変換できません。明示的な変換が存在します (キャストが不足していないかどうかを確認してください)
Derived dr = (Derived)fb();

デリゲートでの逆転のとこコード

 public class Base { public Base() { } }
 public class Derived : Base { public Derived() { } }
 public interface ICovariantDelegate<out T>
 {
     T GetItem();
     Func<T> GetAnItemLater();
     void GiveAnItemLater(Action<T> whatToDo);
 }
 public class CovariantDelegate<T>  : ICovariantDelegate<T> where T:new()
 {
     public T GetItem() { return new T(); }
     public Func<T> GetAnItemLater() { return () => new T(); }
     public void GiveAnItemLater(Action<T> whatToDo) { whatToDo(new T()); }
 }
ICovariantDelegate<Base> ib = new CovariantDelegate<Derived>();
Func<Base> fb = ib.GetAnItemLater();
Base b = fb();
> fb.GetType()
[System.Func`1[Submission#0+Derived]]
> b.GetType()
[Submission#0+Derived]
> ib.GiveAnItemLater((Base b) => { Console.WriteLine(b.GetType().Name); })
Derived

public interface IContravariantDelegate<in T>
{
    void ActOnAnItem(T item);
    void GetAnItemLater(Func<T> item);
    Action<T> ActOnAnItemLater();
}
public class ContravariantDelegate<T> : IContravariantDelegate<T> where T : new()
{
    public void ActOnAnItem(T item) { }
    public void GetAnItemLater(Func<T> item) { var t = item(); Console.WriteLine(t.GetType().Name); }
    public Action<T> ActOnAnItemLater() { return (T t) => { Console.WriteLine(t.GetType().Name); }; }
}
IContravariantDelegate<Derived> id = new ContravariantDelegate<Base>();
> id.GetAnItemLater(() =>  new Derived() );
Derived
> Action<Derived> ad = id.ActOnAnItemLater();
> ad.GetType()
[System.Action`1[Submission#0+Base]]
> ad(new Derived());
Derived

参考とか

ufcpp.net

docs.microsoft.com

*1:文中やMSDNでは分散とあるけどたぶん変性

*2:コードは後述。CovariantDelegateはICovariantDelegateを実装したクラス