3.函数介绍

/ 0评 / 0

3.1 函数定义

kotlin的函数以关键字fun开头,参数列表中,变量名称写前面,类型写后面,返回结果写在参数列表后面,如果没有返回结果,可以不写,或者写Unit,例如

fun add(a:Int,b:Int):Unit{}

fun add2(a:Int,b:Int):Int{
    return a + b
}

如果方法函数只有一行,可以省去{},用=连接两个语句:

fun add2(a:Int,b:Int):Int = a+b

3.1.1 参数列表

3.1.1.1 默认参数

fun add(a:Int,b:Int = 10):Int = a + b //函数1
add(10)

参数可以有默认值,如果不传的话,可以使用参数列表中定义的默认值,上述例子中输出的结果是20.

参数的默认值甚至可以来源于其他参数:

fun foo(list: List<Int>,size: Int = list.size){

}

3.1.1.2 可变长参数

在可变长参数上,kotlin的特性和java保持一致:只能有一个变长参数,且必须要放在参数列表的最后一个.

fun foo(int: Int,vararg strings: String){

}
//调用
foo(10,*arrayOf("a", "b", "c"));

3.1.2 参数命名化

我们在调用函数1时,也可以指定参数的名称

add(a = 10,b = 20)
add(a = 10)

指定参数的名称的好处在于当参数列表特别长 的时候,可以很大程度的提高可阅读性.

3.1.3 多返回值

kotlin的多返回值比较奇葩,它是通过返回对象,然后接收者用多变量来接收,这么一个语法糖

fun twoReturn():Pair<Int,String> = 18 to "Pantheon"

fun threeReturn():Triple<Int,String,Int>  = Triple(18,"Pantheon",180)

fun main() {
    val (age,name) = twoReturn()
    val (age1,name1,height) = threeReturn()

}

两个返回值返回pair,三个Triple,四个应该叫Quadra,但是似乎kotlin没有实现,我们可以自己实现,需要定义一个叫Quara的数据类:

data class Quadra<out A, out B, out C,out D>(
    public val first: A,
    public val second: B,
    public val third: C,
    public val fourth :D
) ;

然后就可以有四个返回值,当然我们也可以定义Penta(5个返回值)等等其他一些多返回值的数据类.

3.2 函数类型

函数在kotlin里面是头等公民,函数可以随处定义,随处引用,可以当做参数传递,也可以当做返回值.函数可以通过委托的方法来获取到引用,比如:

fun add(a: Int,b:Int):Int{
    return a + b;
}

class Sub{
    fun sub(a: Int,b:Int):Int{
        return a - b;
    }
}

fun main(args: Array<String>) {
    var operator1 = ::add
    var operator2 = Sub::sub
    var operator3 = Sub()::sub

}

我们通过静态委托,类委托以及对象委托的方式获取到了对象的引用,函数在kotlin中是有类型的,这个类型由函数的入参和返回值决定,operator1operator3入参和返回都一样,所以他们可以互相赋值,而没有编译错误

operator1 = operator3

operator2类委托,我们知道执行一个类的方法,如果它不是静态方法,则必须要创建出对象才可以执行,比如我们要执行operator2的方法:

operator2(Sub(),10,10)

所以它的类型,肯定和operator1,operator3是不一样的,我们用idea的反编译工具来反编译一下,看看函数的类型到底是怎么回事

 public static final void main(@NotNull String[] args) {
      Intrinsics.checkNotNullParameter(args, "args");
      KFunction operator1 = null.INSTANCE;
      ((Function2)operator1).invoke(10, 20);
      KFunction operator2 = null.INSTANCE;
      ((Function3)operator2).invoke(new Sub(), 10, 10);
      KFunction operator3 = new Function2(new Sub()) {
         // $FF: synthetic method
         // $FF: bridge method
         public Object invoke(Object var1, Object var2) {
            return this.invoke(((Number)var1).intValue(), ((Number)var2).intValue());
         }

         public final int invoke(int p1, int p2) {
            return ((Sub)this.receiver).sub(p1, p2);
         }
      };
      ((Function2)operator3).invoke(3, 4);
   }

可以看到operator1operator3其实是一个Function2类型,而operator2是一个Function3

