五、表达式:对数据的操作

我们已经用过几次表达了。每当你需要给一个变量赋值,需要函数调用参数,或者需要给某种语言结构赋值时,你就需要一个表达式。表达式也会出现在你意想不到的地方,如果我们不需要,它们可以被忽略。

表达式示例

表达式可以细分为不同的类型:数字表达式、布尔表达式、字符串和字符表达式、作用于位和字节的表达式,以及一些未分类的表达式。在我们开始详细解释它们之前,这里有一些例子:

4 * 5         // multiplication
3 + 7         // addition
6  1         // subtraction
"a" + "b"     // concatenation
( 1 + 2 )     // grouping
-5            // negation
a && b        // boolean a AND b
"Hello"       // constant (String)
78            // another constant (Int)
3.14          // another constant (Double)
'A'           // another constant (Char)
arr[43]       // index access
funct(...)    // function invocation
Clazz()       // instantiation
Obj           // singleton instance access
q.a           // dereferencing
q.f()         // another dereferencing
if(){ }       // language construct
when(){ }     // another language construct

表达式的普遍性

与许多其他计算机语言不同,在 Kotlin 中,几乎所有东西都是表达式。例如,看看函数调用funct()。你可能会认为一个没有像在fun funct() { ... }中那样声明返回值的函数不是一个表达式,因为它似乎不能被赋值给一个变量。试试看,写一写

fun a() {
}

val aa = a()

令人惊讶的是,编译器没有将这段代码标记为错误。事实上,这样的函数确实会返回值;它是Unit类的实例,被称为Unit。你不能用它做任何有趣的事情,但它是一个值,它使一个函数不显式返回任何东西,而是隐式返回一些东西。

在本章的其余部分,我们将介绍不同的表达式类型以及它们之间的转换。

数字表达式

数字表达式是由文字、属性和子表达式等元素构建的结构,可能由运算符组合在一起并产生一个数字。涉及加、减、乘、除的一组众所周知的运算符通常被称为算法。在计算中,这组标准运算符通常会增加一个递增和递减运算符++,以及一个整数除法余数运算符%。对于 Kotlin 内部可用于数值表达式的可能元素的完整列表,见表 5-1

表 5-1

数字表达式元素

|

标志

|

意义

|

例子

| | --- | --- | --- | | 文字 | 字面意思 | 37.5 | | 变量 | 一处房产 | val a = 7; val b = a + 3 | | 函数( ) | 函数的值,如果它返回一个数字 | fun a() = 7; val b = 3 + a() | | [ ] | 访问数组或数字列表中的元素 | arr[0]``list[7] | | ( ) | 替换为内部表达式的结果 | 7 * ( a + b ) | | + | 如果用在表达式前面,则复制数据 | val a = +3``val a = +7.4 | | - | 如果用在表达式前面,则对数据求反 | val a = -(7+2) | | ++ | 可以用在一个var的前面或者后面;如果用在它的前面,则计算为var + 1的当前值;如果在它后面使用,则计算为var的当前值;作为副作用,增加了var | var a = 7``val b = 7 + ++a``val c = 7 + a++ | | -- | 可以用在一个var的前面或者后面;如果用在它的前面,则计算为var - 1 的当前值;如果在它后面使用,则计算为var的当前值;作为副作用,减少了var | var a = 7``val b = 7 + --a``val c = 7 + a-- | | + | 将两个值相加 | 7 + 6 | | - | 减去两个值 | 7 – 6 | | * | 将两个值相乘 | 7 * 6 | | / | 将两个值相除;如果在两个非浮点值之间,则返回一个非浮点值;否则返回一个Double或一个Float | 7 / 6(给出 1)7.0 / 6.0(给出1:16667) | | % | 两个整数值相除的余数 | 5 % 3(给出2) | | subexpr | 用作子表达式的任何表达式,返回一个数字 | 在5 + a / 7中,a/7可以被认为是一个子表达式 |

如果在一个表达式中混合不同类型的数字,具有较大取值范围的数字将被用作返回值的类型,因此用一个Long除以一个Int将得到一个Long:

val l1:Long = 234567890L
val i1:Int = 37
val x = l1 / i1 // -> is a Long

同样,如果您在一个表达式中混合了普通精度的Float元素和双精度的Double元素,Double将获胜:

val f1:Float = 2.45f
val d1:Double = 37.6
val x = f1 / d1 // -> is a Double

将整数与浮点数元素混合会产生浮点数:

val i1:Int = 33
val d1:Double = 37.6
val x = i1 * d1 // -> is a Double

