プログラマー 5.1 向け Kotlin ブートキャンプ: 拡張機能

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

はじめに

この Codelab では、ペア、コレクション、拡張関数など、Kotlin のさまざまな便利な機能を紹介します。

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

前提となる知識

  • Kotlin の関数、クラス、メソッドの構文
  • IntelliJ IDEA で Kotlin の REPL(Read-Eval-Print Loop)を使用する方法
  • IntelliJ IDEA で新しいクラスを作成してプログラムを実行する方法

学習内容

  • ペアとトリプルの使用方法
  • コレクションの詳細
  • 定数の定義と使用
  • 拡張関数の作成

演習内容

  • REPL のペア、トリプル、ハッシュマップについて確認する
  • 定数を整理するさまざまな方法を確認する
  • 拡張関数と拡張プロパティを記述する

このタスクでは、ペアとトリプルとその分解について学習します。ペアとトリプルは、2 つか 3 つの汎用アイテムに対する事前作成済みのデータクラスです。これは、たとえば関数で複数の値を返す場合に役立ちます。

魚の List があり、その魚が淡水魚または海水魚かどうかをチェックする関数 isFreshWater() があるとします。List.partition() は 2 つのリストを返します。1 つは条件が true のアイテムで、もう 1 つは条件が false のアイテムです。

val twoLists = fish.partition { isFreshWater(it) }
println("freshwater: ${twoLists.first}")
println("saltwater: ${twoLists.second}")

ステップ 1: ペア数と 3 つの組み合わせを作成する

  1. REPL(Tools > Kotlin > Kotlin REPL)を開きます。
  2. ペアを作成し、機器と使用目的を関連付け、値を出力します。ペアを作成するには、2 つの文字列(2 つの文字列など)と to というキーワードを組み合わせて式を作成し、.first.second を使用してそれぞれの値を参照します。
val equipment = "fish net" to "catching fish"
println("${equipment.first} used for ${equipment.second}")
⇒ fish net used for catching fish
  1. トリプルを作成し、toString() で出力してから、toList() でリストに変換します。3 つの値を含む Triple() を使用してトリプルを作成します。それぞれの値を参照するには、.first.second.third を使用します。
val numbers = Triple(6, 9, 42)
println(numbers.toString())
println(numbers.toList())
⇒ (6, 9, 42)
[6, 9, 42]

上の例では、ペアまたはトリプルのすべての部分で同じ型を使用していますが、必須ではありません。パーツは、文字列、数字、リストなどで使用できます(別のペアやトリプルなど)。

  1. ペアの最初の部分がペアになるように、ペアを作成します。
val equipment2 = ("fish net" to "catching fish") to "equipment"
println("${equipment2.first} is ${equipment2.second}\n")
println("${equipment2.first.second}")
⇒ (fish net, catching fish) is equipment
⇒ catching fish

ステップ 2: ペアとトリプルを分解する

ペアとトリプルを部品に分割することを「分解」と呼びます。ペアまたはトリプルを適切な数の変数に割り当てると、Kotlin は各部分の値を順番に割り当てます。

  1. ペアを分解して値を出力します。
val equipment = "fish net" to "catching fish"
val (tool, use) = equipment
println("$tool is used for $use")
⇒ fish net is used for catching fish
  1. 三重に分解して値を出力します。
val numbers = Triple(6, 9, 42)
val (n1, n2, n3) = numbers
println("$n1 $n2 $n3")
⇒ 6 9 42

ペアと三重の分解はデータクラスと同じように機能するので、前の Codelab で説明しました。

このタスクでは、リストを含むコレクションと、新しいコレクション タイプであるハッシュマップについて詳しく説明します。

ステップ 1: リストの詳細を確認する

  1. リストと変更可能なリストは、前のレッスンで導入しました。Kotlin は、非常に便利なデータ構造であるため、リスト用の組み込み関数が多数用意されています。この関数についてはリストをご覧ください。詳細なリストについては、ListMutableList の Kotlin ドキュメントをご覧ください。

関数

目的

add(element: E)

ミュータブルなリストに項目を追加します。

remove(element: E)

ミュータブルなリストから項目を削除します。

reversed()

要素のコピーを元の順序で返します。

contains(element: E)

リストにアイテムが含まれている場合は true を返します。

subList(fromIndex: Int, toIndex: Int)

