食べログのアプリDevOpsチームでAndroidエンジニアやっている @sadashi です。
最近はアプリアーキテクチャ設計やコーディング規約を考えたりしています。
食べログ Advent Calendar 2018 6日目の記事ではKotlinでのCoding Tipsについて書きたいと思います。
はじめに
Kotlinでコーディングする際に気をつけた方がいいところであったり、ちょっとしたTipsを書いていきたいと思います。
私自身Androidメインなので、内容がAndroidになっているところもありますが、その点はご了承ください。
環境
IDEはAndroidStudioを使っていて、Kotlinは1.3.10をベースに考えています。
あと、単純にKotlinを書いてみたいならKotlin Playgroundを使うとお手軽で良いですよ!
Tips
Lint
lint
は必ず使いましょう。自分も気づかなかった不具合を見つけてくれるかもしれません。
また合わせてformatter
としても使うことをお勧めします。コーディングの効率が上がり、書き方も統一されるので、可読性も高くなります。
Kotlinに対応しているものは以下のようなものがあります。
トップレベル関数, 拡張関数
頻繁には使わない方が良いと思います。ルールなしで使うとカオスになります。
使いたい場合はスコープを限定するなどを検討すると良いかもしれません。
私としては、明らかに汎用性が高いようなものはあっても良いと思っています。
Androidですが一例としては以下のようなものです。
// Picassoを利用した、URLからImageViewへの反映
fun ImageView.imageUrl(url: String?) {
if (url.isNullOrEmpty()) {
setImageDrawable(null)
return
}
Picasso.with(context).load(url).into(this)
}
// ViewGroupのinflate
fun ViewGroup.inflate(@LayoutRes layoutRes: Int): View {
return LayoutInflater.from(context).inflate(layoutRes, this, false)
}
型の明記
型は基本的には書いた方がわかりやすいと思います。
省略するとしたら、プリミティブな型かコンストラクタを直接呼ぶ場合のみがおすすめです。
// NG
val huga = factory.create() // 型がよくわからない
val hugaInterface = HugaImpl() // 抽象化したいのに略すと実装したクラスになってしまう
// OK
val primitiveInt = 0
val primitiveString = ""
val directConstractor = Hoge()
val huga: Huga = factory.create()
val hugaInterface: HugaInterface = HugaImple()
Null Check
null
チェックしてearly return
する場合はエルビス演算子(?:
)を使うと良いです
fun dummy(hoge: Hoge?) {
val huga = hoge ?: return
// ...
}
何か処理をさせたい場合はスコープ関数(run
or let
)を使います
fun dummy(hoge: Hoge?) {
val huga = hoge ?: run {
// do something
return
}
// ...
}
逆にnull
ではない場合に処理させたければこんな感じです。
fun dummy(hoge: Hoge?) {
hoge?.let {
it.hogehoge()
}
// or
hoge?.let {
// smart cast
hoge.hogehoge()
}
}
こういった場合はスマートキャストの方がわかりやすいと思います。
!!
(not-null assertion operator, force unwrap)
null
に対して行うと即クラッシュしてしまうので、基本的には利用しないようにした方が良いと思います。
early return
を意識して、smart cast
をするのが一番わかりやすいです。
どうしても必要なときはrequireNotNull
でメッセージをつけると、意思が伝わります。
val a: Int? = null
val b: Int = requireNotNull(a, {"ここでnullになってはいけないよ"}) // 第二引数の文字列は任意
スコープ関数(let
,run
,apply
,also
)
ここは好みがでるところかもしれません。
個人的にはalso
とlet
のみを使います。
理由としてはapply
/run
はレシーバでthis
をとりますが、クラス内のメンバに同じ名前が存在すると意図しない挙動となるためです。
// NGパターン
class Hoge {
var text = ""
}
fun init() {
var text = ""
val hoge = Hoge().apply {
text = "hoge"
}
hoge.text // "hoge"になっていない
}
this
をつければいいんじゃね?って意見もあると思いますが、それだとit
つけるのと変わらないので、明示的に使わないという方向に倒しているだけです。
ただし、以下の例のように、初期化時の設定はapply
の方が意味がわかりやすいとか、エルビス演算子(?:
)の後の処理はrun
の方が直感的とか、人によって意見は色々あると思いますが、この辺りはどういったときに問題が起こるかを把握した上で議論し、しっかりと「共通認識」を持つことが大事かなと思っています。
// initialize
val intent = Intent().apply { // <- 自分ルールだとalso
it.putExtra("key1", "value1")
it.putExtra("key2", "value2")
it.putExtra("key3", "value3")
}
// null check
val hoge = str ?: run { // <- 自分ルールだとlet
// なんか処理する
return
}
Singlton
Dagger
などのDIライブラリを利用されている方はあまり使わないかもしれませんが、シングルトンは以下のように書くといいです。
// Singleton
class SingeltonHoge {
companion object {
private var INSTANCE: SingeltonHoge? = null
fun getInstance(): SingeltonHoge {
return INSTANCE ?: SingeltonHoge().also { INSTANCE = it }
}
}
}
Android StudioやIntelliJならLive Template
に以下を登録しておくと便利です。
companion object {
private var INSTANCE: $CLASS$? = null
fun getInstance(): $CLASS$ {
return INSTANCE ?: $CLASS$().also { INSTANCE = it }
}
}
Custom Setter
Custom Setterは変数の委譲のみに留め、他の用途ではあまり使わない方が良いかと思います。
private val user = User()
var userName: String
get() = user.name
set(value) {
user.name = value
}
もし、以下のように設定ついでに更新や通知処理などを行うと以下の問題が起こります。
- 値の更新だけをすることができない
- 他の値と一緒に更新することができないなど
// NG
private val user = User()
var userName: String
get() = user.name
set(value) {
user.name = value
update()
notify()
}
Custom Getter
処理に時間がかかったり、何か状態を変えるような副作用がある場合には、Custom Getterを使わずに通常の関数を利用した方がよいでしょう。
処理に時間がかからず、副作用も特にない場合はCustom Getterを使っても問題ないと思います。
この辺の話は公式のKotlinコーディング規約(Functions vs Properties)にも記載があります。
メンバ変数の初期化方法
メンバ変数を初期化する方法として以下の4種類があります。
-
lazy
- 最初にアクセスがあった時に値が計算されて、それ以降同じ値を返す
- 不変のため、
val
でしか宣言できない
-
custom getter
- アクセスがあるたびに計算される
- 通常の代入
- インスタンス生成時に計算される
-
lateinit
- 初期化されていない状態をもち、どこかで初期化される前提となる宣言
- 初期化されていない状態でアクセスすると例外が発生する
- 必ず初期化以外の代入が行われる前提なので
var
でしか宣言できない -
Nullable
は認めない
これらを理解せずに曖昧に使ってしまうと痛い目にあうので気をつけましょう。
使う場合には以下のような観点でまとめると良いと思います。
-
lazy
- 初期化時には値が確定しない、不変
-
custom getter
- 状態が変わると値も変わる
- 通常の代入
- 初期化時に値が確定、不変
-
lateinit
- 初期化時には値が確定しない、利用シーンは要検討(後述します)
不変でない値についてはcustom getter
か通常の代入になるかと思います。
lateinit
については後述するルール次第になります。
lateinit
lateinit
は利用箇所をルール化することをオススメします。
例えば以下のようなケースです。
-
Activity
/Fragment
などのonCreate()
などで確実に代入するメンバ変数 -
Dagger
などのDIライブラリを利用してInjection
するメンバ変数
lateinit
の変数が初期化されているかチェックするisInitalized
がありますが、これを使うということは未初期化の状態を管理しなければならないため、Nullable
で初期値にnull
を代入していることと変わりません。
上記のようなルール外では、Nullable
を利用した方が混乱が少なく済むでしょう。
lazy
とlateinit
の違い
どちらも遅延初期化という点は同じですが、動作は大きくことなります。
初期化のタイミング | var / val | Nullable | |
---|---|---|---|
lazy | 初回アクセスされたとき | val | OK |
lateinit | 明示的に初期化する | var | NG |
注意点としてはView
でlazy
を使うとFragment
で渡すView
が更新されないなどの不具合に繋がるため、View
ではlazy
ではなくlateinit
を使ってください。
最後に
ここにまとめたのは色々な情報収集をした上で、自分なりに整理したものですので、まったく違ったご意見もあるかとは思います。
そういったところがあれば是非コメントいただけると幸いです。
明日の7日目は@weakbosonさんより「Rails の trusted proxy めんどいところとワークアラウンド」です。お楽しみに!