ジェネリック、オブジェクト、拡張機能

1. はじめに

数十年にわたり、プログラマーはより優れたコードの作成に役立つプログラミング言語をいくつも考案してきました。たとえば、同じアイデアをより少ないコードで表現したり、複雑なアイデアを表現する抽象化を行ったり、自分以外の開発者が誤解することを防止したりすることができるコードを記述する言語が考案されてきました。Kotlin 言語もその例外ではなく、開発者がより表現力に富んだコードを記述できるような機能が数多く用意されています。

残念ながら、そうした機能は、初めてのプログラミングでは開発者を戸惑わせることがあります。有用に思える機能であっても、その有用性の範囲と、機能によって解決される問題が常に明白であるとは限りません。Compose などのライブラリで使用されている機能の一部については、すでにご存じかもしれません。

経験に勝るものはありませんが、この Codelab では、大規模なアプリの構造化に役立つ次のような Kotlin のコンセプトを紹介します。

  • ジェネリック
  • さまざまな種類のクラス(列挙型クラスとデータクラス)
  • シングルトン オブジェクトとコンパニオン オブジェクト
  • 拡張プロパティと拡張関数
  • スコープ関数

この Codelab を修了すると、このコースですでに学習したコードに関するより深い知識を身につけ、自分のアプリでこれらのコンセプトを確認および使用する場合のいくつかの例を理解できるようになります。

前提条件

  • オブジェクト指向プログラミングの概念(継承など)に精通していること。
  • インターフェースを定義して実装する方法を習得していること。

学習内容

  • クラスの汎用型パラメータを定義する方法。
  • 汎用クラスをインスタンス化する方法。
  • 列挙型クラスとデータクラスを使用するタイミング。
  • インターフェースを実装する必要がある汎用型パラメータを定義する方法。
  • スコープ関数を使用してクラスのプロパティとメソッドにアクセスする方法。
  • クラスのシングルトン オブジェクトとコンパニオン オブジェクトを定義する方法。
  • 新しいプロパティとメソッドで既存のクラスを拡張する方法。

必要なもの

  • Kotlin プレイグラウンドにアクセスできるウェブブラウザ。

2. ジェネリックを使用して再利用可能なクラスを作成する

たとえば、このコースですでに見たクイズと同様のオンライン クイズ用のアプリを作成するとします。クイズ問題には、穴埋め問題や正誤問題など、さまざまなタイプがあります。個々のクイズ問題は、複数のプロパティを含むクラスで表現できます。

クイズの問題テキストは文字列で表現できます。クイズ問題は、その解答も表現する必要があります。ただし、正誤問題のようなタイプの問題では、別のデータ型を使用して解答を表現する必要があります。次の 3 つのタイプの問題を定義してみましょう。

  • 穴埋め問題: 解答は String で表現される単語です。
  • 正誤問題: 解答は Boolean で表現されます。
  • 計算問題: 解答は数値です。単純な計算問題の解答は Int で表現されます。

さらに、この Codelab で扱うクイズ問題の例では、どのタイプの問題にも難易度ランクがあります。難易度ランクは、"easy""medium""hard" という 3 つの可能な値の文字列で表現されます。

個々のタイプのクイズ問題を表すクラスを定義します。

  1. Kotlin プレイグラウンドに移動します。
  2. main() 関数の上に、穴埋め問題を表すクラスを FillInTheBlankQuestion という名前で定義します。このクラスは、questionText を表す String プロパティ、answer を表す String プロパティ、difficulty を表す String プロパティで構成されます。
class FillInTheBlankQuestion(
    val questionText: String,
    val answer: String,
    val difficulty: String
)
  1. FillInTheBlankQuestion クラスの下に、正誤問題を表す別のクラスを TrueOrFalseQuestion という名前で定義します。このクラスは、questionText を表す String プロパティ、answer を表す Boolean プロパティ、difficulty を表す String プロパティで構成されます。
class TrueOrFalseQuestion(
    val questionText: String,
    val answer: Boolean,
    val difficulty: String
)
  1. 最後に、上記の 2 つのクラスの下に、NumericQuestion クラスを定義します。このクラスは、questionText を表す String プロパティ、answer を表す Int プロパティ、difficulty を表す String プロパティで構成されます。
class NumericQuestion(
    val questionText: String,
    val answer: Int,
    val difficulty: String
)
  1. 作成したコードを見てみましょう。繰り返しに気づきましたか?
class FillInTheBlankQuestion(
    val questionText: String,
    val answer: String,
    val difficulty: String
)

