天天看点

Kotlin学习---函数的定义和调用(上)

上一篇文章中,我们对Kotlin中的类,属性,函数,目录结构有了一个大致的了解,在这篇文章中,将对Kotlin中函数相关的特性做介绍。

文章目录

    • 1.1 在Kotlin中创建集合
    • 1.2 让函数更好调用
      • 1.2.1 命名参数
      • 1.2.2 默认参数值
      • 1.2.3 消除静态工具类:顶层函数和属性
    • 1.3 给别人的类添加方法:扩展函数和属性
      • 1.3.1 导入和扩展函数
      • 1.3.2 从Java中调用扩展函数
      • 1.3.3 作为扩展函数的工具函数
      • 1.3.4 不可重写的扩展函数
      • 1.3.5 扩展属性

1.1 在Kotlin中创建集合

先来看一下Kotlin中几种集合的创建方式

val set = hashSetOf(1, 7, 53)
val list = arrayListOf(1, 7, 53)
val map = hashMapOf(1 to "one", 7 to "seven", 53 to "fifty_three") // 里面的 to 的用法并不是Kotlin里面的特殊结构,它对应的只是一个普通函数。这种用法叫做中缀调用,后面章节会有详细的介绍。
           

这些创建集合的函数在Kotlin中都是 顶层函数 可以在任何地方调用。从函数名称中就可以很容易的看出来我们创建的集合的类型。set 变量创建的集合类型在Java中对应的是 HashSet 类 ,list变量创建的集合类型在Java中对应的是 ArrayList 类 ,map变量创建的集合类型在Java中对应的是 HashMap 类。为了更加直观的展示所创建的集合类型,我们运行下面的代码看一下:

>>>  println(set.javaClass)  //Kotlin中的javaClass等价于Java中的getClass() 是一个扩展属性
class java.util.HashSet


>>>  println(list.javaClass)
class java.util.ArrayList


>>>  println(map.javaClass)
class java.util.HashMap
           

可以看到跟我们上面分析的结果是一致的。在Kotlin中没有采用自己的集合类,而是使用了标准的Java集合类,这对于熟悉Java的开发者来说是一个好消息,所有你熟知的集合相关的知识都可以在Kotlin中使用并且不需要考虑兼容性的问题。Kotlin中使用Java集合的而没有使用自己专门的集合的原因也就在这里。

尽管Kotlin中的集合使用的是标准Java中的集合,但是Kotlin对其做了很多扩展,以方便开发者的使用,例如可以通过下面的方式获取一个列表中的最后一个元素,或者得到数字列表中的最大值:

val strings = listOf("first", "second", "fourteenth")
>>>  println(strings.lastIndex)
fourteenth


val numbers = setOf(1, 14, 2)
>>> println(numbers.max())
14
           

在Kotlin中还存在很多像 lastIndex 和 max() 这样的扩展属性和扩展方法。这些扩展元素提高了开发者在平时开发过程中的效率,并且使用起来是如此的简单 。后续章节将会详细的讲到是如果实现的。接下来先让我们看一下函数声明。

1.2 让函数更好调用

在Java集合中有一个默认的 toString 实现,但是它的输出是固定的,结果往往不是我们想要的格式,这个时候我们通过会使用一些第三方库,比如说 Guava 和 Apache Commons ,或者通过重写对应的 toString() 函数来实现自己想要的格式。对于这个问题,Kotlin中专门有一个函数来解决。

本章节就让我们一步一步的实现这个函数,在实现过程中我们会学习到很多函数相关的特性。

首先描述一下函数要实现的功能。下面的 joinToString 函数展现了通过元素中间添加分隔符号,在最前面添加前缀,在最末尾添加后缀的方式把集合的元素逐个添加到一个 StringBuilder 的过程:

