プログラマー向け Kotlin ブートキャンプ 5.2: 汎用型

この Codelab は、プログラマー向け Kotlin ブートキャンプ コースの一部です。このコースを最大限に活用するには、Codelab を順番に進めることをおすすめします。理解度によっては、特定のセクションの概要を読むだけで済む場合があります。このコースは、オブジェクト指向言語の知識があり、Kotlin を学習したいプログラマーを対象としています。

はじめに

この Codelab では、汎用のクラス、関数、メソッドと、Kotlin でのそれらの動作について説明します。

このコースのレッスンは、1 つのサンプルアプリを作成するのではなく、知識を深めるように設計されています。また、各レッスンは半独立しているため、よく知っているセクションは読み飛ばすことができます。これらの例を関連付けるため、多くは水族館をテーマにしています。水族館の物語の全体像を確認したい場合は、Udacity の Kotlin ブートキャンプ(プログラマー向け)コースをご覧ください。

前提となる知識

  • Kotlin の関数、クラス、メソッドの構文
  • IntelliJ IDEA で新しいクラスを作成してプログラムを実行する方法

学習内容

  • 汎用のクラス、メソッド、関数を操作する方法

演習内容

  • 汎用クラスを作成して制約を追加する
  • in 型と out 型を作成する
  • 汎用関数、メソッド、拡張関数を作成する

ジェネリクスの概要

多くのプログラミング言語と同様に、Kotlin にもジェネリック型があります。ジェネリック型を使用すると、クラスをジェネリックにすることができ、クラスの柔軟性が大幅に向上します。

アイテムのリストを保持する MyList クラスを実装しているとします。ジェネリクスを使用しない場合、型ごとに MyList の新しいバージョンを実装する必要があります。Double 用、String 用、Fish 用の 3 つです。ジェネリクスを使用すると、リストを汎用にできるため、任意の型のオブジェクトを保持できます。これは、多くの型に適合するワイルドカード型を作成するようなものです。

汎用型を定義するには、クラス名の後に山かっこ <T> で囲んだ T を置きます。(別の文字や長い名前を使用することもできますが、汎用型の慣例は T です)。

class MyList<T> {
    fun get(pos: Int): T {
        TODO("implement")
    }
    fun addItem(item: T) {}
}

T は、通常の型であるかのように参照できます。get() の戻り値の型は T で、addItem() のパラメータの型は T です。もちろん、汎用リストは非常に便利なので、List クラスは Kotlin に組み込まれています。

ステップ 1: 型階層を作成する

このステップでは、次のステップで使用するクラスを作成します。サブクラス化については前の Codelab で説明しましたが、ここで簡単に復習します。

  1. 例をわかりやすくするために、src の下に新しいパッケージを作成し、generics と呼びます。
  2. generics パッケージに、新しい Aquarium.kt ファイルを作成します。これにより、競合することなく同じ名前を使用して再定義できるため、この Codelab の残りのコードはこのファイルに記述します。
  3. 給水タイプの型階層を作成します。まず、WaterSupplyopen クラスにして、サブクラス化できるようにします。
  4. ブール値のパラメータ varneedsProcessing)を追加します。これにより、ゲッターとセッターとともに、変更可能なプロパティが自動的に作成されます。
  5. WaterSupply を拡張するサブクラス TapWater を作成し、needsProcessingtrue を渡します。これは、水道水には魚に有害な添加物が含まれているためです。
  6. TapWater で、水を浄化してから needsProcessingfalse に設定する addChemicalCleaners() という関数を定義します。needsProcessing プロパティは、デフォルトで public であり、サブクラスからアクセスできるため、TapWater から設定できます。完成したコードは次のとおりです。
package generics

open class WaterSupply(var needsProcessing: Boolean)

class TapWater : WaterSupply(true) {
   fun addChemicalCleaners() {
       needsProcessing = false
   }
}
  1. WaterSupply のサブクラスとして、FishStoreWaterLakeWater という 2 つのクラスをさらに作成します。FishStoreWater は処理する必要はありませんが、LakeWaterfilter() メソッドでフィルタする必要があります。フィルタリング後は再度処理する必要がないため、filter()needsProcessing = false を設定します。
class FishStoreWater : WaterSupply(false)

class LakeWater : WaterSupply(true) {
   fun filter() {
       needsProcessing = false
   }
}

追加情報が必要な場合は、Kotlin の継承に関する前のレッスンをご覧ください。

ステップ 2: 汎用クラスを作成する

