Kotlin扩展函数与运算符重载超详细解析

目录
  • 一、扩展函数
  • 二、运算符重载

一、扩展函数

不少现代高级编程语言中有扩展函数这个概念,Java却一直以来都不支持这个功能,Kotlin对扩展函数有了很好的支持。

扩展函数表示即使在不修改某个类的源码的情况下,仍然可以打开这个类,向该类添加新的函数。

比如有一个功能:一段字符串中可能包含字母、数字和特殊符号等字符,现在我们希望统计字符串中字母的数量,要怎么实现这个功能?如果按照一般的编程思维,可能会很自然的写出如下函数:

object StringUtil{
    fun  lettersCount(str:String):Int{
        var count=0
        for(char in str){
            if(char.isLetter()){
                count++
            }
        }
        return count
    }
}

这里先定义了一个StringUtil单例类,然后在这个单例类中定义一个lettersCount()函数,该函数接收一个字符串参数。在lettersCount()方法中,我们使用for-in循环去遍历字符串中的每一个字符。如果该字符是一个字母的话,那么计数器加1,最终返回计数器的值。

现在我们需要统计某个字符串中的字母数量时,只需要编写如下代码:

 val str="ABCdsw1242"
 val Count = StringUtil.lettersCount(str)

这种写法可以正常工作,这也是Java编程中的标准实现思维,但有了扩展函数之后就不一样了,我们可以使用一种更加面向思维来实现这个功能,比如说lettersCount()函数添加到String类当中。

我们先来学习一下定义扩展函数的语法结构,如下所示:

fun ClassName.methodName(param1:Int,param2:Int):Int{
return 0
}

想定义普通的函数,定义扩展函数只需要在函数名的前面加上一个ClassName.的语法结构,就表示将该函数添加到指定类当中。

接下来使用扩展函数的方式来优化刚才的统计功能。

由于我们希望向String类中添加一个扩展函数,因此需要先创建一个String.kt文件。文件名虽然没有固定的要求,但是我建议向哪个类中添加扩展函数,就定义一个同名的Kotlin文件,这样便于你以后查找。当前,扩展函数也是可以定义在任何一个现有类当中的,并不一定非要创建新文件。不过通常来说,最好将它定义成顶层方法,这样可以让扩展函数拥有全局的访问域。

现在在String.kt文件中编写如下代码:

fun String.lettersCount():Int{
var count=0
for(char in this){
if(char.isLetter()){
count++
}
}
return count
}

注意这里的代码变化,现在我们将lettersCount()方法定义成了String类的扩展函数,那么函数中就自动拥有了String实例的上下文。因此lettersCount()函数就不再需要接收一个字符串参数了,而是直接遍历this即可,因为现在this就代表着字符串本身。

定义好了扩展函数之后,统计某个字符串中的字母数量只需要这样写即可:

val count="ABCdsw1242".lettersCount()

这样看上去是String类中自带了lettersCount()方法一样。

扩展函数在很多情况下可以让API变得更加简洁、丰富,更加面向对象。我们再次以String类为例,这是一个final类,任何一个类都不可以继承它,也就是说它的API只有固定的那些而已,至少在Java中就是如此。然而到了Kotlin中就不一样了,我们可以向Kotlin类中扩展任何函数,使他的API变得更加丰富。比如,你会发现Kotlin中的String甚至还有reverse()函数用于反转字符串,capitalize()函数用于对首字母进行大写,等等,这都是Kotlin语言自带的一些扩展函数。

二、运算符重载

在Java中有许多语言内置的运算符关键字,如+,-,*,/,%,++,–。而Kotlin允许我们将所有的运算符甚至其他的关键字进行重载,从而拓展这些运算符和关键字的用法。

我们基本上都使用过加减乘除这种四则运算符,在编程语言里面,两个数字相加表示求这两个数字之和,两个字符串相加表示对这两个字符串进行拼接。但是在Kotlin语言中,kotlin运算符重载却允许我们让任意两个对象相加,或者进行更多其他的运算操作。

运算符重载使用的是operator关键字,只要在指定函数的前面加上operator关键字,就可以实现运算符重载的功能了。但问题是在于这个指定函数是什么?这是运算符重载里面比较复杂的一个问题,因为不同的运算符对应的重载函数也是不同的。比如说加号运算符对应的是plus()函数,减号运算符对应的是minus()函数。

