ニートでもわかった! ジェネリックの基礎

はいどーもみなさんこんにちは。
本日講師を務めますキングオブニートことツミオと申します。

みなさんジェネリック医薬品は使用していますか?
僕が今飲んでいるなんちゃらとか言う薬もジェネリックのようです。
ジェネリックの意味はよくわかっていませんが、薬剤師共が何かと理由をつけて勧めてくるので、断るのも面倒だしそのままジェネリック医薬品を受け取っています。

さて本日はそんなジェネリック医薬品とは何の関係もない、C#プログラマ向けのジェネリックの基礎講座です。
どうぞよろしくお願いいたします。

ジェネリックとは

ジェネリックを一言で表現するならば「型をパラメーターとして渡せる機能」ではないでしょうか。

例えばみなさんはメソッドに引数を渡すことってありますよね。

これで言えばvalueのことです。

このとき「intという型をパラメーターとして渡せると便利だなあ」と思ったことはないでしょうか?
例えばこのHogeメソッドをオーバーライドし、stringやdouble型……その他いくつかの型に対応させたいが、具体的な処理自体は基本的に変わることがない、というケースが考えられるでしょう。
こういった場合、メソッドをオーバーライドして様々な型に対応させるよりも、型をそもそもパラメーターとして渡せたほうが便利なのは明白です。

それを可能にする機能――それこそがジェネリックです。

みんな大好きList<T>型

関連のある要素を一つにまとめるとき、配列を使う人も多いと思います。
しかしC#における配列の要素数は固定されています。
これが不便な場合もあるので、代わりにLitst<T>型を使う人もいるでしょう。

このList<T>のT。
まさにここにジェネリックが使用されています!

List<T>はTの部分にint型でもstring型でもWindow_Base型でも何でも入れることができます。
言い換えれば、「型をパラメーターとして渡している」ことになります。

このジェネリックを自分で作成したクラスやメソッドにも適用しちゃおう、というのが本記事の目的でございます。

ジェネリックなクラスを作ってみる

ではさっそくジェネリックなクラスを作ってみましょう。

List<T>みたいな感じでGenericTest<T, V>となっていますね。
コンストラクタでT型のパラメータを受け取り、T型の自動実装プロパティGenericPropertyに代入しています。
FireメソッドはV型のパラメータを受け取り、T型の値を返すメソッドです。
受け取ったV型の結果をコンソールにも出力していますね。

Mainメソッドの中にこんなものを作成してみてください(コンソールアプリを想定)。

List<T>型と使い方はほとんど変わりませんね。
GenericTest<T, V>の部分に好きな型を設定するだけです。
今回はstring型とint型を設定しましたが、別の型も設定してみてください。
全く問題なく動作することがわかると思いますよ。

型に制約を加える

さて便利なジェネリックですが、制約を加えたくなるときがあります。
例えばジェネリックな型を使いたいが、その型には必ず特定のインターフェイスを実装していてほしい、といった場合ですね。
今回は例として、IComparable<T>型を実装してみましょう。

IComparable<T>型はint型を返し、T型を受け取るメソッドCompareToを実装する必要があります。
すなわち以下の通り。

「現在のインスタンスを同じ型の別のオブジェクトと比較し、現在のインスタンスの並べ替え順序での位置が、比較対象のオブジェクトと比べて前か、後か、または同じかを示す整数を返します」とのことです。
雑に言えば、CompareToで比較した結果「0」が返ってきたら、比較したインスタンス同士は等しいということです。

where T : IComparable<T>というのがまず目につきますね。
これが型の制約です。
T型は必ずIComparable<T>インターフェイスを実装していなければなりませんよー、ということです。

あとはIsSmaeValueメソッドが追加されていますね。
T型の引数を受け取り、その受け取った引数とGenericPropertyの値を比較しています。
今回は同じ値であるかどうかを判定していますよ。

Fireメソッドもこれに合わせ、多少変えています。

Mainメソッドをこんな感じにしてください。

比較した結果、Trueが表示されているはずです。
「ほげえええ」の片方を「ほげ」やらなんやらに変えたらFalseが返ってきます。
試してみてください。

注*以下少しややこしい話です。今回はstring型同士の比較をしましたが、このstring型は少し特殊な比較方法が採用されています。すなわち、通常であれば参照型に==演算子が適用されると、参照が等しいかどうかが判定されます。しかしstring型は参照が別であっても、保持している文字列が等しければTrueを返すように作られています。「参照が違うのになんでTrueが返ってくるんだ?」と思った方は、実装を参照してみるとよいのではないでしょーか知らんけど。

注2*「単純に==演算子で比較できないのか?」と思った方もいるかなと思いますが、何の制約も指定されていない場合は比較できません。これはジェネリックの扱いにくいところだなあと思いました。

なお、今回はIComparable<T>インターフェイスを使用しましたが、自作のインターフェイスを制約に加えることも当然できますよ。

メソッドにもジェネリックを適用してみる

メソッドをジェネリックに対応させることも可能です。
というわけで早速具体的なコードをドン。
先ほどのGenericTestクラスに追加してください。

このメソッドは、型パラメーターEを使用しています。
しかしGenericTestクラスはT型V型しか利用していませんでしたから、新しい型が登場したことになりますね。

ジェネリッククラスがわかれば特にコードの説明は不要かなと思うので、Mainメソッド用のコードもさっさと書いておきます。

さて今回はフルバージョンと省略形が出てきました。
フルバージョンの意味はわかると思います。
E型はint型であると宣言し、その通り引数に100を渡しているだけです。

しかし実は今回のジェネリックメソッドGenericFireは、型パラメータを省略することもできるのです!
それはなぜでしょうか?

省略形の引数をよく見てください。
この引数はE型を受け取ります。
つまり、引数がint型であれば、E型はint型であると推論することができます。
今回の場合ですと、100という値が設定されています。
100という値はint型ですので(byte型とかにはならないの? と思った方はルールについてググってみてください)、E型もint型であると推論でき、結果として型パラメーターを省略できるのですね。

それでは以下の場合はどうでしょう。

前者はE型をint型だと宣言しているのに、string型の”あいうえお”を引数に渡しているのでエラーが出ます。
後者は型の推論が働き、E型はstring型として処理されるのでエラーが出ません。

型の推論は便利ですので、推論できるときはいちいち<int>とか書かないほうがスマートでよいですね。

その他の課題

ここまで長々と見てきましたが、ジェネリックは非常に奥の深い機能です。
みなさんもここまで読んで、気になることがいくつも出てきたことと思います。
例えば以下のようなことでしょうか。

・object型と何が違うのか
・制約はいくつまで加えることが可能なのか
・他の制約の種類について
・ジェネリックデリゲートやジェネリック構造体は存在するのか。また、その使用方法について

これらについてはみなさんの課題とし、本日のジェネリックの基礎講座を終わりたいと思います。
医薬品の話を期待してた方はごめんちゃい!

おわりに

はい、というわけでジェネリックについて書いてみました。

僕自身、ジェネリックについてはあやふやな部分が多々ありました。
この記事を書くことを通してジェネリックについて復習できたらいいなーくらいの気持ちで本記事をしたためたのですが、存外にも理解できていない機能が数多くあることが判明しました。
いやあ、不勉強が身にしみます!
これからも色々と学んでいきたいと思いますよ。

ほなそんな感じでまた。

参考サイト

ジェネリックについてより詳しく知りたい方は、有名どころですが以下のサイトが参考になるのではないでしょーか。
http://ufcpp.net/study/csharp/sp2_generics.html
https://docs.microsoft.com/ja-jp/dotnet/csharp/programming-guide/generics/

フォローする