読者です 読者をやめる 読者になる 読者になる

kotlin tips: 自己を参照される可能性のあるコールバックとか単一処理を複数のコールバックで扱うことについて

※とりあえず五月雨式にメモしておく。結論はまだない。

戻り値方式とクロージャ方式の例

下記の returnValue1.kt は、Model の関数が結果を戻り値で返しています。

//
// returnValue1.kt
//
package returnValue1

import returnValue1.AppendResult.Correct
import returnValue1.AppendResult.Wrong

interface Model {
    val isCompleted: Boolean
    fun append(c: Char): AppendResult
}

sealed class AppendResult {
    class Correct(val isComplete: Boolean) : AppendResult()
    class Wrong : AppendResult()
}

class ModelImpl : Model {
    override var isCompleted = false
        private set
    private val inputLetters = StringBuilder()
    
    override fun append(c: Char): AppendResult {
        return when (c) {
            in '0'..'9' -> {
                inputLetters.append(c)
                Correct((inputLetters.length == 2).apply { isCompleted = this })
            }
            else        -> {
                Wrong()
            }
        }
    }
}

object view {
    fun onCorrectLetter(model: ModelImpl) {
        println("『ピッ!』")
    }
    
    fun onWrongLetter(model: ModelImpl) {
        println("『ブブーッ!』")
    }
    
    fun onComplete(model: ModelImpl) {
        println("『ピロリロリン!』[isCompleted = ${model.isCompleted}]")
    }
}

// controller
fun main(args: Array<String>) {
    
    val model = ModelImpl()
    
    fun append(c: Char) {
        with(model.append(c)) {
            when (this) {
                is Correct -> {
                    view.onCorrectLetter(model)
                    if (isComplete) view.onComplete(model)
                }
                is Wrong   -> view.onWrongLetter(model)
            }
        }
    }
    
    append('1') // 数字なので正常受付。『ピッ!』と鳴る。
    append('a') // 数字じゃないのでエラーを表示。『ブブーッ!』っと鳴る。
    append('2') // 数字なので正常受付。『ピッ!』と鳴る。入力完了音が『ピロリロリン!』と鳴る。
}

実行結果

『ピッ!』
『ブブーッ!』
『ピッ!』
『ピロリロリン!』[isCompleted = true]

下記の closure1.kt は、Model 関数にクロージャが渡され、結果をそのクロージャ経由で通知しています。

結果毎の処理を Model 側でキックしてくれるので、controller 側に条件分岐が必要無くなっています。

//
// closure1.kt
//
package closure1

interface Model {
    val isCompleted: Boolean
    fun append(c: Char, onCorrectLetter: (model: ModelImpl) -> Unit, onWrongLetter: (model: ModelImpl) -> Unit, onComplete: (model: ModelImpl) -> Unit)
}

class ModelImpl : Model {
    override var isCompleted = false
        private set
    private val inputLetters = StringBuilder()
    
    override fun append(c: Char, onCorrectLetter: (model: ModelImpl) -> Unit, onWrongLetter: (model: ModelImpl) -> Unit, onComplete: (model: ModelImpl) -> Unit) {
        when (c) {
            in '0'..'9' -> {
                inputLetters.append(c)
                onCorrectLetter(this)
                if (inputLetters.length == 2) {
                    isCompleted = true
                    onComplete(this)
                }
            }
            else        -> {
                onWrongLetter(this)
            }
        }
    }
}

object view {
    fun onCorrectLetter(model: ModelImpl) {
        println("『ピッ!』")
    }
    
    fun onWrongLetter(model: ModelImpl) {
        println("『ブブーッ!』")
    }
    
    fun onComplete(model: ModelImpl) {
        println("『ピロリロリン!』[isCompleted = ${model.isCompleted}]")
    }
}

// controller
fun main(args: Array<String>) {
    
    val model = ModelImpl()
    
    model.append('1', view::onCorrectLetter, view::onWrongLetter, view::onComplete) // 数字なので正常受付。『ピッ!』と鳴る。
    model.append('a', view::onCorrectLetter, view::onWrongLetter, view::onComplete) // 数字じゃないのでエラーを表示。『ブブーッ!』っと鳴る。
    model.append('2', view::onCorrectLetter, view::onWrongLetter, view::onComplete) // 数字なので正常受付。『ピッ!』と鳴る。入力完了音が『ピロリロリン!』と鳴る。
}

実行結果

『ピッ!』
『ブブーッ!』
『ピッ!』
『ピロリロリン!』[isCompleted = true]

下記の closure2.kt は、Model にあらかじめクロージャが渡され、関数が結果をそのクロージャ経由で通知しています。

関数呼び出し毎に異なるクロージャを渡す必要がない場合はこちらのほうがさらにシンプルです。

//
// closure2.kt
//
package closure2

interface Model {
    val isCompleted: Boolean
    fun append(c: Char)
}