我们这里还是以加号运算符为例,如果想要实现让两个对象相加的功能,那么它的语法结构如下:

class obj{
   operator fun plus(obj:Obj):Obj{
  //处理相加的逻辑
}
}

在上述的语法结构中,关键字operator和函数名plus都是固定不变的,而接收的参数和函数返回值可以根据你的逻辑自行设定。那么上述代码就表示一个Obj对象可以与另一个Obj对象相加,最终返回一个新的Obj对象。对应的调用方式如下:

val obj1=Obj()
val obj2=Obj()
val obj3=obj1+obj2

这种obj1+obj2的语法其实就是Kotlin给我们提供的一种语法糖,它会在编译的时候被转换成obj1.plus(obj2)的调用方式。

举一个例子实现让两个Money对象相加

首先定义Money类的结构,这里让Money的主构造函数接收一个value参数,用于表示钱的金额。创建Money.kt文件,代码如下所示:

class Money(val value:Int)

定义好了Money类的结构,接下来我们就使用运算符来重载实现让两个Money对象相加的功能:

class Money(val value:Int) {
    operator fun plus(money: Money):Money{
        val sum=value+money.value
        return Money(sum)
    }
}

可以看到,这里使用了operator关键字来修饰plus()函数,这是必不可少的。在plus()函数中,我们将当前Money对象的value和参数传入的Money对象的value相加,然后将得到的和传入给一个新的Money对象并将该对象返回。这样两个Money对象就可以相加了。

我们可以使用如下代码进行测试

 val money1 = Money(5)
 val money2 = Money(10)
 val money3=money1+money2
 println(money3.value)

最终结果为15

Money对象能够直接和数字相加,因为Kotlin允许我们对同一个运算符进行多重重载,代码如下:

class Money(val value:Int) {
    operator fun plus(money: Money):Money{
        val sum=value+money.value
        return Money(sum)
    }
    //对同一个运算符进行多重重载
    operator fun plus(newValue:Int):Money{
        val sum=value+newValue
        return Money(sum)
    }
}

这里我们又重载了一个plus()函数,不过这次接收的参数是一个整型数字,其他代码基本一样。

那么现在Money对象就拥有了和数字相加的能力:

 val money1 = Money(5)
 val money2 = Money(10)
 val money3=money1+money2
 val money4=money3+20
 println(money4.value)

打印结果为35

Kotlin允许我们重载的运算符和关键字多达十几个。表中列出了所有可能常用的可重载运算符和关键字对应的语法糖表达式,以及它们会被转换成的实际调用函数。如果想重载其中的某一种运算符或关键字,只要参考刚才加号运算符重载的写法去实现就行了。

语法糖表达式 实际调用函数
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++ a.inc()
a– a.dec()
+a a.unaryPlus
-a a.unaryMinus
!a a.not
a==b a.equals(b)
a>=b a.compareTo(b)
a…b a.rangeTo(b)
a[b] a.get(b)
a[b]=c a.set(b,c)
a in b b.contains(a)

注意,最后一个a in b的语法糖表达式对应的实际调用函数是b.contains(a),a、b对象顺序是反过来的。因为a in b表示判断a是否在b当中,而b.contains(a)表示b是否包含a,因此这两种表达式是等价的。

举个例子,Kotlin中的String类就对contains()函数进行了重载,因此当我们判断“hello”字符串中是否包含“he”子串时,首先可以这样写:

if("hello".contains("he")){
}

而借助重载的语法糖表达式,也可以这样写:

if("he" in "hello"){
}

结合学习的扩展函数以及运算符重载的知识,对以下功能进行优化

fun getRandomLengthString(str:String):String{
val n=(1..20).random()
val builder=StringBuilder()
repeat(n){
builder.append(str)
}
return builder.toString()
}

其实这个函数的核心思想就是将传入的字符串重复n次,而Kotlin能够让我们使用str*n这种写法来表示让str字符串重复n次,使语法更精简。

要让一个字符串可以乘以一个数字,那么肯定要在String类中重载乘号运算符才行,但是String类是系统提供的类,我们无法修改这个类的代码。这个时候就可以借助扩展函数功能向String类中添加新函数了。

既然是向String类中添加扩展函数,那么我们还是打开刚才创建的String.kt文件,然后加入如下代码:

operator fun String.times(n:Int):String{
val builder=StringBuilder()
repeat(n){
builder.append(this)
}
return builder.toString()
}

