Kotlin学习教程之操作符重载详解

前言

在 Kotlin 中,我们可以用 约定的操作符,代替 调用代码中以特定的命名定义的函数,来实现 与之对应的操作。例如在类中定义了一个名为 plus 的特殊方法,就可以使用加法运算符 + 代替 plus() 的方法调用。由于你无法修改已有的接口定义,因此一般可以通过 扩展函数 来为现有的类增添新的 约定方法,从而使得 操作符重载 这一语法糖适应任何现有的 Java 类。

算术运算符

我们就从最简单直接的例子 + 这一类算术运算符开始。

data class Point(val x: Int, val y: Int) {
 operator fun plus(other: Point) = Point(x + other.x, y + other.y)
 operator fun plus(value: Int) = "toString: ${Point(x + value, y + value)}"
}

fun main(args: Array<String>) {
 val p1 = Point(1, 2)
 val p2 = Point(3, 4)
 println(p1 + p2)
 println(p1 + 3)
}

/*
Point(x=4, y=6)
toString: Point(x=4, y=5)
*/
  • operator 修饰符是必须的,否则 plus 只是一个普通方法,不能通过 + 调用。
  • 操作符是有优先级的,比较 * 优先级高于 +,不论这个操作符应用于什么对象,这种优先级都是固定存在的。
  • plus 方法的参数类型是任意的,因此可以方法重载,但是 参数数量只能是 1 ,因为 + 是一个二元操作符。plus 方法的返回值类型也是任意的。
  • 如果出现多个方法签名相同的 operator 扩展方法,根据 import 决定使用哪个一,例如:
// 第一个文件:
package package0
operator fun Point.times(value: Int) = Point(x * value, y * value)
// 第二个文件:
package package1
operator fun Point.times(value: Int) = Unit // Do nothing.
// 使用第一个扩展操作符:
import package0.times
val newPoint = Point(1, 2) * 3

Kotlin 为一些基本类型预定义了一些操作符方法,我们平时常写的基本数据计算也可以翻译成调用这些操作符方法,比如 (2 + 3) * 4 可以翻译成 2.plus(3).times(4),2 + 3 * 4 可以翻译成 2.plus(3.times(4))。根据扩展函数的语法,扩展函数无法覆盖与类已有的方法签名相同的方法,因此,不必担心随随便便给 Int 自定义一个 plus 扩展方法就能让 1 + 1 变得不等于 2。

同时,所有操作符都针对基本类型做了优化,比如 1 + 2 * 3、4 < 5,不会为它们引入函数调用的开销。

所有可重载的算术运算符有:

表达式  翻译为
a + b a.plus(b)
a - b a.minus(b)
a * b a.times(b)
a / b a.div(b)
a % b a.rem(b)、 a.mod(b) (在 Kotlin 1.1 中被弃用)
a..b a.rangeTo(b)

它们的优先级与普通的数字类型运算符优先级相同。其中 rangeTo 会在下面说明。

广义赋值操作符

表达式  翻译为
a += b a.plusAssign(b)
a -= b a.minusAssign(b)
a *= b a.timesAssign(b)
a /= b a.divAssign(b)
a %= b a.remAssign(b)、 a.modAssign(b) (在 Kotlin 1.1 中被弃用)

对于以上广义赋值操作符:

  • 如果对应的二元算术运算符函数也 可用 ,则报错。plus 对应 plusAssign。minus、times 等也类似。
  • 返回值类型必须为 Unit。
  • 如果执行 a += b 时 plusAssign 不存在,会尝试生成 a = a + b,其中的 a + b 使用的就是 plus 操作符方法,相当于调用 a = a.plus(b)。并且此时会 要求 a + b 的 plus 方法的返回值类型必须与 a 类型一致(如果单独使用 a + b 不做此要求)。
data class Size(var width: Int = 0, var height: Int = 0) {
 operator fun plus(other: Size): Size {
 return Size(width + other.width, height + other.height)
 }
 operator fun plusAssign(other: Size) {
 width += other.width
 height += other.height
 }
}

fun main(args: Array<String>) {
// var s1 = Size(1, 2) // 如果这么写,执行 += 时会报错.
 val s1 = Size(1, 2)
 val s2 = Size(3, 4)
 s1 += s2
}