fun <T> joinToString(collection: Collection<T>,
                    separator: String,
                    prefix: String,
                    postfix: String): String {
    val result = StringBuilder(prefix)

    for ((index, element) in collection.withIndex()) {
        if ((index) > 0) result.append(separator)  //不会在第一个元素前面添加分隔符
        result.append(element)
    }

    result.append(postfix)
    return result.toString()
}
           

在这个函数中用到了泛型:它支持元素为任意类型的集合。这个函数运行起来的效果如下:

val list = listOf(1, 2, 3)
>>>  println(joinToString(list, "; ", "( ", ")"))
(1; 2; 3)
           

我们已经基本上实现了给集合添加分隔符,前缀和后缀输出的函数了。接下来我们会聚焦到它的声明:要怎么修改,让这个函数调用更加的简洁?有没有什么办法可以让我们不用每次都调用的时候都传入四个参数呢?

1.2.1 命名参数

首先让看一下这个函数的可读性问题。举个栗子,如果我这样调用 joinToString :

你能够第一时间看出来这些参数都代表什么吗?如果不去看函数声明的话就很难回答这些问题。

在Kotlin中我们可以这样处理:

在调用Kotlin定义的函数时,我们可以显式的表明一些参数的名称,同时这个时候并不关心原函数中的参数顺序。需要注意的是,如果在调用一个函数时,指明了一个参数的名称,为了避免混淆(编译器会报错),那它之后(之前的参数不受影响)的所有参数都需要标明名称 。

当你调用Java函数时,不能采用命名参数,不管是JDK中的函数,还是Android框架的函数。把参数名称存到.class文件时是Java8及其更高版本的一个可选功能,而Kotlin需要保持和Java6的兼容性。所以,编译器不能识别出调用函数的参数名称,然后把这些参数名对应到函数的定义的地方。

1.2.2 默认参数值

Java中另一个普通存在的问题就是一些类的重载函数实在是太多了。可以看一下 java.lang.Thread 类,里面有8个构造方法,我的天!当然这些重载是为了向后兼容,方便这些API的使用者,但是这就导致了一个结果:重复。这些参数名称和类型被重复一遍又一遍,同时如果你是一个良好公民的话,还必须每次重载的时候都重复大部分的文档。真的是想想都麻烦。我自己在项目中也经常遇到这种情况,也是非常的无奈。

而在Kotlin中可以通过在声明函数的时候,指定参数的默认值,这样就可以避免创建重载的函数。让我们使用默认参数值来改进一下 joinToString 函数。在大多数情况下,字符串可以不加前缀或者后缀并用逗号分隔。所以,我们把这几个参数都设置成默认值:

fun <T> joinToString(collection: Collection<T>,
                    separator: String = ", ",
                    prefix: String = "",
                    postfix: String = ""): String 
           

来看看该如何调用这个有默认值的函数:

>>>  println(joinToString(list))
1, 2, 3


>>>  print(joinToString(list, "; "))
1; 2; 3
           

使用起来是不是跟重载一个效果呢。使用 默认参数值 ,就不需要再写那么多的重载函数了。

当使用常规的语法时,必须按照函数声明中定义的顺序来给定参数,可以省略的只有排在 末尾的参数。如果使用命名参数,可以省略中间的一些参数,也可以以你想要的任意顺序只给定你需要的参数:

>>>  joinToString(list, postfix = ";", prefix = '# ')
# 1, 2, 3;
           

需要注意,参数的默认值是被编码到 调用的函数 中的,而不是调用的地方。

默认值和Java

如果你的方法是用Kotlin写的,在Java中调用这个方法时,你不想每次调用都传入所有的参数,希望能够实现Java中写多个重载函数一样的效果,这个时候你可以在Kotlin的方法上添加 @JvmOverloads 注解。当有这个注解时,编译器会自动生成Java重载函数,从最后一个开始省略每个参数。给 joinToString 函数添加这个注解的效果:

/* java */
String joinToString(Collection<T> collection, String separator, String prefix, String postfix);

String joinToString(Collection<T> collection, String separator, String prefix);