public interface Function2<in P1, in P2, out R> : Function<R> {
    /** Invokes the function with the specified arguments. */
    public operator fun invoke(p1: P1, p2: P2): R
}
public interface Function3<in P1, in P2, in P3, out R> : Function<R> {
    /** Invokes the function with the specified arguments. */
    public operator fun invoke(p1: P1, p2: P2, p3: P3): R
}

看了上面的定义,其实函数的类型是由它的入参和出参决定的,如果要用java的语法来什么operator1operator3是这样

Function2<Integer, Integer, Integer>  operator1 = xxx
Function2<Integer, Integer, Integer>  operator3 = xxx

所以它们两的类型当然一样

operator3,则是

Function3<Sub,Integer,Integer,Integer> operator2 = xxx

所以它和operator1,operator2当然不一样.

那看到Function2,Function3其实就是表示入参的个数,假如入参个数很多呢?kotlin最多支持多少个呢?kotlin给出的答案是22个,也就是如果定义了一个函数的参数个数超过了22个,那它将不会被kotlin支持.

3.3 拓展函数

我们再spring里面经常需要做各种配置,spring bean的核心接口是BeanFactory,假如我想在BeanFactory上面添加一些整合我们自己项目的方法,比如伪代码这样

beanFactory.loadMyService();

这个loadMyService并不是BeanFactory的接口,而是我们项目中定义的一个方法,但是任何一个实例化的beanFactory实例都有这个方法,这种方法叫做拓展方法.我们可以这样实现:

fun BeanFactory.loadMyService(config:String){
    println("${this.toString()}")
    println("---loadMyService--");
}

fun main(args: Array<String>) {
   var beanFactory:BeanFactory = DefaultListableBeanFactory()
    beanFactory.loadMyService("")
}

拓展方法的语法非常简单,只是在对象名称前面加了一个类名(官方一点的说法叫做receiver),表示是给哪一个类添加方法.

刚刚说的对象的拓展方法,调用这个方法必须要创建出实例,那类的静态拓展方法呢?

在kotlin中取消了static关键字,实现静态属性,静态方法需要用companion来创建一个匿名的伴生对象,比如实现一个foo的静态方法:

class Foo{
    companion object{
        fun foo(){
            println("static method")
        }
    }
}

所以类的静态拓展方法是给类的伴生对象添加拓展方法,语法如下:

fun Foo.Companion.bar() = println("bar")

拓展方法的实现非常巧妙,我们以beanFactoryloadMyService为例,反编译一下:

   public static final void loadMyService(@NotNull BeanFactory $this$loadMyService, @NotNull String config) {
      Intrinsics.checkNotNullParameter($this$loadMyService, "$this$loadMyService");
      Intrinsics.checkNotNullParameter(config, "config");
      String var2 = String.valueOf($this$loadMyService.toString());
      System.out.println(var2);
      var2 = "---loadMyService--";
      System.out.println(var2);
   }

kotlin会给拓展方法生成一个静态且不可覆盖的方法,第一个参数就是调用者,并且对this关键字做了转义,所以使用拓展函数,就像使用类内部定义的方法一样.

3.4 Infix函数

infix函数的特性是这样,我们再调用对象函数的时候,通常是对象.方法名(参数),kotlin中的infix函数调用时,可以省略.(),这样调用对象 方法名 参数,如我们创建一个pair对象:

 val pair = 2 to 3

定义infix函数有这样几个要求:

  1. 只能有一个参数
  2. 必须是拓展函数
  3. 参数不能有默认值,且不能是可变长参数

假如我们实现以5 add 2这样调用方式调用的函数,定义如下:

infix fun Int.add(int: Int) = this + int

这个写法其实就是在拓展函数前面加了一个infix关键字,infix函数可以说是拓展函数的特例.

3.5 operator函数

python中,我们判断一个数在不在数组中,用num in list的语法,其实就等价于list.contains(num),这种函数,我们把它称之为operator 函数.

比如,我们再kotlin中实现一个inoperator函数

class InOperator{
    var list = mutableListOf<Int>(1,2,3,4)
    operator fun contains(int: Int):Boolean{
        return list.contains(int)
    }
}