class TrueOrFalseQuestion(
    val questionText: String,
    val answer: Boolean,
    val difficulty: String
)
class NumericQuestion(
    val questionText: String,
    val answer: Int,
    val difficulty: String
)

3 つのクラスすべてに、まったく同じプロパティ questionTextanswerdifficulty があります。唯一違うのは answer プロパティのデータ型です。この繰り返しに対する解決策は単純明快で、questionTextdifficulty に親クラスを作成し、各サブクラスで answer プロパティを定義することだと思われるかもしれません。

しかし、継承を使用しても上記と同じ問題が残ります。新しいタイプの問題を追加するたびに、answer プロパティを追加しなくてはなりません。違うのはデータ型だけです。また、解答プロパティを持たない親クラス Question があるのも不自然です。

データ型が異なるプロパティを使用したい場合、サブクラス化は解決策になりません。これに代わる解決策として、Kotlin には「汎用型」という機能があります。これにより、特定のユースケースに応じて、さまざまなデータ型を持つ単一のプロパティを使用できます。

汎用データ型とは

汎用型(「ジェネリック」)を使用すると、あるデータ型(クラスなど)が、そのプロパティとメソッドで使用できる不明なプレースホルダのデータ型を表現できます。これは正確にはどういうことでしょうか。

上記の例では、可能なデータ型ごとに解答プロパティを定義する代わりに、任意の問題を表す単一のクラスを作成し、answer プロパティのデータ型としてプレースホルダ名を使用することができます。実際のデータ型(StringIntBoolean など)は、クラスをインスタンス化する際に指定します。プレースホルダ名が使用されているすべての箇所で、クラスに渡されるデータ型が使用されます。クラスの汎用型を定義する構文は次のとおりです。

67367d9308c171da.png

汎用型は、クラスをインスタンス化する際に指定されるため、クラス署名の一部として定義する必要があります。クラス名の後に、左山かっこ(<)、データ型のプレースホルダ名、右山かっこ(>)の順に記述します。

このプレースホルダ名は、クラス内の実際のデータ型を使用する任意の箇所で使用できます(プロパティのデータ型など)。

81170899b2ca0dc9.png

これは他のプロパティ宣言と同じですが、データ型の代わりにプレースホルダ名を使用する点だけが異なります。

クラスは、使用するデータ型を最終的にどうやって知るのでしょうか。汎用型が使用するデータ型は、クラスをインスタンス化する際に、山かっこで囲んだパラメータとして渡します。

9b8fce54cac8d1ea.png

クラス名の後に、左山かっこ(<)、実際のデータ型(StringBooleanInt など)、右山かっこ(>)の順に記述します。汎用プロパティ用に渡す値のデータ型は、山かっこで囲まれたデータ型と一致する必要があります。解答プロパティをジェネリックにすることにより、解答が StringBooleanInt のいずれであろうと、または任意のデータ型であろうと、1 つのクラスであらゆるタイプのクイズ問題を表すことができます。

コードをリファクタリングしてジェネリックを使用する

コードをリファクタリングして、ジェネリックの解答プロパティを含む Question という名前の単一のクラスを使用するようにします。

  1. FillInTheBlankQuestionTrueOrFalseQuestionNumericQuestion のクラス定義を削除します。
  2. Question という名前の新しいクラスを作成します。
class Question()
  1. クラス名の後、かっこの前に、左山かっこと右山かっこで囲んだ汎用型パラメータを追加します。汎用型に T という名前を付けます。
class Question<T>()
  1. questionTextanswer、および difficulty プロパティを追加します。questionText の型は String にする必要があります。answer の型は T にする必要があります。これは、Question クラスをインスタンス化する際にそのデータ型を指定するためです。difficulty プロパティの型は String にする必要があります。
class Question<T>(
    val questionText: String,
    val answer: T,
    val difficulty: String
)
  1. これが複数の問題タイプ(穴埋め問題や正誤問題)でどのように機能するかを確認するため、次に示すように、main() 内に Question クラスの 3 つのインスタンスを作成します。
fun main() {
    val question1 = Question<String>("Quoth the raven ___", "nevermore", "medium")
    val question2 = Question<Boolean>("The sky is green. True or false", false, "easy")
    val question3 = Question<Int>("How many days are there between full moons?", 28, "hard")
}
  1. コードを実行して、すべてが正常に機能することを確認します。3 つの異なるクラスを作成したり継承を使用したりする代わりに、Question クラスの 3 つのインスタンス(それぞれ解答のデータ型が異なる)を作成しました。これで、解答のタイプが異なる複数の問題を処理する場合に、同じ Question クラスを再利用できます。

3. 列挙型クラスを使用する

