Kotlinの場合、結構な場面で型情報を省略可能です。
初期化式と同時に書く場合は変数の型は省略可能だし
val hoge = hoge()
メソッドも代入式で書く場合は戻り値を省略できますね。
fun hoge() = fuga()
自明な情報を何度も何度も書く必要がなくなってすっきりーですね。
でも、これって省略できる時は必ず省略する。でいいのだろうか?特にチーム開発では安易に省略することで将来面倒なことになったりしないだろうか?でも、なんとなく不安だから、どんな場合も省略不可、みたいなアホなルールを作りたくはない。
と、いうわけでいろいろ考えたので、私見を書かせてもらいます。あくまで私見です。
判断軸
省略してよいかどうかの条件としての判断軸を考えてみます。
一つは、文脈ですね。
以下のように、リテラルとかコンストラクタが直接現れる場合は省略しても混乱しなさそうですし、ない方が可読性が高そうだったりします。
val hoge = 0L
val fuga = Rect()
一方、以下のように、初期化式が複雑だったり、メソッドの戻り値を使っている場合は、型を推測するのが(少なくとも人間にとって)難しくなるので、省略しすぎると混乱が起きそうな気がしてきます。
val hoge = if (fuga()) {
piyo1()
} else {
piyo2()
}
もう一つはスコープですね。
ローカル変数の場合は省略してもスコープが狭いので混乱は少なそうですし、パブリックなメソッド・フィールドだと考慮すべき範囲が広くなりますので、省略しない方が良いシーンがありそうな気がします。
文脈による違い
リテラル・コンストラクタ
先に書いていますが、リテラルやコンストラクタを直接記述している場合、わかりやすく混乱の余地は少なそうです。むしろ型を指定した方が冗長で読みにくさを感じる場合もあります。
val hoge = 0L
val hoge: Long = 0L
val fuga = Rect()
val fuga: Rect = Rect()
ただし、0L
を間違って0
と書いてしまったなどのミスは起こりそうなので、この辺の区別を明確にしたい場合は型指定もありでしょう。
また、初期化式の戻り値より抽象的な型で扱いたい場合もあるでしょう。
val hoge: List<String> = ArrayList()
この場合は省略すると意味が変わってしまいますので省略はできませんね。
式
式を使っている場合はどうでしょう?
val hoge = if (piyo()) 0L else 1L
これぐらいだとLongだと分かりますが、
val hoge = when(piyo()) {
...
}
等となってくると、共通の型ってなんだっけとIDEの補助などがないとぱっと分からないですし、式に間違いがあって型が変わってしまった場合のチェックが難しくなってきそうです。
拡張関数とかも同様ですね。
val hoge = Rect().let {
...
}
シンプルな式ならともかく、複雑になればなるほど型を推測するのが難しくなります。
加えて、式を修正したことで型が意図せず変わってしまうリスクがあります。
また少し別の観点ですが、変数の型を書くことでジェネリクスの型引き数が省略できる場合もありますね。
val textView: TextView = findViewById(R.id.textView)
val textView = findViewById<TextView>(R.id.textView)
どちらでも記述量は変わりませんが、私個人的には型引数を省略できる記述の方が好きです。
メソッド
メソッドの戻り値で初期化している場合はどうでしょう。
まず、ファクトリーメソッド系の場合は
val hoge = mapOf()
val fuga = hashMapOf()
こちらもわかりやすく混乱は少ないかもしれませんが、hoge
はMap
、fuga
はHashMap
になります。その辺をよく理解していないとmapOf
をhashMapOf
に書き換えた際に、インターフェースが変化したことに気づかない可能性があります。
一般メソッドの戻り値の場合どうでしょう?
val hoge = fuga()
fuga()
の実装がどうなっているか知っていないと型がどうなるのか分からないですね。
で、その実装を見に行くと
val hoge = fuga()
fun fuga() = piyo()
fun piyo() = piyo1()
fun piyo1() = piyo2()
どこまで行けば型が分かるねん!ってことが起こりえますね。
こういう構成だと上流のメソッドの戻り値が変わると広い範囲の型が変わってしまうと言う状況に陥ってしまします。
この場合は、省略しない方がよいでしょう。少なくとも代入に使用しているメソッドについては。
スコープによる違い
メソッドローカル
fun hoge() {
val fuga = ...
}
メソッドローカルの場合、スコープはそのメソッドの中に限られるので、初期化式が多少複雑でも、型を省略しても混乱は少ないでしょう。記述することで冗長になり可読性が悪くなる可能性もあります。
NonNullであることを明確にしておきたい、初期化式が非常に複雑、などの理由がなければ削除しても良さそうです。
private
class Hoge {
private val hoge = ...
privateなメソッド、フィールドの場合はスコープはそのファイル内程度ですが、複数のメソッドから参照される可能性があり、型が変化した場合などの影響範囲が大きくなる可能性があります。型が明確な初期化式の場合は省略してもいいかもしれませんが、そうでなければ省略しない方がよいと思います。
public
class Hoge {
val hoge = ...
publicの場合は、どこから参照されるか分からないですし、もし型が変わってしまったらそれはインターフェースの変更という非常に影響度の大きな変更を意図せず行ってしまうリスクがあります。
結論
ざっくりまとめるとこんな感じでしょうか?
スコープ\初期化式 | リテラル・コンストラクタ | シンプルな式 | 複雑な式、メソッド |
---|---|---|---|
ローカル | 省略した方がよい | 省略した方がよい | どちらでも |
private | どちらでも | 省略しない | 省略しない |
public | 省略しない | 省略しない | 省略しない |
基本的にスコープが狭い場合や初期化式から型が明確になる場合は省略した方がよいでしょう。
逆にスコープが広かったり、初期式などから型が明確・容易に分からない場合(ジェネリクス含む)、型が変化すると困る場合は省略しない方がよいでしょう。
また、初期化式より抽象的な型であつかいたい場合など、省略すると意味が変わってしまう場合などは省略できないです。
何はともあれ、一律省略する、一律で省略しない、どちらか一辺倒ではなく、書いた方がよい場合、省略した方がよい場合、両方があるので、状況に応じて判断しましょうと言うことです。