131
108

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

はじめよう、Kotlin(上級編)

Last updated at Posted at 2017-07-26

導入編
基本編
上級編 ★イマココ

はじめに

Kotlin基本編で触れたものでも
「お、やるじゃん」と思った方もいらっしゃると思いますが
ここではKotlinの真髄に触れていきます

プログラマー初心者の方は「?」となるかもしれませんが
中規模以上のプロジェクトになれば便利になるコーディング方法かと思います

Scope Functions

スコープ関数はインスタンスに続けてラムダと一緒に使うことで、
そのインスタンスに対する処理をラムダにまとめて記載することができます
一覧に載せておきます

スコープ関数 レシーバの表現(※1) 戻り値
let it (T) Any (R)
run this (T) Any (R)
also it (T) receiver (T)
apply this (T) receiver (T)
with this (T) Any (R)
takeIf (※2) this (T) Boolean
takeUnless (※2) this (T) Boolean

※1: レシーバとは、スコープ関数をコールするときに使うインスタンスを指します
※2: 最後の2つは特殊なケースで、異常ケースを書くときに役立つ

これ、かなり使えます
この味をしめると、Javaに戻れません、まじで
なにより レビューやデバッグが早くなり非常に助かります

個人的に思う利点は以下4点

  • インスタンスへの操作をラムダ内にまとめることができる
  • 影響範囲をラムダ内におさめることができる
  • 無駄なインスタンス生成をなくせる
  • これらにより、長いコードが読みやすくなる

例えばこれが

val data = Data()
data.initialize()
data.firstName = "Hiroki"
data.lastName = "Honda"
data.age = "29"
data.language = "Japanese"

dao.insert(data)

こうなります

dao.insert(Data().apply {
    initialize()
    firstName = "Hiroki"
    lastName = "Honda"
    age = "29"
    language = "Japanese"
})

let

Nullableな変数の後ろにつける形をよく見ます

  • ラムダ内ではitでレシーバを表現する
  • ラムダ内の戻り値は何でもいいです
val upperCase: String? = foo?.let { it.toUpperCase() }

これでいうと、Unitが返っています(戻り値なし)
仮に戻り値を指定したい場合は明示的に最終行に指定するか、
あるいは戻り値のもつfunctionをコールします

run

  • ラムダ内ではthisでレシーバを表現する(省略可)
  • ラムダ内の戻り値は何でもいいです
val frame: JFrame? = frameOrNull()
frame?.run {
    size = Dimension(600, 400)
    defaultCloseOperation = JFrame.EXIT_ON_CLOSE
    setVisible(true)
}

これでいうと、Unitが返っています(戻り値なし)
仮に戻り値を指定したい場合は明示的に最終行に指定するか、
あるいは戻り値のもつfunctionをコールします

also

  • ラムダ内ではitでレシーバを表現する
  • ラムダ内の戻り値はレシーバ
val button = Button(this).also {
    it.text = "Click me"
    it.setOnClickListener { 
        startActivity(Intent(this, NextActivity::class.java))
    }
}

これでいうと、buttonが勝手に返ります

apply

  • ラムダ内ではthisでレシーバを表現する(省略可)
  • ラムダ内の戻り値はレシーバ
val button = Button(this).apply {
    text = "Click me"
    setOnClickListener { 
        startActivity(Intent(this, NextActivity::class.java))
    }
}

これでいうと、buttonが勝手に返ります

with

  • ラムダ内ではthisでレシーバを表現する(省略可)
  • ラムダ内の戻り値は何でもいいです

あまり利用用途が思い浮かばない子
applyの劣化版といったイメージです

applyと異なり、戻り値に指定がありません
利用シーンは難しいですが、あるインスタンスに対しての処理をまとめたいときは便利かも

でも、使ったことない

val frame: JFrame = with(JFrame("My App")) {
    size = Dimension(600, 400)
    defaultCloseOperation = JFrame.EXIT_ON_CLOSE
    setVisible(true)
    this
}

takeIf

  • ラムダ内ではthisでレシーバを表現する(省略可)
  • ラムダ内の戻り値はBoolean
  • 戻り値がfalseでnullを返す

エルビス演算子と組み合わせて使うと、
異常ケースをまとめて書けるので便利です

val params = request.takeIf{ isValidParameter(it) }.params ?: throw RuntimeException()

追記:nullチェックと一緒のとき、超便利にかけました

val text = ""

