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

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

はじめに

この Codelab では、ペア、コレクション、拡張関数など、Kotlin のいくつかの便利な機能について学習します。

このコースのレッスンは、1 つのサンプルアプリを作成するのではなく、知識を深めるように設計されています。また、各レッスンは半独立しているため、よく知っているセクションは読み飛ばすことができます。これらの例を関連付けるため、多くは水族館をテーマにしています。水族館の物語の全体像を確認したい場合は、Udacity の 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: ペアとトリプルを作る

  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 番目のインデックスまで(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 は明示的に必要ではなく、関数は単一の式に縮小して返すことができるため、その周りの波かっこ {} も必要ありません。
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() プリント GreenLeafyPlantaquariumPlant.print()plant の値が割り当てられているため、aquariumPlant.print()GreenLeafyPlant を出力すると予想されるかもしれません。ただし、型はコンパイル時に解決されるため、AquariumPlant が出力されます。

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

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

  1. REPL で作業を続け、AquariumPlant に拡張プロパティ isGreen を追加します。色は緑の場合、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 で、nullable レシーバーを受け取る pull() メソッドを定義します。これは、型の後にドットの前に疑問符 ? を付けて示されます。本文内で、疑問符とドットの適用 ?.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 ブートキャンプ: コースへようこそ」をご覧ください。