Skip to content

Instantly share code, notes, and snippets.

@matarillo
Last active August 29, 2015 14:00
Show Gist options
  • Save matarillo/92c076d3c6cd972d4b09 to your computer and use it in GitHub Desktop.
Save matarillo/92c076d3c6cd972d4b09 to your computer and use it in GitHub Desktop.

#99 ジェネリクスの基礎とクラス設計への応用 ... のC#版

色つけるの面倒なので手抜き。

(追記)ryoasaiさんの記事が参考になりそう。

##はじめに補足

元スライドに書いていないことをちょっとだけ補足。

Javaのジェネリクスはオブジェクトを対象にしたもので、プリミティブ型はオブジェクトではないから型引数に使えない(ラッパー型はOK)。

.NETには値型と参照型があるが、どちらも型引数に指定することができる上、値型を指定したときもボクシングされない。 (intboolなどのプリミティブ型は、値型の一種である構造体であって、言語に組み込まれているもの。)

List<int> list = new List<int>(){ 0, 1, 2 }; // OK

もうひとつ興味深いのは、.NETのT型の配列はIList<T>インターフェースを実装している。(要素の追加削除はできないけれど)

int[] array = { 0, 1, 2 };
IList<int> list = array; // OK
list[0] = 10;            // OK
list.Add(3);             // throws NotSupportedException

##ジェネリクスの2種類のスコープ

ジェネリックメソッド(メソッドの中でのみ有効なジェネリクス)は、型パラメータを置く場所が違う。

public static void Hoge<T>(T t) {}

ジェネリック型の型パラメータは、スタティックメンバからも見える。 インスタンスの中でのみ有効なJavaとは異なる。

class Hoge<T>
{
	T instanceField;
	static T staticField;

	class Piyo // ネストクラスは常にスタティック
	{
		T instanceField;
		static T staticField;
	}
	...
}

なお、型パラメータに実際に型が指定された(nagiseさんの資料と同じように「型変数をバインドした」と言ってもいい)型は、 型引数が異なっていれば完全に別の型となる。 List<int>List<string>の実行時の型はもちろん一致しないし、 型パラメータを一切使わないメソッドを呼び出すときでさえ、Javaみたいに

List<?> l = new ArrayList<Integer>();
l.clear();