class ModelImpl(val onCorrectLetter: (model: ModelImpl) -> Unit, val onWrongLetter: (model: ModelImpl) -> Unit, val onComplete: (model: ModelImpl) -> Unit) : Model {
    override var isCompleted = false
        private set
    private val inputLetters = StringBuilder()
    
    override fun append(c: Char) {
        when (c) {
            in '0'..'9' -> {
                inputLetters.append(c)
                onCorrectLetter(this)
                if (inputLetters.length == 2) {
                    onComplete(this)
                    isCompleted = true
                }
            }
            else        -> {
                onWrongLetter(this)
            }
        }
    }
}

object view {
    fun onCorrectLetter(model: ModelImpl) {
        println("『ピッ!』")
    }
    
    fun onWrongLetter(model: ModelImpl) {
        println("『ブブーッ!』")
    }
    
    fun onComplete(model: ModelImpl) {
        println("『ピロリロリン!』[isCompleted = ${model.isCompleted}]")
    }
}

// controller
fun main(args: Array<String>) {
    
    val model = ModelImpl(view::onCorrectLetter, view::onWrongLetter, view::onComplete)
    
    model.append('1') // 数字なので正常受付。『ピッ!』と鳴る。
    model.append('a') // 数字じゃないのでエラーを表示。『ブブーッ!』っと鳴る。
    model.append('2') // 数字なので正常受付。『ピッ!』と鳴る。入力完了音が『ピロリロリン!』と鳴る。
}

クロージャ方式にてバグを埋め込む可能性のある例

closure1.kt と closure2.kt については、Model が受け取るコールバック内部から自身が参照される場合、バグを埋め込む可能性があります。
具体的には、以下のようにコードを変更すると結果表示が変わってしまいます。

// 正しい順序
inputLetters.append(c)
onCorrectLetter(this)
if (inputLetters.length == 2) {
    isCompleted = true           // isCompleted を true に変更してから onComplete(this) を呼び出している
    onComplete(this)
}

// 順序を変更してバグを埋め込む例
inputLetters.append(c)
onCorrectLetter(this)
if (inputLetters.length == 2) {
    onComplete(this)             // この時点では isCompleted を true に変更する前に onComplete(this) を呼び出している
    isCompleted = true
}

実行結果

『ピッ!』
『ブブーッ!』
『ピッ!』
『ピロリロリン!』[isCompleted = false]


returnValue1.kt のように戻り値を返す方式の場合、間違えようとしても間違えようがないので、コールバック方式よりも戻り値方式のほうが保守性が高そうです。

要件変更への耐性

余談になりますが、要件変更への耐性の例として、以下の要件変更を考えてみる。

  • 完了時は『ピッ!』という音を出さずに『ピロリロリン!』だけを鳴らす。

戻り値方式の場合の変更点

1行削除でOK。

when (this) {
    is Correct -> {
        view.onCorrectLetter(model)             // ← この1行を削除すればOK
        if (isComplete) view.onComplete(model)
    }
    is Wrong   -> view.onWrongLetter(model)
}

コールバック方式の場合

controller と model の両方に変更が発生する例。

//
// closure3.kt
//
package closure3

interface Model {
    val isCompleted: Boolean
    fun append(c: Char)
}

// 変更前: class ModelImpl(val onCorrectLetter: (model: ModelImpl) -> Unit, val onWrongLetter: (model: ModelImpl) -> Unit, val onComplete: (model: ModelImpl) -> Unit) : Model {
class ModelImpl(val onCorrectLetter: (isCompleted: Boolean, model: ModelImpl) -> Unit, val onWrongLetter: (model: ModelImpl) -> Unit) : Model {
    override var isCompleted = false
        private set
    private val inputLetters = StringBuilder()
    
    override fun append(c: Char) {
        when (c) {
            in '0'..'9' -> {
                inputLetters.append(c)
    
                // 変更前
                // onCorrectLetter(this)
                // if (inputLetters.length == 2) {
                //     onComplete(this)
                //     isCompleted = true
                // }
                
                // 変更後
                onCorrectLetter((inputLetters.length == 2).apply { isCompleted = this }, this)
            }
            else        -> {
                onWrongLetter(this)
            }
        }
    }
}

object view {
    fun onCorrectLetter(model: ModelImpl) {
        println("『ピッ!』")
    }
    
    fun onWrongLetter(model: ModelImpl) {
  k      println("『ブブーッ!』")
    }
    
    fun onComplete(model: ModelImpl) {
        println("『ピロリロリン!』[isCompleted = ${model.isCompleted}]")
    }
}

// controller
fun main(args: Array<String>) {
    
    // 変更前
    // val model = ModelImpl(view::onCorrectLetter, view::onWrongLetter, view::onComplete)
    
    // 変更後
    val model = ModelImpl(
            { isCompleted, model ->
                when (isCompleted) {
                    true  -> view.onComplete(model)
                    false -> view.onCorrectLetter(model)
                }
            },
            view::onWrongLetter
    )
    
    model.append('1') // 数字なので正常受付。『ピッ!』と鳴る。
    model.append('a') // 数字じゃないのでエラーを表示。『ブブーッ!』っと鳴る。
    model.append('2') // 数字なので正常受付。入力完了音が『ピロリロリン!』と鳴る。
}


