本文最后更新于:1 个月前

Keywords: val、var、null、when、data class、lazy、目录结构、源文件名称、源文件组织、类布局、命名规则、代码格式、until

其实在前年还是什么时候,就已经把菜鸟教程里的 Kotlin 语言教程给看了一遍,结果由于疏于使用,忘了个一干二净。去年寒假学习安卓的时候,又大致把 kotlin 看了一遍。为了跟敲郭霖大神的开源项目趣图,今天打算再把 Kotlin 过一遍,顺便做个记录。

基本语法

变量

只读局部变量使用关键字 val 定义,只能为其赋值一次。
可重复赋值的变量使用 var 关键字

字符串模板

var a = 1
// 模板中的简单名称:
val s1 = "a is $a"

a = 2
// 模板中的任意表达式:
val s2 = "${s1.replace("is", "was")}, but now is $a"

空值与 null 检测

当某个变量的值可以为 null 的时候,必须在声明处的类型后添加 ? 来标识该引用可为空。

If not null 缩写

val files = File("Test").listFiles()
​
println(files?.size)

If not null and else 缩写

val files = File("Test").listFiles()
​
println(files?.size ?: "empty")

if null 执行一个语句

val values = ……
val email = values["email"] ?: throw IllegalStateException("Email is missing!")

在可能会空的集合中取第一元素

val emails = …… // 可能会是空集合
val mainEmail = emails.firstOrNull() ?: ""

if not null 执行代码

val value = ……
​
value?.let {
    …… // 代码会执行到此处, 假如data不为null
}

映射可空值(如果非空的话)

val value = ……
​
val mapped = value?.let { transformValue(it) } ?: defaultValue
// 如果该值或其转换结果为空,那么返回 defaultValue。

类型检测与自动类型转换

is 运算符检测一个表达式是否某类型的一个实例。 如果一个不可变的局部变量或属性已经判断出为某类型,那么检测后的分支中可以直接当作该类型使用,无需显式转换:

fun getStringLength(obj: Any): Int? {
    // `obj` 在 `&&` 右边自动转换成 `String` 类型
    if (obj is String && obj.length > 0) {
      return obj.length
    }

    return null
}

when 表达式(java switch)

fun describe(obj: Any): String =
    when (obj) {
        1          -> "One"
        "Hello"    -> "Greeting"
        is Long    -> "Long"
        !is String -> "Not a string"
        else       -> "Unknown"
    }

使用区间(range)

  • in
  • !in
  • .. step downTo

集合

使用 lambda 表达式来过滤(filter)与映射(map)集合:

val fruits = listOf("banana", "avocado", "apple", "kiwifruit")
fruits
  .filter { it.startsWith("a") }
  .sortedBy { it }
  .map { it.toUpperCase() }
  .forEach { println(it) }

习惯用法

创建 DTOs(POJOs/POCOs)

data class Customer(val name: String, val email: String)

会为 Customer 类提供一下功能:

  • 所有属性的 getters(对于 var 定义的还有 setters)
  • equals()
  • hasCode()
  • toString()
  • copy()
  • 所有属性的 component1()component2()……等等

过滤 list

val positives = list.filter { x -> x > 0 }

or

val positives = list.filter { it > 0 }

延迟属性

val p: String by lazy {
    // 计算该字符串
}

创建单例

object Resource {
    val name = "Name"
}

对一个对象实例调用多个方法 (with)

class Turtle {
    fun penDown()
    fun penUp()
    fun turn(degrees: Double)
    fun forward(pixels: Double)
}
​
val myTurtle = Turtle()
with(myTurtle) { // 画一个 100 像素的正方形
    penDown()
    for (i in 1..4) {
        forward(100.0)
        turn(90.0)
    }
    penUp()
}

配置对象的属性(apply)

val myRectangle = Rectangle().apply {
    length = 4
    breadth = 5
    color = 0xFAFAFA
}

这对于配置未出现在对象构造函数中的属性非常有用。

交换两个变量

var a = 1
var b = 2
a = b.also { b = a }

TODO():将代码标记为不完整

Kotlin 的标准库有一个 TODO() 函数,该函数总是抛出一个 NotImplementedError。 其返回类型为 Nothing,因此无论预期类型是什么都可以使用它。 还有一个接受原因参数的重载:

fun calcTaxes(): BigDecimal = TODO("Waiting for feedback from accounting")

IntelliJ IDEA 的 kotlin 插件理解 TODO() 的语言,并且会自动在 TODO 工具窗口中添加代码指示。

编码规范