なんてことはできない (C#のジェネリクスにワイルドカードはないが、そのせいというより、 そもそも型に互換性がないから、そういう書き方も用意されていない)。

同様に、Hoge<T>の例では、ネストクラスHoge<int>.PiyoHoge<string>.Piyoにも互換性がない。全く別の型となる。

###コラム

C#にはデリゲートという型もあって、これはJava8でいう関数型インターフェースみたいなものだけど、

public delegate TValue Func<T, TValue>(T arg);

のように1行で宣言するし、これを上のどちらに分類しておくのかはちょっと迷う。 まあ、Java8の話を混ぜちゃうと長くなるので、やめておきますか……?

##ジェネリックメソッドの例

上で書いたように、ワイルドカードがないので

public static void shuffle(List<?> list)

みたいな仮引数は書けない。

ところで、このメソッドってジェネリックメソッドなの?

ジェネリックメソッドの呼び出し方

型引数の置き場所が異なる。そのため、明示的にバインドしたい場合でもthisや自クラス名を省略できる。

using System.Collections.Generic;

class Program
{
    static void Main(string[] args)
    {
        List<string> list = new List<string>();
        list.Add("hoge");
        Collections.ReplaceAll<string>(list, "hoge", "piyo");

        List<int> intList = new List<int>();
        intList.Add(42);
        Collections.ReplaceAll<int>(intList, 42, 41);
    }
}

static class Collections
{
    public static bool ReplaceAll<T>(IList<T> list, T oldVal, T newVal)
    {
        var replaced = false;
        for (var i = 0; i < list.Count; i++)
        {
            if (list[i].Equals(oldVal))
            {
                list[i] = newVal;
                replaced = true;
            }
        }
        return replaced;
    }
}

3種類の<>

2番目と3番目の整理の仕方に違和感。

Javaだと、ワイルドカードが使えるかどうかが整理の軸なのかな?(3番目はワイルドカードが使える)

(追記)元資料はこういう意味だろうか。

分類 コード
型変数の宣言 ジェネリック型の型パラメータ class Foo<T> {}
ジェネリックメソッドの型パラメータ static <T> T add(T x, T y) {}
型変数へのバインド ジェネリック型の継承時 class Bar extends Foo<String> {}
ジェネリックメソッドの呼び出し時 int sum = Foo.<Integer>add(1, 2);
ジェネリッククラスのコンストラクタ呼び出し時 new ArrayList<Integer>();
パラメータ化された型 インスタンス変数・ローカル変数の宣言 List<Integer> list = null;
メソッドシグネチャの仮引数の型 static int count(List<?> list) {}
メソッドシグネチャの戻り値の型 static List<Integer> repeat(int value, int count) {}

.NETのジェネリクスにおいては、「型変数へのバインド」の中の「ジェネリック型の継承時」と「ジェネリッククラスのコンストラクタ呼び出し時」は、「パラメータ化された型」と実質的な違いがない。たぶんダイヤモンド演算子やワイルドカードがないからだと思われる。

「ジェネリックメソッドの呼び出し時」だけは、引数の型から型が推論できるときだけ、山かっこごと省略できる、という特徴があるので、他と区別される。

(追記終わり)

// 型パラメーターの宣言
class Hoge<T> {}
public void Hoge<T>() {}

// 型パラメーターのバインド、というのが正式名称なのかは不明。
// MSDNでは、仮引数(type parameters)のことを型パラメータ―、
// 実引数(type arguments)のことを型引数と呼んでいるようである。
// なので、型引数の指定、でもいいのかもしれない。
Collections.ReplaceAll<string>(list, "hoge", "piyo");

// クローズ構築型(closed constructed type)というのが
// 正式名称らしいが、MSDNでも表記ゆれがあったりする。
new List<string>();
class Hoge : List<string> {}
List<string> list;

型のバインド

ジェネリックメソッドの例

//宣言側 型パラメーター(type parameters)
public static bool ReplaceAll<T>(IList<T> list, T oldVal, T newVal)

//利用側 型引数(type arguments)
Collections.ReplaceAll<string>(list, "hoge", "piyo");

ジェネリッククラスの例

//宣言側 型パラメーター(type parameters)
public class List<E> {...}

//利用側 型引数(type arguments)
IList<string> list = new List<string>();

型推論

型引数を明示的に指定するときの置き場所が違うだけで、後は同じ。

推論と言っても、HM型推論みたいに計算するわけじゃなくて、 実引数の型から型引数が一意に決まる場合には省略可能、というだけ。 複雑なパターンでは思ったように動いてくれなかったりするのはJavaもC#も同じ。

ラムダのことはどうしようかな……

ダイヤモンド演算子

C#にはダイヤモンド演算子は存在しない。 左辺の型から右辺の型を導出するパターンはデリゲートのときだけ (厳密には、右辺、すなわち匿名関数やラムダ式には型があるとは言えないが、 それはまた別の話)。

「var使えばいいじゃん」派ともいえる。

推論器

ラムダのことはどうしようかな……

継承によるバインド

Javaと同じ。「バインド」と呼ぶのが正式名称かどうかはともかく……

パラメータ化された型の代入互換性

同じ。(配列が共変性を持つのも同じ)

using System.Collections.Generic;

class A { }
class B : A { }
class B2 : A { }
class C : B { }

class Program
{
    static void Main()
    {
        B[] arrayB = new B[1];
        A[] arrayA = arrayB;
        arrayA[0] = new B2(); // throws ArrayTypeMismatchExcaption

        List<B> listB = new List<B>();
        List<A> listA = listB; // コンパイルエラー
        listA.Add(new B2());
    }
}

ワイルドカードの使用

.NETでは、 型の利用側(「3種類の<>」で言うところの3番目、変数定義とか仮引数定義とか)で ワイルドカードを使用できない。

逆に、型の定義側(「3種類の<>」で言うところの1番目)で、 その型パラメータがどういう使われ方をするかによって、 型引数に派生型を指定した型に変換してよいのか、 それとも、型引数に基底型を指定した型に変換してよいのかを指定する形になる。

これは図がないと分かりにくいのでざっと書いた。 左がJava、右が.NETだ。

Javaと.NETの変性

Javaの場合は、型引数にワイルドカードを指定した変数に対しては、異なる型のオブジェクトを代入できる。

.NETの場合は、型パラメータに変性を指定したインターフェース(を実装するオブジェクト)は、 異なる型の変数に代入できる。

(Javaでは型パラメータに与える制約でもワイルドカードが使えるけど、その話は後だ)

<? extends ~>の入力制約

出力しかしないパターン。さっきの図だと上半分。

定義側はこのように、出力のみ許すようにインターフェースを定義しておく。 そのインターフェースを実装するクラスなら、BでもAでも取り出せる。

interface IRemovableList<out T>
{
    T RemoveFirst();
}

class RemovableList<T> : List<T>, IRemovableList<T>
{
    public T RemoveFirst()
    {
        var first = this[0];
        this.RemoveAt(0);
        return first;
    }
}

とはいえ、変数そのものはBでもAでも取り出せる型としては定義できない。 その代り、暗黙的に変換できる。利用側はこんな感じだ。

var list = new RemovableList<B>() { new B(), new B() };

IRemovableList<B> removableB = list;
B b = removableB.RemoveFirst();

IRemovableList<A> removableA = list; // 暗黙的に変換できる
A a = removableA.RemoveFirst();      // 取り出しもできる

IList<B> listB = list;
IList<A> listA = list; // NG
                       // 型 'RemovableList<B>' を
                       // 'System.Collections.Generic.IList<A>' に暗黙的に変換できません。

<? super ~>の出力制約

入力しかしないパターン。さっきの図だと下半分。

定義側はこのように、入力のみ許すようにインターフェースを定義しておく。 そのインターフェースを実装するクラスなら、BでもCでも追加できる。

interface IAddableList<in T>
{
    void Add(T element);
}

class AddableList<T> : List<T>, IAddableList<T>
{
}

とはいえ、変数そのものはBでもCでも追加できる型としては定義できない。 その代り、暗黙的に変換できる。利用側はこんな感じだ。

var list = new AddableList<B>();

IAddableList<B> addableB = list;
addableB.Add(new B());
IAddableList<C> addableC = list; // 暗黙的に変換できる
addableC.Add(new C());           // 追加もできる

IList<B> listB = list;
IList<C> listC = list; // NG
                       // 型 'AddableList<B>' を
                       // 'System.Collections.Generic.IList<C>' に暗黙的に変換できません。

まとめ

  • 3種類の<>の整理の仕方が違う
  • 代入互換性は、Javaでは変数側に、.NETではオブジェクト側に指定するイメージ。

中級編

型変数の境界

書き方は違う(whereを使う)が、上界を決めることだけできるというのはJavaと同じ

class Hoge<T>
  where T : B, ISerializable
{
}

再帰ジェネリクス

こっちも制約の書き方が違うだけで、同じように定義可能

public abstract class Hoge<T>
  where T : Hoge<T>
{
  public abstract T GetConcreteObject();
}

再帰する型変数へのバインド

継承しないと使えないのも同じ。

再帰ジェネリクスの効能

同様。

public class Piyo : Hoge<Piyo>
{
  public override Piyo GetConcreteObject()
  {
    return this;
  }
}

Piyo piyo = new Piyo();
Piyo piyo2 = piyo.GetConcreteObject();

再帰ジェネリクスの使用例

うーん、.NET Frameworkの標準ライブラリの範疇ではちょっと思いつかない。 IComparable<T>はあるけど、それを実装した抽象型みたいなのはないからなあ。 enumは数値型をベースにした構造体であって、Javaみたいなタイプセーフeuumではないので。

内部クラスのジェネリクス

Javaでいう内部クラスはなく、すべてが静的なネストした型になる(ラムダ式とか匿名関数とかはJavaの匿名内部クラスに似た感じだけど、それはこの話題の本筋じゃないので省略)。

エンクロージング型がジェネリックだったら、静的ネストクラスも型変数にアクセスできる。 このあたりが型消去ではない.NETの特色で、上でも書いたけどHoge<T>にネストクラスPiyoを定義した場合、Hoge<int>.PiyoHoge<string>.Piyoはコンパイル時も実行時も別の型となっている。

とはいえ、これで何が嬉しいのかというと、すぐには思いつかない。調べてはみますが。

リフレクション

上でも書いた通り(2番目の<>と3番目の<>の違いのところ)、型変数へのバインドだろうがパラメタライズドタイプだろうが型情報はとれるし、コンパイル時にできることはリフレクションを使って実行時にもできる。

たとえば List<T>クラス(JavaでいうArrayList<T>)なら、

Type openType = typeof(List<>);
bool isOpen = openType.IsGenericTypeDefinition; // true
Type[] typeParameters = openType.GetGenericArguments();
Type[][] typeConstraints = typeParameters
    .Select(t => t.GetGenericParameterConstraints())
    .ToArray();

Type closedType = openType.MakeGenericType(typeof(int));
bool isClosed = closedType.IsGenericType; // true;
Type[] typeArguments = closedType.GetGenericArguments();

object o = closedType.GetConstructor(Type.EmptyTypes).Invoke(null);
List<int> list = (List<int>)o;
Console.WriteLine(list.Count);

あるいは、Enumerable.Repeat<T>(T element, int count) なら、

MethodInfo openMethod = typeof(Enumerable).GetMethod("Repeat");
bool isOpen = openMethod.IsGenericMethodDefinition; // true;
MethodInfo closedMethod = openMethod.MakeGenericMethod(typeof(string));
bool isClosed = closedMethod.IsGenericMethod; // true;

object o = closedMethod.Invoke(null, new object[] { "abc", 2 });
string[] array = ((IEnumerable<string>)o).ToArray();
// array is { "abc", "abc" }

まとめ

イレイジャかどうか、かなあ。差異に目を向ければもっと掘り下げることもできそうだけど。

上級編

new T()したい

元スライドにもある通り、ファクトリを作ってもいいけどC#にはデフォルトコンストラクタ制約があるから楽。

継承によるバインドとか、そういうことを考えなくてもtypeof(T)にもアクセスできるので楽。

変態編

再帰での相互参照

まあやれるけど、応用が思いつかない。

内部クラスでグルーピング

うーん、スライドだけ見ると何のためにやるのか(どのあたりが変態なのか)よくわからない。

型変数の部分適用

高階型についてはもっとよく考えてみます。C#には内部クラスがないが、ネストクラスは外側の型変数にアクセスできる。 同じことができるのか、それともできないのか。

(追記)高階型の話をいろいろためしてみている。 まずは序文

(さらに追記)《高階型がないと、モナドとして抽象化や共通化ができない》という話もあるので頑張ってみた。

C#のジェネリクスで型クラスを真似る

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment