跳过正文
  1. 程序笔记/

Kotlin 浅尝笔记

·5891 字·12 分钟·
Kotlin
NaCl - 摸鱼中
作者
NaCl - 摸鱼中
我将堕入厨房,换你一顿饱饭
目录
Kotlin 入门备忘

变量
#

常用数据类型
#

数据类型可包含的数据类型字面量值示例
String文本“Add contact”
Int整数1293490、-59281
Double小数501.0292、-31723.99999
Float小数,数字末尾带有 f 或 F-1630.209f、1.2940278F
Booleantrue 或 false,这俩属于关键字true、false

更多数据类型和范围等信息,参阅 Basic types - Kotlin官网

声明和使用
#

在声明变量时,需要立即为其赋值。数据类型可以不写,会自动确定。用 val 声明不能改变值的变量,用 var 声明可改值的变量。

val 不变的变量: 数据类型 = 值

var 变量名: 数据类型 = 值

var x = 6

以下代码作为实例,演示了变量声明、两种变量调用方式、自增、注释、转义字符等:

val count: Int = 10
println("你有 $count 条信息未读")

// 可以使用自增
count++
count--

/**
 * 小驼峰命名
 * 数据类型不写出也行,会自动确定
 */
val unreadCount = 5
val readCount = 100
println("你一共有 ${unreadCount + readCount} 条信息")

// 转义
println("Say \"hello\"")

// 拼接
val nextMeeting = "Next meeting is:"
val date = "January 1"
val reminder = nextMeeting + date
println(reminder)
// 输出效果 Next meeting is:January 1

函数
#

没有返回值的函数不用写第一行的返回值类型。

形参的值不能改,它相当于用 val 开头声明的。

fun 函数名(abc: Int, 参数2: 参数2类型): 返回值类型 {
    abc = 6 // 这么写会报错,因为形参的值不能改
    函数体
    return 返回值 //没返回值的函数不用写
} 

函数签名
#

形参(以英文逗号分隔)有时称为形参列表。

函数名称及其输入(形参)统称为「函数签名」。函数签名包含返回值类型前面的所有内容,如以下代码段所示。

fun birthdayGreeting(name: String, age: Int)

默认实参
#

fun birthdayGreeting(name: String = "Rover", age: Int): String {
    return "Happy Birthday, $name! You are now $age years old!"
}

具名实参
#

调用函数时添加了形参名称,该名称就称为具名实参。

println(birthdayGreeting(name = "Rex", age = 2))

条件控制
#

比较运算符:==<><=>=!=

if
#

if (abc == "Red") {
    // ***
} else if (i >= 0) {
    // ***
} else {
    // ***
}

when
#

如果需要考虑的分支数量超过两个,应首选使用 when 语句。

when

val trafficLightColor = "Black"

when (trafficLightColor) {
    "Red" -> println("Stop")
    "Yellow" -> println("Slow")
    "Green" -> println("Go")
    // when 支持 else 语句
    else -> println("Invalid traffic-light color")
}

逗号分隔处理多个条件
#

val x = 3

when (x) {
    2, 3, 5, 7 -> println("质数")
    else -> println("不是")
}

使用 in 关键字处理一系列条件
#

val x = 4

when (x) {
    2, 3, 5, 7 -> println("质数")
    in 1..10 -> println("不是")
    else -> println("不在 10 以内")
}

使用 is 关键字检查数据类型
#

以下内容会输出 整数,但不在 10 以内

val x: Any = 20

when (x) {
    2, 3, 5, 7 -> println("质数")
    in 1..10 -> println("不是")
    is Int -> println("整数,但不在 10 以内")
    else -> println("不是整数")
}

使用 if/else 和 when 作为表达式
#

val aaaaa =
    if (x == 1) "壹"
    else if (x == 5) "贰"
    else "其它"

val bbbbb = when(x) {
    1 -> "壹"
    2, 3 -> "贰、叁"
    else -> "其它"
}

null 相关
#

本节参考自: 在 Kotlin 中使用可为 null 性 ,墙裂建议阅读原文!

在 Kotlin 中,有可为 null 类型与不可为 null 类型之分:

  • 可为 null 类型是指可以存储 null 值的变量。
  • 不可为 null 类型是指不能存储 null 值的变量。
