Kotlin 樣式指南

本文件完整定義了對於以 Kotlin 程式設計語言編寫的原始碼,Google 所採用的 Android 程式設計標準。只有在遵循本文所列規則的情況下,Kotlin 來源檔案才能稱為符合 Google Android 樣式。

如同其他程式設計樣式指南一樣,本指南不僅涵蓋了格式美學的問題,也涉及其他類型的慣例或程式設計標準。不過,本文主要說明我們普遍遵守的固定規則,並避免提供無法清楚透過人工/工具實行的建議。

上次更新時間:2023 年 9 月 6 日

來源檔案

所有來源檔案都必須以 UTF-8 格式編碼。

命名

如果來源檔案只包含一個頂層類別,則檔案名稱必須區分大小寫,並加上 .kt 副檔名。在其他情況下,如果來源檔案包含多個頂層宣告,請選擇能說明檔案內容的名稱、套用首字母大寫拼法 (若是複數檔案名稱則可使用駝峰式大小寫),並加上 .kt 副檔名。

// MyClass.kt
class MyClass { }
// 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> = // …
// extensions.kt
fun MyClass.process() = // …
fun MyResult.print() = // …

特殊字元

空白字元

除了行結束字元序列以外,ASCII 水平空格字元 (0x20) 是出現在來源檔案中任何位置的唯一空白字元。這表明:

  • 字串和字元常值中的所有其他空白字元都已逸出。
  • 分頁字元「不會」用於縮排。

特殊逸出序列

對於任何含有特殊逸出序列的字元 (\b\n\r\t\'\"\\\$),系統會使用該序列,而不是對應的萬國碼 (Unicode) (例如 \u000a) 逸出。

非 ASCII 字元

針對其餘非 ASCII 字元,系統會使用實際的萬國碼 (Unicode) 字元 (例如 ) 或對等的萬國碼 (Unicode) 逸出 (例如 \u221e)。如何選擇,僅取決於哪一項使程式碼更容易讀取及理解。我們不建議對任何位置的可顯示字元使用萬國碼 (Unicode) 逸出,而且強烈建議不要在字串常值和註解以外使用這類逸出。

範例 討論
val unitAbbrev = "μs" 最佳:即使沒有註解,也十分清楚。
val unitAbbrev = "\u03bcs" // μs 差:無故將逸出與可顯示字元搭配使用。
val unitAbbrev = "\u03bcs" 差:讀者不明白這是什麼。
return "\ufeff" + content 好:對不可列印字元使用逸出,並視需要加註。

結構

.kt 檔案依序包含下列各項:

  • 版權和/或授權標頭 (選填)
  • 檔案層級註解
  • 套件陳述式
  • 匯入陳述式
  • 頂層宣告

每個部分用一個空白行分隔符。

如果檔案中含有版權或授權標頭,則應放在多行註解的最頂端。

/*
 * Copyright 2017 Google, Inc.
 *
 * ...
 */
 

請勿使用 KDoc 樣式或單行樣式註解。

/**
 * Copyright 2017 Google, Inc.
 *
 * ...
 */
// Copyright 2017 Google, Inc.
//
// ...

檔案層級註解

含有「file」使用場目標的註解會放在任何標頭註解和套件宣告之間。

套件陳述式

套件陳述式不受任何欄限制,也不會自動換行。

匯入陳述式

類別、函式和屬性的匯入陳述式會彙整成一份清單,並以 ASCII 排序。

不得匯入任何類型的萬用字元。

與套件陳述式類似,匯入陳述式不受欄限制,而且一律不會自動換行。

頂層宣告

.kt 檔案可在頂層宣告一或多個類型、函式、屬性或類型別名。

檔案內容應聚焦於單一主題。例如,主題可以是單一公開類型,或對多個接收器類型執行相同作業的一組擴充功能函式。無關聯的宣告應以各自的檔案分隔,且單一檔案中的公開宣告應盡可能最小化。

沒有對檔案內容的數量和順序設立明確的限制。

來源檔案通常從上到下讀取,意味著順序一般會反映出,離頂層較近的宣告有助於理解離頂層較遠的宣告。不同的檔案可能會選擇以不同順序排列內容。同樣,一份檔案可能會包含 100 個屬性、另外 10 個函式和另一個類別。

重要的是,每份檔案都使用某種邏輯順序,其維護者可藉由此順序來解釋檔案。例如,新函式並非只是習慣地在檔案末端加入,因為這會導致「依新增日期排序」,而非依邏輯排序。

類別成員排序

類別中成員的順序採用與頂層宣告相同的規則。

格式設定

大括號

when 分支版本,以及具有不超過一個 else 分支版本和僅占一行的 if 運算式,都不需要大括號。

if (string.isEmpty()) return

val result =
    if (string.isEmpty()) DEFAULT_VALUE else string

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

所有 ifforwhen 分支版本、dowhile 陳述式和運算式都需要大括號,即使主體為空白或只包含單一陳述式也一樣。

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

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

if (string.isEmpty()) return  // WRONG
else doLotsOfProcessingOn(string, otherParametersHere)

if (string.isEmpty()) {
    return  // Okay
} else {
    doLotsOfProcessingOn(string, otherParametersHere)
}

非空白區塊

在非空白區塊和區塊式建構中,大括號遵循 Kernighan 和 Ritchie 樣式 (「埃及大括號」):

  • 左大括號前面不換行。
  • 左大括號後面換行。
  • 右大括號前面換行。
  • 「僅在」右大括號終止陳述式,或終止函式、建構函式或「已命名」類別的主體時,才在右大括號後面換行。例如,如果右大括號後面有 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()
        }
    }
}