前のセクションでは、「easy」、「medium」、「hard」の 3 つの可能な値を持つ難易度プロパティを定義しました。これは機能しますが、問題がいくつかあります。

  1. 3 つの可能な文字列のいずれかの入力を誤った場合、バグが発生する可能性があります。
  2. 値を変更した場合(たとえば "medium" の名前を "average" に変更した場合)、その文字列が使用されている箇所をすべて更新する必要があります。
  3. 開発者自身または他の開発者が 3 つの有効な値のいずれでもない別の文字列を誤って使用することを防止できません。
  4. 難易度の種類を増やすと、コードの保守が困難になります。

Kotlin では、「列挙型クラス」という特別なタイプのクラスにより、これらの問題を解決できます。列挙型クラスは、可能な値の限定されたセットを持つ型を作成するために使用されます。たとえば、現実世界の 4 つの基本方位(東西南北)を列挙型クラスで表現できます。それ以外の方位は不要なので、コードで許可しないようにします。列挙型クラスの構文は次のとおりです。

f4bddb215eb52392.png

個々の可能な列挙の値は「列挙型定数」と呼ばれます。列挙型定数は波かっこ内に配置し、カンマで区切ります。慣例として、定数名はすべて大文字にします。

列挙型定数を参照するには、ドット演算子を使用します。

f3cfa84c3f34392b.png

列挙型定数を使用する

コードを変更して、String の代わりに列挙型定数を使用して難易度を表すようにします。

  1. Question クラスの下に、Difficulty という名前の enum クラスを定義します。
enum class Difficulty {
    EASY, MEDIUM, HARD
}
  1. Question クラス内で、difficulty プロパティのデータ型を String から Difficulty に変更します。
class Question<T>(
    val questionText: String,
    val answer: T,
    val difficulty: Difficulty
)
  1. 3 つのタイプの問題を初期化する際に、難易度を表す列挙型定数を渡します。
val question1 = Question<String>("Quoth the raven ___", "nevermore", Difficulty.MEDIUM)
val question2 = Question<Boolean>("The sky is green. True or false", false, Difficulty.EASY)
val question3 = Question<Int>("How many days are there between full moons?", 28, Difficulty.HARD)

4. データクラスを使用する

Activity のサブクラスなど、これまでに扱ったクラスの多くには、さまざまなアクションを実行するメソッドがいくつかあります。これらのクラスは、データを表現する機能だけでなく、多くの機能を備えています。

一方、Question クラスなどのクラスには、データのみが含まれます。アクションを実行するメソッドは含まれていません。このようなクラスは「データクラス」として定義できます。クラスをデータクラスとして定義すると、Kotlin コンパイラはある種の仮定を行って、いくつかのメソッドを自動的に実装できます。たとえば、toString()println() 関数によって自動的に呼び出されます。データクラスを使用すると、クラスのプロパティに基づいて toString() などのメソッドが自動的に実装されます。

データクラスを定義するには、class キーワードの前に data キーワードを追加するだけです。

e7cd946b4ad216f4.png

Question をデータクラスに変換する

最初に、データクラスではないクラスで toString() などのメソッドを呼び出そうとしたときにどうなるかを確認します。次に、Question をデータクラスに変換します。これにより、このメソッドとその他のメソッドがデフォルトで実装されまます。

  1. main() 内で、question1 に対する toString() の呼び出し結果を出力します。
fun main() {
    val question1 = Question<String>("Quoth the raven ___", "nevermore", Difficulty.MEDIUM)
    val question2 = Question<Boolean>("The sky is green. True or false", false, Difficulty.EASY)
    val question3 = Question<Int>("How many days are there between full moons?", 28, Difficulty.HARD)
    println(question1.toString())
}
  1. コードを実行します。出力には、クラス名と、オブジェクトの一意の識別子のみが示されます。
Question@37f8bb67
  1. data キーワードを使用して Question をデータクラスにします。
data class Question<T>(
    val questionText: String,
    val answer: T,
    val difficulty: Difficulty
)
  1. コードをもう一度実行します。これをデータクラスとしてマークすると、Kotlin は toString() を呼び出す際にクラスのプロパティの表示方法を決定できます。
Question(questionText=Quoth the raven ___, answer=nevermore, difficulty=MEDIUM)

クラスがデータクラスとして定義されると、以下のメソッドが実装されます。

  • equals()
  • hashCode(): このメソッドは、特定のコレクション型を操作する際に使用されます。
  • toString()
  • componentN(): component1()component2()(以下同様)
  • copy()

5. シングルトン オブジェクトを使用する