首先,operator关键字肯定是必不可少的,然后是要重载乘号运算符,参考上面的表,如果要乘那么函数名必须是times,最后由于是定义扩展函数,因此还要在方法名前面加上String.的语法结构。在times()函数中,我们借助StringBuilder和repeat函数将字符串重复n次,最终将结果返回。

现在,字符串就有了和一个数字相乘的能力,比如执行如下代码:

val str="abc"*3
println(str)

最终打印结果是:abcabcabc。

现在我们就可以在getRandomLengthString()函数中使用这种写法了

operator fun String.times(n:Int)=str*(1..20).random()

另外,必须说明的是,其实Kotlin的String类中已经提供了一个用于将字符串重复n遍的repeat()函数,因此times()函数还可以进一步精简成如下形式:

operator fun String.times(n:Int)=repeat(n)

到此这篇关于Kotlin扩展函数与运算符重载超详细解析的文章就介绍到这了,更多相关Kotlin扩展函数与运算符重载内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Kotlin协程之Flow基础原理示例解析

    目录 引言 一.Flow的创建 二.Flow的消费 1.SafeFlow类 2.AbstractFlow类 3. SafeCollector类 4.消费过程中的挂起 引言 本文分析示例代码如下: launch(Dispatchers.Main) { flow { emit(1) emit(2) }.collect { delay(1000) withContext(Dispatchers.IO) { Log.d("liduo", "$it") } Log.d(&qu

  • Kotlin 挂起函数CPS转换原理解析

    目录 正文 1.什么是CPS转换 2.CPS的过程是怎么让参数改变的 3.CPS的过程是怎么让返回值改变的 4.挂起函数的反编译 5.非挂起函数的分析 正文 普通函数加上suspend之后就成为了一个挂起函数,Kotlin编译器会将这个挂起函数转换成了带有参数Continuation<T>的一个普通函数,Continuation是一个接口,它跟Java中的Callback有着一样的功能,这个转换过程被称为CPS转换. 1.什么是CPS转换 挂起函数中的CPS转换就是把挂起函数转换成一个带有Ca

  • Kotlin协程之Flow触发与消费示例解析

    目录 示例 一.Flow的触发与消费 1.onEach方法 2.transform方法 3.collect方法 二.多消费过程的执行 三.总结 示例 代码如下: launch(Dispatchers.Main) { val task = flow { emit(2) emit(3) }.onEach { Log.d("liduo", "$it") } task.collect() } 一.Flow的触发与消费 在Kotlin协程:Flow基础原理的分析中,流的触发与

  • Go语言单元测试超详细解析

    目录 一.单元测试分类及其概念 1.基本分类 2.细说单元测试分类 二.结合代码细说每一种测试 1.基准测试 2.组测试与子测试 三.pprof调试工具 1.对主函数进行传参 2.pprof性能调优 前言: 平时根据需求写代码.人工进行测试往往不会面面俱到,还会因为需求的改变繁琐的进行测试通过完成一个测试函数,可以大大简化测试的步骤,并且在需求该变的时候只需要改变一下测试的输入与期望 一.单元测试分类及其概念 1.基本分类 测试函数 函数前缀为Test 主要用于测试程序的一些逻辑行为是否正确 基

  • 超详细解析C++实现快速排序算法的方法

    目录 一.前言 1.分治算法 2.分治算法解题方法 二.快速排序 1.问题分析 2.算法设计 3.算法分析 三.AC代码 一.前言 1.分治算法 快速排序,其实是一种分治算法,那么在了解快速排序之前,我们先来看看什么是分治算法.在算法设计中,我们引入分而治之的策略,称为分治算法,其本质就是将一个大规模的问题分解为若干个规模较小的相同子问题,分而治之. 2.分治算法解题方法 1.分解: 将要解决的问题分解为若干个规模较小.相互独立.与原问题形式相同的子问题. 2.治理: 求解各个子问题.由于各个子

  • 超详细解析C++实现归并排序算法

    目录 一.前言 分治算法 分治算法解题方法 二.归并排序 1.问题分析 2.算法设计 3.算法分析 三.AC代码 一.前言 分治算法 归并排序,其实就是一种分治算法 ,那么在了解归并排序之前,我们先来看看什么是分治算法.在算法设计中,我们引入分而治之的策略,称为分治算法,其本质就是将一个大规模的问题分解为若干个规模较小的相同子问题,分而治之. 分治算法解题方法 1.分解: 将要解决的问题分解为若干个规模较小.相互独立.与原问题形式相同的子问题. 2.治理: 求解各个子问题.由于各个子问题与原问题

  • C++运算符重载的详细讲解

    加号运算符重载 对于内置数据类型,编译器知道如何运算 但是对于自己封装的类,编译器无法进行运算 这时可以通过自己定义运算符重载进行运算 operator+ 通过成员函数重载+号 #include<iostream> using namespace std; class Person { public: int m_a; int m_b; //通过成员函数实现重载 Person operator+ (Person &p) { //创建一个临时变量 Person temp; temp.m_

  • 使用kotlin集成springboot开发的超详细教程

    目录 一.安装支持插件 二.maven配置 注意 三.创建入口函数类 四.编写入口函数 五.创建数据库对象 六.创建仓库操作接口 七.创建一个业务接口来声明业务 八.创建一个业务接口实现来实现声明的业务 九.创建一个 http服务接口 目前大多数都在使用java集成 springboot进行开发,本文演示仅仅将 java换成 kotlin,其他不变的情况下进行开发. 一.安装支持插件 在 idea中安装 kotlin插件(大多数情况下会默认安装了) 二.maven配置 注意 kotlin目前不支

  • C++带头双向循环链表超详细解析

    目录 什么是带头双向循环链表 带头双向循环链表常用接口实现 上期我们讲完了无头单向非循环链表,这期我们接着来讲链表中结构最复杂的带头双向循环链表! 本期主要内容: 什么是带头双向循环链表? 带头双向循环链表常用接口实现! 顺序表和链表的区别和联系! 什么是带头双向循环链表 什么是带头?双向?循环?(带头双向循环链表) 带头:代表链表存在一个哨兵位节点,也就是头节点,这个节点不存放任何的有效数据! 双向:每个节点都有两个指针,分别指向它的前一个节点和后一个节点! 循环:最后一个节点next不再指向

  • C++ 二叉树的实现超详细解析

    目录 1.树的概念及结构(了解) 1.1树的概念: 1.2树的表示法: 2.二叉树的概念及结构 2.1二叉树的概念: 2.2特殊的二叉树: 2.3二叉树的性质: 2.4二叉树的顺序存储: 2.5二叉树的链式存储: 3.二叉树链式结构的实现 3.1二叉树的前中后序遍历: 3.2求二叉树的节点个数: 3.3求二叉树的叶子节点个数: 3.4销毁二叉树: 1.树的概念及结构(了解) 1.1树的概念: 树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合.把它叫做树是因为它看

  • C++ 栈和队列的实现超详细解析

    目录 1.栈的介绍: 2.栈的常用接口实现 3.队列的介绍 4.队列的常用接口实现 可算是把链表给结束了,很多小伙伴已经迫不及待想看到栈和队列了,那么它来了!相信有了顺序表和链表的基础,栈和队列对于你们来讲也是轻轻松松,那我就废话不多说,直接进入今天的重点: 1.栈的介绍: 栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作.进行数据插入和删除操作的一端称为栈顶,另一端称为栈底.栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则. 压栈:栈的插入操作叫做

  • 详细分析c# 运算符重载

    您可以重定义或重载 C# 中内置的运算符.因此,程序员也可以使用用户自定义类型的运算符.重载运算符是具有特殊名称的函数,是通过关键字 operator 后跟运算符的符号来定义的.与其他函数一样,重载运算符有返回类型和参数列表. 例如,请看下面的函数: public static Box operator+ (Box b, Box c) { Box box = new Box(); box.length = b.length + c.length; box.breadth = b.breadth

  • 一文搞懂C++中的运算符重载

    目录 引入 一.运算符重载是什么 二.运算符重载的格式 三.部分运算符重载的实现 3.1 简单‘ + ’ ‘ - ’ ‘ * ’运算符重载 3.2 ++,- - 运算符 3.3 =运算符 3.4 <<,>>运算符 四.运算符重载注意事项 五.运算符重载的限制 六.MyString的简单实现 MyString.h MyString.cpp 引入 对于基本类型的常量或变量进行运算时,我们可以使用 +.-.*./ 等运算符,但是我们不可以使用运算符来进行对象之间的运算. eg:对象之间的

随机推荐