最初のインデックスから 2 番目のインデックスを除くリストの一部を返します。

  1. 引き続き REPL で番号のリストを作成し、その番号で sum() を呼び出します。これですべての要素をまとめることができます。
val list = listOf(1, 5, 3, 4)
println(list.sum())
⇒ 13
  1. 文字列のリストを作成し、そのリストを合計します。
val list2 = listOf("a", "bbb", "cc")
println(list2.sum())
⇒ error: none of the following functions can be called with the arguments supplied:
  1. この要素が List で直接認識する方法(文字列など)でない場合は、ラムダ関数を使用した .sumBy() を使用して合計方法を指定できます(たとえば、各文字列の長さを合計する)。ラムダ引数のデフォルト名は it です。ここでは、it はリストが走査されるときにリストの各要素を指します。
val list2 = listOf("a", "bbb", "cc")
println(list2.sumBy { it.length })
⇒ 6
  1. リストでできることは他にもたくさんあります。使用可能な機能の 1 つとして、IntelliJ IDEA にリストを作成し、ドットを追加してから、ツールチップのオートコンプリート リストを確認します。これは、どのオブジェクトでも使用できます。ぜひリストをお試しください。

  1. リストから listIterator() を選択してから、for ステートメントでリストを実行し、すべての要素をスペースで区切ります。
val list2 = listOf("a", "bbb", "cc")
for (s in list2.listIterator()) {
    println("$s ")
}
⇒ a bbb cc

ステップ 2: ハッシュマップを試す

Kotlin では、hashMapOf() を使用して、ほぼすべてのものを他のものにマッピングできます。ハッシュマップはペアのリストのようなもので、最初の値がキーとして機能します。

  1. 魚の症状、キー、病気の値(値)に一致するハッシュマップを作成します。
val cures = hashMapOf("white spots" to "Ich", "red sores" to "hole disease")
  1. 症状キーに基づいて、get()、またはさらに短い角かっこ [] を使用して病気の値を取得できます。
println(cures.get("white spots"))
⇒ Ich
println(cures["red sores"])
⇒ hole disease
  1. 地図にない症状を指定してください。
println(cures["scale loss"])
⇒ null

マップにキーがない場合、一致する病気を返そうとすると null が返されます。マップのデータによっては、一致するキーがないことがよくあります。そのような場合、Kotlin には getOrDefault() 関数が用意されています。

  1. getOrDefault() を使用して、一致しないキーを検索してみてください。
println(cures.getOrDefault("bloating", "sorry, I don't know"))
⇒ sorry, I don't know

単に値を返すだけでなく、さらに多くの処理を行う必要がある場合、Kotlin には getOrElse() 関数が用意されています。

  1. getOrDefault() ではなく getOrElse() を使用するようにコードを変更します。
println(cures.getOrElse("bloating") {"No cure for this"})
⇒ No cure for this

単純なデフォルト値を返すのではなく、中かっこ {} 内のコードが実行されます。この例の場合、else は単純に文字列を返しますが、治療法が掲載されているウェブページを検索して返すような実装にすることもできます。

mutableListOf と同様に、mutableMapOf も作成できます。可変マップを使用してアイテムを配置したり削除したりできます。「不変」は「変更できる」だけを表し、「不変」は「変更できない」を意味します。

  1. 変更可能な在庫マップを作成して、機器の文字列をアイテム数にマッピングします。フィッシュ ネットを仕込んだら、put() で在庫に 3 つのタンク スクラバーを追加し、remove() を使ってフィッシュ ネットを取り外します。
val inventory = mutableMapOf("fish net" to 1)
inventory.put("tank scrubber", 3)
println(inventory.toString())
inventory.remove("fish net")
println(inventory.toString())
⇒ {fish net=1, tank scrubber=3}{tank scrubber=3}

このタスクでは、Kotlin の定数と、それらの整理方法について学習します。

ステップ 1: const と val について

  1. REPL で数値定数を作成してみます。Kotlin では、トップレベル定数を作成し、const val を使用してコンパイル時に値を割り当てることができます。
const val rocks = 3

この値は割り当てられており、変更できません。これは、通常の val を宣言するのとよく似ています。const valval の違いは何でしょうか。const val の値はコンパイル時に決定されます。val の値はプログラムの実行中に決定されるため、val は関数によって実行時に割り当てられます。

つまり、val には関数から値を割り当てることができますが、const val には割り当てることができません。