String joinToString(Collection<T> collection, String separator);

String joinToString(Collection<T> collection);
           

1.2.3 消除静态工具类:顶层函数和属性

让我们回想一下,我们的项目中是不是存在很多 Util* 类文件,很多时候可能一个类文件里面就只有几个方法,但是为了清楚的区分这些方法,我们也只能这么处理。这些文件里没有实例函数或者这些类不包含任何的状态,仅仅是一堆静态函数的容器。对于这些类,Kotlin中可以直接放在代码文件的顶层,它不属于任何一个类。可以通过import直接导入这些函数所在的文件,而不需要在引入类这个层级。

让我们把 joinToString 直接放到strings的包中试一下。依照下面这样,创建一个join.kt文件:

package strings
fun joinToString(...): String{...}
           

那么这个文件会被编译成什么样呢?JVM中只能执行类中的代码,所以这个文件最后会被编译成一个跟Java中结构类似的文件。为了方便理解,我们用一段Java代码来表示join.kt编译之后的结构:

/*Java*/
package string;
public class JoinKt{
    public static String joinToString(...) {...}
}
           

可以看到Kotlin文件编译生成的类的名称,对应于包含函数的文件的名称。这个文件中的所有顶层函数编译为这个类的静态函数。我们看一下Java是如何调用这段代码的:

import strings.JoinKt;
...
JoinKt.joinToString(list, ", ", "", "");
           

修改文件类名

如果要修改Kotlin顶层函数生成的类的名称,可以为这个文件添加 @JvmName 的注解,将其放到这个文件的开头,位于包名的前面:

@file:JvmName(“StringFunctions”) //注解指定类名

package strings //包的声明跟在文件注解之后

fun joinToString(…): String {…}

现在我们在Java中可以这样调用这个函数:

import strings.StringFunctions;

StringFunctions.joinToString(list, ", ", “”, “”);

顶层属性

和顶层函数一样,属性也可以放到文件的顶层。比如说可以使用 var 属性来计算一些函数被执行的次数:

var opCount = 0  //声明一个顶层属性

fun performOperation() {
    opCount++  //改变该属性的值
    //...
}

fun reportOperationCount() {
    println("Operation performed $opCount times") //读取属性值
}
           

也可以在代码中用顶层属性来定义常量:

默认情况下,顶层属性和其他任意的属性是一样的,是通过 访问器 暴露给Java使用的(如果val就只有一个getter,如果是var就对应一对getter和setter)。

为了方便使用,如果你想把一个常量以 public static final 的属性暴露给Java,可以用 const 来修饰它(只适用于所有的 基础数据类型 的属性,以及 String类型,其他类型的数据比如自定义的Bean类使用时会报 Const ‘val’ has type ‘Person’. Only primitives and String are allowed 错误)。我们从字节码层面来看一下有 const 和没有 const 修饰的属性有什么不同(这里不展示对应的字节码,大家可以自行去了解,这里我只展示最后分析的结果。如果不知道怎么看Kotlin的字节码,可以看一下我的这篇文章 使用Android studio查看Kotlin的字节码):

//Kotlin
const val UNIX_LINE_SEPARATOR = "\n"
//等同于
//Java
public static final String UNIX_LINE_SEPARATOR = "\n"


//Kotlin 
val UNIX_LINE_SEPARATOR = "\n"
//等同于
//Java
@NotNull
private static final String UNIX_LINE_SEPARATOR = "\n";

@NotNull
public static final String getUNIX_LINE_SEPARATOR() {
    return UNIX_LINE_SEPARATOR;
}


//简单一句话概括,没有 const 和有 const 区别在于,从调用方法上讲一个通过getXXX来调用一个直接通过.XXX来调用。从结构上讲,一个还是属性,一个变成了字段。
           

我们已经对 joinToString 改进了很多了,接下来让我们看看还有没有其他方法能够让 joinToString 更加的好用呢。