controller 側のみに変更を局所化することも一応可能ではあるという例。
フラグ立てまくりでカオスwww

//
// closure4.kt
//
package closure4

interface Model {
    val isCompleted: Boolean
    fun append(c: Char)
}

class ModelImpl(val onCorrectLetter: (model: ModelImpl) -> Unit, val onWrongLetter: (model: ModelImpl) -> Unit, val onComplete: (model: ModelImpl) -> Unit) : Model {
    override var isCompleted = false
        private set
    private val inputLetters = StringBuilder()
    
    override fun append(c: Char) {
        when (c) {
            in '0'..'9' -> {
                inputLetters.append(c)
                onCorrectLetter(this)
                if (inputLetters.length == 2) {
                    onComplete(this)
                    isCompleted = true
                }
            }
            else        -> {
                onWrongLetter(this)
            }
        }
    }
}

object view {
    fun onCorrectLetter(model: Model) {
        println("『ピッ!』")
    }
    
    fun onWrongLetter(model: Model) {
        println("『ブブーッ!』")
    }
    
    fun onComplete(model: Model) {
        println("『ピロリロリン!』[isCompleted = ${model.isCompleted}]")
    }
}

// controller
fun main(args: Array<String>) {
    
    var isCorrectLetter = false
    fun onCorrectLetter(model: Model) {
        isCorrectLetter = true
    }
    
    var isComplete = false
    fun onComplete(model: Model) {
        isComplete = true
    }
    
    val model = ModelImpl(::onCorrectLetter, view::onWrongLetter, ::onComplete)
    
    fun append(c: Char) {
        model.append(c)
        if (isCorrectLetter) {
            if (isComplete) view.onComplete(model) else view.onCorrectLetter(model)
        }
        isCorrectLetter = false
        isComplete = false
    }
    
    append('1') // 数字なので正常受付。『ピッ!』と鳴る。
    append('a') // 数字じゃないのでエラーを表示。『ブブーッ!』っと鳴る。
    append('2') // 数字なので正常受付。入力完了音が『ピロリロリン!』と鳴る。
}

考察

なんか歯切れのよい結論が出せないのだけれども、戻り値方式でもクロージャ方式でも、同じ情報を扱えるので数学的には等価だと思われ。しかし、戻り値で対応できるケースをクロージャ渡しで代用するのは結構 error prone かつ仕様変更に弱い気がする。呼び出しが無限ループするバグを埋め込んだりも結構アリがちだし、、、w

機械的な判断基準が欲しいですね。そうすればビルド時に自動で検出できるし、、、。

ここら辺を論じている書籍や論文など知ってる人よろしくです m(_ _)m

AndroidでUIメソッド呼び出しの共通時刻を取得する

概要

UIスレッドから呼び出される処理全体で初期化やバケツリレー無しで共通時刻を用意したいなーという話。
下記のメソッド1~Nすべてでボタン押下時刻を共通時間として取得するとか。

ボタン押下 → onClick()実行 → メソッド1呼び出し → メソッド2呼び出し … メソッドN 呼び出し

本題

Android アプリ開発では、時間情報として currentTimeMillis()uptimeMillis() を扱うことがあります。

これらのメソッドは現在時刻を返すので、同一スレッド内で連続で呼び出しても異なる時刻が返されます。*1

連続した処理で同じ時刻を扱いたい場合、どこかで時刻を取得して、それをメソッドの呼び出し階層でバケツリレーする方法で対応することが可能です。

しかし、含まれるメソッド数が大きい場合や、自身は時刻情報を利用しないのにバケツリレーのためだけに引数を受け取るメソッドが多数あるような場合は面倒です。そのような場合は独自にスレッドを生成して ThreadLocal にスレッド生成直後の時刻を設定するという方法も考えられます。

しかし、UIスレッドの場合、既にスレッドが存在しているし、描画開始タイミングもアプリ側で制御できません。

そこで、main looper が message の dispatch を開始するタイミングで時刻を設定する方法を試してみました。

object MainLooperMessageDispatchUptimeMillisManager {
    private var isAvailable = false
        private set
    
    var mainLooperMessageDispatchUptimeMillis: Long = -1
        get() {
            assertMainThread()
            return field
        }
        private set
    
    init {
        assertMainThread()
        Looper.getMainLooper().setMessageLogging {
            // 開始タイミングのみ時刻を記録する。
            // 最初に実行されるのは、init{} が実行された後で最初に main looper が message を dispatch した時。
            // init{} が実行された時の main looper の dispatch 終了タイミングでは呼び出しが無い点に注意。
            isAvailable = !isAvailable
            if (isAvailable) mainLooperMessageDispatchUptimeMillis = SystemClock.uptimeMillis()
        }
    }
    