インスタンスが 1 つのみのクラスを使用したい場合は数多くあります。次に例を示します。

  1. モバイルゲームにおける現在のユーザーのプレーヤー統計情報。
  2. 1 台のハードウェア デバイスの操作(スピーカーを介した音声の送信など)。
  3. リモート データソース(Firebase データベースなど)にアクセスするためのオブジェクト。
  4. 一度に 1 人のユーザーにのみログインを許可する認証。

これらののシナリオでは、おそらくクラスを使用する必要があります。しかし、そのクラスをインスタンス化してインスタンスを作成する必要があるのは一度だけです。ハードウェア デバイスが 1 台だけの場合や、一度に 1 人のユーザーだけがログインする場合、複数のインスタンスを作成する理由はありません。同じハードウェア デバイスに同時にアクセスするオブジェクトが 2 つあると、誤った奇妙な動作が発生する可能性があります。

オブジェクトをシングルトンとして定義すると、そのオブジェクトが 1 つのインスタンスしか持たないことをコード内で明確に伝達できます。「シングルトン」とは、単一のインスタンスのみを持つことができるクラスです。Kotlin には、「オブジェクト」と呼ばれる特別な構造体があり、これをシングルトン クラスとして使用できます。

シングルトン オブジェクトを定義する

645e8e8bbffbb5f9.png

オブジェクトの構文はクラスの構文に似ています。class キーワードの代わりに、単に object キーワードを使用します。インスタンスを直接作成できないので、シングルトン オブジェクトにコンストラクタを含めることはできません。その代わりに、すべてのプロパティを波かっこで囲んで定義し、初期値を指定します。

前述の例の一部は、とりわけ特定のハードウェア デバイスを使用したことがない場合や、アプリでまだ認証を扱っていない場合は、わかりにくいかもしれません。しかし、Android 開発の学習を進めると、シングルトン オブジェクトをよくみかけます。ユーザーの状態を表すオブジェクトを使用する単純な例を見てみましょう。この例では、必要なインスタンスは 1 つだけです。

クイズでは、問題の合計数と、受講者がこれまでに解答した問題の数をトラッキングする方法があると便利です。このクラスのインスタンスは 1 つあれば十分です。そこで、クラスとして宣言する代わりに、シングルトン オブジェクトとして宣言します。

  1. StudentProgress という名前のオブジェクトを作成します。
object StudentProgress {
}
  1. この例では、問題が全部で 10 問あり、これまでにそのうちの 3 問が解答済みであるとします。Int プロパティを 2 つ追加します。total の値は 10 で、answered の値は 3 です。
object StudentProgress {
    var total: Int = 10
    var answered: Int = 3
}

シングルトン オブジェクトにアクセスする

前述のとおり、シングルトン オブジェクトのインスタンスを直接作成することはできません。では、そのプロパティにアクセスするにはどうすればよいでしょうか?

StudentProgress は同時に 1 つしか存在しないため、そのプロパティにアクセスするには、オブジェクト自体の名前を指定し、次にドット演算子(.)を付けて、その後にプロパティ名を指定します。

1b610fd87e99fe25.png

main() 関数を更新して、シングルトン オブジェクトのプロパティにアクセスするようにします。

  1. main() 内に、StudentProgress オブジェクトから問題の answeredtotal の数を出力する println() の呼び出しを追加します。
fun main() {
    ...
    println("${StudentProgress.answered} of ${StudentProgress.total} answered.")
}
  1. コードを実行して、すべてが正常に機能することを確認します。
...
3 of 10 answered.

オブジェクトをコンパニオン オブジェクトとして宣言する

Kotlin のクラスとオブジェクトは他の型の内部で定義できるため、コードをわかりやすく整理する方法として使用できます。「コンパニオン オブジェクト」を使用すると、シングルトン オブジェクトを別のクラスの内部で定義できます。コンパニオン オブジェクトを使用すると、オブジェクトのプロパティとメソッドが同じクラスに属している場合、そのクラスの内部からオブジェクトのプロパティとメソッドにアクセスできます。これにより、構文をより簡潔にすることができます。

コンパニオン オブジェクトを宣言するには、object キーワードの前に companion キーワードを追加するだけです。

68b263904ec55f29.png

クイズ問題を格納する新しいクラスを Quiz という名前で作成し、StudentProgressQuiz クラスのコンパニオン オブジェクトにします。

  1. 列挙型クラス Difficulty の下に、Quiz という名前の新しいクラスを定義します。
class Quiz {
}
  1. question1question2question3 を、main() から Quiz クラスに移動します。println(question1.toString()) をまだ削除していない場合は、それを削除する必要もあります。
