この 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 で説明されていますが、ここで簡単に説明します。
- この例を整理するために、src の下に新しいパッケージを作成し、
generics
という名前を付けます。 - generics パッケージでは、新しい
Aquarium.kt
ファイルを作成します。それにより、同じ名前を使用して競合することなく再定義できるため、この Codelab の残りのコードをこのファイルに組み込むことができます。 - 給水器のタイプ階層を作成します。まず、
WaterSupply
をopen
クラスにして、サブクラス化できるようにします。 - ブール値
var
パラメータneedsProcessing
を追加します。これにより、変更可能なプロパティとゲッターおよびセッターが自動的に作成されます。 WaterSupply
を拡張するサブクラスTapWater
を作成し、needsProcessing
にtrue
を渡します。これは、水道水に魚に適さない添加物が含まれているためです。TapWater
で、水をクリーニングした後にneedsProcessing
をfalse
に設定するaddChemicalCleaners()
という関数を定義します。needsProcessing
プロパティは、デフォルトではpublic
であり、サブクラスからアクセスできるため、TapWater
から設定できます。完成したコードは以下のとおりです。
package generics
open class WaterSupply(var needsProcessing: Boolean)
class TapWater : WaterSupply(true) {
fun addChemicalCleaners() {
needsProcessing = false
}
}
WaterSupply
のサブクラスを 2 つ(FishStoreWater
とLakeWater
)作成します。FishStoreWater
は処理不要ですが、LakeWater
はfilter()
メソッドでフィルタリングする必要があります。フィルタリング後は再度処理する必要がないため、filter()
でneedsProcessing = false
を設定します。
class FishStoreWater : WaterSupply(false)
class LakeWater : WaterSupply(true) {
fun filter() {
needsProcessing = false
}
}
追加情報が必要な場合は、Kotlin の継承に関する以前のレッスンをご覧ください。
ステップ 2: 汎用のクラスを作成する
このステップでは、さまざまな水道をサポートするように Aquarium
クラスを変更します。
- Aquarium.kt で
Aquarium
クラスを定義します。クラス名の後に<T>
をかっこで囲んでください。 T
型の不変プロパティwaterSupply
をAquarium
に追加します。
class Aquarium<T>(val waterSupply: T)
genericsExample()
という関数を作成します。これはクラスの一部ではないため、main()
関数やクラス定義など、ファイルの最上位に配置できます。関数内で、Aquarium
を作成し、WaterSupply
を渡します。waterSupply
パラメータは汎用であるため、山かっこ「<>
」でタイプを指定する必要があります。
fun genericsExample() {
val aquarium = Aquarium<TapWater>(TapWater())
}
genericsExample()
では、コードが水族館のwaterSupply
にアクセスできます。型がTapWater
であるため、型キャストなしでaddChemicalCleaners()
を呼び出すことができます。
fun genericsExample() {
val aquarium = Aquarium<TapWater>(TapWater())
aquarium.waterSupply.addChemicalCleaners()
}
Aquarium
オブジェクトの作成時に、山かっこと、それらの間の角かっこを削除できます。これは、Kotlin には型推論があるためです。したがって、インスタンスを作成するときにTapWater
を 2 回指定する必要はありません。型はAquarium
の引数から推測できます。ただし、型はTapWater
のAquarium
になります。
fun genericsExample() {
val aquarium = Aquarium(TapWater())
aquarium.waterSupply.addChemicalCleaners()
}
- 何が起きているかを確認するには、
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}")
}
main()
関数を追加してgenericsExample()
を呼び出し、プログラムを実行して結果を確認します。
fun main() {
genericsExample()
}
⇒ water needs processing: true water needs processing: false
ステップ 3: より具体的にする
全般的とは、ほとんど何でも渡すことができることを意味します。場合によっては、これが問題になります。このステップでは、Aquarium
クラスに何を含めることができるかを具体的にします。
genericsExample()
でAquarium
を作成し、waterSupply
の文字列を渡して、水族館のwaterSupply
プロパティを出力します。
fun genericsExample() {
val aquarium2 = Aquarium("string")
println(aquarium2.waterSupply)
}
- プログラムを実行して結果を確認します。
⇒ string
結果は、渡された文字列です。Aquarium
は、T.
を含むすべての型を String
に制限できないため、渡すことはできません。
genericsExample()
に別のAquarium
を作成し、waterSupply
にnull
を渡します。waterSupply
が null の場合は、"waterSupply is null"
を出力します。
fun genericsExample() {
val aquarium3 = Aquarium(null)
if (aquarium3.waterSupply == null) {
println("waterSupply is null")
}
}
- プログラムを実行して結果を確認します。
⇒ waterSupply is null
Aquarium
の作成時に null
を渡せるのはなぜですか。これが可能なのは、デフォルトで T
が null 許容の Any?
型(型階層の最上位にある型)を表すためです。次のコードは、前に入力した内容と同じです。
class Aquarium<T: Any?>(val waterSupply: T)
null
を渡さないようにするには、Any
の後の?
を削除して、Any
型のT
を明示的にします。
class Aquarium<T: Any>(val waterSupply: T)
ここでの Any
は、一般的な制約と呼ばれます。これは、null
である限り、任意の型を T
に渡すことができることを意味します。
- 本当に必要なのは、
T
にWaterSupply
(またはそのサブクラスの 1 つ)のみを渡すことです。Any
をWaterSupply
に置き換えて、より汎用的な制約を定義します。
class Aquarium<T: WaterSupply>(val waterSupply: T)
ステップ 4: チェックを追加する
このステップでは、コードが期待どおりに動作することを確認するために check()
関数を使用する方法について説明します。check()
関数は、Kotlin の標準ライブラリ関数です。これはアサーションとして動作し、引数が false
と評価されると IllegalStateException
をスローします。
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()
は例外をスローします。
genericsExample()
で、LakeWater
を使用してAquarium
を作成するコードを追加し、そこに水を追加します。
fun genericsExample() {
val aquarium4 = Aquarium(LakeWater())
aquarium4.addWater()
}
- プログラムを実行すると、水がろ過されるため例外が発生します。
⇒ Exception in thread "main" java.lang.IllegalStateException: water supply needs processing first at Aquarium.generics.Aquarium.addWater(Aquarium.kt:21)
- 水をフィルタする呼び出しを追加してから、
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 の型システムのディレクティブです。型システム全体について説明することは、このブートキャンプの範囲外です(ただし、かなりの手間がかかります)。ただし、コンパイラは in
と out
としてマークされていない型にフラグを付けるため、それらについて把握する必要があります。
ステップ 1: 出力タイプを定義する
Aquarium
クラスで、T: WaterSupply
をout
型に変更します。
class Aquarium<out T: WaterSupply>(val waterSupply: T) {
...
}
- 同じファイルで、クラスの外で、
Aquarium
がWaterSupply
になる関数addItemTo()
を宣言します。
fun addItemTo(aquarium: Aquarium<WaterSupply>) = println("item added")
genericsExample()
からaddItemTo()
を呼び出し、プログラムを実行します。
fun genericsExample() {
val aquarium = Aquarium(TapWater())
addItemTo(aquarium)
}
⇒ item added
Kotlin は、WaterSupply
が out
型として宣言されているため、汎用の WaterSupply
を使用して addItemTo()
が安全でない動作をしないよう制御できます。
out
キーワードを削除すると、Kotlin はaddItemTo()
を呼び出したときにエラーになります。これは、Kotlin ではその型で安全でないことがないことが保証できないためです。
ステップ 2: 種類を定義する
in
型は out
型に似ていますが、関数にのみ渡され、返されない汎用型用です。in
型を返そうとすると、コンパイラ エラーが発生します。この例では、インターフェースの一部として in
型を定義します。
- Aquarium.kt では、
WaterSupply
に制約される汎用のT
を取るインターフェースCleaner
を定義します。これはclean()
の引数としてのみ使用するため、in
パラメータにすることができます。
interface Cleaner<in T: WaterSupply> {
fun clean(waterSupply: T)
}
Cleaner
インターフェースを使用するには、化学物質を追加してTapWater
をクリーニングするためにCleaner
を実装するクラスTapWaterCleaner
を作成します。
class TapWaterCleaner : Cleaner<TapWater> {
override fun clean(waterSupply: TapWater) = waterSupply.addChemicalCleaners()
}
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")
}
}
genericsExample()
のサンプルコードを更新して、TapWaterCleaner
とTapWater
を含むAquarium
を作成し、クリーナーを使用して水を追加します。必要に応じてクリーナーを使用します。
fun genericsExample() {
val cleaner = TapWaterCleaner()
val aquarium = Aquarium(TapWater())
aquarium.addWater(cleaner)
}
Kotlin は、コードがジェネリクスを安全に使用するために、in
と out
の型情報を使用します。Out
と in
は覚えやすいものです。out
型は戻り値として外側に、in
型は引数として内部に渡すことができます。
さまざまな種類の問題と解決できない問題についてさらに詳しく調べる場合は、ドキュメントで詳しく説明しています。
このタスクでは、汎用関数とその使用方法について説明します。一般的には、関数が汎用型を持つクラスの引数を取るたびに、汎用関数を作成することをおすすめします。
ステップ 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
要件を削除できます。
- 関数を汎用化するには、キーワード
fun
の後に汎用型T
と制約(この場合はWaterSupply
)を山かっこで囲みます。WaterSupply
ではなくT
により制約されるように、Aquarium
を変更します。
fun <T: WaterSupply> isWaterClean(aquarium: Aquarium<T>) {
println("aquarium water is clean: ${!aquarium.waterSupply.needsProcessing}")
}
T
は、水族館の汎用タイプを指定するために使用する isWaterClean()
のタイプ パラメータです。これは非常に一般的なパターンなので、少し時間を置いてから確認することをおすすめします。
isWaterClean()
関数を呼び出すには、関数名の直後に、かっこの前にかっこで囲むタイプを指定します。
fun genericsExample() {
val aquarium = Aquarium(TapWater())
isWaterClean<TapWater>(aquarium)
}
- 引数
aquarium
による型の推論のため、型は不要なため削除します。プログラムを実行し、出力を確認します。
fun genericsExample() {
val aquarium = Aquarium(TapWater())
isWaterClean(aquarium)
}
⇒ aquarium water is clean: false
ステップ 2: 具体化された型を持つ汎用メソッドを作成する
汎用型は、独自の汎用型を持つクラスであっても、メソッドにも使用できます。このステップでは、型が WaterSupply
かどうかをチェックする汎用メソッドを Aquarium
に追加します。
Aquarium
クラスで、WaterSupply
に制限されている汎用パラメータR
(T
がすでに使用されています)を取り、waterSupply
の型がR
の場合はtrue
を返すメソッドhasWaterSupplyOfType()
を宣言します。これは、以前に宣言した関数に似ていますが、Aquarium
クラス内です。
fun <R: WaterSupply> hasWaterSupplyOfType() = waterSupply is R
- 最後の
R
には赤い下線が表示されます。エラーにカーソルを合わせると、エラーの内容が表示されます。 is
チェックを行うには、型が具体化または実数であり、関数で使用できることを Kotlin に指示する必要があります。そのためには、inline
をfun
キーワードの前に、reified
を汎用タイプR
の前に指定します。
inline fun <reified R: WaterSupply> hasWaterSupplyOfType() = waterSupply is R
型が具体化されると、インライン後の実数型であるため、通常の型と同じように使用できます。つまり、その型を使用して is
チェックを実行できます。
ここで reified
を使用しないと、Kotlin は is
のチェックを行うのに十分なタイプになりません。これは、サイズ変更されていないものはコンパイル時にしか使用できず、実行時にプログラムで使用できないためです。これについては、次のセクションで詳しく説明します。
- 型として
TapWater
を渡します。汎用関数の呼び出しと同様に、汎用メソッドは、関数名の後の型で山かっこを使用して呼び出します。プログラムを実行して結果を確認します。
fun genericsExample() {
val aquarium = Aquarium(TapWater())
println(aquarium.hasWaterSupplyOfType<TapWater>()) // true
}
⇒ true
ステップ 3: 拡張関数を作成する
具体化された型と拡張関数にも、具体化された型を使用できます。
Aquarium
クラスの外に、渡されたWaterSupply
が特定の型(TapWater
など)であるかどうかをチェックするisOfType()
という拡張関数をWaterSupply
で定義します。
inline fun <reified T: WaterSupply> WaterSupply.isOfType() = this is T
- メソッドと同様に、拡張関数を呼び出します。
fun genericsExample() {
val aquarium = Aquarium(TapWater())
println(aquarium.waterSupply.isOfType<TapWater>())
}
⇒ true
これらの拡張関数では、Aquarium
である限り、Aquarium
の型(Aquarium
、TowerTank
などのサブクラス)は問題になりません。star-projection 構文を使用すると、さまざまな一致を簡単に指定できます。スター投影を使用すると、Kotlin は安全でない動作も行いません。
- スター投影を使用するには、
Aquarium
の後に<*>
を記述します。hasWaterSupplyOfType()
は、Aquarium
のコア API の一部ではないため、拡張関数にします。
inline fun <reified R: WaterSupply> Aquarium<*>.hasWaterSupplyOfType() = waterSupply is R
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
修正済みとは:
▢ オブジェクトの実際の実行に対する影響が計算されました。
▢ 制限付きのエントリ インデックスがクラスに設定されている。
▢ 汎用型のパラメータが実際の型になりました。
▢ リモート エラー インジケーターがトリガーされました。
次のレッスンに進みましょう。
他の Codelab へのリンクを含むコースの概要については、プログラマー向け Kotlin ブートキャンプ: コースへようこそをご覧ください。