fun main(args: Array<String>) {
    var inOperator = InOperator()
    val b = 3 in inOperator // 等价于 inOperator.contains(3)
    println(b)
}

可以看到operator函数的申明和普通函数并没有什么区别,只是多了一个关键字operator,函数的名称是固定的,只能是contains,并且参数的个数也是固定(除了get,set,invoke),也就是说用户需要实现kotlin内置的特定方法,可以以另外一种方式来调用这个方法.

除此之外operator函数也是可以用拓展函数实现的,语法也只是在拓展函数前面加一个operator

operator fun InOperator.get(index: Int):Int{
    return list.get(index)
}
fun main(args: Array<String>) {
    var inOperator = InOperator();
    val num = inOperator[3] //等价于 inOperator.get(3)
    println(num)
}

kotlin中operator函数总共有这么几个类型

3.5.1 Unary operations

Unary 的意思是只有一个元素,也就是被调用者,没有参数,Unary operator中有的需要写成前缀的形式,如+a:

表达式 等价表达式
+a a.unaryPlus()
-a a.unaryMinus()
!a a.not()
a++ a.inc()
a-- a.dec()

举个unaryPlus的例子

data class UnaryPlusObjData(val num: Int);
operator fun UnaryPlusObjData.unaryPlus():UnaryPlusObjData = UnaryPlusObjData(this.num + 1)

fun main(args: Array<String>) {
    val objData = UnaryPlusObjData(1)
    val data = +objData
    println(data.num)
}

3.5.2 Binary operations

3.5.2.1 Arithmetic operators

表达式 等价表达式
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..b a.rangeTo(b)

3.5.2.2 in operator

表达式 等价表达式
a in b b.contains(a)
a !in b !b.contains(a)

3.5.2.3 Indexed access operator

表达式 等价表达式
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)

3.5.2.4 invoke operator

表达式 等价表达式
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)

3.5.2.5 Augmented assignments

表达式 等价表达式
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)

3.5.2.6 Equality and inequality operators

表达式 等价表达式
a == b a?.equals(b) ?: (b === null)
a != b !(a?.equals(b) ?: (b === null))

3.6 lambda表达式

在java中,我们定义lambda函数,通常先需要定义一系列的接口,然后打上FunctionalInterface的标识,而在kotlin中则完全不用这样.比如,定义个加法器:

fun main(args: Array<String>) {
    var lambda = { a: Int,b:Int -> a + b }
    var sum = lambda(1, 2)
}

当函数体只有一行的时候,编译器会根据执行的结果推测出是否需要返回结果,这个例子给出的返回结果就是3,当然我们也可以指定lambda的返回类型为unit,也就是不返回结果

  var lambda:(Int,Int)->Unit = { a: Int,b:Int -> a + b }

所以lambda的语法要求有这样几点:

3.7 匿名函数

我们在lambda表达式中看到,lambda是不用指定返回类型的,因为它可以通过上下文自动推断出来,那如果我们需要指定返回类型,除了在lambda定义的时候指定lambda的类型,还可以通过匿名函数来实现:

fun(num:Int):Boolean = num > 10

我们也可以把它赋值给某一个变量,然后后面的函数可以调用它:

var a = fun(num:Int):Boolean = num > 10
val mutableListOf = mutableListOf<Int>(1, 2, 3, 11, 12)
mutableListOf.filter(a)

也可以直接在调用处定义匿名函数:

val mutableListOf = mutableListOf<Int>(1, 2, 3, 11, 12)
mutableListOf.filter (fun(item):Boolean = item>10)

当然也可以省略返回类型:

val mutableListOf = mutableListOf<Int>(1, 2, 3, 11, 12)
mutableListOf.filter (fun(item) = item>10)

匿名函数和普通函数差不多,只不过没有名字,要使用它,要么把它赋值给变量,然后调用,要么直接在高阶函数里面定义且调用.

3.8 高阶函数

高阶函数的定义不是很难,就是指入参有函数,或者返回值为函数的函数,比如集合的filter函数,

val mutableListOf = mutableListOf<Int>(1, 2, 3, 11, 12)
mutableListOf.filter {it>10}