text?.let {
    if (it.isNotEmpty() {
        // ...
    }
}

これがこう書ける

val text = ""

text?.takeIf { it.isNotEmpty() }?.let {
    // ...
}

takeUnless

  • ラムダ内ではthisでレシーバを表現する(省略可)
  • ラムダ内の戻り値はBoolean
  • 戻り値がtrueでnullを返す

takeIfの逆パターン

TIPS

itには名前をつけることができます
可読性悪いなぁというときに使ってみましょう

val button = Button(this).also {
    it.text = "Click me"
    it.setOnClickListener { 
        startActivity(Intent(this, NextActivity::class.java))
    }
}

// or

val button = Button(this).also { button ->
    button.text = "Click me"
    button.setOnClickListener { 
        startActivity(Intent(this, NextActivity::class.java))
    }
}

// or

val button = Button(this).also { button: Button ->
    button.text = "Click me"
    button.setOnClickListener { 
        startActivity(Intent(this, NextActivity::class.java))
    }
}

Extension Function

Kotlinではクラスに外部拡張functionの追加が可能です

class C

data class D(val param: Int): C()

fun C.foo() = "extended C.foo()"

fun D.foo() = "extended D.foo(), param=$param"
extended C.foo()
extended D.foo(), param=4

もちろん既存クラスであるIntなどにも拡張可能です

Example 1

利用シーンとしては、例えばDateクラスに対して火曜日かどうか判定するUtilityクラスを作ったとします
これがひとつだけであれば問題ありませんが、肥大化するにつれてやはり見づらくなる欠点があります

static boolean isTuesday(Date date) {
    return date.getDay() == 2;
}

呼び出す側としても、Utilityクラスを書くことで少しコードも長くなります

boolean tuesday = DateUtils.isTuesday(date);

また、このケースの場合は完全に引数のDateに依存するメソッドですので
できることならDateクラスに実装したいのに、と思うと思います

そんなときにExtensionを使いましょう

fun Date.isTuesday(): Boolean = getDay() == 2

するとこのように、Dateインスタンスに対するコールのため、非常に読みやすくなります

val tuesday = date.isTuesday()

Example 2

例えばCursorから特定のColumnを特定し、値を取得したい場合、
Javaだとこのように長々と書く必要があります

String firstName = cursor.getString(
        cursor.getColumnIndexOrThrow("first_name"));
println(firstName != null ? firstName : "Unknown");

しかしKotlinのExtensionを使えばこのように完結に書くための
UtilityメソッドをCursorクラスに拡張して作成することが可能です

fun Cursor.getStringOrNull(columnName: String): String? {
    val index = getColumnIndexOrThrow(columnName)
    return if (isNull(index)) null else getString(index)
}

fun Cursor.getString(columnName: String): String =
    getStringOrNull(columnName)!!
val firstName = cursor.getStringOrNull("first_name")
println(firstName ?: "Unknown");

High-Order Functions

高階関数というのが使えます
「は?」と思った方、ご安心ください、私も初めはそう思いました

([引数]) -> [戻り値] 

こう書くと、 functionなんかを擬似的に渡すことが可能になります

とっつきにくいと思うので、
例として、高階関数を引数にとるExtensionを紹介します

Example 1

fun <T> List<T>.filter(predicate: (T) -> Boolean): List<T> {
    // ...
}

この書き方を解説すると

<T> List<T> ... 冒頭の<T>はジェネリック使いますよ!その中身はTですよ!の定義
List<T>.filter(...): List<T> ... List<>.filter 拡張関数を作る、戻り値はもちろん`List<> (predicate: (T) -> Boolean)...(T)はひとつの処理、ラムダ{} を指し、その引数がTであることを指します。そして、その戻り値を->`で指定します。
つまりこの場合は 「引数が T で戻り値がBooleanのラムダ{}」を引数としてとります
ここを高階関数というそうです

中身をつめると

fun <T> List<T>.filter(predicate: (T) -> Boolean): List<T> {
    val newList = ArrayList<T>()
    for (item in this) {
        if (predicate(item)) {
            newList.add(item)
            // ...
        }
    }
    return newList
}

利用する場合はこんな感じ

val names = listOf("Jake", "Jesse", "Matt", "Alec")
val jakes = names.filter { it == "Jake" }

filter内のitは、namesリストの中身、つまりこのケースだとString型の変数を指します

Example 2

例えばある特定のオブジェクトに対して同期処理を行いたい場合、
Javaではsynchronizedを使っていたと思います

それがメンテナンスしやすい形でこのようにかけます

data class Lock<T>(private val obj: T) {
    public fun acquire(func: (T) -> Unit) {
        synchronized (obj) {
            func(obj)
        }
    }
}
val readerLock = Lock(JsonReader(stream))

// Later
readerLock.acquire {
    println(it.readString())
}

メソッドコールの段階で処理を書けばよくなるので、
いちいちsynchronizedを書く必要もなくなり、コードもスッキリするのではないでしょうか

Example 3

今度はトレーニングも兼ねて、validateWith()がどのような高階関数を引数にもつ拡張関数になるか考えてみましょう

あるベージがあり、クレジットカードの情報を入力するページがあったとして、
その入力完了時に、以下のような情報の入力情報の妥当性チェックを行いたいとします

val notEmpty: (String) -> Boolean = { !it.isEmpty() }
val atLeastFour: (String) -> Boolean = { it.length() > 4 }
val fourDigits: (String) -> Boolean = { it.matches("\\d{4}")}
val validCreditCard: (String) -> Boolean = { luhnCheck(it) }

firstName.validateWith(notEmpty)
lastName.validateWith(notEmpty)
userName.validateWith(atLeastFour)
pin.validateWith(fourDigits)
creditCard.validateWith(validCreditCard)

無駄なく書こうとすれば、こうでしょうか

val notEmpty: (String) -> Boolean = { !it.isEmpty() }

firstName.validateWith(notEmpty)
lastName.validateWith(notEmpty)
userName.validateWith { it.length() > 4 }
pin.validateWith { it.matches("\\d{4}")}
creditCard.validateWith { luhnCheck(it) }

すると validateWith はどうなるでしょうか...!?

:
:
:
:
:
:
:
:
:

では正解です

// Stringの場合
fun String.validateWith(body:(String) -> Boolean): Boolean {
    return body(this)
}

// EditTextの場合
fun EditText.validateWith(body:(String) -> Boolean): Boolean {
    return body(this.text.toString())
}

ここでのthisは、それぞれの拡張もとクラスインスタンスを指す、ということに注意してください

Inline function

インライン関数は、functionのように見えますが、
コンパイル時にその箇所に直接コードが挿入されます

実行時の動きは変わらないように見えますが、
内部動作としては関数コール分が省けるのでパフォーマンスがあがるそうです

イメージ

fun testPrint() {
    println("testPrint!")
}

println("pre call testPrint")
testPrint()
println("did call testPrint")

こう書くと

 1. call println("pre call testPrint")
 2. call testPrint()
 3. call println("testPrint!")
 4. call println("did call testPrint")

こんな感じですが、inlineを使うと、こうなると

inline fun testPrint() {
    println("testPrint!")
}

println("pre call testPrint")
testPrint()
println("did call testPrint")
 1. call println("pre call testPrint")
 2. call println("testPrint!")
 3. call println("did call testPrint")

このケースだと恩恵をあまり感じなさそうですが、
たとえばこのtestPrint()をforループ内で読んでいるとどうでしょうか?

とはいえ、通常の開発ではあまりフィーチャーされることはなさそうです。
自分のような下っ端には必要なさそうです。はい。

Example 1

たとえば、エルビス演算子 ?: の後ろに書くものを想像します
実はこの後ろには1行しか処理が書けません

sth?.let {
    // NonNull case
} ?: { // DOES NOT WORK!!
    // Null case
}

しかしインライン関数を使うと複数行にわたって書くことが可能です
例えばnullだったときの処理をまとめて書きたいときに便利です

inline fun guard(body: () -> Unit) = body()
val sth = intent.getStringExtra("SOMETHING")

sth?.let {
    // NonNull case
} ?: guard {
    // Null case
}

コメント欄にご指摘いただきましたが、
このレベルであれば run を使うことも可能です
(runも実はinline関数です)

というかinline関数である必要も実はありません
つまり、この例は全然うまくない!笑

sth?.let {
    // NonNull case
} ?: run {
    // Null case
}

Example 2

AndroidのNotificationを考えてみましょう
通知するのにBuilderを取得して設定して...面倒ですよね
そんなときはサクッとインライン関数をかいちゃうと便利です

inline fun notification(context: Context,
        func: Notification.Builder.() -> Unit): Notification {
    val builder = Notification.Builder(context)
    builder.func()
    return builder.build()
}
val n = notification(context)
    setContentTitle("Hi")
    setSubText("Hello")
}

おまけ

お気づきの方もいらっしゃるかと思いますが、
apply, filter系の便利functionはすべてインライン関数やExtensionを使っています

なのでもちろん私たちも、プロジェクトによってよく使われる処理を
インライン関数やExtensionにすることによって、
よりメンテナンス性の高いコードを書くことが可能になります

わくわくしてきません?

参考

http://qiita.com/ngsw_taro/items/d29e3080d9fc8a38691e
https://www.youtube.com/watch?v=A2LukgT2mKc
http://hydrakecat.hatenablog.jp/entry/2015/12/08/Kotlin_Inline_Functions

131
108
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
131
108

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?