Android
Kotlin
AndroidDay 9

GoogleによるAndroid Kotlin GuidesのStyle guideを読んでみる。

この記事は Android Advent Calendar 2017 9日目の記事です。

少し前に公開されたAndroid Kotlin Guides内の、Style Guideページを読んで、ざっくりまとめてみようと思います。
このガイドはGoogleによるKotlinでのAndroidコーディングスタンダード(コーディング規約)らしいです。
あくまでGoogleでの指針なので、必ずしもこのルールに全て従う必要はないと思いますが、一応公式に近い位置づけな気がするので、参考にはなるんじゃないでしょうか。

書いている途中で気が付いたのですが、全然Android関係なかったです。すみません。
とはいえ、昨今では多くのAndroidエンジニアがKotlinを使用し始めていると思うので、どうかご容赦を。。。

注意:これは英語で書かれた上記のスタイルガイドを@kiriminが勝手に噛み砕いて意訳したもので、内容が正確ではない可能性があります。英語が読める方は原文の方を読む事を強くオススメします。また、一部内容を省略していたりもします。あくまで忙しい人向けに概要を伝えるというくらいの温度感だと思っていただければ有り難いです。

ソースファイル

命名

・ソースファイルに含まれるのがトップレベルの一つのクラスのみであれば、クラス名+.ktとしてください。
・一つのソースファイルに複数のトップレベル宣言が含まれている場合、PascalCaseでファイルの内容を説明する名前とし、.ktを付けてください。

// Foo.kt
class Foo { }

// Bar.kt
class Bar { }
fun Runnable.toBar(): Bar = // …

// Map.kt
fun <T, O> Set<T>.map(func: (T) -> O): List<O> = // …
fun <T, O> List<T>.map(func: (T) -> O): List<O> = // …

構造

.ktファイルは以下のものを順に含みます。

1.コピーライト and/or ライセンスヘッダー (任意)
2.ファイルレベルアノテーション
3.パッケージ文
4.Import文
5.トップレベル宣言

import文

ワイルドカードimportは許容されません

トップレベル宣言

一つの.ktファイルには複数の型、関数、property、type-aliasesを含める事ができます。
ただし、ファイルのコンテンツは一つのテーマにフォーカスしてください。例えば一つのみのpublicな型や、複数の型への関連する拡張関数などです。
関係のない宣言が含まれる場合は2つファイルに分割し、一つのファイルを小さく保つようにしましょう。

ファイル内の具体的なコンテンツの数や順番は制限していません。

ソースファイルは普通上から下へと順番に読まれるため、通常上に宣言されたものほど理解されやすいでしょう。
異なるファイルではコンテンツの順番は異なる事があります。

重要なことは、各クラスが理由を聞かれた時に答えられるような一貫性のあるロジカルな順序をしている事です。例えば新しい関数はなんとなくクラスの一番下に追加しないこと、これはロジカルな順序ではありません。

クラスメンバーの順序

クラス内のメンバーもトップレベル宣言のルールに従います。

フォーマット

カッコ

elseの無いifや1行で済むif/whenのボディではカッコは必須ではありません。

if (string.isEmpty()) return

when (value) {
    0 -> return
    // …
}

他のあらゆるif,for,when分岐、doやwhile文ではボディが空や1行であってもカッコが必須です。

if (string.isEmpty())
    return  // WRONG!

if (string.isEmpty()) {
    return  // Okay
}

空じゃないブロック

空じゃないブロック構造のカッコはKernighan and Ritchie style (“Egyptian brackets”)に従います。

・開始カッコの前に改行をしない
・開始カッコの後に改行をする
・閉じカッコの前に改行をする
・閉じカッコの後に改行をする(elseなどが続かずブロック構造がそこで終了する場合のみ)

return Runnable {
    while (condition()) {
        foo()
    }
}

return object : MyClass() {
    override fun foo() {
        if (condition()) {
            try {
                something()
            } catch (e: ProblemException) {
                recover()
            }
        } else if (otherCondition()) {
            somethingElse()
        } else {
            lastThing()
        }
    }
}

空ブロック

空ブロックもKernighan and Ritchie styleに従います。

try {
    doSomething()
} catch (e: Exception) {} // WRONG!
try {
    doSomething()
} catch (e: Exception) {
} // Okay

if/else を式として使用する時のカッコは、一行で書く事が適している場合のみ省略が許容されます。

val value = if (string.isEmpty()) 0 else 1  // Okay
val value = if (string.isEmpty())  // WRONG!
                0
            else
                1
val value = if (string.isEmpty()) { // Okay
    0
} else {
    1
}