如果我们需要组合三个值(或子表达式)并在一行中有两个操作符,如

  • expr``1expr``2exprT5】**

问题是先评估哪个操作符。这称为运算符优先级,其 Kotlin 规则如表 5-2 所示。

表 5-2

算术运算符的优先级

|

优先

|

经营者

|

例子

| | --- | --- | --- | | one | ++ --作为后缀 | a++ | | Two | -(在一个表达式前面)+(在一个表达式前面)++ --作为前缀 | –(3 + 4)``--a | | three | * / % | 7 * a | | four | + - | 7 – a |

您总是可以使用圆括号( ... )来指定任何运算符的求值顺序。就像在数学中使用的一样,在使用括号内的解之前,首先计算括号内的值。

练习 1

Math.sqrt(...)表示平方根√,用 Kotlin 代码写下:

$$ \sqrt{\frac{a+\frac{b-x}{2}}{b^2-7\cdot x}} $$

假设a, b,x是现有属性。

布尔表达式

布尔表达式是评估为布尔值truefalse之一的表达式。如果我们需要决定程序的哪些部分参与程序流,我们经常使用布尔表达式。参与布尔表达式的对象和运算符列于表 5-3

表 5-3

布尔表达式元素

|

标志

|

意义

|

例子

| | --- | --- | --- | | 文字 | 字面意思 | truefalse | | 变量 | 一处房产 | val a = true; val b = a | | funct() | 函数的值,如果它返回一个布尔值 | fun a() = true; val b = a() | | [ ] | 访问数组或布尔列表中的元素 | arr[0]``list[7] | | ( ) | 替换为内部表达式的结果 | b1 && ( a &#124;&#124; b )(注意:&& = AND,|| = OR) | | && | 和操作;只有当ab都为真时,a && b才为真;注意,如果左边的求值结果为false,那么&&的右边永远不会被求值 | true && true(产量→ true) | | &#124;&#124; | 或者运营;只有当ab中至少有一个为真时,a &#124;&#124; b才为真;注意,如果左边的求值结果为true,那么&#124;&#124;的右边永远不会被求值 | true &#124;&#124; false(产量→ true) | | ! | 对以下布尔表达式求反 | val b = true; val r = !b(yields r为假) | | a == b | 如果ab相等,则产生trueab是任意对象或子表达式;如果布尔或数字子表达式的值相同,则它们相等;如果对象abhashCode()函数返回相同的值,并且a.equals(b)返回true,则它们相等;如果两个字符串都包含相同的字符,则它们相等;如果一个特定数据类的两个实例的所有属性都相等,那么它们就是相等的 | a == 3(如果a的值为 3,则为true)a == "Hello" ( true如果a是字符串“你好”) | | a != b | 不相等,同!( a == b ) | true)true)``false)false) | | a < b | 如果数字a小于数字b,则为真;还评估是否在对象ab上定义了接口Comparable | a < 7 (→ true如果a小于 7) | | a > b | 如果数字a大于数字b,则为真;还评估是否在对象ab上定义了接口Comparable | a > 3 (→ true如果a大于 3) | | a <= b | 如果数字a小于或等于数字b,则为真;还评估是否在对象ab上定义了接口Comparable | a <= 7 (→ true如果a小于或等于 7) | | a >= b | 如果数字a大于或等于数字b,则为真;还评估是否在对象ab上定义了接口Comparable | a >= 3 (→ true如果a大于或等于 3) | | a is b | 如果对象a实现了类或接口b则为真 | true)true) | | a !is b | 同!(a is b) | true)true) | | a === b | 检查引用是否相等;如果对象是same并因此强于==比较,则返回true;通常不经常使用,因为使用==操作符的语义检查在大多数情况下更有意义 | class A``val a = A(); val b = A()``val c = a === b``false)false) |

与上一节中的数值表达式类似,如果使用带有更多运算符的表达式,布尔表达式运算符具有优先权。布尔运算符优先级的 Kotlin 规则如表 5-4 所示。

表 5-4

布尔运算符的优先级

|

优先

|

经营者

|

例子

| | --- | --- | --- | | one | !(在一个表达式前面) | val a = true; val b = !a | | Two | is!is | a in b && c | | three | <<=>=> | a < 7 && b > 5 | | four | ==!= | a == 7 && b != 8 | | five | && | a == 4 && b == 3 | | six | &#124;&#124; | a == 4 &#124;&#124; a == 7 |

对于数值表达式,您可以使用圆括号强制不同的优先顺序:

val b1 = a == 7 && b == 3 || c == 4
val b2 = a == 7 && (b == 3 || c == 4)

如你所见,它们是不同的。在第一行中,&&获胜并首先被计算,因为它比||具有更高的优先级。在第二行中,||获胜,因为它在一个括号内。

字符串和字符表达式

字符串没有太多的表达式元素。但是,您可以连接字符串并执行字符串比较。字符串表达式元素的完整列表见表 5-5

表 5-5

字符串表达式元素

|

标志

|

意义

|

例子

| | --- | --- | --- | | 文字 | 字面意思 | "Hello world"或者"""Hello world""" | | 变量 | 一处房产 | val a = "abc"; val b = a | | funct() | 函数的值,如果它返回一个字符串 | fun a() = "abc"; val b = a() | | [ ] | 访问数组或字符串列表中的元素 | arr[0]列表[7] | | str[ ] | 从字符串中提取字符 | "Hello" [1](产量" e ") | | ( ) | 替换为内部表达式的结果 | "ab" + ("cd" + "ef" ) | | + | 串并置 | val a = "Hello " + "world"(产量→ "Hello world") | | a == b | 检查是否相等;如果两个字符串都包含相同的字符,则它们相等 | a == "Hello" ( true如果a是字符串“Hello”) | | a != b | 不相等,同!( a == b ) | true)true) | | a < b | 如果字符串a在字典顺序上小于字符串b则为真 | true)true) | | a > b | 如果字符串a在字典上比字符串b大,则为 True | true)true) | | a <= b | 如果字符串a在字典上小于或等于字符串b则为真 | true)true)``false)false) | | a >= b | 如果字符串a在字典上大于或等于字符串b则为真 | true)true) | | a in b | 如果a是一个Chartrue如果b包含a;如果a是字符串本身,true如果a是字符串b的一部分 | true)true)``true)true) | | a !in b | 同!(a in b) | true)true) |

