|
|
連載:C# 4入門
第6回 in/outキーワードと共変性と反変性
株式会社ピーデー 川俣 晶
2010/12/17 |
|
|
実は単純ではないジェネリック
先の問題のポイントは以下にある。
- 安全と見なせる別の型を受け入れることは問題ない
- しかし、引数と戻り値では安全と見なせる型が違う
つまり、「安全な変換」は2種類あり、安全な変換を許す場合には分類して扱わねばならない。
そこで導入されたのが「in/outキーワード」である。この問題を詳しく説明していこう。以下がその利用例である。
using System;
public interface ISample1<in T>
{
void SetValue(T t);
}
public interface ISample2<out T>
{
T GetValue();
}
public class A { }
public class B : A { }
public class C : B { }
public class Sample1<T> : ISample1<T>
{
public void SetValue(T t)
{
Console.WriteLine("setting:{0}", t);
}
}
public class Sample2<T> : ISample2<T>
{
T ISample2<T>.GetValue()
{
return default(T);
}
}
class Program
{
static void Main(string[] args)
{
ISample1<C> c = new Sample1<B>();
ISample2<A> a = new Sample2<B>();
c.SetValue(new C());
Console.WriteLine(a.GetValue());
}
}
|
|
リスト3 |
一見、ISample1インターフェイスとISample2インターフェイスの型パラメータ「T」は同じ定義でよいように思えるが、それでは以下の2行を有効にできない。
ISample1<C> c = new Sample1<B>();
ISample2<A> a = new Sample2<B>();
|
|
Sample1<B>はもちろん、ISample1<B>を実装しているから暗黙の変換が安全にできる。また、Sample2<B>はもちろん、ISample2<B>を実装しているから暗黙の変換が安全にできる。
しかし、疑問なく変換できるのはそこまでだ。ISample1には以下のような変換ルールが期待されている。
- ISample1<B>からISample1<C>への変換は安全である
- ISample1<B>からISample1<A>への変換は安全ではない
しかし、ISample2に期待されるルールは逆転している。
- ISample2<B>からISample2<A>への変換は安全である
- ISample2<B>からISample2<C>への変換は安全ではない
その違いを明示的に示しているのがin/outキーワードというわけである。リスト3では、ISample1のTにはinが、ISample2のTにはoutが付加されている。
in/outキーワードの使いどころ
実は、このin/outキーワードは使い方が難しい。より正確にいうと、使いどころを探すのが難しい。
まず、型パラメータに作用する機能であるため、そもそも型パラメータが登場しないと出番がないが、一般的に型パラメータを使った定義を行う機会は少ない。例えばList<T>クラスを利用するケースは多いが、List<T>に相当するクラスを書く機会は少ないのである。
さらにいえば、実はコレクションに相当するクラスを書いたとしても、それで出番があるわけではない。in/outキーワードはインターフェイスやデリゲートで使用できる機能であるため、クラスを自分で実装しても、クラス・ライブラリのインターフェイスを実装するだけなら自分で書く必要がない。
つまり、コレクションを自作で書くだけでは足りない。コレクションの体系そのものをゼロから再実装するような状況でも訪れないと使うことはないかもしれない。だから、はっきりいって実際に書く可能性は極めて低い。
では知らなくてよいのかといえば、そうでもない。なぜなら、in/outキーワードの利用者になるのはとても簡単だからだ。
例えば、相互に関係ないクラスであっても、1つの列挙で処理を行うコードを書くことは簡単だ。以下のようなコードである。
using System;
using System.Collections.Generic;
class A
{
public override string ToString()
{
return "I'm cass A";
}
}
class B
{
public override string ToString()
{
return "I'm cass B";
}
}
class Program
{
public static IEnumerable<A> Sample1()
{
yield return new A();
yield return new A();
yield return new A();
}
public static IEnumerable<B> Sample2()
{
yield return new B();
yield return new B();
yield return new B();
}
public static void EnumObjects(IEnumerable<object> e)
{
foreach (var n in e) Console.WriteLine(n);
}
static void Main(string[] args)
{
EnumObjects(Sample1());
EnumObjects(Sample2());
}
}
|
|
リスト4 |
I'm cass A
I'm cass A
I'm cass A
I'm cass B
I'm cass B
I'm cass B
|
|
リスト4の実行結果 |
しかし、このコードが動くのは、IEnumerable<A>やIEnumerable<B>からIEnumerable<object>への暗黙変換が存在することを示すoutキーワードがインターフェイスの定義に付いているからである。ソース・コードに自分で書かないとしても、恩恵は簡単に受けられるわけである。
outだけではない。inの恩恵を受けるのも簡単だ。実はActionデリゲートにinキーワードが付いているので、これを使って暗黙の型変換をさせるだけでよい。
using System;
class Program
{
private static void output(Action<string> proc)
{
proc("Hello C#");
}
static void Main(string[] args)
{
Action<object> proc = (s) => Console.WriteLine(s);
output(proc);
}
}
|
|
リスト5 |
ここで重要なことは、Action<object>からAction<string>への変換は安全であり、C# 4以降では許されるという点だ。だから、C# 3.0までのように「変換できない」と思い込んでいると、回りくどいコードを書いてしまう可能性もある。それを意識して使いこなすのが、恐らく本当のin/outの使いこなしであろう。
Insider.NET 記事ランキング
本日
月間