我们使用这个例子来理解:为什么使用 var 定义的 s1 会导致 += 报错呢?因为理论上,执行 += 时,既可以调用 s1 = s1 + s2,也就是 s1 = s1.plus(s2),又可以调用 s1.plusAssign(s2),都符合操作符重载约定,这样就会产生歧义,而如果使用 val 定义 s1,则只可能执行 s1.plusAssign(s2),因为 s1 不可被重新赋值,因此 s1 = s1 + s2 这样的语法是出错的,永远不能能调用,那么调用 s1 += s2 就不会产生歧义了。

既然编译器会帮我把 a += b 解释成 a = a + b,那是不是意味着我只需要 plus 永远不需要 plusAssign 了呢?比较好的实践方式是:

  • + (plus) 始终返回一个新的对象
  • += (plusAssign) 用于内容可变的类型,修改自身的内容。

Kotlin 标准库中就是这么实现的:

fun main(args: Array<String>) {
 val list = arrayListOf(1, 2)
 list += 3 // 添加元素到自身集合, 没有新的对象被创建, 调用的是 add 方法.
 val newList = list + 4 // 创建一个新的 ArrayList, 添加自身元素和新元素并返回新的 ArrayList.
}

in

表达式  翻译为
a in b b.contains(a)
a !in b !b.contains(a)
println("hello" in arrayListOf("hello", ", ", "world"))
/*
true
*/

在 for 循环中使用 in 操作符会执行迭代操作,for(x in list) { /* 遍历 */ } 将被转换成 list.iterator() 的调用,然后在上面重复调用hasNext 和 next 方法。

rangeTo

rangeTo 用于创建一个区间。例如 1..10 也就是 1.rangeTo(10) 代表了从 1 到 10 这 10 个数字,Int.rangeTo 方法返回一个 IntRange 对象,IntRange 类定义如下:

/**
 * A range of values of type `Int`.
 */
public class IntRange(start: Int, endInclusive: Int) : IntProgression(start, endInclusive, 1), ClosedRange<Int> {
 override val start: Int get() = first
 override val endInclusive: Int get() = last
 override fun contains(value: Int): Boolean = first <= value && value <= last
 override fun isEmpty(): Boolean = first > last
 override fun equals(other: Any?): Boolean =
 other is IntRange && (isEmpty() && other.isEmpty() ||
 first == other.first && last == other.last)
 override fun hashCode(): Int =
 if (isEmpty()) -1 else (31 * first + last)
 override fun toString(): String = "$first..$last"
 companion object {
 /** An empty range of values of type Int. */
 public val EMPTY: IntRange = IntRange(1, 0)
 }
}

它的基类 IntProgression 实现了 Iterable 接口,因此 1..10 可以用来迭代:

for (index in 1..10) {
 // 遍历 1 到 10, 包括 1 和 10.
}

IntRange 还实现了接口 ClosedRange ,可以用来判断某元素是否属于该区间。

Kotlin 为 Comparable 定义了扩展函数 rangeTo:

/**
 * Creates a range from this [Comparable] value to the specified [that] value.
 *
 * This value needs to be smaller than [that] value, otherwise the returned range will be empty.
 * @sample samples.ranges.Ranges.rangeFromComparable
 */
public operator fun <T: Comparable<T>> T.rangeTo(that: T): ClosedRange<T> = ComparableRange(this, that)

因此所有的 Comparable 对象都可以使用 .. 区间操作符,例如:

fun main(args: Array<String>) {
 val c1 = Calendar.getInstance() // 代表今天.
 val c2 = Calendar.getInstance()
 c2.add(Calendar.DATE, 10) // 代表 10 天后.
 val c3 = Calendar.getInstance()
 c3.add(Calendar.DATE, 3) // 代表 3 天后.
 val c4 = Calendar.getInstance()
 c4.add(Calendar.DATE, 13) // 代表 13 天后.

 // 判断某日期是否在某两个日期范围内.
 println(c3 in c1..c2)
 println(c4 in c1..c2)
}
/*
true
false
*/

一元前缀操作符

表达式  翻译为
+a a.unaryPlus()
-a a.unaryMinus()
!a a.not()
data class Point(val x: Int, val y: Int)
operator fun Point.unaryMinus() = Point(-x, -y)
val point = Point(10, 20)
println(-point)
/*
Point(x=-10, y=-20)
*/

递增与递减

表达式  翻译为
a++ a.inc()
a– a.dec()

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

索引访问操作符

