おはようエンジニア

まともなエンジニアになりたい

『Kotlinイン・アクション』第6章を読む (その2)

前回に続き、第6章「Kotlinの型システム」です。 後編はプリミティブ型と基本的な型に関する内容です。

『Kotlinイン・アクション』第6章を読む (その1) - おはようエンジニア

プリミティブ型の扱い

  • Javaの場合
    • プリミティブ型(intなど)と参照型(Stringなど)が区別される
    • コレクションのためにラッパークラスが必要になる
int num = 10;  // プリミティブ型
List<Integer> list = new ArrayList<>();  // 参照型であるラッパークラスが必要
  • Kotlinはプリミティブ型と参照型(ラッパークラス)の区別はない
    • null許容性によって変換先が変わる
      • null非許容型Javaのプリミティブ型に変換される
      • null許容型Javaのラッパークラスに変換される
num: Int = 10;  // 通常の型 (Javaのプリミティブ型に相当)
list: List<Int> = listOf();  // 通常の型 (Javaの参照型ラッパークラスに相当)

特殊な型

Any型

  • Any型 はKotlinのnull非許容型の最上位型
    • JavaではObjectにコンパイルされる
    • toString, equals, hashCode の3つのメソッドを持っている

Unit型

  • Unit型 は戻り値がない関数の戻り値に使う型
    • Javaの void と同じ機能だが以下が異なる
      • ジェネリクス関数の型引数として使える
      • 値を持ち、returnを書かなくても暗黙的に値 Unit を返す

Nothing型

  • Nothing型 は値を返さないことを示すときに使う型
    • Unitと同じく型引数として使えるが、値を返さない
    • 関数が正常に終了しないことをコンパイラに伝えることができる
fun fail(message: String): Nothing {
    throw IllegalStateException(message)
}

コレクションの型とインターフェイス

型引数のnull許容性

  • 型引数もnull許容性を持つ 例: List<Int?>
  • 変数自身と型引数のnull許容性に注意
    • List<Int?> : 「Int または null」 のリスト
    • List<Int>? : Int の「null を許容するリスト」

読み取り専用とミュータブルインターフェイス

  • コレクションは Collection インターフェイスを実装している
  • MutalbeCollection は Collection を継承している
  • 原則としてできる限り読み取り専用インターフェイスを使う
    • どこでデータが変更されるのかを理解しやすくなる
  • 読み取り専用コレクションは必ずしもイミュータブルではない
    • 別のコードによって参照先が変更される可能性がある

コレクション生成関数

タイプ 読み取り専用 ミュータブル
List listOf mutableListOf, arrayListOf
Set setOf mutableSetOf, hashSetOf, linkedSetOf, sortedSetOf
Map mapOf mutableMapOf, hashMapOf, linkedMapOf, sortedMapOf

Kotlin->Javaの注意点

  • Javaのコレクションはすべてミュータブル扱い
  • Javaコードにコレクションを渡す場合、Kotlin側でデータの変更を防ぐ仕組みはない

Java->Kotlinの注意点

  • Javaのコードはnull許容性の情報を持っていないのでKotlinではプラットフォーム型になる (前回記事参照)
    • null許容/null非許容 どちらとしても扱える
  • さらにJavaのコレクションは変更可能性の情報も持っていない
    • 読み取り専用/ミュータブル どちらとしても扱える
  • 以下の3つの条件で判断してKotlinコードの実装者が型を決める
    • コレクション自身がnull許容か
    • コレクションの要素がnull許容か
    • メソッドがコレクションを変更するか

配列

  • 基本的にKotlinでは配列ではなくコレクションを使用すべき
  • Array クラスはJavaの配列にコンパイルされる
    • 型パラメータを持つ (Array<Int>など)
    • コレクションと同様のメソッドをサポートしている
  • 生成には以下ような方法がある
    • arrayOf : 要素を明示的に渡して配列を生成する
    • arrayOfNulls : 上記のnull許容版
    • コンストラクタ : ラムダを渡して要素を初期化する
    • toTypedArray()でコレクションから配列に変換できる