行ごとの要素

各要素は改行を伴う。セミコロンは使わない。

ワンラインでの改行

1行は100文字を上限とし改行します。ただし以下に書いたような場合は例外とする。全ての行は後述するルールに従います。

例外
・上限に従うことが明らかに適さない場合(KDoc内の長いURLなど)
package文やimport
・コメントに書かれた、コマンドラインへコピペする用のシェルコマンド

どこで改行するか

重要な指針はより高い構文レベルを改行位置として選ぶことです。

1.非代入処理で上限を超えた場合は、記号の前で改行します。
 ・.::が該当します。
2.代入処理の場合は、記号の後で改行します。
3.メソッドやコンストラクタでは続く開始カッコ(までを切り離さず、その後で改行します。
4.,は直前の要素と切り離さず、その後で改行します。
5.->は直前の引数リストと切り離さず、その後で改行します。

Note:改行ルールの最優先の目的はコードをクリアに保つ事です。行数を最小限に保つ事ではありません。

改行時のインデント

ワンラインでの改行時のインデントは、元のインデントから+8します。

関数

関数宣言を一行で書くのが適さない場合、各パラメーター宣言ごとに改行を入れます。
各パラメーターは+8インデントです。閉じカッコと返り値の型宣言はインデントを増やしません。

fun <T> Iterable<T>.joinToString(
        separator: CharSequence = ", ",
        prefix: CharSequence = "",
        postfix: CharSequence = ""
): String {
    // …
}

式関数

関数が1行で済む場合は式として関数を書く事が出来ます。

override fun toString(): String {
    return "Hey"
}
override fun toString(): String = "Hey"

式関数を改行するべきではありません。改行が必要になる場合は普通の関数宣言とreturnを使用するべきです。

プロパティ

プロパティの初期化が1行に収まらない場合、=の後に改行を入れます。

private val defaultCharset: Charset? =
        EncodingRegistry.getInstance().getDefaultCharsetForPropertiesFiles(file)

特別な構造

Enumクラス

関数を持たず、Docも必要のないEnumは1行で宣言します。

enum class Answer { YES, NO, MAYBE }

Enumの要素を改行して書く場合、要素にボディがある場合を除いて間に空行を挟む必要はありません。

enum class Answer {
    YES,
    NO,

    MAYBE {
        override fun toString() = """¯\_(ツ)_/¯"""
    }
}

Enumはクラスの一種なので、クラスのルールに従います。

命名

パッケージ名

パッケージ名は単語を切り離さないlowercaseです。(アンダースコアを使用しない)

// Okay
package com.example.deepspace
// WRONG!
package com.example.deepSpace
// WRONG!
package com.example.deep_space

型名

クラス名は一般的に名詞によるPascalCaseです。例えば、CharacterImmutableListのように。

インターフェイス名も大抵同じですが、形容詞である場合もあります。(例えば、Readable)

テストクラス名はテスト対象のクラスにTestを付けた名前にします。

関数名

関数名は一般的に動詞によるcamelCaseです。例えば、sendMessagestopのように。
アンダースコアはテスト関数では許容されます。

@Test fun pop_emptyStack() {
    // …
}

定数名

定数名には単語をアンダースコアで分割したUPPER_SNAKE_CASEを使用します。
しかし、定数とはなんでしょうか?

定数とは、valプロパティでカスタムされたget関数を持たないものです。定数は徹底してイミュータブルで、副作用を持ちません。
これにはイミュータブルな型やStringのようなイミュータブルな型のみを持ったイミュータブルなコレクションも含まれます。

const val NUMBER = 5
val NAMES = listOf("Alice", "Bob")
val AGES = mapOf("Alice" to 35, "Bob" to 32)
val COMMA_JOINER = Joiner.on(',') // Joiner is immutable
val EMPTY_ARRAY = arrayOf<SomeMutableType>()

それらの名前は一般的に名詞です。
定数はobject内かトップレベルにのみ宣言されるべきです。クラス内に定義された場合は定数ではない名前を使うべきです。

非定数名

インスタンスプロパティ、ローカルプロパティ、パラメータなどの定数じゃない名前にはcamelCaseを使用します。

val variable = "var"
val nonConstScalar = "non-const"
val mutableCollection: MutableSet<String> = HashSet()
val mutableElements = listOf(mutableInstance)
val mutableValues = mapOf("Alice" to mutableInstance, "Bob" to mutableInstance2)
val logger = Logger.getLogger(MyClass::class.java.name)
val nonEmptyArray = arrayOf("these", "can", "change")