Generics(ジェネリクス)の使い方を徹底解説!JavaやTypeScript・C#の言語ごとに使える機能を紹介
「包括的な」「全体的な」という意味をもつGenerics。
IT業界は機能の名前として使われていますが、どのような働きをするものなのでしょうか。
プログラミング言語であるJavaやTypeScript、C#ごとに使える機能も変わってきます。
そんなGenericsの機能について解説しましょう。
Generics(ジェネリクス)とは
Genericsとは主にJavaで使われるクラスやメソッドを作る機能です。
総称型ともいいます。<>記号で囲まれたデータの型名を、クラスやメソッドに付加されるものです。
Integer型やString型など、様々な型に対応する汎用的なクラスやメソッドが生成されます。
Genericsを使うことで、データ型不一致による実行時エラーを回避できるのです。
TypeScriptやC#でもサポートしています。
- Integer型:数値を扱う形式
- String型:文字列を扱う形式
プログラミング技法としての定義
オブジェクト指向との違い
Generics(ジェネリクス)は「ジェネリックプログラミング技法」を生みました。
データ型に束縛されずに型そのものをパラメータとして使えます。
ちなみにJavaではJavaSE5.0から導入されました。
オブジェクト指向とは
念のため、オブジェクト指向についても触れておきましょう。
オブジェクトは英語で「物」のことです。
プログラミングを手順ではなく「物」としてとらえ、物の作成と操作といった概念に分けます。
例えば、ある機能をパッケージ化したプログラムを用意しておきます。それを組み込むことであらゆるプログラミングが楽に行えるのです。
すべてを1個のプログラムに集約してもよいのですが、類似のプログラムが10,000種類あったら、そのメンテナンスの手間は大変なものになります。
それを共通部分はオブジェクトにしておくと、オブジェクトだけ修正すれば対応可能です。
圧倒的にメンテナンスの時間が短縮できますし、バグが発生する確率も格段に下がります。
Javaでジェネリクスを使う手法について
T型変数(型変数)
例えば、“class class sample <T>”と<T>を付け加えることで、データ型の指定がObject型からT型変数に変更できます。
- public Box(T o){
- This.o = o;
- public T get(){
- return o;
- }
数値型データと文字列型のデータをgetメソッドで取得したいこともあるでしょう。
従来はInteger用のクラスとstring用のBoxクラスを定義する必要がありました。
これをジェネリクスを使うことでメンテナンスがしやすい、スッキリしたコードにできます。
型違いの似たような名称のBoxを作る手間が省けるのです。
名前付けのルール
名前付けには大文字が使われます。
- E 要素(Javaコレクションフレームワークでよく使われます)
- K キー
- N 数値
- T 型
- V 値
- S,U,V:二番目、三番目の型
このルールに従わなかったからといって、コンパイルエラーになることはありません。
ただ慣例を守ることによってわかりやすいコードになり、第三者がメンテナンスを行う際の効率が上がります。
ワイルドカードが使える
ワイルドカードを使うとデータ型を後で宣言できます。データ型を宣言することなく、変数を一時保管も可能です。
ワイルドカードを表す”?”を用いて、<? Extends ~>と書くこともできます。
コードで指定したクラスのサブクラスなら何でも保管するという意味合いになるのです。
注意点
「newが使えない」という点は覚えておきましょう。
コンパイル時に型パラメータが消えるので、T型変数が生成できません。
例えば、”public T create;{ return new T();”というコードを書いても、T型変数は生成されないのです。
どうしても生成したい場合は以下のようにします。
- public T create (class<T>clavv) throws Exception{ return clavv.newInstance();
TypeScriptとGenericsとの関係について
Typescriptの概説
Typescriptとはweb開発用フレームワークの一種です。2014年頃にMaicrosoftがJavaScriptを拡張して開発しました。
静的型付けを用いたクラスベースオブジェクト指向言語です。
型を宣言してから開発できるので、静的型付けの方が作業効率という観点では有利といえるでしょう。
動的型付けは実行してみないと結果がわからないという面があります。
また、Typescriptでもジェネリックが適用できるのも特徴。
Typescriptをコンパイルすると、JavaScriptのコードに変換可能です。
よって、JavaScriptが使えるシステム環境であればすぐに使用できます。
JavaScriptのライブラリも使用可能になっていて、互換性も担保されているのです。
ジェネリックとは型引数を使って、実際に使用されるまで型が決まらないクラスや関数を定義しています。
変数宣言について
変数宣言には”const”と”let”があります。変数はとりあえずconstで宣言し、再度代入が必要な場合のみ、letを使いましょう。
constは再代入ができません。
その代わり変数に格納された配列に要素を追加したり、オブジェクトの属性を変更したりする機能はあるので、使い勝手はよいです。
余談ですが、JavaScriptで変数宣言できるのは”var”のみ。
こちらはスコープの範囲が関数単位なので、影響範囲が広くなります。
宣言する前にアクセスしてもエラーになりませんが、安全性には問題がありますので、積極的な使用は控えましょう。
Typescriptにおけるジェネリックのデータ型指定
“a<string>”は文字列だけ、”a<number>”は数値だけが入力できるように定義されます。
ただし、「AでもBでもよい」という柔軟な記述も可能です。
- 例 let birthyear : number | string;
ジェネリックの書き出し
“public name: U,” といった記述になります。
型引数には”T”や”U”が使われるケーが多いのですが、”T1や”T2”という表現形式が使われることもあります。
プログラムを所有している組織で独自のルールがある場合はそれに従いましょう。
ちなみに動的型付けとは、プログラムを書く際に変数や関数を特に指定しない手法のことです。
インタープリタ言語でよく用いられています。
規模の小さいプログラムや型の変化が激しいプログラムでは実装が容易です。
どのような型であっても格納できるのは便利ですが、1byteで収まるデータにも8byteのメモリが使用されます。
大容量のメモリが主流になった現在ではあまり気にしなくても問題はありません。
ただし、「節約する」という概念はないという点は理解しておきましょう。
C#で使用する場合の概要
サポートされているバージョン
C#2.0から、ジェネリクスの機能が搭載されています。
様々なデータ型に対応すべく型パラメータを与え、その型に対応したクラスや関数を生成するのです。
構文事例
- “static void swap<T>”
型パラメータ<T>はメソッド名の後に”swap<T>”といった表現をするものです。
複数形の場合は”swap<T,U>”といった形式になります。
outキーワード
outキーワードはパラメータが共変であることを示します。
基底クラスをデリゲートとするクラスで、派生クラスが戻り値です。
数学用語ではcovariance(コーバリアンス)と呼びます。
ちなみにデリゲートとはメソッドを参照する型のことです。
C#におけるジェネリクス
コードの書き出し
C#でジェネリクスを用いる場合は以下のように記述します。
- class クラス名<型引数>
- where 型引数が満たすべき条件
- {
- クラス定義
- {
- アクセスレベル 戻り値の型 メソッド名<型引数><引数リスト>
- }
- メソッド定義
- }
配列や連結リストとの関連
「配列」や「foreach」に代表される連結リスト。
複数の値をひとくくりで管理するクラスは「コンテナクラス」または「コレクションクラス」と呼ばれています。
- 要素・方式・操作
- 要素の型
- int
- double
- string
- 格納方式
- Stack
- StackInt
- StackDouble
- ListDouble
- ListString
- 配列、可変長配列、連結リスト、両端キュー
- 操作
- 置換
- 検索
- 総和計算
依存性の少ないコード
依存性の高いコードとは?
格納する型の種類がj個、格納方式の数がk個、操作の数がl個と仮定しましょう。
これらの組み合わせで様々な種類のコンテナを書くことを想定すると、J×k×l個のコードを書く必要が出てきます。
要素の型、格納方式、操作が相互に依存しているからです。
依存性の低いコードとは?
それに対して、任意の型を格納できるコンテナがあったらどうでしょう。
任意の種類のコンテナを操作できる関数があれば、j+k+l個のコードを書けば事足ります。
ジェネリックを使うことで、こうした依存性の低いコードを書くことが可能です。
C#7.3での追加事項
unmanaged
- 例 where T :unmanaged
“unmanaged”制約を付けると、その型をポインター化できるようになります。
基底型制約との同時使用はできなくなるので注意してください。Null値も認められません。
Enum
- 例 where T: Enum
Structと同時に指定可能です。
どちらかといえば、クラスというよりもインターフェースに近い概念でしょう。
Delegate
- 例 where T:Delegate
Delegateというメソッドを間接的に呼び出す機能がサポートされています。
Delegateのもともとの意味は「委任する」「代理を立てる」です。
プログラミングの世界ではオブジェクトが一部の処理を別のそれに実行を委任しているケースがあります。
条件に応じて、委任先のオブジェクトやメソッドは変更可能です。イベント処理でよく用いられています。
イベント発生時に呼び出す「イベントハンドラ」と呼ばれるメソッドをDelegateに登録することで、一括で実行することも可能です。
C#8.0での追加
not null
- 例 where T: notnull
Null許容参照型を有効にすると、class制約や基底クラス制約の意味が変わります。
「非null」という意味です。nullを許容したい場合は制約条件に”?”を付けます。
newについて
- 例 where T :new()
New T()を使って要素の初期化を行いつつ、配列を作る処理も作成できるようになります。
Java・templateとの相違点
実装方式
- C#
MSILに.NET2.0で追加された、generics用の命令が存在します。
キャストに必要なコードがなくなり、実行効率がよくなっています。
- Java
バイトコードの単位ではJavaコンパイラがキャストを自動で挿入しています。
実体
- C#
IL上はList<int>とList<string>でほとんど同じように扱われます。
値型と参照型の違いを埋める命令もILに追加されているでしょう。
参照型同士(List<string>とList<object>など)ならJITの結果も共有されていることが多いです。
- Java
「型消去」の形式になります。Vector<int>とVector<string>の実体は同じです。
型の安全性
- C#
List<int>とList<string>は別の型として明確に区別されます。
リフレクションを用いても正確に型が採取可能です。
- Java
Vector<int>とVector<string>が区別できません。また、リフレクションでは要素の型を取得することも不可能です。
ちなみにリフレクションとは、プログラムの実行過程でプログラム自身の構造を読み取ったり、書き換えたりする技術のことです。
キャスト
キャストは変数の型を別の型に変換することです。
- C#
内部的にはobjectのListと同じ扱いですが、MSILレベルで対応しているため、キャストの必要がありません。
特にboxing/unboxingが不要になるので、実行効率が上がります。
- Java
コンパイラが自動的にキャストコードを挿入しています。
MSILとはMaicrosoftの.NETで使用される、実行可能なコードを記述するための中間言語のことです。
以前はCILと呼ばれていました。
特定の開発言語に依存しないので、開発言語側でコンパイラを用意すれば、どのような言語からでもMSIL形式のコードが生成できます。
メンバ参照方式
- C#
インターフェースを使った型制約に基づきます。
- Java
こちらもインターフェースを使った型制約に基づくものです。
その他の相違点
- C#
C#4.0で共変性・反変性がサポートされています。
共変性とは最初に指定された型よりも強い派生型が使える方式のことです。
型の制限を強めて、対象範囲を狭めます。そうすることで、継承先の派生クラスに変更可能になるのです。
反変性は型の制限を弱めて、対象範囲を広げることです。対象範囲を広げることで、継承元の基底クラスに変更します。
- Java
変性の代わりにワイルドカードを利用しましょう。
互換性重視になっており、J2SE5.0でコンパイルしても、古いバージョンでコンパイルしたものも問題なく動きます。
まとめ
Genericsのメリット
Java、TypeScript、C#のいずれのケースでもコードの可変性が高まります。
プログラムに修正はつきものですが、修正箇所が多ければ多いほど時間もかかりますし、新たなバグを生み出すリスクも高まるでしょう。
Genericsを使うことでコードをシンプルなものにして、そうしたリスクを低減させることができるのです。
基礎知識の重要性
「ジェネリックプログラミング」の対義語ともいえる「オブジェクト指向」に関する知識やJavaにおける変数の概念は覚えておきましょう。
従来、これらはメジャーな方式として認知されていました。
Genericsがそれをどのように改善して、どういったメリットをもたらすのかを見極めてから、実務で使うと効果的です。
影響範囲を確認してから更新する
いつの時代のプログラミングもそうですが、プログラムの集合体がシステムであり、システムを用いて企業は日々の業務を推進しています。
Genericsはシステムの柔軟性を増す技術であることに間違いありません。
ただし、他のプログラムへの影響や改変箇所の特定を済ませてから実践する段階に進みましょう。
十分な検証が行われないままだと、思わぬトラブルに見舞われます。