1.3 给别人的类添加方法:扩展函数和属性

一个Java项目,如果我们使用Kotlin的话肯定会遇到需要在原Java类中增加新函数的时候,当然你可以把原来的Java类用Kotlin重写一遍,但是有没有更好的办法,在即不用改变原来Java类的基础上又享受到Kotlin的语法特点呢?这就要说到Kotlin中的扩展函数了。

从理论上来讲,扩展函数 非常的简单,它就是一个类的 成员函数 ,不过这个成员函数是定义在类外部的。通过代码来感受一下什么是 扩展函数 。我们来写一个获取到一个字符串的最后一个字符的函数:

package strings

fun String.lastChar(): Char = this[this.length - 1]

//String 叫做接收者类型
//this 叫做接收者对象
           

我们可以看到定义一个 扩展函数 十分的方便。我们只需要在要定义的函数名称前加上你要扩展的 类或者接口 的名称就可以了。这个类名称被叫做 接收者类型;用来调用这个扩展函数的对象,叫做 接收者对象,可以看代码中的注释。接收者类型是由扩展函数定义的,接收者对象是该类型的一个实例。

我们看一下该如何使用调用这个函数:

>>> println("Kotlin".lastChar())
n
           

在这个例子中 Kotlin 是接收对象者,String 是接收者类型。

从某种意义上来讲,你已经为String类添加了自己的方法了。即使字符串不是代码的一部分,也没有类的源码,你仍然可以在自己的项目中根据需要去扩展方法。

在扩展函数中可以像其他成员函数一样用 this 。而且也可以像普通的成员函数一样,省略它。

package strings 
fun String.lastChar(): Char = this[length - 1]
           

在扩展函数中,可以直接访问被扩展的类的其他方法和属性,但是扩展函数是不允许你打破它的

封装性

的。和定义在内部的方法不同的是,扩展函数不能访问私有的或者是受保护的成员。

1.3.1 导入和扩展函数

我们定义的扩展函数不会自动在整个项目范围内生效,所以需要在用到的时候跟类和其他方法一样需要导入。这样是为了避免命名冲突。Kotlin中允许用和导入类一样的语法来导入单个函数:

import strings.lastChar

val c = "Kotlin".lastChar()
           

当然也可以用 * 来导入:

import strings.*

val c = "Kotlin".lastChar()
           

可以使用关键词 as 来修改导入的类或者函数的名称:

import strings.lastChar as last

val c = "Kotlin".last()
           

在项目中可能存在不同包名,但是是重名的函数,这个时候如果同时导入了这两个函数,那么导入时给它重新命名就显得很重要了。一般对于重名的函数还有一种处理方式就是:选择用 全名来指出这个类或者函数。但是对于扩展函数,Kotlin的语法要求你用 简短的名称,所以这个时候关键词 as 就是解决冲突的唯一方式了。

1.3.2 从Java中调用扩展函数

说完了Kotlin中怎么使用扩展函数,接下来我们看一下在Java中该如何调用扩展函数。从本质上来讲,扩展函数是 静态函数 ,它把调用对象作为了它的第一个参数。调用扩展函数 不会创建适配的对象或者任何运行时的消耗。

所以Java中调用扩展函数就相当于:调用这个静态函数,然后把接收者对象作为第一个参数传进去。看一个例子:

/* Java */
char c = StringUtilKt.lastChar("Java");

//StringUtilKt 是lastChar这个扩展函数所在的Kotlin文件名。
           

1.3.3 作为扩展函数的工具函数

前面铺垫了这么久,最后我们终于可以写一个 joinToString 函数的终极版本了,它和你在Kotlin标准库中看到的是一模一样的。