    private fun assertMainThread() {
        if (Thread.currentThread() != Looper.getMainLooper().thread) throw IllegalStateException("Current thread is not main thread.")
    }
}


これを Application#onCreate()などで一度評価することによって初期化します。*2

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        // 式として評価して初期化する
        MainLooperMessageDispatchUptimeMillisManager
    }
}


使うときは static import しておくと便利。

import mypackage.MainLooperMessageDispatchUptimeMillisManager.mainLooperMessageDispatchUptimeMillis as uptimeMillis


呼び出し例

val handler1 = Handler()
val handler2 = Handler()

fun f1(handler: String, name: String) { println("f1(): $handler: $name: $uptimeMillis"); Thread.sleep(10) }
fun f2(handler: String, name: String) { println("f2(): $handler: $name: $uptimeMillis"); Thread.sleep(10) }

handler1.postDelayed(100) { f1("handler1", "a1"); f2("handler1", "a2") }
handler1.postDelayed(100) { f1("handler1", "b1"); f2("handler1", "b2") }
handler2.postDelayed(100) { f1("handler2", "c1"); f2("handler2", "c2") }
handler2.postDelayed(100) { f1("handler2", "d1"); f2("handler2", "d2") }

実行結果

f1(): handler1: a1: 844721417 ← 同一スレッドでは時間が共通となる。
f2(): handler1: a2: 844721417 ← 同一スレッドでは時間が共通となる。
f1(): handler1: b1: 844721438 ← main looper から dispatch されるタイミングが異なるので上記の処理と時刻がずれる。
f2(): handler1: b2: 844721438
f1(): handler2: c1: 844721459 ← 同一 handler でも時刻が異なるのだから別 handler でも当然そうなる。
f2(): handler2: c2: 844721459
f1(): handler2: d1: 844721480
f2(): handler2: d2: 844721480


Looper#setMessageLogging(android.util.Printer) はどこからでもセットできてしまう点は注意すべきですね。

*1:ミリ秒精度で一致することはありますが。

*2:生成と初期化を分けてもいいし方法は何でもいいですw

kotlin tips:extension function reference

kotlin のドキュメントに拡張関数参照の方法がさらっと書かれている。
あまりにもさらっと書かれすぎていて一度スルーしてしまったので、例を交えてメモしておくw

If we need to use a member of a class, or an extension function, it needs to be qualified. e.g. String::toCharArray gives us an extension function for type String: String.() -> CharArray.

Reflection - Kotlin Programming Language


以下は、本家にあった拡張関数のコピペ。

fun MutableList<Int>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this' corresponds to the list
    this[index1] = this[index2]
    this[index2] = tmp
}


上記の拡張関数を参照として扱いたい場合のコードは以下。

MutableList<Int>::swap

kotlin小ネタ:Handler#postDelayed()のextension function化

普通に Handler#postDelayed() を利用すると以下のようになり、結構見づらい。

mHandler.postDelayed(
    {
        println("postDelayed()")
    },
    2000)


kotlinでは、関数の最終引数が関数の場合には、ラムダ式を括弧の外側に記述できる。
Higher-Order Functions and Lambdas - Kotlin Programming Language

In Kotlin, there is a convention that if the last parameter to a function is a function, and you're passing a lambda expression as the corresponding argument, you can specify it outside of parentheses


ということで、以下のような extension function を用意する。*1

fun Handler.postDelayed(delayMillis: Long, r: () -> Unit) {
    postDelayed(r, delayMillis)
}


すると、ラムダ式を関数の body のような感じで書ける。

mHandler.postDelayed(2000){
    println("postDelayed()")
}


いい感じ(`・ω・´)

*1:kotlin と android の両方に依存するスコープ用のライブラリ内に配置すると便利。

やる気とかいろいろ

  • やる気は無いのが前提。
  • やる気は偶然の幸運。
  • やる気が必要なことは計画しない。
  • 突然現れる直感は大体信じてOK。*1
  • 迷いは強引な即決か思考停止で対応。*2
  • 集中力の切れた状態やら頭の飽和状態とやらは、往々にして現実逃避。*3

*1:直感は純粋に最適解であることが多く、直感の通りに活動することは楽じゃないことが多い。そのため、直感を元にその先を思考するのはOKだが、直感を一案として総合的に思考するのと直感を否定する言い訳探しになり、導かれる結果は怠惰でナイーブな願望となる

*2:人生は有限なので、時間をかけて正しい選択をするよりも短時間で数多くの選択を重ねるべき。また、迷いに対する思考は往々にして自己正当化のための精神活動であり、結論は怠惰でナイーブな願望となる。

*3:息抜きに他の事しようと思ったら100%現実逃避。寝たい場合は寝ればいいw

kotlin小ネタ:Activity 関連の試行錯誤

Activity を扱う際の実験コード。

とりあえず思い付きで作ってみた。
カオスなのでダメ出し歓迎(`・ω・´)