class Quiz {
    val question1 = Question<String>("Quoth the raven ___", "nevermore", Difficulty.MEDIUM)
    val question2 = Question<Boolean>("The sky is green. True or false", false, Difficulty.EASY)
    val question3 = Question<Int>("How many days are there between full moons?", 28, Difficulty.HARD)

}
  1. StudentProgress オブジェクトを Quiz クラスに移動します。
class Quiz {
    val question1 = Question<String>("Quoth the raven ___", "nevermore", Difficulty.MEDIUM)
    val question2 = Question<Boolean>("The sky is green. True or false", false, Difficulty.EASY)
    val question3 = Question<Int>("How many days are there between full moons?", 28, Difficulty.HARD)

    object StudentProgress {
        var total: Int = 10
        var answered: Int = 3
    }
}
  1. StudentProgress オブジェクトを companion キーワードでマークします。
companion object StudentProgress {
    var total: Int = 10
    var answered: Int = 3
}
  1. println() の呼び出しを更新して、Quiz.answeredQuiz.total でプロパティを参照するようにします。これらのプロパティは StudentProgress オブジェクト内で宣言されていますが、Quiz クラスの名前のみを使用したドット表記でアクセスできます。
fun main() {
    println("${Quiz.answered} of ${Quiz.total} answered.")
}
  1. コードを実行して出力を確認します。
3 of 10 answered.

6. 新しいプロパティとメソッドでクラスを拡張する

Compose を扱っているとき、UI 要素のサイズを指定する場合に、いくつかの興味深い構文に気づいたかもしれません。Double などの数値型には、ディメンションを指定する dpsp などのプロパティがあります。

a25c5a0d7bb92b60.png

Kotlin 言語の設計者が、組み込みデータ型のプロパティと関数、具体的には Android UI の作成用のものを含めたのはなぜでしょうか。彼らは将来を予測できたのでしょうか。Compose が存在する前から、Kotlin は Compose で使用できるように設計されていたのでしょうか。

もちろんそうではありません。クラスを作成しているとき、別の開発者がアプリでそのクラスをどのように使用するか(どのように使用する予定なのか)が正確にわからないことはよくあります。将来のユースケースをすべて予測することは不可能です。また、予想外のユースケースに備えて不必要にコードを肥大化させるのは賢明ではありません。

これに対する Kotlin 言語の解決策は、他の開発者が既存のデータ型を拡張して、ドット構文でアクセスできるプロパティとメソッドをあたかもそのデータ型の一部であるかのように追加できるようにすることです。Kotlin で浮動小数点型を扱ったことがない開発者は、たとえば Compose ライブラリを構築する場合に、UI ディメンション固有のプロパティとメソッドを追加する方法を選択するかもしれません。

この構文は、最初の 2 つのユニットで Compose を学習したときに目にしたはずです。ここでは、この構文が内部的にどのように機能するかを学習します。既存の型を拡張するために、プロパティとメソッドを追加します。

拡張プロパティを追加する

拡張プロパティを定義するには、変数名の前に型名とドット演算子(.)を追加します。

1e8a52e327fe3f45.png

main() 関数のコードをリファクタリングして、クイズの進行状況を拡張プロパティに出力するようにします。

  1. Quiz クラスの下に、Quiz.StudentProgress の拡張プロパティを progressText という名前で定義し、String 型を指定します。
val Quiz.StudentProgress.progressText: String
  1. 拡張プロパティのゲッターを定義して、以前 main() で使用したのと同じ文字列を返します。
val Quiz.StudentProgress.progressText: String
    get() = "${answered} of ${total} answered"
  1. main() 関数のコードを、progressText を出力するコードに置き換えます。これはコンパニオン オブジェクトの拡張プロパティなので、クラス名 Quiz を使用したドット表記でアクセスできます。
fun main() {
    println(Quiz.progressText)
}
  1. コードを実行して、正常に機能することを確認します。
3 of 10 answered.

拡張関数を追加する

拡張関数を定義するには、関数名の前に型名とドット演算子(.)を追加します。

879ff2761e04edd9.png

クイズの進行状況を進行状況バーとして出力する拡張関数を追加します。Kotlin プレイグラウンドでは実際に進行状況バーを作成することができないため、テキストを使用してレトロスタイルの進行状況バーを出力します。

  1. StudentProgress オブジェクトに、printProgressBar() という名前の拡張オブジェクトを追加します。この関数はパラメータを受け取らず、戻り値を返しません。
fun Quiz.StudentProgress.printProgressBar() {
}
  1. repeat() を使用して、 という文字を answered 回出力します。進行状況バーのこの色が濃い部分は、解答済みの問題の数を表します。各文字の後に改行は入れないので、print() を使用します。