mutableListOf.filter {it>10}这种调用方式需要说明三个问题:

  1. 当高阶函数的最后一个参数是函数时,调用的时候可以不写在()里面,而以lambda的形式跟在调用处后面,这一个特性为日后实现DSL埋下了基础

    比如我定义了这样的高阶函数,功能很简单就是比较5和一个provider产生的一个数进行比较大小:

    fun compare(num:Int,provider:()->Int):Boolean =  num > other();

    provider是一个函数类型,没有入参,返回值为Int,所以调用的时候我定义了一个lambda表达式,而又因为它是函数的最后一个参数,可以这样调用:

    val compare = compare(10) {kotlin.random.Random.nextInt()}
  2. 当参数函数只有一个参数的时候,可以用it来作为默认参数名称,并且可以省略lambda->.当然我们也可以重命名参数的名称:

    mutableListOf.filter {a->a>10}

    当高阶函数参数函数中的参数我们不需要的时候,可以用_来代替参数名称,听着有点拗口,来个例子:

    val mapOf = mapOf<Int, Int>()
    mapOf.forEach{ (_, value) -> println(value)}

    是不是一目了然,我们不需要key嘛.

    高阶函数好像看着不算难,但是真正一旦把之前的函数一旦结合起来看,就飞天了,甚至后面会研究的DSL功能,看似合理,但是不知道写的是什么,这里我们先研究研究常用的几个高阶函数

    3.8.1 with函数

    with是随着的意思,with函数表达的意思是,调用receiver函数的方法时,就象在方法体内部调用一样,举个例子:

    class MyWith{
    
       fun with1():String = "with1";
    
       fun with2(string: String) = "${string}+with2"
    
    }
    
    fun main(args: Array<String>) {
       val myWith = MyWith()
       with(myWith){
           val with1 = with1()
           val with2 = with2(with1)
           println(with2)
       }
    }

    with可以省去很多对象名称.方法的调用方式,调用方式时,像是对象内部调用.with当然还可以有返回值,比如我们创建一个数据类时,也可以这样去创建:

    class People{
       lateinit var  name:String;
       var  age by Delegates.notNull<Int>();
       lateinit var  from:String;
       lateinit var  to:String;
       override fun toString(): String {
           return "People(name='$name', age=$age, from='$from', to='$to')"
       }
    }
    fun main(args: Array<String>) {
       var people =  with(People()) {
           name = "Pantheon"
           age = 10
           from = "JiangSu"
           to = "Beijing"
           this
       }
    }

    这样的话可以不用写对象名称,很简洁,注意main的最后一行,只有一个this,lambda我们说过,如果lambda有返回值,单独写在最后一行.

    我们琢磨下with的语法是咋实现的:

    public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
       contract {
           callsInPlace(block, InvocationKind.EXACTLY_ONCE)
       }
       return receiver.block()
    }

    首先参数是两个,第一个是泛型,第二个是函数类型,无入参,返回泛型,并且这个函数还是T的拓展函数,这个是实现我们刚刚使用的语法的关键.之前讲拓展函数的时候讲过,拓展函数就像其他函数一样,在拓展函数里面给receiver属性赋值,就像是给this的属性赋值,所以可以不用对象.方法的形式调用.我们在看receiver.block(),也就是调用了拓展函数

    3.8.2 let函数

    kotlin中每一个变量都有let的函数,可以使用let函数实现对象的转换,比如:

    data class People(var name:String,var age:Int);
    
    data class Student(var name: String,var age: Int,var  grade:String)
    val people = Student("Pantheon", 30, "4th").let {
           People(it.name, it.age)
    }

    也可以实现链式调用的功能:

    val people = Student("Pantheon", 30, "4th").let {
           People(it.name, it.age)
       }.let {
           println(it.name)
       }.let {
           println("----")
       }

    类似的还有also函数,和let的区别在于also返回的类型是this.

    3.8.3 run函数

    run函数和with意思一致,只不过调用方式不一致,with我们这样调用:

    val people =  with(Student("Pantheon", 30, "4th")){
           People(name, age)
    }

    run函数只有一个参数,且也是调用者的拓展函数,所以可以这样调用

    val people = Student("Pantheon", 30, "4th").run {
                   People(name,age)
    }
    