/**
 * Activity を扱う際のいろんなネタを詰め込んでみた実験コード。
 * ※書式だけまとめたもので、実際に動くコードではない。
 *
 * ポイントは以下。
 * ・Intent を利用せずに Activity を起動するチート。
 * ・XxxActivityHelper を分離することにより Activity の制約とは無縁のインスタンスで Activity の処理を記述できる。
 * ・コンストラクタ引数の評価時(thisが生成される前)に可能な限り初期化を行うことにより初期化前のプロパティを参照
 *  してしまうバグの入れ込みを不可能にする。※コンパイラレベルで排除される
 * ・テスト用にコンストラクタの動的な差し替えが可能。※newInstance を var で定義している。
 * ・ほぼすべてのメソッドが単体でユニットテスト可能。this も差し替えられるように extension を使用している。
 * ・メソッド階層をすべて辿ったうえで var への代入が行われるコードかどうかをコンパイラレベルでチェック可能。
 */
package com.example

import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import com.example.XxxActivityHelperImplDelegate.getPcaSomethingCreatedByConstructor
import com.example.XxxActivityHelperImplDelegate.onBackPressed_
import com.example.XxxActivityHelperImplDelegate.onBeforeConstructor
import com.example.XxxActivityHelperImplDelegate.onPause_
import org.jetbrains.anko.AnkoLogger
import org.jetbrains.anko.startActivity
import com.example.XxxActivityHelperImpl as Assignable
import com.example.XxxActivityHelperImpl as Impl

/**
 * [Activity] 本体
 *
 * 以下の理由で [onCreate] 以外の処理をすべて [XxxActivityHelper] に委譲している。
 * ・ヘルパーを利用することにより [onCreate] 以降に生成された値を  [XxxActivityHelper] の val として扱える。
 * ・[Activity] の仕様に特化した処理とそれ以外を分離できる。※[onPause] で super.onPause() を呼ぶ処理など。
 */
class XxxActivity : AppCompatActivity(), AnkoLogger {
    // backing field を持つ唯一のプロパティ
    // これ以外はすべて Helper 側に含まれる
    private lateinit var helper: XxxActivityHelper
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // Intent を利用せずに Activity を起動するチート用の処理。
        // XxxHelper に委譲する前に必要な処理なのでここに記述している。
        val activityArg1 = activityArg1
        val activityArg2 = activityArg2
        if (activityArg1 != null && activityArg2 != null) {
            helper = XxxActivityHelper.newInstance(this@XxxActivity, activityArg1, activityArg2)
            Companion.activityArg1 = null
            Companion.activityArg2 = null
        }
        else {
            finish()
        }
    }
    
    override fun onBackPressed() {
        helper.onBackPressed()
    }
    
    override fun onPause() {
        super.onPause()
        helper.onPause()
    }
    
    companion object {
        /**
         * [Intent] を利用せずに [Activity] を起動するチート用の情報
         * [startActivity] 経由で起動される保証はないので lateinit にはできない。※端末の履歴機能からの Activity 起動など。
         */
        private var activityArg1: Something1? = null
        private var activityArg2: Something2? = null
        
        /**
         * [Intent] を利用せずに [Activity] を起動するチート
         */
        fun startActivity(activity: XxxActivity, activityArg1: Something1, activityArg2: Something2) {
            Companion.activityArg1 = activityArg1
            Companion.activityArg2 = activityArg2
            activity.startActivity<XxxActivity>()
        }
    }
}

/**
 * [XxxActivity] から処理を移譲されるヘルパークラス
 */
interface XxxActivityHelper {
    fun onBackPressed()
    
    fun onPause()
    
    companion object {
        // テスト時には必要に応じて newInstance を差し替える
        var newInstance = ::Impl
    }
}

class XxxActivityHelperImpl private constructor(
        override val activity: XxxActivity,
        override val activityArg: Something1,
        override val somethingCreatedByConstructor: Something3
) : XxxActivityHelper, XxxActivityHelperImplDelegate.NonAssignable {
    // Secondary constructors to create primary constructor arguments --------------------------------------------------
    // this を参照しなくても生成可能な情報はすべてコンストラクタ引数として扱う。
    // これにより、property initializer もしくは initializer block にて非 null に初期化のプロパティが null にならない
    // という勘違いによるバグを防ぐ。※初期化前に参照できるので
    @Suppress("unused") // for inspector's bug.
    constructor(activity: XxxActivity, activityArg1: Something1, activityArg2: Something2) : this(
            // どうしても評価前に行わなきゃならん処理がある場合の苦肉の策。
            onBeforeConstructor(activity, activityArg1, activityArg2).run {
                activity
            },
            activityArg1,
            getPcaSomethingCreatedByConstructor(activityArg2)) {
        // ここには可能な限りコードを書かない。
    }
    
    // Properties with backing fields that refer to this@Impl ----------------------------------------------------------
    override var isBackPressed = false
    
    // Initializer block -----------------------------------------------------------------------------------------------
    init {
        // ここには可能な限りコードを書かない。
    }
    
    // Delegate functions ----------------------------------------------------------------------------------------------
    
    override fun onBackPressed() {
        onBackPressed_()
    }
    
    override fun onPause() {
        onPause_()
    }
}