表达式  翻译为
a[i] a.get(i)
a[i, j] a.get(i, j)
a[i_1, ……, i_n] a.get(i_1, ……, i_n)
a[i] = b a.set(i, b)
a[i, j] = b a.set(i, j, b)
a[i_1, ……, i_n] = b a.set(i_1, ……, i_n, b)
@Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST")
operator fun <T> SharedPreferences.get(key: String, defValue: T) = when (defValue) {
 is String -> getString(key, defValue)
 is Int -> getInt(key, defValue)
 is Long -> getLong(key, defValue)
 is Float -> getFloat(key, defValue)
 is Boolean -> getBoolean(key, defValue)
 else -> throw RuntimeException()
} as T

@SuppressLint("CommitPrefEdits")
operator fun <T> SharedPreferences.set(key: String, value: T) = with(edit()) {
 when (value) {
 is String -> putString(key, value)
 is Int -> putInt(key, value)
 is Long -> putLong(key, value)
 is Float -> putFloat(key, value)
 is Boolean -> putBoolean(key, value)
 else -> throw RuntimeException()
 }.apply()
}

fun main(args: Array<String>) {
 val version = sp["key_version", 47] // 读 sp.
 sp["key_version"] = 48 // 写 sp.
}

调用操作符

表达式  翻译为
a() a.invoke()
a(i) a.invoke(i)
a(i, j) a.invoke(i, j)
a(i_1, ……, i_n) a.invoke(i_1, ……, i_n)

相等与不等操作符

表达式  翻译为
a == b a?.equals(b) ?: (b === null)
a != b !(a?.equals(b) ?: (b === null))

这在 Any 中被定义。Java 的 a.equals(b) 相当于 Koltin 的 a == b,Java 的 a == b 相当于 Kotlin 的 a === b(同一性检查)。要自定义 == 操作符其实就是覆写 equals 方法。Kotlin 中 === 不可被重载。

比较操作符

表达式  翻译为
a > b a.compareTo(b) > 0
a < b a.compareTo(b) < 0
a >= b a.compareTo(b) >= 0
a <= b a.compareTo(b) <= 0

要求 compareTo 返回值类型必须为 Int ,这与 Comparable 接口保持一致。

data class Movie(val name: String, val score: Int, val date: Date, val other: Any = Any()) : Comparable<Movie> {
 override fun compareTo(other: Movie): Int {
  return compareValuesBy(this, other, Movie::score, Movie::date, Movie::name) // 如果将 Movie::other 也用作比较会报错, 因为 other 不是 Comparable 类型的。
 }
}

fun main(args: Array<String>) {
 val df = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
 val movie0 = Movie("马戏之王", 8, df.parse("2018-01-31"))
 val movie1 = Movie("神秘巨星", 7, df.parse("2018-01-01"))
 val movie2 = Movie("移动迷宫", 7, df.parse("2018-01-02"))
 println(movie0 < movie1)
 println(movie1 < movie2)
}
/*
false
true
*/

其中的 compareValuesBy 方法如下:

/**
 * Compares two values using the specified functions [selectors] to calculate the result of the comparison.
 * The functions are called sequentially, receive the given values [a] and [b] and return [Comparable]
 * objects. As soon as the [Comparable] instances returned by a function for [a] and [b] values do not
 * compare as equal, the result of that comparison is returned.
 *
 * @sample samples.comparisons.Comparisons.compareValuesByWithSelectors
 */
public fun <T> compareValuesBy(a: T, b: T, vararg selectors: (T) -> Comparable<*>?): Int {
 require(selectors.size > 0)
 return compareValuesByImpl(a, b, selectors)
}

private fun <T> compareValuesByImpl(a: T, b: T, selectors: Array<out (T)->Comparable<*>?>): Int {
 for (fn in selectors) {
  val v1 = fn(a)
  val v2 = fn(b)
  val diff = compareValues(v1, v2)
  if (diff != 0) return diff
 }
 return 0
}

我们定义一个 Movie 类,它实现了 Comparable 接口,在比较时,希望按照 评分 、 上映日期 、 电影名称 的优先级顺序排序。可以简单的使用比较操作符对 Movie 对象进行“大小比较”。

操作符函数与 Java

Java 中调用 Kotlin 中的操作符方法,就跟调用普通方法一样,你不能期望在 Java 中写 new Point(1, 2) + new Point(3, 4) 这样的语法,只能乖乖调用 new Point(1, 2).plus(new Point(3, 4))。