var x: String = "abc123"
x = null    // 报错

如需声明可为 null 的变量,需要在相应类型的末尾添加 ? 运算符。否则,Kotlin 编译器会推断该变量属于不可为 null 类型。例如,String? 类型可以存储字符串或 null,而 String 类型只能存储字符串。

var x: String? = "abc123"
println(x)  // 输出 abc123

x = null
println(x)  // 输出 null

使用 ?. 安全调用运算符
#

在此示例中,系统不允许直接引用 x 变量的 length 属性,因为该变量有可能是 null,因此代码在编译时失败。可以使用 ?. 安全调用运算符访问可为 null 变量的方法或属性。

var x: String? = "abc123"
println(x.length)   // 报错
println(x?.length)   // 不报错

使用 !! 非 null 断言运算符
#

使用 !! 非 null 断言运算符,即表示您断言变量的值不是 null,无论变量是否为该值都是如此。

?. 安全调用运算符不同,当可为 null 的变量确实为 null 时,使用 !! 非 null 断言运算符可能会导致系统抛出 NullPointerException 错误。

var x: String? = null
println(x!!.length)   // 报错,NullPointerException

除非确定变量不为 null,否则不建议使用 !! 非 null 断言运算符。只有在变量始终为不可为 null 或设置了适当的异常处理时,才应使用该断言运算符。

使用 if/else 执行 null 检查
#

如果有多行代码使用可为 null 的变量,那么将 null 检查与 if 条件搭配使用会更方便。相比之下,?. 安全调用运算符更适用于对可为 null 变量的单次引用。

var x: String? = null

if (x != null) {
    println("字符串长度为${x.length}")
} else {
    println("null")
}

val length = if(x != null) {
    x.length
} else {
    0
}
println("字符串长度为$length")

使用 ?: Elvis 运算符
#

?: Elvis 运算符可以与 ?. 安全调用运算符搭配使用。如果搭配使用 ?: Elvis 运算符,您便可以在 ?. 安全调用运算符返回 null 时添加默认值。这与 if/else 表达式类似,但更为常用。

val x: String? = "abc123"

val length = x?.length ?: 0

println("字符串长度为$length")

类和对象
#

class Box {
    // 类属性
    val name = "a black box"
    var status = "close"

    // 类方法
    fun openBox() {
        println("打开盒子")
    }

    fun test() {
        openBox()   // 调用类方法
        println("test")
    }
}

fun main() {
    val box = Box()   // 创建类的实例
    println("name = ${box.name}")    // 属性
    box.openBox()  // 调用类方法
}

属性中的 getter 和 setter 函数
#

定义可变属性的完整语法是以变量定义开头,后跟可选的 get()set() 函数。

如果没有为属性定义 gettersetter 函数,Kotlin 编译器会在内部创建这些函数。

val 类型的变量为只读变量,因此不含 set() 函数。

var myAge = 18
    get() = field
    set(value) {
        field = value
    }

Kotlin 属性使用后备字段在内存中存储值。从根本上来讲,后备字段是在属性内部定义的类变量。后备字段的作用域限定为相应属性,这意味着您只能通过 get()set() 属性函数访问该字段。

如果想读取 get() 函数中的属性值或更新 set() 函数中的值,您需要使用对应属性的后备字段。该字段是由 Kotlin 编译器自动生成,并通过 field 标识符来引用。

警告:请勿使用属性名称来获取或设置值。例如,在 set() 函数中,如果您尝试将 value 形参赋给 myAge 属性本身,代码就会进入无限循环,因为 Kotlin 运行时会尝试更新 myAge 属性的值,而不断触发对 setter 函数的调用。

构造函数
#

Kotlin 编译器会自动生成默认构造函数。默认构造函数不含形参。定义默认构造函数如下:

class Box constructor() {
    ...
}

代码可简化。如果构造函数中没有任何注解可见性修饰符,可以移除 constructor 关键字。如果构造函数中没有任何形参,您还可以移除圆括号。如下:

class Box {
    ...
}

形参化构造函数
#

class Box(val name: String) {
    ...
}

辅助构造函数
#