源代码组织

源文件名称

如果 Kotlin 文件包含单个类(以及可能相关的顶层声明),那么文件名应该与该类的名称相同,并追加 .kt 扩展名。如果文件包含多个类或只包含顶层声明, 那么选择一个描述该文件所包含内容的名称,并以此命名该文件。 使用首字母大写的驼峰风格(例如 ProcessDeclarations.kt)。
文件的名称应该描述文件中代码的作用。因此,应避免在文件名中使用诸如“Util”之类的无意义词语。

源文件组织

鼓励多个声明(类、顶级函数或者属性)放在同一个 Kotlin 源文件中, 只要这些声明在语义上彼此紧密关联并且文件保持合理大小 (不超过几百行)。

特别是在为类定义与类的所有客户都相关的扩展函数时, 请将它们放在与类自身定义相同的地方。而在定义仅对指定客户有意义的扩展函数时,请将它们放在紧挨该客户代码之后。不要只是为了保存 “Foo 的所有扩展函数”而创建文件。

类布局

通常,一个类的内容按以下顺序排列:

  • 属性声明与初始化块
  • 次构造函数
  • 方法声明
  • 伴生对象

不要按字母顺序或者可见性对方法声明排序,也不要将常规方法与扩展方法分开。而是要把相关的东西放在一起,这样从上到下阅读类的人就能够跟进所发生事情的逻辑。选择一个顺序(高级别优先,或者相反)并坚持下去。

将嵌套类放在紧挨使用这些类的代码之后。如果打算在外部使用嵌套类,而且类中并没有引用这些类,那么把它们放到末尾,在伴生对象之后。

接口实现布局

在实现一个接口时,实现成员的顺序应该与该接口的成员顺序相同(如果需要, 还要插入用于实现的额外的私有方法)。

重载布局

在类中总是将重载放在一起。

命名规则

  • 包的名称总是小写且不使用下划线。如不必要,不要使用连词,如果确实需要使用多个词,可以将它们连接在一起或使用驼峰风格。
  • 类与对象的名称使用 Pascal 风格。

函数名

  • 函数、属性与局部变量的命名使用驼峰风格。(例外:用于创建类实例的工厂函数可以与抽象返回类型具有相同的名称。)
  • 当且仅当在测试中,可以使用反引号括起来的带空格的方法名。(Android 运行时暂不支持。)测试代码中也允许方法名使用下划线。
class MyTestCase {
     @Test fun `ensure everything works`() { /*...*/ }

     @Test fun ensureEverythingWorks_onAndroid() { /*...*/ }
}

属性名

  • 常量名称(标有 const 属性,或者保存不可变数据的没有自定义 get 函数的顶层/对象 val 属性)应该使用大写、下划线分隔的名称。
  • 保存带有行为的对象或者可变数据的顶层/对象属性的名称应该使用驼峰风格名称。
  • 保存单例对象引用的属性的名称可以使用与 object 声明相同的命名风格。
  • 对于枚举常量,可以使用大写、下划线分隔的名称 (enum class Color { RED, GREEN })也可使用首字母大写的常规驼峰名称,具体取决于用途。
  • 幕后属性的名称 如果一个类有两个概念上相同的属性,一个是公共 API 的一部分,另一个是实现细节,那么使用下划线作为私有属性名称的前缀。

选择好名称

  • 类的名称通常是用来解释类是什么的名词或者名词短语:ListPersonReader
  • 方法的名称通常是动词或动词短语,说明该方法做什么:closereadPersons。修改对象或者返回一个新对象的名称也应遵循建议。例如sort 是对一个集合就地排序,而 sorted 是返回一个排序后的集合副本。
  • 名称应该表明实体的目的是什么,所以最好避免在名称中使用无意义的单词(ManagerWrapper 等)。
  • 当使用首字母缩写作为名称的一部分时,如果缩写由两个字母组成,就将其大写(IOStream);而如果缩写更长一些,就只大写其首字母(XmlFormatterHttpInputStream)。

格式化

  • 使用 4 个空格缩进。不要使用tab。
  • 对于花括号,将左花括号放在结构起始处的行尾,而将右花括号放在与左括结构横向对齐的单独一行。