反之,Kotlin 中调用 Java 代码却可以同 Kotlin 中自定义操作符方法一样方便。只要一个类提供了满足操作符方法签名的方法,哪怕它只是一个普通方法,不需要加 operator 修饰符(Java 中也没有这个修饰符),就可以在 Kotlin 中以操作符的方式调用。例如:arrayList[0] 相当于 Java 中 arrayList.get(0),尽管这个 get 方法是 Java 中定义的。又比如所有实现了 Comparable 的类实例都可以使用比较操作符 >、< 等进行比较。

Java 中的位运算符在 Kotlin 中是没有的 ,它们只能使用普通方法加中缀表达式使用,只能用于 Int 和 Long,对应关系如下:

Java 中   Kotlin 中
« 有符号左移 shl(bits)
» 有符号右移 shr(bits)
»> 无符号右移 ushr(bits)
& 与 and(bits)
| 或 or(bits)
^ 异或 xor(bits)
! 非 inv()

操作符重载与属性委托、中缀调用

我们在使用委托属性时也用过 operator 修饰符:

class Delegate {
  operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
    //...
  }
  operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
    //...
  }
}

符合这样方法签名的 getValue 、 setValue 也是操作符函数,用于委托属性的 getter 和 setter。

可以看出,操作符重载并不是一定要用如 * 、 + 、 < 这样的符号来表示的,比如之前的 in 操作符,这里的 getter 、 setter。

除了以上这些标准的可被重载的操作符外,我们也可以通过中缀函数的调用来模拟自定义中缀操作符,实现形如 a in list 这样的语法。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

您可能感兴趣的文章:

  • Kotlin基础教程之操作符与操作符重载
  • Kotlin新手基础学习之Elvis操作符
(0)