Kotlin 中的构造函数主要有两类:

  • 主要构造函数:一个类只能有一个主要构造函数(在类标头中定义)。主要构造函数可以是默认构造函数,也可以是形参化构造函数。主要构造函数没有函数体,这意味着其中不能包含任何代码。

    // 使用主要构造函数初始化类标头中的属性
    // 传递给构造函数的实参会赋给属性
    class 类名 constructor(参数列表) {
        // 主要构造函数没有函数体,这里写的是类的主体,比如属性和方法什么的
    }
  • 辅助构造函数:一个类可以有多个辅助构造函数。您可以定义包含形参或不含形参的辅助构造函数。辅助构造函数可以初始化类,具有包含初始化逻辑的主体。如果类有主要构造函数,则每个辅助构造函数都需要初始化该主要构造函数。

辅助构造函数包含在类的主体中,其语法包括以下三个部分:

  • 辅助构造函数声明:定义以 constructor 关键字开头,后跟圆括号,根据需要填形参。
  • 主要构造函数初始化:初始化以冒号开头,后面依次跟 this 关键字和一对圆括号,根据需要填形参。
  • 辅助构造函数主体:在主要构造函数的初始化后跟一对花括号,写辅助构造函数的主体。

语法如下图所示:

constructor

class Box(val name: String) {
    var status = "close"

    // 根据传入的 statusCode,给 status 赋对应的字符串
    constructor(name: String, statusCode: Int) : this(name) {
        status = when (statusCode) {
            0 -> "close"
            1 -> "open"
            else -> "unknown"
        }
    }
    ...
}

继承
#

在 Kotlin 中,所有类默认都是最终类,无法扩展。因此必须定义类之间的关系

// 前面添加 open 关键字
open class Box(val name: String) {
    ...
}

// boxName 开头没写 val 或 var
// 也就是说,没有指定可变的还是不可变的
// 因此它不是类属性,只是 constructor 形参
// 只能用于传给父类
class SmallBox(boxName: String) :
    Box(name = boxName) {

    var size = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun check() {
        println("size = $size")
    }
}

类之间的关系
#

relationship


IS-A 关系:是继承关系。比如,每个鸭子「都是」鸟类。而且,如果对象 phone 是继承自类 Device,则也是类 Device 的实例。

class SmallBox : Box() {
}

不要只为了实现代码的可重用性而使用继承。在做出决定之前,请检查这两个类彼此是否相关。


HAS-A 关系:不是继承关系。比如,住宅中包含智能设备,即住宅「拥有」智能设备。两个类之间的 HAS-A 关系也称为「组合」。在 HAS-A 关系中,对象可以拥有其他类的实例,且实际上不必是该类本身的实例

// 住宅 HAS-A 智能电视设备
class TinyBox(val sb: SmallBox) {
    // 实例化后,实例内包含一个 sb 变量
    fun checkSize() {
        sb.check()
    }
}

// 能同时包含多个类的对象
class AAA(
    val bbb: BBB,
    val ccc: CCC
) {
    ...
}

重载,替换子类中的父类方法
#

class Box(val name: String) {
    var status = "close"

    // 在父类方法前添加 open 关键字
    open fun openBox() {
        println("open")
    }
}

class SmallBox(val boxName: String) :
    Box(name = boxName) {

    // 在子类中,定义同名方法,添加 override 关键字
    override fun openBox() {
        status = "open"
        println("open in child class")
    }
}

使用 super 关键字在子类中重用父类代码
#

open class Box(val name: String) {

    var status = "close"

    open fun openBox() {
        status = "open"
    }
}

class SmallBox(boxName: String) :
    Box(name = boxName) {

    override fun openBox() {
        super.openBox()
        println("$name 已打开")
    }
}

重载,替换子类中的父类属性
#

与方法重载类似。

open class Box(val name: String) {

    open val type = "unknown"
    ...
}

class SmallBox(boxName: String) :
    SmartDevice(name = boxName) {

    override val type = "Smart Box"
    ...
}

可见性修饰符
#

作用:

  • 在类中隐藏自己的属性方法,防止在类外未经授权的访问。
  • 在软件包中隐藏接口,防止在软件包外未经授权的访问。