以下為列舉類別的幾個例外狀況。

空白區塊

空白區塊或區塊式結構必須採用 K&R 樣式。

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
}

縮排

每當新區塊或區塊式結構開啟時,縮排才會增加四個空格。區塊結束後,縮排會回到先前的縮排層級。縮排層級會套用至整個區塊內的程式碼和註解。

每行採用一個陳述式

每個陳述式後面都要換行。請勿使用分號。

換行

程式碼的欄限制為 100 個字元。如下所述,除下文另有說明外,任何超過這個限制的行都必須換行。

例外狀況:

  • 無法遵守欄限制的行,例如 KDoc 中的長網址
  • packageimport 陳述式
  • 註解中的指令列 (可剪下及貼到殼層內)

換行位置

換行的最佳指令是:傾向於在較高的語法層級中換行。另外:

  • 在運算子或中置函式名稱中換行時,應在運算子或中置函式名稱後面換行。
  • 在以下「類似運算子的」符號中換行時,應在符號前面換行:
    • 點分隔符 (.?.)。
    • 成員參考資料的兩個冒號 (::)。
  • 方法或建構函式名稱會附在後面的左括號 (() 上。
  • 半形逗號 (,) 會附在其前面的權杖上。
  • lambda 箭頭 (->) 會附在其前面的引數清單上。

函式

如果函式簽名無法只占一行,各項參數宣告應各占一行。採用這種格式的參數必須使用一個縮排 (+4)。右括號 ()) 與傳回類型獨占一行,且不加入其他縮排。

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

函式如果只包含單一運算式,就能以運算式函式表示。

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

屬性

當屬性初始設定程式無法只占一行時,請在等號 (=) 後面換行,並使用縮排。

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

宣告 get 和/或 set 函式的屬性應各占一行,並加上正常縮排 (+4)。請使用與函式相同的規則設定格式。

var directory: File? = null
    set(value) {
        // …
    }
唯讀屬性可使用只占一行的較短語法。
val defaultExtension: String get() = "kt"

空白字元

垂直