val value1 = complexFunctionCall() // OK
const val CONSTANT1 = complexFunctionCall() // NOT ok

また、const val はトップレベルでのみ機能し、object で宣言されたシングルトン クラスでは通常のクラスでは機能しません。これを使用して、定数のみを含むファイルまたはシングルトン オブジェクトを作成し、必要に応じてインポートできます。

object Constants {
    const val CONSTANT2 = "object constant"
}
val foo = Constants.CONSTANT2

ステップ 2: コンパニオン オブジェクトを作成する

Kotlin にはクラスレベルの定数の概念はありません。

クラス内で定数を定義するには、companion キーワードで宣言されたコンパニオン オブジェクトにラップする必要があります。コンパニオン オブジェクトは、基本的にクラス内のシングルトン オブジェクトです。

  1. 文字列定数を含むコンパニオン オブジェクトを使用してクラスを作成します。
class MyClass {
    companion object {
        const val CONSTANT3 = "constant in companion"
    }
}

コンパニオン オブジェクトと通常のオブジェクトの基本的な違いは次のとおりです。

  • コンパニオン オブジェクトは、それを含むクラスの静的コンストラクタから初期化されます。つまり、そのオブジェクトの作成時に作成されます。
  • 通常のオブジェクトは、オブジェクトへの最初のアクセス、つまり最初に使用されるときに遅延初期化されます。

他にも多くの機能がありますが、ここで知っておく必要があるのは、定数をコンパニオン オブジェクトのクラスにラップすることだけです。

このタスクでは、クラスの動作を拡張する方法を説明します。クラスの動作を拡張するユーティリティ関数を作成することは非常に一般的です。Kotlin には、ユーティリティ関数(拡張関数)を宣言するための便利な構文が用意されています。

拡張関数を使用すると、ソースコードにアクセスせずに既存のクラスに関数を追加できます。たとえば、パッケージに含まれる Extensions.kt ファイルで宣言できます。実際にはクラスは変更されませんが、このクラスのオブジェクトに対して関数を呼び出す際、ドット表記を使用できます。

ステップ 1: 拡張関数を作成する

  1. 引き続き REPL で作業して、単純な拡張関数 hasSpaces() を作成し、文字列にスペースが含まれているかどうかを確認します。関数名の先頭に、操作するクラスの名前が付きます。関数内で、this は呼び出されるオブジェクト、itfind() 呼び出しのイテレータを表します。
fun String.hasSpaces(): Boolean {
    val found = this.find { it == ' ' }
    return found != null
}
println("Does it have spaces?".hasSpaces())
⇒ true
  1. hasSpaces() 関数を簡略化できます。this は明示的には必要ありません。関数を 1 つの式に減らして返すことができるため、中かっこ {} も不要です。
fun String.hasSpaces() = find { it == ' ' } != null

ステップ 2: 拡張機能の制限事項を確認する

拡張関数は、拡張するクラスの公開 API にのみアクセスできます。private 変数はアクセスできません。

  1. private とマークされたプロパティに拡張関数を追加してみましょう。
class AquariumPlant(val color: String, private val size: Int)

fun AquariumPlant.isRed() = color == "red"    // OK
fun AquariumPlant.isBig() = size > 50         // gives error
⇒ error: cannot access 'size': it is private in 'AquariumPlant'
  1. 以下のコードを確認して、出力される内容を確認します。
open class AquariumPlant(val color: String, private val size: Int)

class GreenLeafyPlant(size: Int) : AquariumPlant("green", size)

fun AquariumPlant.print() = println("AquariumPlant")
fun GreenLeafyPlant.print() = println("GreenLeafyPlant")

val plant = GreenLeafyPlant(size = 10)
plant.print()
println("\n")
val aquariumPlant: AquariumPlant = plant
aquariumPlant.print()  // what will it print?
⇒ GreenLeafyPlant
AquariumPlant

plant.print()GreenLeafyPlant を出力します。aquariumPlant.print() には plant という値が割り当てられているため、GreenLeafyPlant も出力される場合があります。型はコンパイル時に解決されるため、AquariumPlant が出力されます。

ステップ 3: 拡張プロパティを追加する

拡張関数に加えて、Kotlin では拡張プロパティを追加できます。拡張関数と同様に、拡張するクラス、ドット、プロパティ名の順に指定します。

  1. 引き続き REPL で拡張プロパティ isGreenAquariumPlant に追加します。色が緑色の場合は true です。