fun Quiz.StudentProgress.printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
}
  1. repeat() を使用して、 という文字を出力します。この文字の数は、total から answered を差し引いた値です。進行状況バーのこの色の薄い部分は、未解答の問題の数を表します。
fun Quiz.StudentProgress.printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
    repeat(Quiz.total - Quiz.answered) { print("▒") }
}
  1. 引数なしで println() を使用して改行を出力し、次に progressText を出力します。
fun Quiz.StudentProgress.printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
    repeat(Quiz.total - Quiz.answered) { print("▒") }
    println()
    println(Quiz.progressText)
}
  1. main() のコードを更新して、printProgressBar() を呼び出すようにします。
fun main() {
    Quiz.printProgressBar()
}
  1. コードを実行して出力を確認します。
▓▓▓▒▒▒▒▒▒▒
3 of 10 answered.

この手法の使用が必須かというと、もちろん違います。しかし、拡張プロパティと拡張メソッドを使用できるようにしておくと、他の開発者にコードを公開したときに、選択肢がさらに増えます。他の型でドット構文を使用すると、開発者自身にとっても他の開発者にとっても、コードが読みやすくなります。

7. インターフェースを使用して拡張関数を書き換える

前のページでは、拡張プロパティと拡張関数を使用して、StudentProgress オブジェクトにコードを直接追加せずに、プロパティとメソッドを追加する方法を確認しました。これは、定義済みの 1 つのクラスに機能を追加するための優れた方法ですが、ソースコードにアクセスできる場合は、必ずしもクラスを拡張する必要はありません。特定のメソッドまたはプロパティのみが存在し、どのような実装を行うべきかわからない場合もあります。それぞれの動作が異なる複数のクラスが必要で、各クラスに同じ追加のプロパティとメソッドがある場合は、そのようなプロパティとメソッドをインターフェースで定義できます。

たとえば、クイズ以外に、アンケートやレシピのステップなど、進行状況バーを使用できる順序付きデータのクラスがあるとします。ここでインターフェースを定義すると、各クラスに含める必要があるメソッドまたはプロパティを指定できます。

eeed58ed687897be.png

インターフェースを定義するには、interface キーワードの後に、UpperCamelCase 形式の名前、左波かっこと右波かっこを続けて記述します。波かっこの中で、インターフェースに従うすべてのクラスが実装する必要があるメソッド シグネチャまたは get のみのプロパティを定義できます。

6b04a8f50b11f2eb.png

インターフェースは一種のコントラクトです。クラスがインターフェースに従うことを「インターフェースを拡張する」と言います。クラスがインターフェースを拡張することを宣言するには、コロン(:)の後に、スペース、インターフェース名を続けて記述します。

78af59840c74fa08.png

クラスでは、インターフェースで指定されているすべてのプロパティとメソッドを実装する必要があります。これにより、インターフェースを拡張する必要があるすべてのクラスに、まったく同じメソッド シグネチャを持つまったく同じメソッドを簡単に実装できます。プロパティまたはメソッドの追加または削除、メソッド シグネチャの変更など、なんらかの方法でインターフェースを変更した場合は、インターフェースを拡張するクラスをすべて更新し、コードの一貫性を維持して保守を容易にすることがコンパイラによって要求されます。

インターフェースを使用すると、インターフェースを拡張するクラスの動作にバリエーションを持たせることができます。実装を提供するかどうかはクラスごとに決定します。

インターフェースを使用するように進行状況バーを書き換え、Quiz クラスでインターフェースを拡張する方法を見てみましょう。

  1. Quiz クラスの上で、ProgressPrintable という名前のインターフェースを定義します。ProgressPrintable という名前を選択したのは、このインターフェースを拡張するクラスが進行状況バーを出力できるようにするためです。
interface ProgressPrintable {
}
  1. ProgressPrintable インターフェース内で、progressText という名前のプロパティを定義します。
interface ProgressPrintable {
    val progressText: String
}
  1. Quiz クラスの宣言を変更して、ProgressPrintable インターフェースを拡張します。
class Quiz : ProgressPrintable {
    ...
}
  1. Quiz クラスに、ProgressPrintable インターフェースで指定した、progressText という名前の String 型のプロパティを追加します。このプロパティは ProgressPrintable から取得されるため、val の前に override キーワードを付けます。
override val progressText: String
  1. 元の progressText 拡張プロパティからプロパティ ゲッターをコピーします。
override val progressText: String
        get() = "${answered} of ${total} answered"
  1. 元の progressText 拡張プロパティを削除します。