fun <T> Collection<T>.joinToString(separator: String = ", ",    // 为 Collection<T> 声明了一个扩展函数
                                   prefix: String = "",   //默认参数
                                   postfix: String = ""): String {
    val result = StringBuilder(prefix)
    for ((index, element) in this.withIndex()) {   //this指向接收者对象:T的集合
        if (index > 0) result.append(separator)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}

>>>  val list = listOf(1, 2, 3)
>>>  println(list.joinToString(separator = "; ", prefix = "(", postfix = ")"))
(1; 2; 3;)
           

本质上扩展函数无非就是静态函数的一个 高效的语法糖 ,可以使用更具体的类型来作为接收者类型,而不是一个类。比如你像要一个 join 函数,只能由字符串的集合来触发。

fun Collection<String>.join(separator: String = ", ",
                            prefix: String = "",   
                            postfix: String = "") = joinToString (separator, prefix, postfix)
                            
>>>  println(listOf("one", "two", "eight").join(" "))
one two eight
           

如果用其他类型来调用,将会报错:

>>>  listOf(1, 2, 8).join()

Error: Type mismatch: inferred type is List<Int> buy Collection<String> was expected.
           

扩展函数的静态性质决定了扩展函数不能被子类重写。

1.3.4 不可重写的扩展函数

在Kotlin中,重写成员函数是一件很平常的事情。但是不能重写扩展函数。假设有两个类,View和它的子类Button,然后Button重写了父类的click函数。

open class View{
    open fun click() = println("View clicked")
}

class Button: View(){
    override fun click() = println("Button clicked")
}

>>>  val view: View = Button()
>>>  view.click()   //具体调用哪个方法,由实际的view的值来决定
Button clicked
           

我们可以看到当你声明了类型为View的变量,同时它被赋值为Button类型的对象,当你调用click函数时,会调用到Button中重写的函数。但是对于扩展函数来说,并不是这样。

扩展函数并不是类的一部分,它是声明在类外的。虽然可以给基类和子类都定义一个同名的函数,但是最后调用的时候它是由该变量的 静态类型 决定的,而不是这个变量的 运行时类型 。

fun View.showOff() = println("I'm a view!")
fun Button.showOff() = println("I'm a button!")

>>>  val view: View = Button()
>>>  view.showOff()
I'm a view!    //扩展函数被静态地解析
//虽然view这个对象最后赋值的是Button类的实例,但是在调用扩展函数时,仍然调用的是View类中的showOff扩展函数。
           

如你所见,扩展函数并不存在 重写 ,因为Kotlin会把他们当作静态函数对待。

注意:如果一个类的成员函数和扩展函数有相同的签名,成员函数往往会被优先调用。

看完扩展函数,让我们来看一下Kotlin中的扩展属性。

1.3.5 扩展属性

上一节中的lastChar函数我们把它转换成一个属性试试:

val String.lastChar: Char   //只能定义成val属性,因为没有支持字段
    get() = this[length-1]
           
  • 和扩展函数一样,扩展属性也像接收者的一个普通的成员属性一样。
  • 必须定义getter()函数,因为没有 支持字段(Backing Fields),因此没有默认getter的实现。
  • 初始化也是不可以的:因为没有地方存储初始化值。

如果在StringBuilder上定义一个相同的属性,可以置为var,因为StringBuilder的内容是可变的。

var StringBuilder.lastChar: Char
    get() = get(length - 1)
    set(value) = this.setCharAt(length - 1, value)
           

可以像访问成员属性一样访问它:

>>>  println("Kotlin".lastChar)
n
>>>  val sb = StringBuilder("Kotlin?")
>>>  sb.lastChar = '!'
>>>  println(sb)
Kotlin!
           

注意,当你需要从Java中访问扩展属性的时候,应该显式的调用它的getter函数:

关于扩展函数和扩展属性相关的内容已经讲完了,下面一篇文章将会详细的讲一讲Kotlin中集合、字符串、正则表达式等等内容,如果感兴趣小伙伴,记得关注一下哈~

对于文中有疑惑的地方,或者有任何意见和建议的地方都可以评论留言,我会第一时间回复~与君共勉。