相关推荐

  • Kotlin基础教程之操作符与操作符重载

    Kotlin基础教程之操作符与操作符重载 Kotlin操作符的使用方法与其他语言差不多,最大的特点就在于infix function call(事实上infix function call并不是操作符的特性,而是函数的特性)和操作符重载. 可以看到在Kotlin中大部分的操作符都与一个函数相对应,之所以这样做,大概是为了进行操作符重载. Kotlin官方文档中强调"=赋值"操作在Kotlin中不是操作符,换句话说类似于x = y = z这样的连续赋值并不被允许(甚至赋值操作也不能和常规

  • Kotlin新手基础学习之Elvis操作符

    Kotlin简介 Kotlin是一门与Swift类似的静态类型JVM语言,由JetBrains设计开发并开源.与Java相比,Kotlin的语法更简洁.更具表达性,而且提供了更多的特性,比如,高阶函数.操作符重载.字符串模板.它与Java高度可互操作,可以同时用在一个项目中. kotlin 语言特性 kotlin中一切皆为对象 没有像java一般的基本数据类型,数值类型为:Int, Float, Double等 函数也是对象,可作为参数和返回值 自然就会有高阶函数和lambda 语法特性及规范

  • Kotlin学习教程之操作符重载详解

    前言 在 Kotlin 中,我们可以用 约定的操作符,代替 调用代码中以特定的命名定义的函数,来实现 与之对应的操作.例如在类中定义了一个名为 plus 的特殊方法,就可以使用加法运算符 + 代替 plus() 的方法调用.由于你无法修改已有的接口定义,因此一般可以通过 扩展函数 来为现有的类增添新的 约定方法,从而使得 操作符重载 这一语法糖适应任何现有的 Java 类. 算术运算符 我们就从最简单直接的例子 + 这一类算术运算符开始. data class Point(val x: Int,

  • kotlin 官方学习教程之基础语法详解

    kotlin 官方学习教程之基础语法详解 Google 在今天的举行了 I/O 大会,大会主要主要展示内有容 Android O(Android 8.0)系统.Google Assistant 语音助手.Google 智能音箱.人工智能.机器学习.虚拟现实等.作为一个 Android 开发者,我关心的当然是 Android O(Android 8.0)系统了,那么关于 Android O 系统的一个重要消息是全面支持 Kotlin 编程语言,使得 Kotlin 成为了 Android 开发的官方

  • Python入门教程之运算符重载详解

    目录 如何重载Python中的运算符 在 Python中重载比较运算符 重载相等和小于运算符 用于运算符重载的 Python 魔术方法或特殊函数 二元运算符 比较运算符 赋值运算符 一元运算符 运算符重载意味着赋予超出其预定义的操作含义的扩展含义.例如运算符 + 用于添加两个整数以及连接两个字符串和合并两个列表.这是可以实现的,因为 '+' 运算符被 int 类和 str 类重载.您可能已经注意到,相同的内置运算符或函数对不同类的对象显示不同的行为,这称为运算符重载. # Python 程序显示

  • java学习教程之常量折叠详解

    前言 为什么会写着篇博客,因为昨天看了关于final关键字的解析.但是有个问题始终没有得到解决,于是请教了我qq上之前添加的知乎大神.他给我回复的第一条消息:常量折叠.身为渣渣猿的我立马查询了这个概念.这是第一次知道这个概念.知乎大神还给我讲了好多.让我终于明白了这个常量折叠的概念 所谓常量折叠是Java在编译期间做的一个优化,简单的来说就是在编译期就把一些表达式计算好,不需要在运行时进行计算. 下面话不多说了,来一起看看详细的介绍吧 实例解析 昨天,让我迷惑的代码是下面这段代码 public

  • Perl学习教程之单行命令详解

    前言 本文主要给大家介绍了关于Perl单行命令的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. 与One-Liner相关的perl参数 -a 自动分隔模式,用空格分隔$并保存在@F中,也就是@F=split //, $ -F 指定-a的分隔符 -l 对输入的内容进行自动chomp,对输出的内容自动加换行符 -n 相当于while(<>) -e 执行命令,也就是脚本 -p 自动循环+输出,也就是while(<>){命令(脚本); print;} 记住以上几

  • React Native学习教程之自定义NavigationBar详解

    前言 在刚开始学习React Native的时候,版本还是0.20,问题一大堆,Navigation这个问题更是很多,首先,是NavigationBar的问题,NavigationIOS有NavigationBar,Navigation却需要自定义一个,最后,我想了想,还是自定义一个view,岂不更好,现在新公司不用RN,我正好有点时间,就把自定义的NavigationBar分享给大家.好了少废话,上代码: 示例代码 // NavigationBar 导航条的自定义封装 // create by

  • Angular 2.x学习教程之结构指令详解

    结构指令是什么? 结构指令通过添加和删除 DOM 元素来更改 DOM 布局.Angular 中两个常见的结构指令是 *ngIf 和 *ngFor . 了解 * 号语法 * 号是语法糖,用于避免使用复杂的语法.我们以 *ngIf 指令为例: (图片来源:https://netbasal.com/) Angular 把 host (宿主元素) 包装在 template 标签里面 Angular 将 ngIf 转换为属性绑定 - [ngIf] 创建结构指令 首先,让我们了解如何创建一个结构指令. 接下

  • ES6学习教程之模板字符串详解

    模板字符串(template strings) ES6 中引进的一种新型的字符串字面量语法 - 模板字符串.书面上来解释,模板字符串是一种能在字符串文本中内嵌表示式的字符串字面量.简单来讲,就是增加了变量功能的字符串. ES6为我们提供了模板字符串,语法使用反引号`.模板字符串具有以下三个优点: 多行文本 字符串中插入变量 字符串中插入表达式 基本语法 模板字符串和 ES5的字符串的声明一样. // ES5 var name = 'xixi'; console.log(name);// xixi

  • Laravel学习教程之广播模块详解

    前言 本文主要给大家介绍了关于Laravel广播模块的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍: 注意:本文是基于Laravel 5.4版本的路由模块代码进行分析书写: 简介 广播是指发送方发送一条消息,订阅频道的各个接收方都能及时收到消息:比如 A同学写了一篇文章,这时候 B同学在文章底下评论了,A同学在页面上是不用刷新就能收到提示有文章被评论了,这个本质上就是A同学收到了广播消息,这个广播消息是由B同学评论这个动作触发了发送广播消息: 在整个广播行为中,有一个重

  • Vue.js学习教程之列表渲染详解

    本文主要给大家介绍了关于Vue.js列表渲染的相关资料,分享出来给大家参考学习,下面来看看详细的介绍: v-for 可以使用 v-for 指令基于一个数组渲染一个列表.这个指令使用特殊的语法,形式为item in items,items 是数据数组,item 是当前数组元素的别名: 示例: <ul id="example-1"> <li v-for="item in items"> {{ item.message }} </li>

随机推荐