Webエンジニアのメモ帳

技術的な話を中心に書いています。

【Kotlin Koans 解説】ジェネリック (Generics)

Kotlin KoansでKotlinを勉強したメモをまとめています。

問題

リンクは以下。

https://play.kotlinlang.org/koans/Generics/Generic%20functions/Task.kt

少し簡略化して説明すると、以下のコードが動くように、リストに入った要素を条件によって2つに分けるpartitionTo()メソッドを実装するというものです。

val (words, lines) = listOf("a", "a b", "c", "d e")
  .partitionTo(ArrayList(), ArrayList()) { s -> !s.contains(" ") }
check(words == listOf("a", "c"))
check(lines == listOf("a b", "d e"))

解答と解説

まず、解答例は以下です。

fun <T, C : MutableCollection<T>> Collection<T>.partitionTo(first: C, second: C, predicate: (T) -> Boolean): Pair<C, C> {
    for (element in this) {
        if (predicate(element)) {
            first.add(element)
        } else {
            second.add(element)
        }
    }
    return Pair(first, second)
}

このコードはジェネリックを学習するのが目的ですが、Kotlinの初心者からすると何点かポイントがあるので解説します。

1. 拡張関数について

Kotlin Koansにも登場しますが、Kotlinには拡張関数という機能があり、別のクラスにメソッドを追加することができます。

この解答例では、CollectionクラスにpartitonToメソッドを追加しています。

2. ジェネリックについて

partitionTo()は3つの引数を受け取ります。

始めの2つはArrayListなどの配列型で、その要素の型は何でもOKです。

3つめの要素は関数ですが、その引数は↑の要素の方と同じである必要があります。

つまりpartitionTo()の定義の中で、型が一位に定まらないものが2つあります。そのため、以下のように記述する必要があります。

<T, C>

さらに、TはpartitionTo()の第1・2引数で使われ、Cはその要素、および第3引数の関数の引数です。

TがCのコレクション型であることを明記するため

<T, C : MutableCollection<T>>

という記述をしています。

3. 引数の関数を()の外に出す

partitionToを実行するコードで

partitionTo(ArrayList(), ArrayList()) { s -> !s.contains(" ") }

のように書いていましたが、これは

partitionTo(ArrayList(), ArrayList(), { s -> !s.contains(" ") } )

のように書くのと同じです。

これは、引数の最後が関数の場合にのみできる書き方です。

ちなみに、関数を引数に渡す書き方はとっつきにくいですが、自分は以下のサイトが分かりやすいなと思いました。 https://pouhon.net/kotlin-higher-order-function/1342/

【Room・SQLite】数字の0埋めでFORMATが使えない場合の対処法

SQLiteのFORMAT関数

SQLiteでは、以下のようにFORMAT関数で0埋めを行うことができます。

SELECT FORMAT(1,'D3')
# 001と出力される

FORMATはRoomでは使えない

しかし、Androidの開発で使われるRoomでは、FORMATを使うことはできません。

使おうとすると、以下のエラーが表示されます。

There is a problem with the query: [SQLITE_ERROR] SQL error or missing database (no such function: FORMAT)

その原因は、Roomの最新バージョンで使われているSQLiteのバージョンが古いためです。(2023年10月時点の情報)

対処法

以下のように、SUBSTR関数で代用します。

SELECT SUBSTR('00' || :num , -3 ,3)

SUBSTR関数は文字列の一部を切り出す関数で、各引数は以下の通りです。

  • 第1引数 対象の文字列
  • 第2引数 何文字目から切り出すか (今回だと後ろから3文字目)
  • 第3引数 何文字切り出すか

つまり今回の例で言うと、numが10だとしたら、一度0010にしてから、後ろの3文字を切り出します。

このようにして、変数numを0埋めで3桁にすることができます。

参考

stackoverflow.com

【Android】Roomの@Queryアノテーションで正規表現を利用する方法

例えば以下のようなSQLがあるとします。