プリミティブ型の配列

  • 配列の型引数はオブジェクト型扱い
    • Kotlinの Array<Int>JavaInteger[]
  • プリミティブ型の配列は専用のクラスを使う
    • Kotlinの IntArrayJavaint[]

まとめ

Kotlinではプリミティブ型のことを意識しなくてもパフォーマンスの問題がないように変換されることが分かりました。 その一方で、null許容性とコレクションの変更可能性についてはよく意識してコードを書く必要がありそうです。

これで Kotlinイン・アクション 第1部「Kotlinを知る」は終了です。第2部は「Kotlinを愛でる」で、自分でAPIを作成するためのより詳細な機能や仕様について記載されています。まとめ記事は一旦ここまでとして、以降は気が向いたら書く方針で行こうと思います。

Kotlinイン・アクション の感想

本書はプログラミング言語の本によくある、簡単な言語仕様(演算子など)から順番に書かれているような解説本と違い、読者の気になるところを先取りしてうまく興味を惹くような構成になっており、読み物のように飽きずに読み進めることができました。

内容はかなりJava開発者目線なものになっていますが、私のように普段Javaをゴリゴリ書かない人でも読むことできるように思います。読んでいると実際にコードを書いて試したいと思わせる良書です。

『Kotlinイン・アクション』第6章を読む (その1)

第6章は「Kotlinの型システム」です。 本記事は型システムの中で非常に重要とされるnull許容性に関する内容です。

null許容型と演算子

  • Kotlinは null許容型 と null非許容型 を明確に区別している
    • null許容型でなければnullの参照を保持できない
>>> val s: String? = null
>>> s.toUpperCase() // nullの可能性があるのでコンパイルエラー

安全呼び出し演算子 (?.)

  • 非nullの場合 -> そのまま呼び出す
  • nullの場合 -> nullを返す
// s1 と s2 は等価
val s1 = if (s != null) s.toUpperCase() else null
val s2 = s?.toUpperCase()  // 安全呼び出し

複数のnull許容型のプロパティをつなげて呼ぶときに便利

val country: String? = this.company?.address?.country

エルビス演算子 (?:)

// 安全呼び出しの結果がnullなら 0 を返す
fun strLenSafe(s: String): Int = s?.length ?: 0

安全キャスト (as?)

  • キャストできる場合 -> キャストする
  • キャストできない場合 -> nullを返す