このステップでは、さまざまな種類の給水に対応するように Aquarium クラスを変更します。

  1. Aquarium.kt で、クラス名の後の角かっこ内に <T> を指定して、Aquarium クラスを定義します。
  2. AquariumT 型の不変プロパティ waterSupply を追加します。
class Aquarium<T>(val waterSupply: T)
  1. genericsExample() という関数を記述します。これはクラスの一部ではないため、main() 関数やクラス定義のように、ファイルのトップレベルに配置できます。関数で Aquarium を作成し、WaterSupply を渡します。waterSupply パラメータは汎用であるため、山かっこ <> で型を指定する必要があります。
fun genericsExample() {
    val aquarium = Aquarium<TapWater>(TapWater())
}
  1. genericsExample() では、コードから水族館の waterSupply にアクセスできます。型が TapWater であるため、型キャストなしで addChemicalCleaners() を呼び出すことができます。
fun genericsExample() {
    val aquarium = Aquarium<TapWater>(TapWater())
    aquarium.waterSupply.addChemicalCleaners()
}
  1. Kotlin には型推論があるため、Aquarium オブジェクトを作成するときに、山かっこ(<>)とその間の内容を削除できます。したがって、インスタンスを作成するときに TapWater を 2 回指定する必要はありません。型は Aquarium の引数から推測できます。それでも、TapWater 型の Aquarium が作成されます。
fun genericsExample() {
    val aquarium = Aquarium(TapWater())
    aquarium.waterSupply.addChemicalCleaners()
}
  1. 何が起きているかを確認するには、addChemicalCleaners() を呼び出す前後に needsProcessing を出力します。完成した関数を以下に示します。
fun genericsExample() {
    val aquarium = Aquarium<TapWater>(TapWater())
    println("water needs processing: ${aquarium.waterSupply.needsProcessing}")
    aquarium.waterSupply.addChemicalCleaners()
    println("water needs processing: ${aquarium.waterSupply.needsProcessing}")
}
  1. genericsExample() を呼び出す main() 関数を追加し、プログラムを実行して結果を確認します。
fun main() {
    genericsExample()
}
⇒ water needs processing: true
water needs processing: false

ステップ 3: 具体的にする

ジェネリックはほぼすべてのものを渡すことができるため、問題が発生することがあります。このステップでは、Aquarium クラスに格納できるものをより具体的にします。

  1. genericsExample() で、waterSupply の文字列を渡して Aquarium を作成し、水族館の waterSupply プロパティを出力します。
fun genericsExample() {
    val aquarium2 = Aquarium("string")
    println(aquarium2.waterSupply)
}
  1. プログラムを実行して結果を確認します。
⇒ string

AquariumT. に制限を設けていないため、String を含む任意の型を渡すことができます。結果は渡された文字列になります。

  1. genericsExample() で、別の Aquarium を作成し、waterSupplynull を渡します。waterSupply が null の場合、"waterSupply is null" を出力します。
fun genericsExample() {
    val aquarium3 = Aquarium(null)
    if (aquarium3.waterSupply == null) {
        println("waterSupply is null")
    }
}
  1. プログラムを実行して結果を確認します。
⇒ waterSupply is null

Aquarium の作成時に null を渡すことができるのはなぜですか?これは、デフォルトで T が null 許容の Any? 型(型階層の最上位の型)を表すためです。以下は、先ほど入力した内容と同等です。

class Aquarium<T: Any?>(val waterSupply: T)
  1. null の受け渡しを許可しないようにするには、Any の後に ? を削除して、T を明示的に Any 型にします。
class Aquarium<T: Any>(val waterSupply: T)

このコンテキストでは、Any汎用制約と呼ばれます。つまり、null でない限り、任意の型を T に渡すことができます。

  1. 実際には、TWaterSupply(またはそのサブクラスのいずれか)のみを渡せるようにする必要があります。AnyWaterSupply に置き換えて、より具体的な汎用制約を定義します。
class Aquarium<T: WaterSupply>(val waterSupply: T)

ステップ 4: チェックを追加する

このステップでは、コードが想定どおりに動作していることを確認するのに役立つ check() 関数について学習します。check() 関数は、Kotlin の標準ライブラリ関数です。これはアサーションとして機能し、引数が false と評価されると IllegalStateException をスローします。

  1. Aquarium クラスに addWater() メソッドを追加して水を追加します。check() を使用すると、水を最初に処理する必要がなくなります。
class Aquarium<T: WaterSupply>(val waterSupply: T) {
    fun addWater() {
        check(!waterSupply.needsProcessing) { "water supply needs processing first" }
        println("adding water from $waterSupply")
    }    
}