object XxxActivityHelperImplDelegate : AnkoLogger {
    // Constants -------------------------------------------------------------------------------------------------------
    
    // onBeforeConstructor ---------------------------------------------------------------------------------------------
    fun onBeforeConstructor(activity: XxxActivity, activityArg1: Something1, activityArg2: Something2) {
    }
    
    // Primary constructor argument initializers -----------------------------------------------------------------------
    fun getPcaSomethingCreatedByConstructor(activityArg: Something2): Something3 {
        return Something3()
    }
    
    // Property initializers for properties that internally refer to this@Impl -----------------------------------------
    
    // Initializer block delegation ------------------------------------------------------------------------------------
    
    // Direct delegates ------------------------------------------------------------------------------------------------
    
    fun Assignable.onBackPressed_() {
        isBackPressed = true
    }
    
    fun NonAssignable.onPause_() {
        with(activity) {
            // プロパティへの代入の無い処理をここで行う
        }
    }
    
    // Members that assign values to backing fields --------------------------------------------------------------------
    
    // Members that have side effects ----------------------------------------------------------------------------------
    
    // Non-assignable interface of implementation class ----------------------------------------------------------------
    // NonAssignable が receiver のメソッドはプロパティへの代入が無いことがコンパイラにより保証される。保守時に便利。
    // NonAssignable であってもイミュータブルである保証はない点に注意。
    interface NonAssignable : AnkoLogger {
        val activity: XxxActivity
        val activityArg: Something1
        val somethingCreatedByConstructor: Something3
        val isBackPressed: Boolean
    }
}

class Something1
class Something2
class Something3

kotlin小ネタ:initializer block や property initializer は可能な限り避ける

※あまり深く考えてないのでまだアイデアレベルのメモ。ちゃんと考えないと一般化はできないと思われる。

initializer block や property initializer を利用する状況というのは、インスタンスが生成された状態、つまり this が参照できる状態で val/var の初期化完了が保証されていない状況と言える。そのため、『val で宣言と同時に初期化してるから確実に値が入ってる』と思ったが実際は null でしたなんてことが起こり得る。


以下のコードを考えてみる。
※fullNameを取得する処理が非常に重く、毎回 firstName と lastName から作り出すのは要件的に許容できないと仮定する。

class Person(first: String, last: String) {
    val firstName: String = first                  // "John"
    val lastName: String = last                    // "Smith"
    val fullName: String = "$lastName, $firstName" // "Smith, John"
}

fun main(args: Array<String>) { with(Person("John", "Smith")) {
    println("firstName = [$firstName], lastName = [$lastName], fullName = [$fullName]") }
}

// 実行結果: firstName = [John], lastName = [Smith], fullName = [Smith, John]


このコードに対して、メンバをアルファベット順にするリファクタリングをしようとするとコンパイルエラーになる。

// メンバをアルファベット順にするリファクタリングをしようとしてコンパイルエラーになる例
class Person(first: String, last: String) {
    val firstName: String = first
    val fullName: String = "$lastName, $firstName" // <- Variable 'lastName' must be initialized
    val lastName: String = last
}

これを見る限り val を用いれば非 null が保証されるように思える。

しかし、関数を経由するとそうはならず、バグを埋め込むことになる。

// メンバをアルファベット順にするリファクタリングをしたつもりがバグを埋め込んでしまった例
class Person(first: String, last: String) {
    val firstName: String = first       // "John"
    val fullName: String = toFullName() // "null, John" ← null が入っている
    val lastName: String = last         // "Smith"

    private fun toFullName() = "$lastName, $firstName"
}

fun main(args: Array<String>) { with(Person("John", "Smith")) {
    println("firstName = [$firstName], lastName = [$lastName], fullName = [$fullName]") }
}

// 実行結果: firstName = [John], lastName = [Smith], fullName = [null, John]

非 null の val を参照して null が返るのは、property initializer *1 のコードが走る時点でインスタンスの生成が完了しているため、つまり this が参照可能な状態のためと考えられる。

ということは、this を参照する関数(fun/get()/set())を呼び出すコードを書く場合は、関数が直接的及び間接的に参照するプロパティをすべて洗い出し、それらが初期化されていることを確認する必要があるということになる。それは、プロパティの初期化に関するコードを変更する際にも、そのプロパティを参照する全関数のコードを調査する必要があるということになる。