​ 与其相似的还有一个apply函数,apply返回的类型是this,别的没有什么区别

3.9 inline&noline&crossinline

inline函数是一种优化手段,在生成字节码的时候,inline函数会被重新编织字节码,直接在被调用处生成相应的字节码,这样的好处就在于,减少了创建函数的开销,函数在kotlin中存在的形式是以FunctionN的对象来存在,inline函数就直接减少了FunctionN对象的创建,来看个demo:

inline fun gogo(){
    print("gogo begin")
    print("gogo")
    print("gogo end")
}

fun invokeGoGo(){
    print("invokeGoGo begin")
    gogo()
    print("invokeGoGo end")
}

gogoinline修饰,并被invokeGoGo调用,在编译阶段,会直接把gogo的代码拷贝到invokeGoGo方法体内,从而减少创建gogo函数对象的开销,编译后的效果等价于:

fun invokeGoGo(){
    print("invokeGoGo begin")
    print("gogo begin")
    print("gogo")
    print("gogo end")
    print("invokeGoGo end")
}

inline函数不仅可以将函数体拷贝到被调用处,还可以将高阶函数的参数拷贝到被调用处

inline fun gogo(block:()->Unit){
    print("gogo begin")
    block()
    print("gogo end")
}

fun invokeGoGo(){
    print("invokeGoGo begin")
    gogo{
        println("开始了")
        println("start")
    }
    print("invokeGoGo end")
}

编译后的效果等价于这样:

fun invokeGoGo(){
    print("invokeGoGo begin")
    print("gogo begin")
    println("开始了")
    println("start")
    print("gogo end")
    print("invokeGoGo end")
}

但是当我们的inline高阶函数需要返回入参的时候,却出了个问题,因为入参函数被解析成了一行行的代码,丢掉了函数本身自己的类型,编译器不知道你要返回什么给函数调用者:

inline fun gogo(block:()->Unit):()->Unit{
    print("gogo begin")
    block()
    print("gogo end")
    return block
}

这段函数编译期就会报错,这个时候noline就显示出了作用,noline的作用就是在于inline函数中,有些函数,它不能被解析成一行行的函数,就可以在参数上加noline

inline fun gogo(noinline block:()->Unit):()->Unit{
    print("gogo begin")
    block()
    print("gogo end")
    return block
}

再比如,gogo方法需要掉另外一个方法,这个方法需要传入block,block函数应该作为一个整体,所以也应该加上noinline

最后在说一下crossinline的关键字,当block发生间接调用时,也就是将block当做参数,传递给别的高阶函数时,这个时候就会出现一个问题,就是return的问题.之前介绍过return,inline函数内部是可以出现return的,并且表示结束的是被调用方,但是如果出现间接调用,该如何处理return的结束范围呢?

比如:

inline fun f( body: () -> Unit) {
    println("go")
    body()
    println("end")
}
fun invokeGoGo(){
    f{
        return
    }
}

这段话最终只会输出go,因为invokeGoGo生成的字节码是这样:

fun invokeGoGo(){
  println("go")
  return
  println("end")
}

但是如果我需要把body这个函数当做参数传递给别的函数呢?

inline fun f( body: () -> Unit) {
    println("go")
    go{
        body()
    }
    body()
    println("end")
}

fun go(body: () -> Unit){
    println("go")
    body()
}

fun invokeGoGo(){
    f{
        return
    }
}

编译期应该就懵逼了,这个return到底是指的谁,指的是go还是f还是invokeGoGo,所以对于这种间接调用的情况,kotlin无奈选择了一个很不优雅的解决方法,就是间接调用需要加crossinline关键字,并且无法使用return语句

inline fun f( crossinline body: () -> Unit) {
    println("go")
    go{
        body()
    }
    body()
    println("end")
}

fun go(body: () -> Unit){
    println("go")
    body()
}

fun invokeGoGo(){
    f{

    }
}

看似是一个调用的问题,实际是引入了inline优化方案而引起的一系列语法上的冲突,既无奈又心酸,谁让java底层对lambda支持这么差,需要上层像各种优化办法呢?

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注