削除するコード:

val Quiz.StudentProgress.progressText: String
    get() = "${answered} of ${total} answered"
  1. ProgressPrintable インターフェースに、パラメータを受け取らず戻り値を返さない printProgressBar という名前のメソッドを追加します。
interface ProgressPrintable {
    val progressText: String
    fun printProgressBar()
}
  1. Quiz クラスに、override キーワードを使用して printProgressBar() メソッドを追加します。
override fun printProgressBar() {
}
  1. 元の printProgressBar() 拡張関数のコードを、インターフェースの新しい printProgressBar() に移動します。最後の行を変更して Quiz への参照を削除し、インターフェースの新しい progressText 変数を参照するようにします。
override fun printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
    repeat(Quiz.total - Quiz.answered) { print("▒") }
    println()
    println(progressText)
}
  1. 拡張関数 printProgressBar() を削除します。これで、この機能は ProgressPrintable を拡張する Quiz クラスに含まれるようになりました。

削除するコード:

fun Quiz.StudentProgress.printProgressBar() {
    repeat(Quiz.answered) { print("▓") }
    repeat(Quiz.total - Quiz.answered) { print("▒") }
    println()
    println(Quiz.progressText)
}
  1. main() のコードを更新します。printProgressBar() 関数は Quiz クラスのメソッドになったため、まず Quiz オブジェクトをインスタンス化してから printProgressBar() を呼び出す必要があります。
fun main() {
    Quiz().printProgressBar()
}
  1. コードを実行します。出力に変更はありませんが、コードのモジュール性が向上しました。コードベースの増大に伴い、同じインターフェースに従うクラスを簡単に追加できるため、スーパークラスから継承しなくてもコードを再利用できます。
▓▓▓▒▒▒▒▒▒▒
3 of 10 answered.

コードの構造化に役立つインターフェースのユースケースは数多くあり、共通のユニットで頻繁に使用されている例を目にするはずです。Kotlin を使用する際によく目にするであろうインターフェースの例を次にいくつか示します。

  • 手動の依存関係インジェクション。依存関係のすべてのプロパティとメソッドを定義するインターフェースを作成します。依存関係(アクティビティ、テストケースなど)のデータ型としてインターフェースを要求し、インターフェースを実装する任意のクラスのインスタンスを使用できるようにします。これにより、基盤となる実装の入れ替えが可能になります。
  • 自動テスト用のモックの作成。モッククラスと実際のクラスの両方が同じインターフェースに従います。
  • Compose マルチプラットフォーム アプリ内での同じ依存関係へのアクセス。たとえば、基盤となる実装がプラットフォームごとに異なる場合でも、Android とパソコンに共通のプロパティとメソッドのセットを提供するインターフェースを作成します。
  • Compose のいくつかのデータ型(Modifier など)はインターフェースです。これにより、基盤となるソースコードへのアクセスまたは変更を行わずに、新しい修飾子を追加できます。

8. スコープ関数を使用してクラスのプロパティとメソッドにアクセスする

すでに見てきたように、Kotlin には、コードをより簡潔にできる機能が数多く用意されています。

Android 開発の学習を進める中でよく目にするそうした機能の一つに、「スコープ関数」があります。スコープ関数を使用すると、変数名に繰り返しアクセスしなくても、クラスからプロパティとメソッドに簡単にアクセスできます。これは正確にはどういうことでしょうか。例を見てみましょう。

スコープ関数を使用してオブジェクト参照の繰り返しをなくす

スコープ関数は、オブジェクトの名前を参照せずにオブジェクトのプロパティとメソッドにアクセスすることを可能にする高階関数です。スコープ関数という呼称は、渡される関数の本体が、スコープ関数が呼び出されるオブジェクトのスコープを引き継ぐことに由来します。たとえば、一部のスコープ関数では、あたかも関数がクラスのメソッドとして定義されているかのように、そのクラス内のプロパティとメソッドにアクセスできます。これにより、長いオブジェクト名を指定する際に名前を省略できるので、コードが読みやすくなります。

もっとよく理解できるように、このコースに後ほど登場するスコープ関数をいくつか見てみましょう。

let() を使用して長いオブジェクト名を置き換える

let() 関数を使用すると、オブジェクトの実際の名前の代わりに識別子 it を使用して、ラムダ式でオブジェクトを参照できます。これにより、複数のプロパティにアクセスする際に、説明的な長いオブジェクト名を繰り返し使用することを回避できます。let() 関数は、ドット表記を使用して任意の Kotlin オブジェクトで呼び出すことができる拡張関数です。