(´・ω・`)

それならば、this が作られる前に処理を行ってしまえばいいのでは?

// 引数をすべて primary constructor で扱うことにより this を参照することが無い保証をした場合
class Person private constructor(val firstName: String, val lastName: String, val fullName: String) {
    constructor(firstName: String, lastName: String) : this(
            firstName,                      // "John"
            lastName,                       // "Smith"
            toFullName(firstName, lastName) // "Smith, John"
    )

    companion object {
        private fun toFullName(firstName: String, lastName: String) = "$lastName, $firstName"
    }
}

fun main(args: Array<String>) { with(Person("John", "Smith")) {
    println("firstName = [$firstName], lastName = [$lastName], fullName = [$fullName]") }
}

// 実行結果: firstName = [John], lastName = [Smith], fullName = [Smith, John]

private primary constructor で対応することができた。

上記でバグ埋め込みの原因となった宣言順序変更と同様のリファクタリングを行ってみるとどうなるか。

// メンバの宣言順序をアルファベット順にリファクタリングする例
class Person private constructor(val firstName: String, val fullName: String, val lastName: String) {
    constructor(firstName: String, lastName: String) : this(
            firstName,                      // "John"
            toFullName(firstName, lastName),// "Smith, John"
            lastName                        // "Smith"
    )

    companion object {
        private fun toFullName(firstName: String, lastName: String) = "$lastName, $firstName"
    }
}

fun main(args: Array<String>) { with(Person("John", "Smith")) {
    println("firstName = [$firstName], lastName = [$lastName], fullName = [$fullName]") }
}

this を参照しない、つまり、受け取った値のみを利用しているため、初期化順序に影響されずに対応ができた。

実験のための実験として、primary constructor arguments の順序変更によりバグを埋め込む例が作れるか試してみる。

// 実験のための実験1
class Person private constructor(val firstName: StringBuilder, val lastName: StringBuilder, val fullName: StringBuilder) {
    constructor(firstName: StringBuilder, lastName: StringBuilder) : this(
            firstName,                                           // "John"
            lastName.apply { setLength(0); append("William")  }, // "William"
            toFullName(firstName, lastName)                      // "William, John"
    )
    
    companion object {
        private fun toFullName(firstName: StringBuilder, lastName: StringBuilder) = StringBuilder("$lastName, $firstName")
    }
}

fun main(args: Array<String>) { with(Person(StringBuilder("John"), StringBuilder("Smith"))) {
    println("firstName = [$firstName], lastName = [$lastName], fullName = [$fullName]") }
}

// 実行結果: firstName = [John], lastName = [William], fullName = [William, John]

上記の例では、secondary constructor の lastName の評価の際に引数に副作用を加えている。その結果、"Smith" が "William" に書き換えられている。

primary constructor の引数の順序変更をIDEリファクタリング機能で行ってみる。

// 実験のための実験2
class Person private constructor(val firstName: StringBuilder, val fullName: StringBuilder, val lastName: StringBuilder) {
    constructor(firstName: StringBuilder, lastName: StringBuilder) : this(
            firstName,                                          // "John"
            toFullName(firstName, lastName),                    // "Smith, John"
            lastName.apply { setLength(0); append("William")  } // "William"
    )
    
    companion object {
        private fun toFullName(firstName: StringBuilder, lastName: StringBuilder) = StringBuilder("$lastName, $firstName")
    }
}

fun main(args: Array<String>) { with(Person(StringBuilder("John"), StringBuilder("Smith"))) {
    println("firstName = [$firstName], lastName = [$lastName], fullName = [$fullName]") }
}

// 実行結果: firstName = [John], lastName = [William], fullName = [Smith, John]

IDEリファクタリング機能を使っただけなのに結果が変わってしまった。(fullName が "William John" ではなく "Smith John" になっている)
そうなるように書いたコードなので当然ですがw

ここで重要なポイントは、以下のようなものになると思う。

  • どんなコードでも実験のための実験2で書かれたような無茶なことはできる。
  • しかしそのようなコードが製品のコードとして実際に書かれる可能性は著しく低い。 *2


primary constructor 方式は this を参照するコードを書こうとしてもコンパイルエラーになるという点がステキかも。

// 保守時に『無用なバケツリレーをメンバ参照に改善しよう』と改善するつもりでコンパイルエラーを起こす例
class Person(val firstName: String, val fullName: String, val lastName: String) {
    constructor(firstName: String, lastName: String) : this(
            firstName,
            // Cannot access 'toFullName' before superclass constructor has been called と怒られる
            toFullName(),
            lastName
    )

    private fun toFullName() = "$lastName, $firstName"
}


では、primary constructor 引数だけで対応できるかというと、当然 this を必要とするケースでは対応できないし、get()/set() を伴う場合など言語仕様的に対応できないケースも多々ある。

以下、転落人生の例:

class Person(val firstName: String, val fullName: String, val lastName: String, address: String) {
    constructor(firstName: String, lastName: String, address: String) : this(
            firstName,
            toFullName(firstName, lastName),
            lastName,
            address
    )
    
    var address: String by Delegates.observable(address) {
        _, old, new ->
        println("address: [$old] -> [$new]")
    }
    
    companion object {
        private fun toFullName(firstName: String, lastName: String) = "$lastName, $firstName"
    }
    
    // 実行結果
    // firstName = Brandon, lastName = Walsh, fullName = Walsh, Brandon, address = Beverly Hills, 90210
    // address: [Beverly Hills, 90210] -> [No fixed abode]
    // firstName = Brandon, lastName = Walsh, fullName = Walsh, Brandon, address = No fixed abode
}

fun main(args: Array<String>) {
    with(Person("Brandon", "Walsh", "Beverly Hills, 90210")) {
        println("firstName = $firstName, lastName = $lastName, fullName = $fullName, address = $address")
        address = "No fixed abode"
        println("firstName = $firstName, lastName = $lastName, fullName = $fullName, address = $address")
    }
}

primary constructor の引数の評価が始まる前(や処理中)に処理を入れ要とする場合、強引に入れ込むなら以下のような方法がある。

class Something private constructor(val firstName: String, val lastName: String, val fullName: String) {
    constructor(firstName: String, lastName: String):this(doSomething().run { firstName }, lastName, "$lastName, $firstName")
    
    companion object {
        fun doSomething() = println("doSomething()")
    }
    
    // 実行結果: doSomething()
}

fun main(args: Array<String>) {
    Something("John", "Smith")
}

でも保守性悪そう(´・ω・`)
primary constructor と secondary constructor の signature が同じ場合は使えないし、、、。
そういう場合は設計から考え直すが吉ですかね。