横向空白

  • 在二元操作符左右留空格(a + b)。例外:不要在“range to”操作符(0..i)左右留空格。
  • 不要在一元运算符左右留空格(a++
  • 在控制流关键字(ifwhenfor 以及 while)与相应的左括号之间留空格。
  • 不要在主构造函数声明、方法声明或者方法调用的左括号之前留空格。
  • 绝不在 ([ 之后或者 ]) 之前留空格。
  • 绝不在 . 或者 ?. 左右留空格:foo.bar().filter { it > 2 }.joinToString(), foo?.bar()
  • // 之后留一个空格:// 这是一条注释
  • 不要在用于指定类型参数的尖括号前后留空格: class Map<K, V> { …… }
  • 不要在 :: 前后留空格:Foo::classString::length
  • 不要在用于标记可空类型的 ? 前留空格:String?
  • 作为一般规则,避免任何类型的水平对齐。将标识符重命名为不同长度的名称不应该影响声明或者任何用法的格式。

冒号

在以下场景中的 : 之前留一个空格:

  • 当它用于分隔类型与超类型时;
  • 当委托给一个超类的构造函数或者同一类的另一个构造函数时;
  • object 关键字之后。

而当分隔声明与其类型时,不要在 : 之前留空格。

类头格式化

具有少数主构造函数参数的类可以写成一行:

class Person(id: Int, name: String)

具有较长类头的类应该格式化,以使每个主构造函数参数都在带有缩进的独立的行中。 另外,右括号应该位于一个新行上。如果使用了继承,那么超类的构造函数调用或者所实现接口的列表应该与右括号位于同一行:

class Person(
    id: Int,
    name: String,
    surname: String
) : Human(id, name) { /*……*/ }

对于多个接口,应该将超类构造函数调用放在首位,然后将每个接口应放在不同的行中:

class Person(
    id: Int,
    name: String,
    surname: String
) : Human(id, name),
    KotlinMaker { /*……*/ }

对于具有很长超类型列表的类,在冒号后面换行,并横向对齐所有超类型名:

class MyFavouriteVeryLongClassHolder :
    MyLongHolder<MyFavouriteVeryLongClass>(),
    SomeOtherInterface,
    AndAnotherOne {

    fun foo() { /*...*/ }
}

为了将类头与类体分隔清楚,当类头很长时,可以在类头后放一空行 (如上例所示)或者将左花括号放在独立行上:

class MyFavouriteVeryLongClassHolder :
    MyLongHolder<MyFavouriteVeryLongClass>(),
    SomeOtherInterface,
    AndAnotherOne
{
    fun foo() { /*...*/ }
}

修饰符

如果一个声明有多个修饰符,按照以下顺序安放:

public / protected / private / internal
expect / actual
final / open / abstract / sealed / const
external
override
lateinit
tailrec
vararg
suspend
inner
enum / annotation
companion
inline
infix
operator
data

将所有注解放在修饰符前:

@Named("Foo")
private val foo: Foo

尽量省略多余的修饰符(例如 public )。

注解格式化

  • 注解通常放在单独的行上,在它们所依附的声明之前,并使用相同的缩进。
  • 无参数的注解可以放在同一行。
  • 无参数的单个注解可以与相应的声明放在同一行。

文件注解

文件注解位于文件注释(如果有的话)之后、package 语句之前,并且用一个空白行与 package 分开(为了强调其针对文件而不是包)。

函数格式化

  • 如果函数签名不适合单行,请使用以下语法:
fun longMethodName(
    argument: ArgumentType = defaultValue,
    argument2: AnotherArgumentType
): ReturnType {
    // 函数体
}
  • 函数参数使用常规缩进(四个空格)。
  • 对于由单个表达式构成的函数体,优先使用表达式形式。

表达式函数体格式化

如果函数的表达式函数体与函数声明不适合放在同一行,那么将 = 留在第一行。 将表达式函数体缩进 4 个空格。

属性格式化

  • 对于非常简单的只读属性,考虑单行格式。val isEmpty: Boolean get() = size == 0
  • 对于更复杂的属性,总是将 getset 关键字放在不同的行上。
  • 对于具有初始化器的属性,如果初始化器很长,那么在等号后增加一个换行并将初始化器缩进四个空格。

格式化控制流语句

  • 如果 ifwhen 语句的条件有多行,那么在语句体外边总是使用大括号。 将该条件的每个后续行相对于条件语句起始处缩进 4 个空格。 将该条件的右圆括号与左花括号放在单独一行。
  • elsecatchfinally 关键字以及 do/while 循环的 while 关键字与之前的花括号放在相同的行上。
  • when 语句中,如果一个分支不止一行,可以考虑用空行将其与相邻的分支块分开,将短分支放在与条件相同的行上,无需花括号。

方法调用格式化

在较长参数列表的左括号后添加一个换行符。按 4 个空格缩进参数。 将密切相关的多个参数分在同一行,在分隔参数名与值的 = 左右留空格。

链式调用换行

  • 当对链式调用换行时,将 . 字符或者 ?. 操作符放在下一行,并带有单倍缩进。
  • 调用链的第一个调用通常在换行之前,当然如果能让代码更有意义也可以忽略这点。

Lambda 表达式格式化

  • 在 lambda 表达式中,应该在花括号左右以及分隔参数与代码体的箭头左右留空格。 如果一个调用接受单个 lambda 表达式,应该尽可能将其放在圆括号外边传入。
  • 如果为 lambda 表达式分配一个标签,那么不要在该标签与左花括号之间留空格。
  • 在多行的 lambda 表达式中声明参数名时,将参数名放在第一行,后跟箭头与换行符。
appendCommaSeparated(properties) { prop ->
    val propertyValue = prop.get(obj)  // ……
}
  • 如果参数列表太长而无法放在一行上,请将箭头放在单独一行。

文档注释

  • 对于较长的文档注释,将开头 /** 放在一个独立行中,并且后续每行都以性好开头。
  • 简短注释可以放在一行内。
  • 通常,避免使用 @param@return 标记。而是将参数与返回值的描述直接合并到文档注释中,并在提到参数的任何地方加上参数链接。 只有当需要不适合放进主文本流程的冗长描述时才应使用 @param@return

避免重复结构

一般来说,如果 Kotlin 中的某种语法结构是可选的并且被 IDE 高亮为冗余的,那么应该在代码中省略之。为了清楚起见,不要在代码中保留不必要的语法元素 。

  • 将简单变量传入到字符串模版中时不要使用花括号。只有用到更长表达式时才使用花括号。

语言特性的惯用法

不可变性

  • 优先使用不可变(而不是可变)数据。初始化后未修改的局部变量与属性,总是将其声明为 val 而不是 var
  • 总是使用不可变集合接口(Collection, List, Set, Map)来声明无需改变的集合。使用工厂函数创建集合实例时,尽可能选用返回不可变集合类型的函数。

默认参数值

优先声明带有默认参数的函数而不是声明重载函数。

类型别名

如果有一个在代码库中多次用到的函数类型或者带有类型参数的类型,那么最好为它定义一个类型别名:

typealias MouseClickHandler = (Any, MouseEvent) -> Unit
typealias PersonIndex = Map<String, Person>

Lambda 表达式

在简短、非嵌套的 lambda 表达式中建议使用 it 用法而不是显式声明参数。而在有参数的嵌套 lambda 表达式中,始终应该显式声明参数。

在 lambda 表达式中返回

避免在 lambda 表达式中使用多个返回到标签。请考虑重新组织这样的 lambda 表达式使其只有单一退出点。 如果这无法做到或者不够清晰,请考虑将 lambda 表达式转换为匿名函数。

不要在 lambda 表达式的最后一条语句中使用返回到标签。

具名参数

当一个方法接受多个相同的原生类型参数或者多个 Boolean 类型参数时,请使用具名参数语法, 除非在上下文中的所有参数的含义都已绝对清楚。

使用条件语句

优先使用 tryifwhen 的表达形式。例如:

return if (x) foo() else bar()

return when(x) {
    0 -> "zero"
    else -> "nonzero"
}

if 还是 when

二元条件优先使用 if 而不是 when
如果有三个或多个选项时优先使用 when

在条件中使用可空的 Boolean 值

如果需要在条件语句中用到可空的 Boolean, 使用 if (value == true)if (value == false) 检测。

使用循环

优先使用高阶函数(filtermap 等)而不是循环。例外:forEach(优先使用常规的 for 循环, 除非 forEach 的接收者是可空的或者 forEach 用做长调用链的一部分。)

当在使用多个高阶函数的复杂表达式与循环之间进行选择时,请了解每种情况下所执行操作的开销并且记得考虑性能因素。

区间上循环

使用 until 函数在一个开区间上循环:

for (i in 0..n - 1) { /*……*/ }  // 不良
for (i in 0 until n) { /*……*/ }  // 良好

使用字符串

  • 优先使用字符串模板而不是字符串拼接。
  • 优先使用多行字符串而不是将 \n 转义序列嵌入到常规字符串字面值中。
  • 如需在多行字符串中维护缩进,当生成的字符串不需要任何内部缩进时使用 trimIndent,而需要内部缩进时使用 trimMargin

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!

Kotlin的学习(二)——基础 上一篇
进程和线程 下一篇