override fun equals(o: Any?) : Boolean {
    val otherPerson = o as? Person ?: return false
    ...

非null表明 (!!)

  • 非nullの場合 -> null非許容型に変換する
  • nullの場合 -> 例外(NullPointerException)を投げる
fun ignoreNulls(s: String?) {
    val sNotNull: String = s!!
    println(sNotNull.length)
}

null許容型の扱い方

安全呼び出し + let関数

  • let関数
    • 呼び出されたオブジェクトをラムダの引数へ変換する
    • 安全呼び出しと組み合わせることで、null非許容型の引数をとる関数にnull許容型を渡す場合に役立つ
fun sendEmailTo(email: String) { ... }
val email: String? = "hoge@example.com"

// パターン1: nullチェックしてnull非許容型として扱う
if (email != null) sendEmailTo(email)
// パターン2: letの中でnull非許容型として扱う
email?.let { sendEmailTo(it) }

遅延初期化プロパティ

  • lateinit をつけることでプロパティをコンストラクタで初期化しなくてよくなる
    • すぐに初期化できないプロパティもnull非許容型で宣言できる
  • 遅延初期化プロパティはvarになる
  • 初期化前に参照すると例外が発生する

null許容型の拡張関数

  • null許容型の拡張関数を定義すると安全呼び出しせずに関数を呼べる
    • 空文字チェックとnullチェックを同じ関数で実行したい場合など
    • 通常はnull非許容型の関数として定義するのがよい

型パラメータのnull許容性

型パラメータはnull許容型になる

fun <T> printHash(t: T) {
    println(t?.hashCode())  // null許容型なので安全呼び出しが必要
} 

null非許容にする場合は明示的に指定する

fun <T: Any> printHash(t: T) {
    println(t.hashCode())  // 安全呼び出し不要
}

Javaとの相互運用におけるnull許容型

  • Javaアノテーションがnull許容性の決定に使われる
  • アノテーションがない場合はプラットフォーム型になる
    • プラットフォーム型はnull許容と非許容どちらとしても扱える
    • コンパイラはnull許容性の情報を持っていないため実装者が責任を持つ
Java Kotlin
@Nullable + Type Type? (null許容型)
@NotNull + Type Type (null非許容型)
Type Type or Type? (プラットフォーム型)
  • KotlinでJavaAPIを扱う場合はnullを返すかどうか調べてチェックを追加する必要がある

Kotlinではnull許容型を安全で簡潔に呼び出す手段が豊富に提供されていることが分かりました。

特に、NullPointerExceptionを無くすことを目指しつつできるだけ開発者に冗長なnullチェックをさせないための配慮が随所に感じられました。

後半はプリミティブ型の扱いについてです。

『Kotlinイン・アクション』第5章を読む (その2)

第5章 ラムダを使ったプログラミングの続きです。

『Kotlinイン・アクション』第5章を読む (その1) - おはようエンジニア

遅延評価

  • 先行評価
    • コレクション操作 (前回記事で登場した map や filter など)
    • 操作をチェインしたときの中間結果は一時的なリストに保持される
  • 遅延評価
    • シーケンス の操作 (APIはコレクションと同じ)
    • 一時オブジェクトを生成しないで計算できる
    • 要素が多いコレクションをチェインする場合、計算効率を高めるためにシーケンスに変換して処理する
    • 要素を順次処理するだけの場合に使えるが、要素の参照などはできない
people.asSequence()  // シーケンスに変換
        .map(Person:name)
        .filter { it.startWith("A") }
        .toList()  // リストに変換

シーケンスの操作

  • 中間操作と終端操作に分かれている
    • 中間操作: シーケンスを変換する (遅延実行される)
      • 例: map, filter など
    • 終端操作: 遅延されていた中間操作をまとめて実行して結果を返す
      • 例: find, any, sum など

処理の順序

  • 先行評価は全要素の操作が完了してから次の操作に移る
    • 操作の順番がパフォーマンスに影響する
  • 遅延評価は要素を取り出して全操作を完了してから次の要素に移る
    • 要素の順番がパフォーマンスに影響する
    • find の呼び出しなど、特定の述部を満たす要素が見つかり次第処理を終了できる

シーケンスの生成

  • generateSequence関数でシーケンスを生成する
    • 1つ前の要素から次の要素を計算する
    • 階層構造を順にたどって処理していくときなどに便利
fun File.isInsideHiddenDirectory() = 
        generateSequence(this) { it.parentFile }.any { it.isHidden }

SAMインターフェイス

// Java
button.setOnClickListener (new OnClickListener() {
    @Override
    public void onClick(View v) {
    ...
    }
}
// Kotlin
button.setOnClickListener { view -> ... }
  • ラムダの代わりに明示的に匿名オブジェクトを渡すこともできる

SAM変換

// SAMコンストラクタでインスタンスを取得する
val listener = OnClickListener { view -> 
    ...
}
// 再利用する
button1.setOnClickListener(listener)

レシーバ付きラムダ

with関数

  • 第1引数に変数、第2引数にラムダをとる関数
    • ラムダの中で第1引数をレシーバとして使える (this参照できる)
    • 返り値はラムダの最後の式の結果
  • 用途
    • あるインスタンスが持ついくつかの異なるメソッドを呼び出すような場合
fun alphabet(): String {
    val stringBuilder = StringBuilder()
    return with(stringBuilder) {
        // ラムダ内では this が stringBuilder になる
        for (letter in 'A'..'Z') {
            append(letter)
        }
        toString()
    }
}

apply関数

  • with関数とほぼ同じ
    • 返り値はレシーバオブジェクト
    • 拡張関数として定義されている
  • 用途
    • インスタンスを生成してからすぐに初期化処理をするような場合 (Builderなど)
    • 関数を式本体スタイルにする場合に便利
fun alphabet() = StringBuilder().apply {
    // ラムダ内では this が stringBuilder になる
    for (letter in 'A'..'Z') {
        append(letter)
    }
}.toString()

まとめ

この章ではラムダを使ってコードを簡潔にする例がたくさん紹介されていました。

特にラムダの用途の代表であるコレクションの操作は実際にコードで頻出するので、使うときには以下の2点に気をつけようと思います。

  • 不要な一時変数を宣言していないか
  • 式本体スタイルに書き直すことができるか
  • 処理の中で同じインスタンス名が何度も登場していないか

ラムダをうまく使ってKotlinらしい書き方を身に着けていきたいです。

『Kotlinイン・アクション』第5章を読む (その1)

Kotlinイン・アクション第5章は「ラムダ式を使ったプログラミング」です。

ラムダ式

ラムダ式とメンバ参照

  • ラムダ式(またはラムダ)とは
    • ほかの関数に渡すことできるコードの断片
  • ラムダ式を使うとどう嬉しいか
    • 関数に「振る舞い」を直接渡すことができる
    • そのために匿名の内部クラスを作ったり、名前付きの関数を宣言する必要がなくなる
  • Kotlinのコレクションの多くはラムダ式を渡して簡潔に利用できるように設計されている

ラムダ式の構文

// ラムダ式を変数に格納して使う
>>> val sum = { x: Int, y: Int -> x + y }
>>> println(sum(1,2))
3
// maxByにラムダ式を渡す
people.maxBy({ p: Person -> p.age })
// ↓↓ 関数の最後の引数がラムダなら {} を外に出せる
people.maxBy() { p: Person -> p.age }
// ↓↓ ラムダが関数唯一の引数なら () を省略できる
people.maxBy { p: Person -> p.age }
// ↓↓ 型推論可能な場合は型を省略できる
people.maxBy { p -> p.age }
// ↓↓ ラムダの引数がただ1つなら引数名を it で置き換えられる
people.maxBy { it.age }
  • 注意点
    • 意味を理解しにくくなる場合は引数や型を明示的に書く
    • 関数の引数が複数ある関数の場合は分かりにくくなるので通常の構文を使ったほうが良い
    • ネストされたラムダの場合は it は使わず明示的に引数を定義したほうが良い

スコープ内の変数アクセス

  • ラムダでは関数のローカル変数や引数にアクセスできる(変更も可)
    • ラムダによるキャプチャと呼ぶ
    • キャプチャされた変数の参照はラムダとともに保持される

メンバ参照

  • メンバ参照によってすでに定義済みの関数を値に変換できる
// ラムダ式による表現
val getAge = { person: Person -> person.age }
// メンバ参照による表現 (より簡潔)
val getAge = Person::age
fun salute() = println("Salute!")
>>> run(::salute) // クラス名を省略する
data class Person(val name: String, val age: Int)

>>> val createPerson = ::Person  // コンストラクタを保持
>>> val p = createPerson("Alice", 29)  // 遅延実行できる

コレクション操作のための関数型API

filter

ラムダが true を返した要素のみ抽出する

>>> val list = listOf(1, 2, 3, 4)
>>> println(list.filter { it % 2 == 0 })
[2, 4]

map

各要素にラムダを適用して新しいコレクションに変換する

>>> val list = listOf(1, 2, 3, 4)
>>> println(list.map { it * it })
[1, 4, 9, 16]
  • メンバ参照を渡すこともできる
  • マップに対してはキーと値を扱う mapKeys と mapValues が用意されている (filterも同様)

all

全ての要素が述部を満たすかどうかを返す

>>> val canBeInClub27 = { p: Person -> p.age <= 27 }  // 述部
>>> val people = listOf(Person("Alice", 27), ("Bob", 31))
>>> println(people.all(canBeInClub27))
false
  • !all (not all) は後述する any で置き換えたほうが見やすい
    • 例えば「全てが3に等しいわけではない」は「少なくとも一つの要素が3ではない」のように、逆条件の any で考える

any

少なくとも一つの要素が述部を満たすかどうかを返す

>>> println(people.any(canBeInClub27))
true

count

述部を満たす要素の数を返す

>>> println(people.count(canBeInClub27))
1

find

述部を満たす要素を返す

>>> println(people.find(canBeInClub27))
Person(name=Alice, age=27)
  • 一つもなければ null を返す
    • firstOrNull は find と同意で、考えを明確に表現したい場合に使う

groupBy

  • 全ての要素をある特性にしたがって分類したマップを返す
    • キーは述部の結果、値は該当する要素
>>> val people = listOf(Person("Alice", 27), ("Bob", 31), ("Carol", 31))
>>> println(people.groupBy{ it.age })
{27=[Person(name=Alice, age=27)],
31=[Person(name=Bob, age=31), Person(name=Carol, age=31)]}

flatMap

各要素を map した結果を一つのリストに結合する

>>> val strings = listOf("abc", "def")
>>> println(strings.flatMap { it.toList() })
[a, b, c, d, e, f]

flatten

ネストしたリスト内の要素を一つのリストに結合する

>>> val deepArray = listOf(listOf(1), listOf(2, 3), listOf(4, 5, 6))
>>> println(deepArray.flatMap{ it.toList() })
[1, 2, 3, 4, 5, 6]

ラムダを使ったいろいろな種類のコレクション操作が登場しました。

ここに出てきた処理はコードを書くときに頻繁に出てくるので積極的に使っていこうと思います。

『Kotlinイン・アクション』第4章を読む (その2)

Kotlinイン・アクション第4章「クラス、オブジェクト、インターフェイス」の続きです。

いろいろなコンストラク

// プライマリコンストラクタを持つクラス
class User(val name: String)

// デフォルトコンストラクタを持つクラスとそれを継承するクラス
open class OpenUser
class TwitterUser: User() // 明示的にコンストラクタの呼び出しが必要

// privateなコンストラクタを持つクラス
class SecretUse private constructor() { }

// セカンダリコンストラクタを持つクラス (※1)
open class SecondUser {
    constructor(age: Int) { }
}

インターフェイスのプロパティ

  • インターフェイスでプロパティを宣言できる
    • その場合、値を取得する手段を提供する必要がある
interface User {
    val nickname: String
}

// 手段1: プロパティを直接宣言
class PrivateUser(override val nickname: String) : User

// 手段2: カスタムgetterを宣言
class SubscribingUser(val email: String) : User {
    override val nickname: String
        get() = email.substringBefore('@')
}

// 手段3: プロパティの初期化処理
class FacebookUser(val accountId: Int) : User {
    override val nickname = getFacebookName(accountId)
}

カスタムアクセサの実装

  • setterの実装
    • setter内では field という特別な識別子でバッキングフィールドの値にアクセスする
class User(val name: String) {
    val address: String = "unspecified"
        set(value: String) {
            /* チェックやログ出力などの追加処理 */
            field = value
        }
}
  • 可視性の変更
    • get/set前に可視性修飾子をつける
val counter: Int = 0
    private set

データクラス

  • データクラスの宣言
    • プロパティに応じて toString, equals, hashCode が適切にオーバーライドされている
  • データクラスの不変性
    • データクラスはプロパティを全てvalで宣言してimmutableにすることを強く推奨
    • 変更する場合はコピー元に影響を及ぼさないcopyメソッドが便利
data class Client(val name: String, val postalCode: Int)

等価性チェック

Kotlinではequalsを正しく実装している場合 == を使えば良い

演算子 Kotlin Java
== 構造等価性を比較(equals) 参照等価性を比較
=== 参照等価性を比較 厳密等価性を比較

クラス委譲

  • 委譲とは

    • あるクラスを拡張する方法の一つ
      • 他の方法に「継承」がある
    • 委譲はデコレーターパターンによる実装が一般的
      • 元のクラスをフィールドとして保持し、振る舞いの変更がないメソッドをもとのクラスのメソッドへ転送するパターン
    • デメリットはボイラープレートコードが増えること
  • Kotlinでの実装

    • byキーワードを使うと振る舞いの変更がないメソッドはコンパイラが自動生成する
    • 変更がある場合のみオーバーライドする
class DelegatingCollection<T>(
        innerList: Collection<T> = ArrayList<T>()
) : Collection<T> by innerList {} 

objectの使い方

1. オブジェクト宣言

  • オブジェクト宣言とは
  • 使いどころ
    • シングルトンパターンの実装
object Payroll { ... }

>>> Payroll.calculateSalary()
  • コンストラクタを宣言できないこと以外はクラスと同じ書き方
  • クラス内オブジェクト宣言も可能
    • 特定のクラス専用のオブジェクトで使うと良い
    • 例えばPersonクラスの比較のためのNameComparatorPersonクラス内オブジェクトとする

2. コンパニオンオブジェクト

  • コンパニオンオブジェクトとは
    • クラスのprivateメンバにアクセスできるオブジェクト
    • クラス内オブジェクト宣言の特殊な例
  • 使いどころ
    • ファクトリパターンの実装など
class A {
    companion object { // 名前を省略できる
        fun bar() { /* ... */ }
    }
}
>>> A.bar() // オブジェクト名を指定せず静的メソッドのように呼び出せる
  • Javaのstaticメソッドの代替
    • Kotlinはstaticメソッドがないが、ほとんどの場合トップレベル関数で代替できる
    • ただし、トップレベル関数ではprivateメンバにアクセスできないのでそういう場合はコンパニオンオブジェクトを使う
  • インターフェイスを実装可能
    • 様々なコンパニオンオブジェクトに共通のファクトリメソッドを実装する場合など
  • 拡張関数も宣言可能
    • コンパニオンオブジェクトに拡張関数を宣言することでクラスから呼び出すメソッドを拡張できる

3. オブジェクト式

  • オブジェクト式とは
    • クラス内部で使うための無名オブジェクトを宣言する式
  • 使い所
    • イベントリスナ内の実装
window.addMouseListener(
    object : MouseAdapter() {
        override fun mouseClicked(e: MouseEvent) { /* ... */ }
        ...
    }
)
  • 無名オブジェクトはシングルトンではないのでオブジェクト宣言時に生成される
  • オブジェクト式で生成したオブジェクトは変数に格納できる

おわりに

クラスやオブジェクトについては用意されている仕組みが多く、実装時には柔軟な選択ができそうです。 その一方で、それらを使いこなすには構文の理解だけでなく、良い設計のパターンをある程度知っていなければいけないと感じました。

『Kotlinイン・アクション』第4章を読む (その1)

Kotlinイン・アクションの第4章は「クラス、オブジェクト、インターフェイス」です。 それではいつものようにかいつまんで書いていきます。

インターフェイス

// インターフェイスの宣言
interface Clickable {
    fun showOff() = println("I'm clickable!")
}
// インターフェイスの実装
class Button : Clickable {
    // override修飾子必須
    override fun showOff() = println("I'm clickable button!")
}

アクセス修飾子

クラスの種類 オーバーライド インスタンス
class (default) 不可
open class
abstract class 不可
メソッドの種類 オーバーライド 実装の有無
fun (default) 不可
open fun
abstract fun

可視性修飾子

  • パッケージプライベートが廃止され、internal追加
  • internalはモジュール内で参照可能
    • モジュールは一度にコンパイルされるファイルのまとまり
修飾子 クラスメンバの参照範囲 トップレベル宣言時の参照範囲
public (default) どこからでも どこからでも
internal モジュール内 モジュール内
protected サブクラス内 -
private クラス内 ファイル内

ネストされたクラスと内部クラス

クラスの種類 外部クラスへの参照 Javaでの宣言 Kotlinでの宣言
ネストされたクラス 保持しない static class A class A
内部クラス 保持する class A inner class A
// Kotlinの内部クラスから外部クラスへのアクセス
class Outer {
    inner class Inner {
        fun getOuterReference(): Outer = this@Outer
    }
}

シールドクラス

  • 型チェックは シールドクラス内に取りうるサブクラスをすべてネストして列挙する
sealed class Expr {
    class Num(val value: Int) : Expr()
    class Sum(val left: Expr, val right: Expr) : Expr()
}
// 型チェック時に全てのケースをカバーできるのでelse分岐を取り除ける
when (e) {
    is Expr.Num -> ...
    is Expr.Sum -> ...
}

今回の内容はクラスの宣言方法、修飾子が中心でした。

修飾子についてはデフォルトで好ましい書き方となるようによく配慮されているように感じました。

次回はコンストラクタから見ていきます。

『Kotlinイン・アクション』第3章を読む (その2)

Kotlinイン・アクション第3章「関数の定義と呼び出し」の続きです。

拡張関数

既存のクラスに対して外側から関数を追加する

// StringクラスにlastChar()関数を追加
// String がレシーバ型, this がレシーバオブジェクト
fun String.lastChar(): Char = this.get(this.length - 1)

// レシーバオブジェクトのメンバの呼び出しはthis省略可
fun String.lastChar(): Char = get(length - 1)
  • 拡張関数のオーバーライド
    • 拡張関数は静的メソッドとして扱われるのでオーバーライドはできない

拡張プロパティ

// StringクラスにlastCharプロパティを追加
val String.lastChar: Char
    get() = get(length - 1)

可変長引数

可変長引数は vararg 修飾子を使う

fun listOf<T>(vararg values: T): List<T> { ... }

中置呼び出し

infix 修飾子で中置呼び出しのメソッドを宣言できる

infix fun Boolean.nand(other: Boolean) = !(this and other)

println(true nand false) // -> true

ローカル関数

  • 関数内に関数を入れ子で定義できる(ローカル関数)
  • ローカル関数は外側の関数の引数や変数にアクセスできる

ローカル関数と拡張関数を使ったコードの整理

以下のように整理していくと、クラスのコードベースを抑えつつ重複を排除できる (ただし2段階以上の入れ子は読みにくくなることが多い)

  1. 関数の中から重複しているコードを見つける
  2. 重複部分をローカル関数として抽出する
  3. ロジックを拡張関数に抽出する

文字列と正規表現

splitで分割

// 必要な数だけ区切り文字を渡す
println("12.345-6.A".split(".", "-")) // -> [12, 345, 6, A]

substringに分割

// ":"より前を抽出 -> http
println("http://example.com".substringBefore(":"))
// 最後の"/"より後を抽出 -> example.com
println("http://example.com".substringAfterLast("/"))

トリプルクオート文字列

  • トリプルクオート内はエスケープ不要
  • インデントを無視するには接頭辞をつけて trimMargin を呼び出すと便利
val str = """
    |1: hoge
    |2: fuga
""".trimMargin("|")

正規表現でURLの簡易チェック

// トリプルクオート文字列でパターンを記述してRegex型に変換
val regex = """(.+)://(.+)/(.+)""".toRegex()
val matchResult = regex.matchEntire("http://example.com/hoge")
if (matchResult != null) {
    // 分割宣言で中身を取り出す
    val (scheme, host, path) = matchResult.destructured
    print("scheme: $scheme, host: $host, path: $path")
}

第3章はここまでです。