この場合、needsProcessing が true の場合、check() は例外をスローします。

  1. genericsExample() で、LakeWater を使用して Aquarium を作成するコードを追加し、水を加えます。
fun genericsExample() {
    val aquarium4 = Aquarium(LakeWater())
    aquarium4.addWater()
}
  1. プログラムを実行すると、水は最初にろ過する必要があるため、例外が発生します。
⇒ Exception in thread "main" java.lang.IllegalStateException: water supply needs processing first
        at Aquarium.generics.Aquarium.addWater(Aquarium.kt:21)
  1. Aquarium に追加する前に水をフィルタリングする呼び出しを追加します。これで、プログラムを実行しても例外はスローされなくなります。
fun genericsExample() {
    val aquarium4 = Aquarium(LakeWater())
    aquarium4.waterSupply.filter()
    aquarium4.addWater()
}
⇒ adding water from generics.LakeWater@880ec60

上記はジェネリクスの基本を説明したものです。次のタスクではさらに詳しく説明しますが、重要なコンセプトは、汎用制約を使用して汎用クラスを宣言して使用する方法です。

このタスクでは、ジェネリクスを使用した in 型と out 型について学習します。in 型は、クラスにのみ渡すことができ、返すことはできない型です。out 型は、クラスからのみ返される型です。

Aquarium クラスを見ると、プロパティ waterSupply を取得するときにのみ、汎用型が返されることがわかります。T 型の値をパラメータとして受け取るメソッドはありません(コンストラクタで定義する場合を除く)。Kotlin では、まさにこのケースのために out 型を定義できます。また、型を安全に使用できる場所に関する追加情報を推測することもできます。同様に、メソッドに渡されるだけで返されない汎用型に対して in 型を定義できます。これにより、Kotlin でコードの安全性をさらにチェックできるようになります。

in 型と out 型は、Kotlin の型システムのディレクティブです。型システム全体の説明はこのブートキャンプの範囲外ですが(かなり複雑です)、コンパイラは inout が適切にマークされていない型にフラグを設定するため、これらの型について知っておく必要があります。

ステップ 1: out 型を定義する

  1. Aquarium クラスで、T: WaterSupplyout 型に変更します。
class Aquarium<out T: WaterSupply>(val waterSupply: T) {
    ...
}
  1. 同じファイルで、クラスの外部に、WaterSupplyAquarium を想定する関数 addItemTo() を宣言します。
fun addItemTo(aquarium: Aquarium<WaterSupply>) = println("item added")
  1. genericsExample() から addItemTo() を呼び出し、プログラムを実行します。
fun genericsExample() {
    val aquarium = Aquarium(TapWater())
    addItemTo(aquarium)
}
⇒ item added

Kotlin は、addItemTo()out 型として宣言されているため、汎用 WaterSupply で型安全でない処理を行わないことを保証できます。

  1. out キーワードを削除すると、Kotlin では型に対して安全でない操作が行われていないことを保証できないため、addItemTo() を呼び出すときにコンパイラがエラーを返します。

ステップ 2: in 型を定義する

in 型は out 型に似ていますが、関数に渡されるだけで返されない汎用型に使用されます。in 型を返そうとすると、コンパイラ エラーが発生します。この例では、インターフェースの一部として in 型を定義します。

  1. Aquarium.kt で、WaterSupply に制約された汎用 T を受け取るインターフェース Cleaner を定義します。これは clean() の引数としてのみ使用されるため、in パラメータにすることができます。
interface Cleaner<in T: WaterSupply> {
    fun clean(waterSupply: T)
}
  1. Cleaner インターフェースを使用するには、化学物質を追加して TapWater を洗浄する Cleaner を実装するクラス TapWaterCleaner を作成します。
class TapWaterCleaner : Cleaner<TapWater> {
    override fun clean(waterSupply: TapWater) =   waterSupply.addChemicalCleaners()
}
  1. Aquarium クラスで、addWater() を更新して T 型の Cleaner を取得し、水を追加する前に浄化します。
class Aquarium<out T: WaterSupply>(val waterSupply: T) {
    fun addWater(cleaner: Cleaner<T>) {
        if (waterSupply.needsProcessing) {
            cleaner.clean(waterSupply)
        }
        println("water added")
    }
}
  1. genericsExample() のサンプルコードを更新して、TapWaterCleanerTapWater を含む Aquarium を作成し、クリーナーを使用して水を加えます。必要に応じてクリーナーを使用します。
fun genericsExample() {
    val cleaner = TapWaterCleaner()
    val aquarium = Aquarium(TapWater())
    aquarium.addWater(cleaner)
}