let() を使用して、question1question2question3 のプロパティにアクセスしてみましょう。

  1. printQuiz() という名前の関数を Quiz クラスに追加します。
fun printQuiz() {

}
  1. 問題の questionTextanswerdifficulty を出力する次のコードを追加します。question1question2question3 の複数のプロパティがアクセスされますが、そのたびに変数名全体が使用されます。変数の名前を変更した場合は、使用されている箇所をすべて更新する必要があります。
fun printQuiz() {
    println(question1.questionText)
    println(question1.answer)
    println(question1.difficulty)
    println()
    println(question2.questionText)
    println(question2.answer)
    println(question2.difficulty)
    println()
    println(question3.questionText)
    println(question3.answer)
    println(question3.difficulty)
    println()
}
  1. question1question2question3 で、questionTextanswerdifficulty の各プロパティにアクセスするコードを、let() 関数の呼び出しで囲みます。各ラムダ式内の変数名をそのコードに置き換えます。
fun printQuiz() {
    question1.let {
        println(it.questionText)
        println(it.answer)
        println(it.difficulty)
    }
    println()
    question2.let {
        println(it.questionText)
        println(it.answer)
        println(it.difficulty)
    }
    println()
    question3.let {
        println(it.questionText)
        println(it.answer)
        println(it.difficulty)
    }
    println()
}
  1. main() のコードを更新して、quiz という名前の Quiz クラスのインスタンスを作成します。
fun main() {
    val quiz = Quiz()
}
  1. printQuiz() を呼び出します。
fun main() {
    val quiz = Quiz()
    quiz.printQuiz()
}
  1. コードを実行して、すべてが正常に機能することを確認します。
Quoth the raven ___
nevermore
MEDIUM

The sky is green. True or false
false
EASY

How many days are there between full moons?
28
HARD

apply() を使用して変数なしでオブジェクトのメソッドを呼び出す

スコープ関数の便利な点の一つは、オブジェクトが変数に割り当てられる前に、オブジェクトでスコープ関数を呼び出せることです。たとえば、apply() 関数は、ドット表記を使用してオブジェクトで呼び出せる拡張関数です。apply() 関数は、そのオブジェクトへの参照も返すので、これを変数に格納できます。

main() のコードを更新して、apply() 関数を呼び出すようにします。

  1. Quiz クラスのインスタンスを作成する際に、右かっこの後で apply() を呼び出します。apply() を呼び出す際にかっこを省略して、後置ラムダ構文を使用できます。
val quiz = Quiz().apply {
}
  1. printQuiz() の呼び出しをラムダ式の内部に移動します。quiz 変数を参照する必要もドット表記を使用する必要もなくなります。
val quiz = Quiz().apply {
    printQuiz()
}
  1. apply() 関数は Quiz クラスのインスタンスを返しますが、使用するところがないため、quiz 変数を削除します。apply() 関数では、Quiz のインスタンスでメソッドを呼び出す変数すら必要ありません。
Quiz().apply {
    printQuiz()
}
  1. コードを実行します。Quiz のインスタンスを参照せずにこのメソッドを呼び出すことができた点に注意してください。apply() 関数は、quiz に格納されたオブジェクトを返しました。
Quoth the raven ___
nevermore
MEDIUM

The sky is green. True or false
false
EASY

How many days are there between full moons?
28
HARD

必要とする出力を得るうえでスコープ関数の使用は必須ではありませんが、上記の例は、コードをより簡潔にし、同じ変数名の繰り返しを回避する方法を示しています。

上記のコードでは 2 つの例のみを示しましたが、このコースで後ほどそれらの使用例を目にしたときは、スコープ関数のドキュメントをブックマークして参照することをおすすめします。

9. まとめ

この Codelab では、Kotlin のいくつかの新機能がどのように機能するかを学びました。ジェネリックを使用すると、データ型をパラメータとしてクラスに渡すことができます。列挙型クラスは、可能な値の限定されたセットを定義します。データクラスは、クラスの有用なメソッドを自動的に生成します。

また、インスタンスが 1 つのみのシングルトン オブジェクトを作成する方法、シングルトン オブジェクトを別のクラスのコンパニオン オブジェクトにする方法、新しい get 専用プロパティと新しいメソッドで既存のクラスを拡張する方法を学びました。最後に、プロパティとメソッドにアクセスする際に、スコープ関数で構文を簡潔にする方法の例を学びました。

Kotlin、Android 開発、Compose について詳しく学習する過程で、これらのコンセプトはこの後のユニットにも登場します。ここでは、それらがどのように機能するかと、それらによってコードの再利用性と可読性がいかに向上するかについて理解を深めました。

10. 関連リンク