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

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

はじめに

この Codelab では、汎用のクラス、関数、メソッド、および Kotlin での動作について紹介します。

このコースのレッスンは単一のサンプルアプリを構築するものではなく、知識を広げることを目的としていますが、相互に依存しない部分があるため、使い慣れたセクションを省略できます。これらをまとめる例の多くは、水族館のテーマを使用しています。また、アクアリウムのストーリー全文については、Udacity の Profiler 向け Kotlin ブートキャンプ コースで詳細をご覧ください。

前提となる知識

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

学習内容

  • 汎用のクラス、メソッド、関数の使用方法

演習内容

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

ジェネリクスの概要

Kotlin は、多くのプログラミング言語と同様に汎用的な型を持ちます。汎用型を使用すると、クラスを汎用化できるため、クラスの柔軟性が向上します。

アイテムのリストを保持する MyList クラスを実装するとします。ジェネリクスがない場合は、タイプごとに新しいバージョンの MyList を実装する必要があります。Double に 1 つ、String に 1 つ、Fish に 1 つ。ジェネリックを使用すると、あらゆる種類のオブジェクトを保持できるように、リストをジェネリックにすることができます。これは、型を多くのタイプに適合するワイルドカードにするようなものです。

汎用型を定義するには、クラス名の後に山かっこ「<T>」をクラス名の後に挿入します。(別の文字や長い名前を使用することもできますが、一般的な名前には T を使います)。

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

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

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

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

  1. この例を整理するために、src の下に新しいパッケージを作成し、generics という名前を付けます。
  2. generics パッケージでは、新しい Aquarium.kt ファイルを作成します。それにより、同じ名前を使用して競合することなく再定義できるため、この Codelab の残りのコードをこのファイルに組み込むことができます。
  3. 給水器のタイプ階層を作成します。まず、WaterSupplyopen クラスにして、サブクラス化できるようにします。
  4. ブール値 var パラメータ needsProcessing を追加します。これにより、変更可能なプロパティとゲッターおよびセッターが自動的に作成されます。
  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 のサブクラスを 2 つ(FishStoreWaterLakeWater)作成します。FishStoreWater は処理不要ですが、LakeWaterfilter() メソッドでフィルタリングする必要があります。フィルタリング後は再度処理する必要がないため、filter()needsProcessing = false を設定します。
class FishStoreWater : WaterSupply(false)

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

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

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

このステップでは、さまざまな水道をサポートするように Aquarium クラスを変更します。

  1. Aquarium.ktAquarium クラスを定義します。クラス名の後に <T> をかっこで囲んでください。
  2. T 型の不変プロパティ waterSupplyAquarium に追加します。
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. Aquarium オブジェクトの作成時に、山かっこと、それらの間の角かっこを削除できます。これは、Kotlin には型推論があるためです。したがって、インスタンスを作成するときに TapWater を 2 回指定する必要はありません。型は Aquarium の引数から推測できます。ただし、型は TapWaterAquarium になります。
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. main() 関数を追加して genericsExample() を呼び出し、プログラムを実行して結果を確認します。
fun main() {
    genericsExample()
}
⇒ water needs processing: true
water needs processing: false

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

全般的とは、ほとんど何でも渡すことができることを意味します。場合によっては、これが問題になります。このステップでは、Aquarium クラスに何を含めることができるかを具体的にします。

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

結果は、渡された文字列です。Aquarium は、T. を含むすべての型を 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 の後の ? を削除して、Any 型の T を明示的にします。
class Aquarium<T: Any>(val waterSupply: T)

ここでの Any は、一般的な制約と呼ばれます。これは、null である限り、任意の型を T に渡すことができることを意味します。

  1. 本当に必要なのは、TWaterSupply(またはそのサブクラスの 1 つ)のみを渡すことです。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 型は、クラスからのみ返すことができます。

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

in 型と out 型は、Kotlin の型システムのディレクティブです。型システム全体について説明することは、このブートキャンプの範囲外です(ただし、かなりの手間がかかります)。ただし、コンパイラは inout としてマークされていない型にフラグを付けるため、それらについて把握する必要があります。

ステップ 1: 出力タイプを定義する

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

Kotlin は、WaterSupplyout 型として宣言されているため、汎用の WaterSupply を使用して addItemTo() が安全でない動作をしないよう制御できます。

  1. out キーワードを削除すると、Kotlin は addItemTo() を呼び出したときにエラーになります。これは、Kotlin ではその型で安全でないことがないことが保証できないためです。

ステップ 2: 種類を定義する

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 クラスで、T 型の Cleaner を取るように addWater() を更新し、水をきれいにしてから追加します。
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 型は引数として内部に渡すことができます。

さまざまな種類の問題と解決できない問題についてさらに詳しく調べる場合は、ドキュメントで詳しく説明しています。

このタスクでは、汎用関数とその使用方法について説明します。一般的には、関数が汎用型を持つクラスの引数を取るたびに、汎用関数を作成することをおすすめします。

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

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

ただし、これを呼び出すには、Aquariumout タイプのパラメータが必要です。入力と出力の両方に型を使用する必要があるため、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: 具体化された型を持つ汎用メソッドを作成する

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

  1. Aquarium クラスで、WaterSupply に制限されている汎用パラメータ RT がすでに使用されています)を取り、waterSupply の型が R の場合は true を返すメソッド hasWaterSupplyOfType() を宣言します。これは、以前に宣言した関数に似ていますが、Aquarium クラス内です。
fun <R: WaterSupply> hasWaterSupplyOfType() = waterSupply is R
  1. 最後の R には赤い下線が表示されます。エラーにカーソルを合わせると、エラーの内容が表示されます。
  2. is チェックを行うには、型が具体化または実数であり、関数で使用できることを Kotlin に指示する必要があります。そのためには、inlinefun キーワードの前に、reified を汎用タイプ R の前に指定します。
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

これらの拡張関数では、Aquarium である限り、Aquarium の型(AquariumTowerTank などのサブクラス)は問題になりません。star-projection 構文を使用すると、さまざまな一致を簡単に指定できます。スター投影を使用すると、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 ブートキャンプ: コースへようこそをご覧ください。