字符串文字有几种特殊情况。

  • 使用三组双引号的字符串被称为原始字符串。它们可以包含任何内容,包括换行符和反斜杠()等特殊字符。书写"Hello\n world"会产生由换行符分隔的“Hello world”。然而,如果你写"""Hello\n world""",输出将是字面上的“Hello \n world”。一个例外是$;你得写${'$'}才能得到。

  • 在原始和普通(“转义”)字符串中,你都可以使用模板:一个${}被包含在花括号中的任何内容的toString()表示所取代。例如:"The sum of 3 and 4 is ${3+4}"得出字符串“3 和 4 之和是 7”。如果它是一个单一的标识符,比如一个房产的名字,你也可以省略括号,写成$propertyName,比如"And the value of a is $a".

字符具有整数表示,因为它们对应于字符表中的索引。这允许一些算术和比较运算符处理字符。字符表达式元素列表如表 5-6 所示。

表 5-6

字符表达元素

|

标志

|

意义

|

例子

| | --- | --- | --- | | 文字 | 字面意思 | 'A'或者'7' | | 变量 | 一处房产 | val a = 'x'; val b = a | | funct() | 函数的值,如果它返回一个字符 | fun a() = 'x'; val b = a() | | [ ] | 访问数组或字符列表中的元素 | arr[0]``list[7] | | - | 字符表中的距离 | val d = 'c' - 'a'(产量→ 2) | | a == b``a != b``a < b``a > b``a <= b``a >= b | 性格比较;比较字符表中的索引 | 'c' > 'a'(产量→ true) |

比特和字节

字节是更面向硬件的数据存储单位。我们知道有一个Byte类型,它的值在128127之间。一个字节对应于一些硬件存储和处理元素,可以以极快的方式访问和使用。在你的应用中,你只是偶尔使用字节,尤其是在使用一些低级系统功能或寻址连接的硬件元素(如摄像头或扬声器)时。

你知道当你写下125这样的十进制数字系统中的一个数字时,你实际上的意思是51 + 210 + 1100。计算机内部不喜欢十进制计数系统,因为如果他们使用它,例如,78之间的差异不能可靠地用一些技术属性来表示,如两个触点之间的电压。计算机能做得很好的是发现某个东西是否被打开,用密码01来表示。因此,他们在内部使用二进制编码系统。如果我们需要一个125它实际上由二进制数01111101表示,意思是1·1 + 0·2 + 1·223+ 1·24+ 1·25+ 1·26+ 0·27。这个数里面的数字被称为,碰巧的是,我们需要 8 位来表示一个字节所有可能的值。**

**因为一个字节是一个数字,你可以用它做所有的事情,我们之前已经讨论过,关于数字表达式。然而,一个字节也是八位的集合,并且有一些特殊的操作可以在位级上进行(见表 5-7 )。注意,ShortIntLong值对应 2、4 和 8 个字节,因此对应 8、16 和 32 位。因此,位级操作不仅可以在字节上执行,还可以在其他整数类型上执行。