Kotlin は inout の型情報を使用して、コードでジェネリックが安全に使用されるようにします。Outin は覚えやすいです。out 型は戻り値として外側に渡すことができ、in 型は引数として内側に渡すことができます。

型と out 型で解決される問題の種類について詳しく知りたい場合は、ドキュメントで詳しく説明されています。

このタスクでは、汎用関数とその使用タイミングについて学習します。通常、関数が汎用型を持つクラスの引数を取る場合は、汎用関数を作成することをおすすめします。

ステップ 1: 汎用関数を作成する

  1. generics/Aquarium.kt で、Aquarium を受け取る関数 isWaterClean() を作成します。パラメータの汎用型を指定する必要があります。1 つのオプションとして WaterSupply を使用できます。
fun isWaterClean(aquarium: Aquarium<WaterSupply>) {
   println("aquarium water is clean: ${aquarium.waterSupply.needsProcessing}")
}

ただし、この場合、Aquarium には out 型のパラメータが必要です。入力と出力の両方に型を使用する必要があるため、out または in が制限的すぎる場合があります。関数を汎用化することで、out の要件を削除できます。

  1. 関数を汎用にするには、キーワード fun の後に山かっこで囲んで汎用型 T と制約(この場合は WaterSupply)を指定します。WaterSupply ではなく T によって制約されるように Aquarium を変更します。
fun <T: WaterSupply> isWaterClean(aquarium: Aquarium<T>) {
   println("aquarium water is clean: ${!aquarium.waterSupply.needsProcessing}")
}

T は、水族館のジェネリック型を指定するために使用される isWaterClean() の型パラメータです。このパターンは非常に一般的です。このパターンを理解しておくことをおすすめします。

  1. 関数名の直後、かっこの前に山かっこで型を指定して、isWaterClean() 関数を呼び出します。
fun genericsExample() {
    val aquarium = Aquarium(TapWater())
    isWaterClean<TapWater>(aquarium)
}
  1. 引数 aquarium から型推論が行われるため、型は不要です。削除します。プログラムを実行して出力を確認します。
fun genericsExample() {
    val aquarium = Aquarium(TapWater())
    isWaterClean(aquarium)
}
⇒ aquarium water is clean: false

ステップ 2: 具現化された型を持つ汎用メソッドを作成する

メソッドにも汎用関数を使用できます。独自の汎用型を持つクラスでも使用できます。このステップでは、AquariumWaterSupply 型かどうかをチェックする汎用メソッドを追加します。

  1. Aquarium クラスで、WaterSupply に制約された汎用パラメータ RT はすでに使用されています)を受け取り、waterSupplyR 型の場合に true を返すメソッド hasWaterSupplyOfType() を宣言します。これは、前に宣言した関数と似ていますが、Aquarium クラス内にあります。
fun <R: WaterSupply> hasWaterSupplyOfType() = waterSupply is R
  1. 最後の R に赤い下線が引かれています。ポインタを合わせると、エラーの内容が表示されます。
  2. is チェックを行うには、型が reified(実体化)されており、関数で使用できることを Kotlin に伝える必要があります。そのためには、fun キーワードの前に inline を、汎用型 R の前に reified を配置します。
inline fun <reified R: WaterSupply> hasWaterSupplyOfType() = waterSupply is R

型が実体化されると、通常の型と同じように使用できます。インライン化後の型は実際の型であるためです。つまり、この型を使用して is チェックを行うことができます。

ここで reified を使用しない場合、Kotlin で is チェックを許可するのに十分な「実体」型になりません。これは、非具現化型はコンパイル時にのみ使用でき、実行時にプログラムで使用できないためです。これについては、次のセクションで詳しく説明します。

  1. 型として TapWater を渡します。汎用関数を呼び出す場合と同様に、関数名の後に型を山かっこで囲んで汎用メソッドを呼び出します。プログラムを実行して結果を確認します。
fun genericsExample() {
    val aquarium = Aquarium(TapWater())
    println(aquarium.hasWaterSupplyOfType<TapWater>())   // true
}
⇒ true

ステップ 3: 拡張機能の関数を作成する

具現化された型は、通常の関数と拡張関数にも使用できます。

  1. Aquarium クラスの外部で、渡された WaterSupply が特定の型(TapWater など)であるかどうかをチェックする isOfType() という WaterSupply の拡張関数を定義します。
inline fun <reified T: WaterSupply> WaterSupply.isOfType() = this is T
  1. 拡張関数はメソッドと同じように呼び出します。
fun genericsExample() {
    val aquarium = Aquarium(TapWater())
    println(aquarium.waterSupply.isOfType<TapWater>())  
}
⇒ true