次は、primary constructor arguments を作成するための一時変数が欲しい場合を考えてみる。
なんかの計算をしてその値を格納するクラス。処理が重いのでキャッシュが必須とする。

class Something(val a: Int, val b: Int, val c: Int) {
    // 計算処理がめちゃ重いのでキャッシュせざるを得ない
    val d = a + b
    // 計算処理がめちゃ重いのでキャッシュせざるを得ない
    val e = d + c
}

fun main(args: Array<String>) {
    Something(3, 5, 7).apply {
        println("a = $a, b = $b, c = $c, d = $d, e = $e")
    }
}

// 実行結果: a = 3, b = 5, c = 7, d = 8, e = 15


全て primary constructor arguments として入れ込もうとすると以下のようになる。

class Something private constructor(val a: Int, val b: Int, val c: Int, val d: Int, val e: Int) {
    // a + b の計算を 2 回やっているのがもったいない
    constructor(a: Int, b: Int, c: Int) : this(a, b, c, a + b, a + b + c)
}

上記の場合、a + b の計算を 2 回やっているのがもったいない

計算を重複して行わないよう、ThreadLocal 上に値を入れ込んでみる。

// ThreadLocal を利用して a + b の計算結果を格納する例
class Something private constructor(val a: Int, val b: Int, val c: Int, val d: Int, val e: Int) {
    constructor(a: Int, b: Int, c: Int) : this(a, b, c, calcD(a, b), calcE(c))
    
    companion object {
        private val tmp = ThreadLocal<Int>()
        private fun calcD(a: Int, b: Int) = (a + b).apply { tmp.set(this) }
        private fun calcE(c: Int) = tmp.get() + c
    }
}

上記の例は正しく動作するが、順序変更のリファクタリングをすることによりバグを埋め込むリスクが生まれる。

// change signature のリファクタリングをしてバグを入れ込む例
class Something private constructor(val a: Int, val b: Int, val c: Int, val e: Int, val d: Int) {
    constructor(a: Int, b: Int, c: Int) : this(a, b, c, calcE(c), calcD(a, b))
    
    companion object {
        private val tmp = ThreadLocal<Int>()
        private fun calcD(a: Int, b: Int) = (a + b).apply { tmp.set(this) }
        private fun calcE(c: Int) = tmp.get() + c // <- ここでヌルポが出る
    }
}

順序を変更しても問題ないバージョン。

class Something private constructor(val a: Int, val b: Int, val c: Int, val d: Int, val e: Int) {
    constructor(a: Int, b: Int, c: Int) : this(a, b, c, d(a, b, c), e(a, b, c))

    object args {
        private var d: ThreadLocal<Int>? = null
        private var e: ThreadLocal<Int>? = null

        private fun init(a: Int, b: Int, c: Int) {
            d = ThreadLocal<Int>().apply { set(a + b) }
            e = ThreadLocal<Int>().apply { set(d!!.get() + c) }
        }

        fun d(a: Int, b: Int, c: Int): Int {
            if (d == null) init(a, b, c)
            return d!!.get()
        }

        fun e(a: Int, b: Int, c: Int): Int {
            if (e == null) init(a, b, c)
            return e!!.get()
        }
    }
}

引数を毎回全部渡さなきゃならんのと、コード量が多いのが面倒ですね。
実際に有効なケースがどの程度あるのかは未知数ですね。

*1:厳密には initializer block も含む body 全体

*2:少なくともおいらは今までに一度も見たことが無い