表 5-7

位表达式元素

|

标志

|

意义

|

例子

| | --- | --- | --- | | a and b | 位级上的 ANDa的每一位都与b的相应位配对,如果两者都是1,那么结果号中的位也将被设置为1 | 13 and 11(评估为 9: 000011010000101100001001) | | a or b | 位级别上的或;a的每个位与b的相应位配对,如果其中一个或两个都是1,结果号中的位也将被设置1 | 13 or 11(评估为 15: 000011010000101100001111) | | a xor b | 比特级的异或运算;a的每一位都与b的相应位配对,如果其中恰好有一位是1,那么结果号中的位也将被置位1 | 13 xor 11(计算结果为 6: 00001101异或0000101100000110) | | inv a | 将某个数字a的所有位从 0 切换到 1,反之亦然 | inv 13(计算结果为 114: inv 0000110111110010 = 114) | | a shl b | 将所有位从a向左移动b位 | 13 shl 2(评估为 52:0000110100110100 = 52) | | a ushr b | 将所有位从a向右移动b位位置;这个名字是无符号右移的缩写,意味着最左边的位没有得到特殊处理 | 13 shr 2(评估为 3: 0000110100000011 = 3) | | a shr b | 将所有位从a向右移动b位位置;如果最左边的位被设置为1,则每次移位后最左边的位也被设置为1 | -7 shr 2(计算结果为-2: 1111100111111110 = -2) |

注意,有符号右移操作的shr运算符指的是位表示中的负数。这样的负数是这样建立的:确保负数的位和它的算术倒数的位相加在一起正好导致溢出。将3表示为一个字节就产生了11111101,因为这个加上00000011(代表+3)就产生了100000000。一个字节的最后一个九位数导致溢出,最高的第九位丢失,导致零。这最终也给了我们所需的二进制表示形式的+3 +-3 = 0

其他操作员

Kotlin 还有一些我们可以在表达式中使用的操作符。它们不适合区分数字、布尔、字符串和字符以及位表达式,所以我们在表 5-8 中单独列出它们。

表 5-8

其他表达元素

|

标志

|

意义

|

例子

| | --- | --- | --- | | a in b | 检查某个a是否包含在b中,b可能是数组,也可能是集合;一般来说,in操作符适用于任何定义了operator fun contains(other:SomeClass): Boolean函数的对象,甚至是你自己的类 | class B``class A { operator fun``contains(other:B):Boolean``{ ... } }``val b = B()``val a = A()``val contained = b in a | | a !in b | a in b的反面;如果为a的类定义了operator fun contains(other:SomeClass): Boolean也有效 | 参见a in b;添加val notContained = b !in a | | :: | 如果像ClassName::class一样使用,它创建一个对类的引用;如果像ClassName::funNameClassName::propertyName一样使用,它会创建一个对函数或属性的引用 | val c = String::class``val f = String::length | | a .. b | 创建从一个整数(文字、ByteShortIntLongChar ) a到另一个整数b的范围 | 1..100 | | a ?: b | Elvis操作员;如果a不是null,取;否则采取b | var s:String? = ...``var ss = s?:"default"``(如果snull,取“默认”代替) | | a ?. b或者a ?. b() | 安全解引用或安全调用运算符;对于某个对象a,仅当a不是null时,从函数b()调用中检索属性b或结果(可以有参数);否则评估为null本身 | var i:Int? = ...``var ss:String? =``i?.toString() | | a!! | 确保a不是null;否则会引发异常 | var s:String? = ...``var ss = s!!.toString() |

表达式末尾的!! operator不仅检查它不是null,还将其转换为不可空的类型:

val c:Int? = ...    // an int or null
val b = c!!         // b is non-nullable!
// the same: val b:Int = c!!

更好的是,Kotlin 记得我们检查了c不是null,并且对于函数的其余部分,将c视为不可空的属性。

警告

即使!!似乎是一个简化编码的通用工具,你也不应该经常使用它。操作符在某种程度上阻碍了 Kotlin 处理可空性的方式。!!打破了不可为空性,并通过区分可为空和不可为空的类型和表达式隐藏了我们的优势。

练习 2

创建一个允许通过函数add(s:String)连接字符串的类Concatenator。添加另一个函数,这样就可以编写下面的代码来查看连接的字符串是否包含子字符串。

val c = Concatenator()
c.add("Hello")
c.add(" ")
c.add("world")
val contained = "ello" in c

转换策略

