Kotlin 知识梳理(8) - 运算符重载及其他约定

Kotlin 知识梳理系列文章

Kotlin 知识梳理(1) - Kotlin 基础
Kotlin 知识梳理(2) - 函数的定义与调用
Kotlin 知识梳理(3) - 类、对象和接口
Kotlin 知识梳理(4) - 数据类、类委托 及 object 关键字
Kotlin 知识梳理(5) - lambda 表达式和成员引用
Kotlin 知识梳理(6) - Kotlin 的可空性
Kotlin 知识梳理(7) - Kotlin 的类型系统
Kotlin 知识梳理(8) - 运算符重载及其他约定
Kotlin 知识梳理(9) - 委托属性
Kotlin 知识梳理(10) - 高阶函数:Lambda 作为形参或返回值
Kotlin 知识梳理(11) - 内联函数
Kotlin 知识梳理(12) - 泛型类型参数


一、本文概要

本文是对<<Kotlin in Action>>的学习笔记,如果需要运行相应的代码可以访问在线环境 try.kotlinlang.org,这部分的思维导图为:


Kotlin中,我们可以通过 调用自己代码中定义的函数,来实现 特定语言结构。这些功能与 特定的函数命名 相关,而不是与特定的类型绑定。例如,如果在你的类中定义了一个名为plus的特殊方法,那么按照约定,就可以在该类的实例上使用+运算符,这种技术称为 约定

因为由类实现的接口集是固定的,而Kotlin不能为了实现其他接口而修改现有的类,因此一般 通过扩展函数的机制 来为现有的类增添新的 约定方法,从而适应任何现有的Java类。

二、重载算术运算符

Kotlin中,使用约定的最直接的例子就是 算术运算符,在Java中,全套的算术运算符只能用于基本数据类型,+运算符可以与String一起使用。下面,我们看一下在Kotlin中,如何使用算术运算符来完成一些其它的事情。

2.1 重载二元运算符

假设已经有一个数据类Point,它包含两个成员变量,分别是x,y点的坐标值,我们希望通过算术运算符+对两个Point对象相加之后,能够得到一个新的Point对象,它的成员变量x,y为原有两个Point对象的x,y之和。


运行结果为:

在上面的代码中,我们为Point类定义了一个扩展函数plus,这样当我们调用first + second,实际上执行的是first.plus(second)方法来得到一个新的Point对象。这里需要注意的是:用于重载运算符的所有函数都需要 用 operator 关键字来标记,用来表示你打算 把这个函数作为相应的约定的实现

所有可重载的二元算术运算符如下,自定义类型的运算符,基本上和标准数字类型的运算符有着相同的优先级。

  • a * btimes
  • a / bdiv
  • a % bmod
  • a + bplus
  • a - bminus

运算符函数和 Java

  • 当从Java调用Kotlin运算符非常容易,只需要像普通函数一样调用即可,例如上面的plus方法。
  • 当从Kotlin调用Java的时候,对于与Kotlin约定匹配的函数(不要求使用operator修饰符,但是参数需要匹配名称和数量)都可以使用运算符语言来调用。如果Java类定义了一个满足需求的函数,但是起了一个不同的名称,可以通过定义一个扩展函数来修正这个函数名用来替代现有的Java方法。

没有用于位运算的特殊运算符

Kotlin没有为标准数字类型IntLong等定义任何位运算符,因此也不允许你为自定类型定义它们。相反,它使用中缀调用语法的函数,可以为自定义类型定义相似的函数,下面我们为Point添加一个and,用于执行位运算。


运行结果为:

这里我们不再使用operator关键字来声明,而是用infix来定义一个中缀调用语法的函数,其它执行位运算的函数包括:shlshrushrandorxorinv

2.2 重载复合赋值运算符

当在定义像plus这样的函数,Kotlin不止支持+号运算,也支持像+=这样的 复合赋值运算符


需要注意,这个只对于可变变量有效,也就是first要声明为var。在一些情况下,定义+=运算符可以 修改使用它的变量所引用的对象,但不会重新分配引用,将一个元素添加到可变集合,就是一个很好的例子:


如果你定义了一个返回值为Unit,名为plusAssign的函数,Kotlin将会在用到+=运算符的地方使用它,其它二元运算符也有命名相似的对应函数:minusAssigntimesAssign等。

当在代码中用到+=的时候,理论上plusplusAssign都可能会被调用,如果两个函数都有定义并且适用,那么编译器就会报错,例如下面这样的定义:


编译时的错误为:

解决方法有两种:

  • 使用 不可变 val 代替可变 var 来修饰first,这样plus运算符就不再适用。
  • 不要同时为一个类添加plusplusAssign运算。如果一个类是 不可变的,那就应该只提供返回一个新值的运算;如果一个类是 可变的,例如构建器,那么只需要提供plusAssign和类似的运算符就够了。

Kotlin的标准库支持集合的这两种方法:

  • +-运算符总是返回一个新的集合
  • +=-=运算符用于可变集合时,始终在一个地方修改它们;而它们用于只读集合时,会返回一个修改过的副本。

作为它们的运算数,可以使用单个元素,也可以使用元素类型一致的其它集合:



运行结果为:


2.3 重载一元运算符

重载一元运算的过程和前面看到的方式相同:用预先定义的一个名称来声明函数,并用修饰符operator标记。下面的例子中重载了-a运算符:


运行结果为:

所有可重载的一元算法运算符包括:

  • +aunaryPlus
  • -aunaryMinus
  • !anot
  • ++a/a++inc
  • --a/a--dec

当你定义incdec函数来重载自增和自减的运算符时,编译器自动支持与普通数字类型的前缀、后缀自增运算符相同的语义。例如后缀运算会先返回变量的值,然后才执行++操作。

三、重载比较运算符

与算术运算符一样,在Kotlin中,可以对任何对象使用比较运算符(==!=><),而不仅仅限于基本数据类型。

3.1 等号运算符,equals

如果在Kotlin中使用==/!=运算符,它将被转换成equals方法的调用,和其他运算符不同的是,==!=可以用于可空运算数,比较a == b会检查a是否为飞空,如果不是就调用a.equals(b),完整的调用如下所示:

a?.equals(b) ?: (b == null)

对于data修饰的数据类,equals的实现将会由编译器自动生成,如果需要手动实现,可以参考下面的做法:

  • 比较是否指向同一对象的引用,如果是,那么直接返回true
  • 类型如果不同,直接返回false
  • 比较作为判断依据的字段

equals函数之所以被标记为override,这是因为这个方法的实现是在Any类中定义的,而operator关键字在基本方法中已经标记了。同时,equals不能实现为扩展函数,因为继承自Any类的实现始终优先于扩展函数。

3.2 排序运算符 compareTo

Kotlin中,对于实现了Comparable接口中定义的compareTo方法的类可以按约定调用,比较运算符<、>、<=、>=的使用将被转换为compareTocompareTo的返回类型必须为int,也就是说p1 < p2表达式等价于p1.compareTo(p2) < 0

下面,我们定义一个Person类,让其根据年龄来比较大小:


运行结果为:

在上面的例子中,我们用到了Kotlin标准库函数中的compareValuesBy函数来简洁地实现compareTo方法,这个函数 接收用来计算比较值的一系列回调,按顺序依次调用回调方法,两两一组分别做比较:

  • 如果值不同,则返回比较结果
  • 如果相同,则继续调用下一个
  • 如果没有更多的回调来调用,则返回0

这些回调函数可以像lambda一样传递,或者像这里做的一样,作为属性引用传递。

四、集合与区间的约定

处理集合最常见的操作包含两种:

  • 通过下标来获取和设置元素,使用语法a[b],称为 下标运算符
  • 检查元素是否属于当前集合,使用in运算符。

4.1 通过下标来访问元素:get 和 set

Kotlin中,下标运算符是一种约定,使用下标运算符读取元素会被转换为get运算符方法的调用,并且写入元素将调用set,下面我们为Point类添加类似的方法:


get的参数可以是任何类型,而不止是Int,例如,当你对map使用下标运算符时,参数类型是键的类型,它可以是任意类型。还可以定义具有多个参数的get方法,例如如果要实现一个类来表示二维数组或矩阵,你可以定义一个方法,例如operator fun get(rowIndex : Int, colIndex : Int),然后用matrix[row, col]来调用。

下面,我们再来看一下set的约定方法:


运行结果为:

定义set函数后,就可以在赋值语句中使用下标运算符,set的最后一个参数用来接收赋值语句中(等号)右边的值,其他参数作为方括号内的下标。

4.2 in 的约定

集合支持的另一个运算符是in运算符,用于检查某个对象是否属于集合,相应的函数叫做contains,下面的例子用于判断某个点是否处于矩形范围之内:


运行结果为:

4.3 rangeTo 的约定

要创建一个区间时,使用的是..语法,例如1..10代表所有从110的数字,..运算符是调用rangeTo函数的一个简洁方法。rangeTo返回一个区间,你可以为自己的类定义这个运算符,但是,如果该类实现了Comparable接口,那么就不需要了,你可以通过Kotlin标准库创建一个任意可比较元素的区间,这个库定义了可以用于任何可比较元素的rangeTo函数

operator fun <T : Comparable<T>> T.rangeTo(that : T) : ClosedRange<T>

这个函数返回一个区间ClosedRanged,可以用来检测其它一些元素是否属于它。

作为例子,我们用LocalData来构建一个日期的区间:


运行结果为:

上面的now..now.plusDays(10)将会被编译器转换为now.rangeTo(now.plusDays(10)),它并不是LocalDate的成员函数,而是Comparable的一个扩展函数。

4.4 在 "for" 循环中使用 "iterator" 的约定

for循环中使用in运算符表示 执行迭代操作,诸如for(x in list) { }将被转换成list.iterator()的调用,然后在上面重复调用hasNextnext方法。


运行结果为:

上面用到了 Kotlin 知识梳理(4) - 数据类、类委托 及 object 关键字 中介绍的通过object来实现匿名内部类的知识。

五、解构声明和组件函数

解构声明的功能允许你展开单个复合值,并使用它来初始化多个单独的变量。它再次用到了约定的原理,要在解构声明中初始化每个变量,将调用名为componentN的函数,其中N是声明中变量的位置。

对于数据类,编译器为每个在主构造方法中声明的属性生成一个componentN函数,下面的例子显示了如何手动为非数据类声明这些功能:


运行结果为:

解构声明主要使用场景之一,是从一个函数返回多个值,这个非常有用。如果要这样做,可以定义一个数据类来保存返回所需的值,并将它作为函数的返回类型。在调用函数之后,可以用解构声明的方式,来轻松的展开它,使用其中的值。

解构声明不仅可以用作函数中的顶层语句,还可以用在其他可以声明变量的地方,例如使用in循环来枚举map中的条目:


运行结果为:


更多文章,欢迎访问我的 Android 知识梳理系列:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容

  • 前言 人生苦多,快来 Kotlin ,快速学习Kotlin! 什么是Kotlin? Kotlin 是种静态类型编程...
    任半生嚣狂阅读 26,168评论 9 118
  • 在前面写了关于集合和范围的内容,里面包括了一点运算符重载的内容,在这里我们来详细了解运算符重载的知识,内容参考《K...
    叫我旺仔阅读 7,432评论 0 10
  • C++运算符重载-上篇 本章内容:1. 运算符重载的概述2. 重载算术运算符3. 重载按位运算符和二元逻辑运算符4...
    Haley_2013阅读 2,291评论 0 51
  • Kotlin 知识梳理系列文章 Kotlin 知识梳理(1) - Kotlin 基础Kotlin 知识梳理(2) ...
    泽毛阅读 2,492评论 0 4
  • 我总是习惯憧憬习惯把未来想得美好,现实好像并不想成全反过来给了我一记响亮的耳光。在这个算不上大学的地方,我见识了一...
    徐小雪阅读 197评论 1 1