SELECT * FROM users WHERE name LIKE "A*"

このSQLをRoomで動かす方法をいくつか説明します。

アノテーション内で||で文字列を結合する方法

@Queryアノテーション内では、以下のように||を使って文字列を結合させることができます。

@Query("SELECT * FROM users WHERE name LIKE :prefix || '%'")
fun getUsers(prefix: String?): List<User>

引数を単純化する方法

%や_などを@Queryアノテーションに含めない方法もあります。

つまり、Daoクラスには以下のように記述し

@Query("SELECT * FROM users WHERE name LIKE :prefix")
fun getUsers(prefix: String?): List<User>

以下のように呼び出します。

getUsers("A%")

参考

https://stackoverflow.com/questions/44184769/android-room-select-query-with-like

【Kotlin】カスタムゲッター・カスタムセッターについて

カスタムゲッター

例えば、以下のようなRectangleクラスがあるとします。

class Rectangle(val width: Int, val height: Int) {
  fun area(): Int = this.width * this.height
}

このクラスはarea()というメソッドを持っており、実行すると以下のようになります。

val rectangle = Rectangle(2,3)
// 6が表示される
println(rectangle.area())

このarea()を、メソッドではなくフィールドとして定義することができます。

class Rectangle(val width: Int, val height: Int) {
  val area: Int
    get() = this.width * this.height
}

実行するときは、area()というメソッドを呼ぶのではなく、areaを参照します。

val rectangle = Rectangle(2,3)
// 6が表示される
println(rectangle.area)

このような仕組みが、カスタムゲッターです。

カスタムセッター

カスタムセッターは、フィールドのセッターを変更できる仕組みです。

例えば以下のクラスがあるとします。nameフィールドの値を外部から変えると、値を変更するだけでなく、文章を出力します。

class Person() {
  var name: String = ""
    set (value) {
      field = value
      println("こんにちは、${value}さん!")
    }
}

これを実行すると以下のようになります。

val person = Person()
// 「こんにちは、山本さん!」と出力される
person.name = "山本"

参考

自分の理解のために記事にしてみましたが、既存の記事も分かりやすいです。特に以下は分かりやすかったです。

pouhon.net

【Kotlin】トリプルクォート文字列への変数の埋め込み

Kotlinでトリプルクォート文字列に変数を埋め込む方法は、通常の文字列に埋め込む方法と同じです。

つまり、文字列中に$変数名と書けばOKです。

たとえば、以下のコードを実行すると

fun main() {
    val name = "鈴木"
    val sentence = """My name is $name"""
    println(sentence)
}

以下のように出力されます。

My name is 鈴木

【Room】エラー対応 DELETE query methods must either return void or int (the number of deleted rows).

エラー発生の経緯

Roomを使ったAndroidアプリにおいて、Daoクラスの以下のメソッドをsuspend関数に変更しました。

@Dao
interface UserDao {
    @Query("DELETE FROM user WHERE id = :id")
    fun deleteById(id: Int)
}

すると、以下のようなエラーが発生しました。

DELETE query methods must either return void or int (the number of deleted rows).

単純にエラーを読むと、deleteByIdの返り値をvoidかintにすれば良いようです。

しかし、すでに返り値はvoidですし、intに変えてみても状況は変わりませんでした。

解決方法

調べたところ、このエラーはKotlinのバージョンが1.7系、Roomのバージョンが2.4系だと発生するようでした。

そこで、私の場合はRoomのバージョンを2.5.2に変えることで解決しました。(build.gradleを以下のように変更)

dependencies {
    implementation 'androidx.room:room-runtime:2.5.2'
    implementation 'androidx.room:room-ktx:2.5.2'
    kapt 'androidx.room:room-compiler:2.5.2'
}

ただし、Roomのバージョンを2.5.2に上げるのであれば、targetSdkの変更も必要になります。

これを避けたいのであれば、Kotlinのバージョンを1.6系に下げるのが良いと思います。

参考

issuetracker.google.com