val AquariumPlant.isGreen: Boolean
   get() = color == "green"

isGreen プロパティには通常のプロパティと同様にアクセスできます。アクセスすると、isGreen のゲッターが呼び出されて値を取得します。

  1. aquariumPlant 変数の isGreen プロパティを出力して、結果を確認します。
aquariumPlant.isGreen
⇒ res4: kotlin.Boolean = true

ステップ 4: null 値許容レシーバーについて

拡張クラスはレシーバーと呼ばれ、そのクラスを null 値許容にすることもできます。その場合、本文で使用する this 変数を null にすることもできます。必ずテストしてください。呼び出し元が null 許容変数で拡張メソッドを呼び出すことを期待している場合や、関数が null に適用されたときにデフォルトの動作を提供する場合は、null 許容レシーバーを受け取ることをおすすめします。

  1. 引き続き REPL で作業を行い、null 値許容レシーバーを受け取る pull() メソッドを定義します。入力すると、?ドットの前にその疑問符が表示されます。本文内では、questionmark-dot-apply ?.apply. を使用して、thisnull でないかどうかをテストできます。
fun AquariumPlant?.pull() {
   this?.apply {
       println("removing $this")
   }
}

val plant: AquariumPlant? = null
plant.pull()
  1. この場合、プログラムの実行時に出力はありません。plantnull であるため、内部 println() は呼び出されません。

拡張関数は非常に強力で、Kotlin 標準ライブラリのほとんどは拡張関数として実装されています。

このレッスンでは、コレクションと定数について学び、拡張関数とプロパティのメリットを体験しました。

  • ペアとトリプルを使用して、関数から複数の値を返すことができます。次に例を示します。
    val twoLists = fish.partition { isFreshWater(it) }
  • Kotlin には、reversed()contains()subList() など、List の便利な機能が多数用意されています。
  • HashMap を使用すると、キーを値にマッピングできます。例:
    val cures = hashMapOf("white spots" to "Ich", "red sores" to "hole disease")
  • const キーワードを使用してコンパイル時の定数を宣言します。トップレベルに配置したり、シングルトン オブジェクトにまとめて配置したり、コンパニオン オブジェクトに配置したりできます。
  • コンパニオン オブジェクトはクラス定義内のシングルトン オブジェクトで、companion キーワードによって定義されます。
  • 拡張関数と拡張プロパティを使用すると、クラスに機能を追加できる。次に例を示します。
    fun String.hasSpaces() = find { it == ' ' } != null
  • null 許容のレシーバを使うと、null の拡張機能を作成できます。?. 演算子を apply と組み合わせて、コードを実行する前に null を確認できます。例:
    this?.apply { println("removing $this") }

Kotlin ドキュメント

このコースに関するトピックについて詳しい情報が必要な場合や、ご不明な点がある場合は、https://kotlinlang.org をご覧ください。

Kotlin のチュートリアル

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

Udacity コース

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

IntelliJ IDEA

IntelliJ IDEA のドキュメントは、JetBrains のウェブサイトにあります。

このセクションでは、インストラクターが主導するコースの一環として、この Codelab に取り組む生徒の課題について説明します。教師は以下のことを行えます。

  • 必要に応じて課題を割り当てます。
  • 宿題の提出方法を生徒に伝える。
  • 宿題を採点します。

教師はこれらの提案を少しだけ使うことができます。また、他の課題は自由に割り当ててください。

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

次の質問に答えてください。

問題 1

リストのコピーを返すのは、次のうちどれですか。

add()

remove()

reversed()

contains()

質問 2

class AquariumPlant(val color: String, val size: Int, private val cost: Double, val leafy: Boolean) にある拡張関数のうち、コンパイラ エラーが発生するのはどれですか。

fun AquariumPlant.isRed() = color == "red"

fun AquariumPlant.isBig() = size > 45

fun AquariumPlant.isExpensive() = cost > 10.00

fun AquariumPlant.isNotLeafy() = leafy == false

問題 3

const val で定数を定義できない場所は次のうちどれですか。

▢ ファイルの最上位にあります

▢ 教室

シングルトン オブジェクトの ▢

コンパニオン オブジェクト内の ▢

次のレッスン「5.2 ジェネリクス」に進みます。

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