Kotlin函数篇

Posted 古月书斋

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Kotlin函数篇相关的知识,希望对你有一定的参考价值。

本文整理自:https://chiclaim.blog.csdn.net/article/details/88624808

一、Kotlin 函数的基本定义

我们先来定义一个基本的函数:

fun max(a: Int, b: Int): Int 
    return if (a > b) a else b

解释一下

  • fun 关键字用来定义一个函数
  • fun 关键字后面是函数名(max)
  • 括号中间是函数参数
  • 冒号后面是返回值类型
  • 语句可以不用分号结尾

如下图所示:

需要注意的是 Kotlin 中没有像 Java 中的 三元运算符 了

在 Java 中上面的 函数体 可以改成这样:

return (a > b) ? a : b

Kotlin 中要使用 if 语句来代替 三目运算符

二、函数调用

1、调用函数时指定参数的名字

假设我们有如下的函数:

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

然后调用该函数(为参数值指定参数名称):

joinToString(collection, separator = " ", prefix = " ", postfix = ".")

2 、为函数参数指定默认值

我们可以把 joinToString 定义改成如下形式:

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

这样也就间接的实现了Java中所谓的重载(overload),代码也更简洁,不用定义多个方法了。

3、Parameter和Argument的区别

定义函数时候的参数称之为 Parameter;调用函数传入的参数称之为 Argument

三、顶级函数和属性

在 Java 中我们需要把函数和属性放在一个类中

在 Kotlin 中我们可以把某个函数或属性直接放到某个 Kotlin 文件中

把这样的函数或属性称之为 顶级(top level)函数或属性

例如在 join.kt 文件中:

package strings

fun joinToString(...): String  
    ... 

在 Java 代码中如何调用该方法呢?因为 JVM 虚拟机只能执行类中的代码

所以 Kotlin 会生成一个名叫 JoinKt 的类,并且顶级函数是静态的

所以可以在 Java 中这样调用顶级函数:

JoinKt.joinToString(...)

在Kotlin中如何调用,如果在不同的包,需要把这个顶级函数导入才能调用

//相当于 import strings.JoinKt.joinToString
import strings.joinToString 

//相当于 import strings.JoinKt.*
import strings.* 

所有的工具类都可以使用这样的方式来定义

顶级属性 同样也是 static 静态的

如果使用 var 来定义会生成对应的静态setter、getter函数

如果使用 val 来定义只会生成对应的静态getter函数

我们知道顶级函数和属性,最终还是会编译放在一个类里面,这个类名就是顶级函数或属性的 Kotlin文件名称+Kt

如果所在的Kotlin文件名被修改,编译生成的类名也会被修改,可以通过注解的方式来固定编译生成的类名:
 

@file:JvmName("StringFunctions")

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

调用的时候就可以这样来调用:

import strings.StringFunctions; 

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

 四、函数和属性扩展

1、函数扩展

何谓 扩展函数 ? 扩展函数是在类的外部定义,但是可以像类成员一样调用该函数

扩展函数的定义格式如下图所示:

其中 receiver type 就是我们扩展的目标类,receiver object 就是目标类的对象(哪个对象调用该扩展函数,这个this就是哪个对象)

lastChar 就是我们为 String 类扩展的函数

package strings

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

然后我们这样来调用该扩展函数:

println("Kotlin".lastChar())

如果扩展函数所在的包名和使用地方的包名不一样的话,需要导入扩展函数

import strings.*
//或者
import strings.lastChar

val c = "Kotlin".lastChar()

扩展函数本质上是静态函数,如上面的扩展函数 lastChar 反编译后对应的 Java 代码:

public static final char lastChar(@NotNull String $receiver) 
  Intrinsics.checkParameterIsNotNull($receiver, "receiver$0");
  return $receiver.charAt($receiver.length() - 1);

编译的时候,会在调用的该扩展函数的地方使用 StringUtilsKt.lastChar("") 代替

所以,如果要在 Java 中使用 Kotlin 定义的扩展函数,也是直接调用该静态方法即可

并且扩展函数是不能被覆写(override) 的,因为它本质上是一个静态函数。

2、属性扩展

扩展属性和扩展函数的定义非常相似:

val String.lastChar: Char 
    get() = this.get(length - 1)

我们必须为这个扩展属性定义 getter 函数,因为扩展属性没有 backing field

扩展属性在定义的时候,也会生成静态方法:

public static final char getLastChar(@NotNull String $receiver) 
  Intrinsics.checkParameterIsNotNull($receiver, "receiver$0");
  return $receiver.charAt($receiver.length() - 1);

如果扩展属性的 receiver object 可以被修改,可以把扩展属性定义成 var

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

五、函数的可变参数和展开操作符

1 、可变参数

在 Java 中通过三个点()来声明可变参数,如:

public static <T> List<T> listOf(T... items) 
	System.out.println(items.getClass()); //数组类型
	return Arrays.asList(items);

Kotlin 和 Java 不一样,Kotlin 使用 vararg 关键来定义可变参数:

fun <T> listOf(vararg items: T): List<T> 
    println(items.javaClass)     //数组类型
    return Arrays.asList(*items) // * spread operator

对于可变参数的函数,调用它的时候可以传递任意个参数

2 、展开操作符

通过上面的两段代码比较我们发现:Kotlin 需要显示的将可变参数通过 * 展开,然后传递给 asList 函数

这里的 * 就是 展开操作符(spread operator),在 Java 中是没有 展开操作符 的

下面我们再来看下,展开操作符的方便之处:

val intArr: Array<Int> = arrayOf(1, 2, 3, 4)
Arrays.asList(0, intArr).run 
	println("size = $size")


//输出结果:
size = 2

可以发现,不用展示操作符的话,集合里面只有两个元素

那我们把它改成使用 展开操作符 的情况:

val intArr: Array<Int> = arrayOf(1, 2, 3, 4)
Arrays.asList(0, *intArr).run 
	println("size = $size")


//输出结果:
size = 5

3、Java中的Arrays.asList()的坑和原理分析
既然上面用到了 Java 中的 Arrays.asList() 函数,下面来讲下该函数的容易遇到的坑及原理分析:

public static void testArrays() 
	int[] intArr = 1, 2, 3;
	List list = Arrays.asList(intArr);
	println(list.size());   //size = 1


public static void testArrays2() 
	Integer[] intArr =1, 2, 3;
	List list = Arrays.asList(intArr);
	println(list.size());  //size = 3

上面的 testArrays 和 testArrays2 函数非常相似,只不过是数组的类型不同,导致 Arrays.asList(arr) 返回的集合大小不一样

只要是 原始类型数组 Arrays.asList 返回的集合大小为 1,如果是 复杂类型的数组,Arrays.asList 返回的集合大小为数组的大小

为什么会产生这种情况呢?下面来分析下:

首先看下 Arrays.asList 是怎么定义的:


public static <T> List<T> asList(T... a) 
	return new ArrayList<>(a);


private static class ArrayList<E> extends AbstractList<E>
	implements RandomAccess, java.io.Serializable

	private static final long serialVersionUID = -2764017481108945198L;
	private final E[] a;

	ArrayList(E[] array) 
		a = Objects.requireNonNull(array);
	
	//省略其他...

经过上面的分析我们知道,如果是一维原始类型的数组传递给可变参数,这个可变参数就是 二维数组

然后把二维数组传递给内部ArrayList的构造方法,通过 E[] 保存下来。这里的泛型 E 就相当于 int[],E[] 相当于 int[][]

需要注意是 Java 不允许 将个二维数组 直接赋值 给一维的泛型数组:
 

int[][] intArray = 1,2;
T[] t = intArray;  //非法

但是 Java 允许 把二维数组传递给参数是一维的泛型数组的函数,如:

public static <T> void testGeneric(T[] data)

int[][] intArray = 1,2;
testGeneric(intArray);

4、Kotlin 展开操作符的原理分析

讲到这里你可能迫不及待的想知道,为什么我们上面的代码使用了展开操作符 Arrays.asList(*intArr) 返回的集合大小就是 5 呢?

val intArr: Array<Int> = arrayOf(1, 2, 3, 4)
Arrays.asList(0, *intArr).run 
	println("size = $size")


//输出结果:
size = 5

反编译后对应的 Java 代码如下:

Integer[] intArr2 = new Integer[]1, 2, 3, 4;
SpreadBuilder var10000 = new SpreadBuilder(2);
var10000.add(0);             //第1个元素
var10000.addSpread(intArr2); //数组里的4个元素
List var2 = Arrays.asList((Integer[])var10000.toArray(new Integer[var10000.size()]));
int var7 = false;
String var5 = "size = " + var2.size();
System.out.println(var5);

原来会通过 SpreadBuilder 来处理展开操作符,SpreadBuilder 里面维护了一个ArrayList

所有的元素都会保存到这个 ArrayList 中,然后把这个集合转成 元素为复杂类型数组,再传给 Arrays.asList(arr) 函数

根据上面我们对 Arrays.asList(arr) 的分析,我们就知道返回的集合大小是 5 了

 六、中缀函数

我们都知道什么是前缀(prefix),后缀(suffix)。那什么是函数的中缀(infix)调用呢?

使用关键字 infix 修饰的函数都能够 中缀调用

被关键字 infix 修饰的函数只能有一个参数

Kotlin 中的 to 就是一个中缀函数:

public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

下面我们来对比下 to 函数的常规调用和中缀调用:

1.to("one")  //普通的函数调用
1 to "one"   //函数的中缀调用

除了 to 函数,还有我们介绍 循环 的时候讲到的 until、downTo、step 也是中缀函数:

public infix fun Int.until(to: Int): IntRange 
    if (to <= Int.MIN_VALUE) return IntRange.EMPTY
    return this .. (to - 1).toInt()


public infix fun Int.downTo(to: Int): IntProgression 
    return IntProgression.fromClosedRange(this, to, -1)


public infix fun IntProgression.step(step: Int): IntProgression 
    checkStepIsPositive(step > 0, step)
    return IntProgression.fromClosedRange(first, last, if (this.step > 0) step else -step)



//使用示例:
for(i in 0 until 100)


for (i in 100 downTo 0 step 2) 

七、 局部函数

局部函数(local function) 是在函数里面定义函数,局部函数只能在函数内部使用j局部函数说白了就是函数嵌套,那什么时候使用局部函数呢?当一个函数里的逻辑很多重复的逻辑,可以把这些逻辑抽取到一个局部函数

以《Kotlin In Action》的代码为例:

fun saveUser(user: User) 
    if (user.name.isEmpty()) 
        throw IllegalArgumentException("Cannot save user $user.id: Name is empty")
    
    if (user.address.isEmpty())  
        throw IllegalArgumentException("Cannot save user $user.id: Address is empty")
    
    // Save user to the database 

这个 saveUser 函数里面有些重复逻辑,如果 name 或 address 为空都会抛出异常

可以使用局部函数优化下:

fun saveUser(user: User) 
    fun validate(value: String, fieldName: String)  
        if (value.isEmpty()) 
            throw IllegalArgumentException("Can't save user $user.id: " + "$fieldName is empty")
        
    
    validate(user.name, "Name") 
    validate(user.address, "Address")
    // Save user to the database   

局部函数避免了模板代码的出现。如果不使用局部函数,我们需要把 validate函数 定义到外面去,但是这个函数只会被 saveUser函数 使用到,从而污染了外面的全局作用域。通过局部函数使得代码更加清晰,可读性更高。

需要注意的是,虽然 Kotlin 允许在函数内部定义函数,但是不要嵌套太深,否则会导致可读性太差

十、 匿名函数

匿名函数顾名思义就是没有名字的函数:如:

fun(x: Int, y: Int): Int 
    return x + y

匿名函数的返回类型的推导机制和普通函数一样:

fun(x: Int, y: Int) = x + y

如果声明了一个匿名函数 ,如何调用呢?

(fun(x: Int, y: Int): Int 
    val result = x + y
    println("sum:$result")
    return result
)(1, 9)

输出结果:
sum:10

 

以上是关于Kotlin函数篇的主要内容,如果未能解决你的问题,请参考以下文章

Kotlin学习之函数

kotlin函数

kotlin函数

Kotlin 中infix,inline,noinline,crossinline ,refied 等的理解

Kotlin——高级篇:高阶函数详解与标准的高阶函数使用

kotlin学习笔记之复合函数f(g(x))