系統會在以下位置顯示單一空白行:

  • 在類別的連續成員「之間」:屬性、建構函式、函式、巢狀類別等。
    • 例外狀況:可選擇在兩個連續屬性 (中間沒有其他程式碼) 之間加入空白行。您可以視需要使用這類空白行來建立屬性邏輯分組,並將屬性與支援屬性 (如果有) 建立關聯。
    • 例外狀況:下文介紹了列舉常數之間的空白行。
  • 「可視需要」在陳述式之間顯示,用於程式碼整理成邏輯的子區段。
  • 「可選擇」在函式中第一個陳述式之前、類別的第一個成員之前或類別的最後一個成員之後加入空白行 (並非建議做法,也不是應避免的做法)。
  • 依照本文其他各節 (例如「結構」一節) 的要求加入空白行。

系統允許使用多個連續的空白行,但我們不鼓勵或要求這麼做。

水平

除了語言或其他樣式規則有所要求,以及除了常值、註解和 KDoc 外,只有下列位置會顯示單個 ASCII 空格:

  • 用於分隔任何保留字 (例如ifforcatch) 與同一行上在它後面的左括號 (()。
    // WRONG!
    for(i in 0..1) {
    }
    
    // Okay
    for (i in 0..1) {
    }
    
  • 用於分隔任何保留字 (例如 elsecatch) 與同一行上在它前面的右大括號 (})。
    // WRONG!
    }else {
    }
    
    // Okay
    } else {
    }
    
  • 顯示在左大括號 ({) 前方。
    // WRONG!
    if (list.isEmpty()){
    }
    
    // Okay
    if (list.isEmpty()) {
    }
    
  • 顯示在任何二元運算子的兩側。
    // WRONG!
    val two = 1+1
    
    // Okay
    val two = 1 + 1
    
    這也適用於下列「類似運算子」的符號:
    • lambda 運算式中的箭頭 (->)。
      // WRONG!
      ints.map { value->value.toString() }
      
      // Okay
      ints.map { value -> value.toString() }
      
    但不適用於以下符號:
    • 成員參照的兩個冒號 (::)。
      // WRONG!
      val toString = Any :: toString
      
      // Okay
      val toString = Any::toString
      
    • 點分隔符 (.)。
      // WRONG
      it . toString()
      
      // Okay
      it.toString()
      
    • 範圍運算子 (..)。
      // WRONG
      for (i in 1 .. 4) {
        print(i)
      }
      
      // Okay
      for (i in 1..4) {
        print(i)
      }
      
  • 只有用於指定基礎類別或介面的類別宣告,或用於泛型條件約束where 子句中時,才會顯示在冒號 (:) 前方。
    // WRONG!
    class Foo: Runnable
    
    // Okay
    class Foo : Runnable
    
    // WRONG
    fun <T: Comparable> max(a: T, b: T)
    
    // Okay
    fun <T : Comparable> max(a: T, b: T)
    
    // WRONG
    fun <T> max(a: T, b: T) where T: Comparable<T>
    
    // Okay
    fun <T> max(a: T, b: T) where T : Comparable<T>
    
  • 顯示在半形逗號 (,) 或冒號 (:) 後方。
    // WRONG!
    val oneAndTwo = listOf(1,2)
    
    // Okay
    val oneAndTwo = listOf(1, 2)
    
    // WRONG!
    class Foo :Runnable
    
    // Okay
    class Foo : Runnable
    
  • 顯示在行末註解開頭雙斜線 (//) 的兩側。此處可以使用多個空格,但並非必要。
    // WRONG!
    var debugging = false//disabled by default
    
    // Okay
    var debugging = false // disabled by default
    

這條規則絕不可解釋為要求或禁止在行的開頭或結尾插入其他空格;它只用於規定內部空格。

特定建構

列舉類別

列舉如果沒有函式和關於其常數的說明文件,則可選擇採用單行的格式。

enum class Answer { YES, NO, MAYBE }

如果列舉中的常數是放置在單獨的行中,除非用於定義主體,否則不需要在這些常數之間插入空白行。

enum class Answer {
    YES,
    NO,

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

由於列舉類別是類別,因此同樣適用其他用於設定類別格式的規則。

註解

成員或類型註解應獨立成行,放在加註的建構前面。

@Retention(SOURCE)
@Target(FUNCTION, PROPERTY_SETTER, FIELD)
annotation class Global

不含引數的註解可獨占一行。

@JvmField @Volatile
var disposable: Disposable? = null

如果只有一個不含引數的註解,系統會將該註解放在與宣告相同的行內。

@Volatile var disposable: Disposable? = null

@Test fun selectAll() {
    // …
}

@[...] 語法只能搭配明確的使用場目標使用,而且只用於合併在一行上的 2 個或以上不含引數的註解。

@field:[JvmStatic Volatile]
var disposable: Disposable? = null

隱式傳回/屬性類型

如果運算式函式主體或屬性初始設定程式是純量值,或可從主體中明確推導傳回類型,則可予省略。

override fun toString(): String = "Hey"
// becomes
override fun toString() = "Hey"
private val ICON: Icon = IconLoader.getIcon("/icons/kotlin.png")
// becomes
private val ICON = IconLoader.getIcon("/icons/kotlin.png")

編寫程式庫時,如果明確的類型宣告為公用 API 的一部分,則請保留該宣告。

命名

ID 只能使用 ASCII 字母和數字,而且在下文所述的少數情況下會加上底線。因此,每個有效 ID 名稱都會以規則運算式 \w+ 比對。

除了幕後屬性 (請參閱「幕後屬性」) 外,系統不會使用特殊前置字元或後置字元,比如範例 name_mNames_namekName 中的相關字元。

套件名稱

套件名稱全為小寫,連續字詞會串連在一起 (沒有底線)。

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

類型名稱

類別名稱以 PascalCase 首字母大寫拼法編寫,通常是名詞或名詞片語。例如,CharacterImmutableList。介面名稱也可能是名詞或名詞片語 (例如 List),但有時也可以是形容詞或形容詞片語 (例如 Readable)。

測試類別的名稱會以所測試的類別的名稱開頭,並以 Test 結尾。例如,HashTestHashIntegrationTest

函式名稱

函式名稱會以 camelCase 駝峰式大小寫拼法編寫,通常為動詞或動詞片語。例如,sendMessagestop

測試函數名稱中可加上底線,以分隔名稱中的邏輯元件。

@Test fun pop_emptyStack() {
    // …
}

加註 @Composable 且傳回 Unit 的函式皆採用大駝峰式命名法,並以名詞形式命名,就像這些函式是類型一樣。

@Composable
fun NameTag(name: String) {
    // …
}

函式名稱不應包含空格,因為每個平台都不支援空格 (特別是 Android 不完全支援空格)。

// WRONG!
fun `test every possible case`() {}
// OK
fun testEveryPossibleCase() {}

常數名稱

常數名稱採用 UPPER_SNAKE_CASE:所有字母皆為大寫英文字母,並以底線分隔字詞。不過,到底什麼「是」常數?

常數是不含自訂 get 函式的 val 屬性,其內容基本上不可變更,其函式沒有可偵測的副作用。這包括不可變的類型、不可變類型的集合,以及標示為 const 的純量和字串。如果執行個體的任何可觀測狀態變更,它就不是常數。只是刻意不改變物件並不足夠。

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()

這些名稱通常是名詞或名詞片語。

常數值只能在 object 內定義,或用做頂層宣告。值如果符合常數的要求,但在 class 內定義,則必須使用非常數名稱。

做為純量值的常數必須使用 const修飾符

非常數名稱

非常數名稱以 camelCase 駝峰式大小寫拼法編寫。這些名稱適用於執行個體屬性、本機屬性和參數名稱。

val variable = "var"
val nonConstScalar = "non-const"
val mutableCollection: MutableSet = 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")

這些名稱通常是名詞或名詞片語。

支援屬性

需要支援屬性時,其名稱必須和實際屬性的名稱完全比對,但前面加上底線的實際屬性名稱除外。

private var _table: Map? = null

val table: Map
    get() {
        if (_table == null) {
            _table = HashMap()
        }
        return _table ?: throw AssertionError()
    }

類型變數名稱

每個類型變數都會以下列任一樣式命名:

  • 單個大寫字母,後面可加上一個數字 (例如 ETXT2)
  • 類別所用形式中的名稱,後面加上大寫字母 T (例如 RequestTFooBarT)

駝峰式大小寫

有時候,將英文片語轉換為駝峰式大小寫的合理方法不止一種,例如縮寫「IPv6」或「iOS」等縮寫或特殊的建構。請使用以下配置來提升可預測性。

從名稱的散式著手:

  1. 將片語轉換為純 ASCII,並移除任何所有格號。 例如,「Müller 的演算法」可能變成「Muellers 演算法」。
  2. 將這個結果分成多個字詞,並在空格和其餘任何標點符號 (通常是連字號) 處分割。「建議」:如果任何字詞在常見用法中已有慣用的駝峰式大小寫樣式,請將字詞分成各個組成部分 (例如「AdWords」會變成「ad words」)。請注意,「iOS」等字詞本身並非實際採用駝峰式大小寫;它並未遵守任何慣例,因此這項建議不適用。
  3. 現在,請全部改為小寫字母 (包括縮寫),然後採取下列任一做法:
    • 以大寫表示每個字詞的第一個字元,呈現大駝峰式命名法的樣式。
    • 除了第一個字詞外,以大寫表示每個字詞的第一個字元,呈現小駝峰式命名法的樣式。
  4. 最後,將所有字詞加入同一個 ID 中。

請注意,原字詞的大小寫幾乎完全忽略了。

散式 正確 錯誤
「XML Http Request」 XmlHttpRequest XMLHTTPRequest
「new customer ID」 newCustomerId newCustomerID
「inner stopwatch」 innerStopwatch innerStopWatch
「supports IPv6 on iOS」 supportsIpv6OnIos supportsIPv6OnIOS
「YouTube importer」 YouTubeImporter YoutubeImporter*

(* 可接受,但不建議使用。)

說明文件

格式設定

KDoc 區塊的基本格式設定見此範例:

/**
 * Multiple lines of KDoc text are written here,
 * wrapped normally…
 */
fun method(arg: String) {
    // …
}

...或見此單行範例:

/** An especially short bit of KDoc. */

系統始終可接受這種基本格式。如果整個 KDoc 區塊 (包括註解標記) 可在一行內顯示,就可以替換單行格式。請注意,這個選項僅適用於沒有區塊標記 (例如 @return) 的情況。

段落

段落之間和區塊標記群組 (如果有) 之前會顯示一條空白行,也就是僅含對齊的前導星號 (*) 的行。

區塊標記

使用的任何標準「區塊標記」都會以 @constructor@receiver@param@property@return@throws@see 的順序顯示,且這些標記絕對不會與空白說明一起顯示。如果區塊標記不能只占一行,連續行會從 @ 的位置縮排 4 個空格。

匯總片段

每個 KDoc 區塊都以簡短的匯總片段開頭。這個片段非常重要:它是出現在特定結構定義 (例如類別和方法索引) 中文字的唯一部分。

這是一個片段,也就是名詞片語或動詞片語,而不是完整的句子。它不以「A `Foo` is a...」或「This method returns...」開頭,也未構成完整的祈使句,例如「Save the record.」。不過,這個片段使用大寫字母和標點符號,就像是完整的句子一樣。

使用方式

至少每種 public 類型以及該類型的每個 publicprotected 成員都有 KDoc,但下文列出了幾個例外狀況。

例外狀況:一目了然的函式

對於 getFoo 等「簡單、明確」的函式和 foo 等屬性,「KDoc」是可選的。在這些情況中,除了「傳回 foo」外,真的沒有任何要說的內容。

引用例外狀況,證明省略一般讀者可能需要知道的相關資訊的合理性,這並不適當。例如,對於名為 getCanonicalName 的函式或名為 canonicalName 的屬性,如果一般讀者可能不知道「標準化名稱」這個字詞的意涵,則不要忽略其說明文件 (理由是它只寫著 /** Returns the canonical name. */)!

例外狀況:覆寫

KDoc 不一定會出現在覆寫超級類型方法的方法中。