如果你有一个val或者var属性或者某种类型的函数参数,问题是如果在赋值中我们提供一个不同类型的表达式会发生什么。如果这种类型不匹配很严重,例如,如果我们需要一个Int号,而提供了一个String,编译器将会失败,我们需要修复它。在其他情况下,例如,如果我们实际上需要一个Long,就提供一个Int,类型之间的简单转换会很好。

Kotlin 通过提供几个可用于手动执行类型转换的函数来帮助我们。在下面的列表中,我们研究了类型不匹配时的选项。

  • 需要一个Int

    • ByteShortIntLong:所有这些都提供了一个到Int()的函数,执行直接转换。

    • Char:有一个toInt()函数,给出字符在字符表中的索引。

    • FloatDouble:提供一个toInt()函数,对于正数,返回给定浮点数下面最接近的Int。对于负数,返回给定浮点数上面最接近的Int。此外,它们还有一个roundToInt()功能,提供向上舍入到下一个整数的功能。

    • String:提供一个toInt()函数,解析给定的字符串,并试图将其转换成一个Int。如果提供的字符串不包含整数,这将失败,因为只允许使用可选符号和 0 到 9 的密码。此外,还有一个toIntOrNull函数处理相同的转换,但不会失败,如果转换不可能,它将返回null。变体toInt(radix:Int)toIntOrNull(radix:Int)使用不同的计数系统(基数)进行转换。例如,对于十六进制基数(使用16作为radix参数),允许使用密码 0 到 9 和字母 A 到 F。

    • Boolean:从布尔值到整数的转换是不可能的。

  • 需要一个LongByteShort

    所有类型ByteShortIntLongCharFloatDoubleString都提供了toLong()toByte()toShort()功能,这些功能遵循与Int目标类型相同的规则,除了适用不同的数字范围。请注意,对于字符串,长文本不允许使用 L 后缀。

  • 需要一个充电器。

    所有整数类型ByteShortIntLong都提供了一个toChar()函数,该函数使用所提供的数字在字符表中执行索引查找。A Char.toChar()原封不动地返回参数。类型FloatDouble提供了一个toChar()函数,该函数首先应用一个toInt(),然后执行字符表查找。字符串不提供到Char的转换,但是您可以使用toCharArray()和索引操作符[]来访问数组元素(例如,"123".toCharArray()[0]给出‘1’)。

  • 需要一个Double或一个Float

    • ByteShortIntLong:这些都提供了toFloat()toDouble()功能,执行明显的转换。

    • Char:字符也有toFloat()toDouble()函数,但是它们返回字符表中转换成浮点数的索引。

    • FloatDouble:这些提供toFloat()toDouble()功能,必要时执行精度转换。

    • String:它有toFloat()toDouble()函数,这些函数试图解析提供的字符串,将其转换成FloatDoubleString可以使用英文格式浮点数表示或科学记数法;比如27.48-3.01.8e4。如果转换不可能,此过程将失败。变量toDoubleOrNull()toFloatOrNull()将尝试相同的转换,但如果出现转换错误,则返回null

    • Boolean:从布尔值到浮点数的转换是不可能的。

  • 需要一个String

    Kotlin 中的任何对象都提供了一个toString()转换,将它翻译成人类可读的表示。对于包含字符的整数,转换很明显;对于浮点数,将选择英语格式;布尔值被翻译成truefalse。类型ByteShortInt,Long也有一个toString(radix:Int)功能,使用提供的编号系统(基数)进行转换。

应用了几个自动转换,所以有可能写val l:Long = 7,这看起来像是自动的IntLong的转换。

注意

根据编码过程中的经验,您可以测试自动转换是否可行,但在大多数情况下,最好显式声明转换。

在运算符起作用的表达式中,适用另一种转换规则。对于任何运营商

  • aT1】

*其中a是类型ATypeb是类型BType,操作符实现决定了操作结果的类型。一个重要的案例是

[Number]   °  [Number]

其中[Number]选自ByteShortIntLongFloat,Double,运算符为任意数值运算符(+ - / * %)。这里,表达式返回的类型在大多数情况下是具有更高精度的类型。精度排名是Byte<Short<Int<Long<Float<Double。例如:

7 + 10_000_000_000L -> Long
34 + 46.7          -> Double

在 Kotlin 程序中,另一种由操作符引起的转换是

String + [Any]

在这里,字符串和[Any]上的.toString()的结果将发生连接。例如:

```kt "Number is " + 7.3 -> "Number is 7.3" "Number is " + 7.3.toString() -> "Number is 7.3" "Hell" + 'o' -> "Hello"

```*