Kotlin 提供了以下四种可见性修饰符,默认为 public

  • public:可在任何位置访问声明。用于想在类外部使用的属性和方法。
  • private:可在相同类或源文件中访问声明。某些属性和方法可能仅在类的内部使用,而且您不一定想让其他类使用。
  • protected:可让子类访问声明。
  • internal:可在相同模块中访问声明。与 private 类似,但可以从类的外部访问内部属性和方法,只要是在相同模块中进行访问即可。
修饰符相同类中访问子类中访问相同模块中访问模块之外访问
private
protected
internal
public

模块 是源文件和构建设置的集合,可让您将项目划分为独立的功能单元。您的项目可以包含一个或多个模块。您可以独立构建、测试和调试每个模块。

软件包就像是用来对相关类进行分组的目录或文件夹,模块则是用来为应用的源代码、资源文件和应用级设置提供容器。一个模块可以包含多个软件包。

属性

attribute

setter

方法

method

您不会在 set() 函数中执行任何操作或检查,只需将 value 形参赋给 field 变量即可。如您之前所学,这与属性 setter 的默认实现类似。在本例中,您可以省略 set() 函数的圆括号和主体:

class SmartDevice {
    var deviceStatus = "online"
        protected set   // 函数体可省略
        
        // protected set(value) {
        //    field = value
        // }
}

构造函数

constructor

class

定义属性委托
#

这部分提到了一些此前没见过的写法和概念,如果感到不好懂,看 这部分逐步讲的原文

以变量声明开头,后面依次跟 by 关键字以及用于为属性处理 gettersetter 函数的委托对象。

var 变量名 by 委托对象

若要实现您可以委托实现的目标类,您必须熟悉接口。接口是实现它的类必须遵循的协议,侧重于操作的「内容」,而不是操作的「方式」。简而言之,接口可帮助您实现抽象。

例如,在建造房子之前,您会告知建筑师自己想要什么。这可能包括卧室、儿童房、起居室、厨房和几间浴室。简而言之,在您指定「需求」后,建筑师会指定「满足需求的方式」。

var 类型创建委托类,需要实现 ReadWriteProperty 接口。为 val 类型创建委托类,需要实现 ReadOnlyProperty 接口。

例如,为 var 类型创建委托:

// 导入 ReadWriteProperty 和 KProperty 接口
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

// 创建 RangeRegulator 类,用于实现 ReadWriteProperty<Any?, Int> 接口
// 不必担心尖括号以及其中的内容。它们属于通用类型,会在以后进行了解。
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    // 定义 fieldData 属性,并使用 initialValue 形参对其进行初始化
    var fieldData = initialValue    // 此属性会充当变量的后备字段

    // 替换 getValue() 和 setValue() 方法
    // 用于充当属性的 getter 和 setter 函数
    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
        return fieldData
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
        // 检查要赋予的 value 形参是否处于 minValue..maxValue 范围内
        if (value in minValue..maxValue) {
            fieldData = value
        }
    }
}

class Phone {
    // 使用以上编写的接口
    private var brightnessLevel by RangeRegulator(initialValue = 0, minValue = 0, maxValue = 100)
}

使用函数类型和 lambda 表达式
#

将函数存储在变量中
#

将函数作为值引用,使用函数引用运算符 ::

fun main() {
    val t = ::test
    t()     // 相当于调用 test()
}

fun test() {
    println("666")
}

使用 lambda 表达式重新定义函数
#

fun main() {
    val t = test
    t()
}

val test = {
    println("666")
}

将函数用作数据类型
#

如果要指定函数参数的类型或返回值类型,则需要了解用于表达函数类型的语法。函数类型由一组圆括号组成,其中包含可选的参数列表-> 符号和返回值类型。语法如下图所示:

funDataType

将函数用作返回值类型
#

val trick = {
    println("No treats!")
}

val treat = {
    println("Have a treat!")
}

fun trickOrTreat(isTrick: Boolean): () -> Unit {
    if (isTrick) {
        return trick
    } else {
        return treat
    }
}

fun main() {
    val treatFunction = trickOrTreat(false)
    val trickFunction = trickOrTreat(true)
    treatFunction()
    trickFunction()
}

将一个函数作为参数传递到另一个函数
#

声明函数类型时,参数不会带有标签。只需指定各个参数的数据类型即可。语法如下:

funType1

为接受参数的函数编写 lambda 表达式时,系统会按参数出现的先后顺序为参数命名。参数名称列在左大括号后面,各名称之间以英文逗号隔开。-> 将参数名称与函数正文隔开。语法如下:

funType2

// 在 isTrick 参数后面,添加类型为 (Int) -> String 的 extraTreat 参数
fun trickOrTreat(isTrick: Boolean, extraTreat: (Int) -> String): () -> Unit {
    if (isTrick) {
        return trick
    } else {
        println(extraTreat(5))
        return treat
    }
}

fun main() {
    // coins() 函数为 Int 参数指定名称 quantity 并返回 String
    val coins: (Int) -> String = { quantity ->
        "$quantity quarters"
        // lambda 表达式不能用 return 关键字
        // 函数中最后一个表达式的结果将成为返回值
    }

    val cupcake: (Int) -> String = {
        "Have a cupcake!"
    }

    val treatFunction = trickOrTreat(false, coins)
    val trickFunction = trickOrTreat(true, cupcake)
    treatFunction()
    trickFunction()
}

val trick = {
    println("No treats!")
}

val treat = {
    println("Have a treat!")
}

可为 null 的函数类型
#

与其他数据类型一样,函数类型可声明为可为 null。在这些情况下,变量可以包含函数,也可以为 null

例如,如果想让 () -> String 类型可为 null,则将其声明为 (() -> String)? 类型。语法如下:

funNull

// 将 extraTreat 参数设置为可为 null
// 这样就不必在每次调用 trickOrTreat() 函数时都提供 extraTreat() 函数
fun trickOrTreat(isTrick: Boolean, extraTreat: ((Int) -> String)?): () -> Unit {
    if (isTrick) {
        return trick
    } else {
        if (extraTreat != null) {
            println(extraTreat(5))
        }
        return treat
    }
}

fun main() {
    val coins: (Int) -> String = { quantity ->
        "$quantity quarters"
    }

    val treatFunction = trickOrTreat(false, coins)
    val trickFunction = trickOrTreat(true, null)
    treatFunction()
    trickFunction()
}

使用简写语法编写 lambda 表达式
#

省略参数名称
#

如果函数只有一个参数,且未提供名称,Kotlin 会隐式为其分配 it 名称,因此您可以省略参数名称和 ->

val coins: (Int) -> String = { quantity ->
    "$quantity quarters"
}

简化后:

val coins: (Int) -> String = {
    "$it quarters"
}

将 lambda 表达式直接传入函数
#

fun main() {
    val coins: (Int) -> String = { quantity ->
        "$quantity quarters"
    }

    val treatFunction = trickOrTreat(false, coins)
    val trickFunction = trickOrTreat(true, null)
    treatFunction()
    trickFunction()
}

简化后:

fun main() {
    // 还可以将 lambda 表达式压缩为一行代码
    val treatFunction = trickOrTreat(false, { "$it quarters" })
    val trickFunction = trickOrTreat(true, null)
    treatFunction()
    trickFunction()
}

使用尾随 lambda 语法
#

用于声明界面的可组合函数接受函数作为参数,并且通常使用尾随 lambda 语法进行调用。

当函数类型是函数的最后一个参数时,可以使用另一个简写选项来编写 lambda。在这种情况下,可以将 lambda 表达式放在右圆括号后面以调用函数。

这会使代码的可读性更强,因为它将 lambda 表达式与其他参数隔开,但没有改变代码的作用。

val treatFunction = trickOrTreat(false, { "$it quarters" })

简化后:

val treatFunction = trickOrTreat(false) { "$it quarters" }

使用 repeat() 函数
#

如果一个函数会返回或接受另一个函数作为实参,该函数就称为高阶函数。上文的 trickOrTreat() 函数是一个高阶函数示例,因为它接受 ((Int) -> String)? 类型的函数作为参数,并返回 () -> Unit 类型的函数。

repeat() 函数是使用函数表达 for 循环的简洁方式。

repeat

相关文章

从零开始的 Neovim 记录
·262 字·1 分钟
Neovim Linux
悬崖怪谈-正文1
·1697 字·4 分钟
悬崖怪谈-正文2
·897 字·2 分钟