これらの拡張関数を使用すると、AquariumAquariumTowerTank、その他のサブクラスのいずれであっても、Aquarium であれば問題ありません。スター プロジェクション構文を使用すると、さまざまな一致を簡単に指定できます。また、スター プロジェクションを使用すると、Kotlin は安全でない操作が行われないようにします。

  1. 星型投影を使用するには、Aquarium の後に <*> を配置します。hasWaterSupplyOfType()Aquarium のコア API の一部ではないため、拡張関数として移動します。
inline fun <reified R: WaterSupply> Aquarium<*>.hasWaterSupplyOfType() = waterSupply is R
  1. 呼び出しを hasWaterSupplyOfType() に変更して、プログラムを実行します。
fun genericsExample() {
    val aquarium = Aquarium(TapWater())
    println(aquarium.hasWaterSupplyOfType<TapWater>())
}
⇒ true

前の例では、Kotlin はコンパイル時だけでなく実行時にも汎用型を認識する必要があるため、汎用型を reified としてマークし、関数を inline にする必要がありました。

すべての汎用型は、コンパイル時に Kotlin によってのみ使用されます。これにより、コンパイラはすべての処理が安全に行われていることを確認できます。実行時にすべての汎用型が消去されるため、消去された型をチェックするというエラー メッセージが表示されます。

コンパイラは、実行時まで汎用型を保持しなくても正しいコードを作成できることがわかりました。ただし、is などの汎用型のチェックなど、コンパイラがサポートできない処理を行う場合もあります。そのため、Kotlin には具体化された型(実型)が追加されました。

具現化型と型消去について詳しくは、Kotlin のドキュメントをご覧ください。

このレッスンでは、コードの柔軟性を高め、再利用しやすくするために重要なジェネリクスについて説明しました。

  • コードの柔軟性を高めるために、汎用クラスを作成します。
  • ジェネリクスで使用される型を制限する汎用制約を追加します。
  • ジェネリクスで in 型と out 型を使用すると、クラスに渡される型やクラスから返される型を制限するための型チェックを強化できます。
  • 汎用型を操作するための汎用関数と汎用メソッドを作成します。例:
    fun <T: WaterSupply> isWaterClean(aquarium: Aquarium<T>) { ... }
  • 汎用拡張関数を使用して、クラスにコア以外の機能を追加します。
  • 型消去のため、具象化された型が必要になることがあります。具現化型は、ジェネリック型とは異なり、ランタイムまで保持されます。
  • check() 関数を使用して、コードが想定どおりに実行されていることを確認します。例:
    check(!waterSupply.needsProcessing) { "water supply needs processing first" }

Kotlin ドキュメント

このコースのトピックについてさらに詳しい情報が必要な場合や、行き詰まった場合は、https://kotlinlang.org を参照することをおすすめします。

Kotlin のチュートリアル

https://try.kotlinlang.org には、Kotlin Koans という豊富なチュートリアル、ウェブベースのインタープリタ、例を含む完全なリファレンス ドキュメントが用意されています。

Udacity コース

このトピックに関する Udacity コースについては、プログラマー向け Kotlin ブートキャンプをご覧ください。

IntelliJ IDEA

IntelliJ IDEA のドキュメントは、JetBrains のウェブサイトで確認できます。

このセクションでは、インストラクター主導のコースの一環として、この Codelab に取り組んでいる生徒向けに考えられる宿題をいくつか示します。インストラクターは、以下のようなことを行えます。

  • 必要に応じて宿題を与える
  • 宿題の提出方法を生徒に伝える
  • 宿題を採点する

インストラクターは、これらの提案を必要なだけ使用し、必要に応じて他の宿題も自由に与えることができます。

この Codelab に独力で取り組む場合は、これらの宿題を自由に使用して知識をテストしてください。

以下の質問に回答してください

問題 1

次のうち、ジェネリック型の命名規則はどれですか?

<Gen>

<Generic>

<T>

<X>

問題 2

汎用型で使用できる型の制限は、次のように呼ばれます。

▢ 一般的な制限

▢ 汎用制約

▢ 曖昧性除去

▢ ジェネリック型の制限

問題 3

具現化とは、次のことを意味します。

▢ オブジェクトの実際の実行の影響が計算されました。

▢ クラスに制限付きエントリ インデックスが設定されている。

▢ ジェネリック型パラメータが実際の型になった。

▢ リモート エラー インジケーターがトリガーされた。

次のレッスンに進みます。6. 機能操作

他の Codelab へのリンクを含むコースの概要については、「プログラマー向け Kotlin ブートキャンプ: コースへようこそ」をご覧ください。