四、详解:函数

130 阅读7分钟

参数默认值与函数重载

Kotlin允许对函数参数设置默认值,调用函数时默认从后到前逐个省略,可通过指定参数名明确传递的是哪个参数。

fun test(param1: Int = 123, param2: String = "") {}
test()//允许,省略2个参数
test(456)//允许,省略第二个参数
test("abc")//不允许,按照从后到前逐个省略的规则,此时相当于省略了第二个参数,所以类型不匹配
test(param2 = "abc")//允许,通过指定参数名只传递第二个参数

Kotlin中如果对函数参数设置了默认值,则会自动生成一个<方法名>$default的函数,所有省略参数传递的调用都会通过此方法补全参数再去调用原函数。

/***
 *  var0:执行对象,如果为静态函数则没有这个参数;
 *  var1:函数的第一个参数;
 *  var2:函数的第二个参数;
 *  var3:二进制数字,记录需要补全的参数位;
 *  var4:目前一直为null;
 */
public static void test$default(int var1, String var2, int var3, Object var4) {
   if ((var3 & 1) != 0) {//第一位参数是否使用默认值
      var1 = 123;
   }
   if ((var3 & 2) != 0) {//第二位参数是否使用默认值
      var2 = "";
   }
   test(var1, var2);
}
//test()实际调用如下
test$default(0, (String)null, 3, (Object)null);

在Kotlin中设置了参数默认值的函数与Java的函数重载达到类似效果,但从Java环境调用此Kotlin函数时仍然需要传递所有参数,如果需要达到与Java中函数重载一致的效果,需要添加@JvmOverloads注解。添加此注解后,编译器会从后到前逐个省略有默认值的参数,自动生成多个方法。

@JvmOverloads
public static final void test(int param1, @NotNull String param2) {
   Intrinsics.checkNotNullParameter(param2, "param2");
}
public static void test$default(int var0, String var1, int var2, Object var3) { ... }
@JvmOverloads
public static final void test(int param1) {
   test$default(param1, (String)null, 2, (Object)null);
}
@JvmOverloads
public static final void test() {
   test$default(0, (String)null, 3, (Object)null);
}

扩展函数

Kotlin能够在不继承的情况下为一个类型添加函数,使用该类型对象的点表达式调用此函数,这种机制称为扩展函数。

fun String.id(): String {...}
val aid = "a".id()

扩展并没有修改被扩展的类,只是添加了一个对应的静态方法,此静态方法的第一个参数为该类型对象,后续参数为扩展方法里的其他参数。

public static final String id(@Nullable String $this$id) {...}

扩展函数支持null的点表达式调用,只需声明被扩展的类型可为空即可。

fun String?.id(): String {...}
val aid = null.id()

Kotlin中对属性也支持扩展,扩展属性必须显示提供getter/setter方法。扩展属性与扩展函数的原理一致,并没有在被扩展类型中新增一个属性,而是在生成一个对应的静态方法,此静态方法的第一个参数为该类型对象,后续参数为getter/setter的其他参数。

var strId: String = ""//被扩展类型中没有新增属性,所有要声明一个承接属性
var String.id: String
    get() {
        return strId
    }
    set(value) {
        strId = value
    }
val aid = "a".id
//编译后生成
@NotNull
public static final String getId(@NotNull String $this$id) {...}
public static final void setId(@NotNull String $this$id, @NotNull String value) {...}

解构函数

<val/var> (<变量1>,<变量名2>…) = <对象> 此方法会创建多个变量,并将对象内的属性值按照声明顺序赋值给对应变量,此过程需要调用对象的componentN函数,或者通过下标查询。

<val/var> <变量1> = <对象>[0]/<对象>.get(0)/<对象>.component1()
<val/var> <变量2> = <对象>[1]/<对象>.get(1)/<对象>.component2()
……
<val/var> <变量n> = <对象>[n-1]/<对象>.get(n-1)/<对象>.componentN()

data class Student(//data class默认创建componentN函数
    val name: String,
    var age: Int,
    var school: String
)
class Example(private val student: Student) {
    fun test() {
        val (s1, s2, s3) = student//通过componentN函数赋值
        val (a1, a2, a3) = arrayOf("1", "2", "3")//通过下标赋值
        val (it1, it2, it3, it4) = arrayListOf("1", "2", "3")//通过get方法复制
        //解构声明也可以用在for循环中:
        for ((a, b) in collection) { …… }
    }
}

kotlin的解构函数严格按照顺序赋值,无需使用的变量使用_跳过;声明componentN函数时需要用operator关键字标记;

Lambda表达式

  • lambda表达式总是括在花括号中。
  • 完整语法形式的参数声明放在花括号内,并有可选的类型标注。
  • 函数体跟在⼀个->之后。
  • 如果推断出的该lambda的返回类型不是Unit,那么该lambda主体中的最后⼀个(或可能是单个)表达式会视为返回值。
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }

⼀个不带标签 return语句总是在⽤fun关键字声明的函数中返回。这意味着 lambda 表达式中的return将从包含它的函数返回,需要使⽤限定的返回语法从lambda显式返回⼀个值。

ints.filter { val shouldFilter = it > 0 return@filter shouldFilter }

在Kotlin中,如果函数的最后⼀个参数是函数,那么作为相应参数传⼊的lambda表达式可以放在圆括号之外:

val product = items.fold(1) { acc, e -> acc * e } 

这种语法也称为拖尾lambda表达式。如果该lambda表达式是调⽤时唯⼀的参数,那么圆括号可以完全省略。

当lambda表达式中只有一个参数时,该参数会隐式声明为it

ints.filter { it > 0 } // 这个字⾯值是“(it: Int) -> Boolean”类型的

如果lambda表达式的参数未使⽤,那么可以⽤下划线_取代其名称:

map.forEach { _, value -> println("$value!") }

高阶函数

Kotlin支持使用函数作为参数或者返回值,这里作为参数或返回值的函数即高阶函数,以高阶函数为参数或返回值的函数也是高阶函数。

当使用函数作为参数或返回值时,需要声明函数类型:(参数类型1,参数类型2,...)->返回类型;

fun sum(param: () -> Int): (Int) -> Int {
    return fun(num: Int): Int {
        return param() + num
    }
}

也可以使用类型别名来表示函数类型:

typealias numberOne = () -> Int
typealias numberTwo = (Int) -> Int
fun sum(param: numberOne): numberTwo {
    return fun(num: Int): Int {
        return param() + num
    }
}

如果匿名函数是在某个函数上定义的最后一个参数,则您可以在用于调用该函数的圆括号之外传递它:

sum { 
    return@sum 123
}

获得函数类型的实例的几种方式:

  1. lambda 表达式: { a, b -> a + b }
  2. 匿名函数: fun(s: String): Int { return s.toIntOrNull() ?: 0 }
  3. 使用已有声明的可调用引用:
    • <类/实例对象>::<内部函数/构造函数/扩展函数>:String::toInt、"123"::toInt
    • <类/实例对象>::<内部属性扩展属性>(Kotlin代理):String::length、"123"::length

内联函数

与Java中的内联一样,通过增加代码到引用处提升运行性能,但也会导致生成的代码增加。inline表明函数内联,并且会将传递给此函数的高阶函数也内联到调用处,如果不希望所有传给内联函数的高阶函数类型的参数也都内联,那么可以⽤noinline修饰符标记不希望内联的函数参数:

inline fun foo(
    inlined: () -> Unit,  //此函数内联
    noinline notInlined: () -> Unit //此函数不内联
    ) { …… }

内联函数⽀持具体化的泛型类型参数:

inline fun TreeNode.findParentOfType(): T? { 
    var p = parent 
    while (p != null && p !is T) { 
        p = p.parent 
    }
    return p as T? 
}

当内联函数的可见性导致起可被外部module调用时(publicprotected),此函数被认为是⼀个模块级的公有API。如果此时修改了此公有API内联函数的相关代码,并重新编译,而调用此内联函数的其他module没有重新编译,则其他module调用的仍是旧代码,此时就会产生兼容问题。为了消除这种由⾮公有API变更引⼊的不兼容的⻛险,公有API内联函数体内不允许使⽤⾮公有声明。使用internal修饰可见性的方法可通过添加@PublishedApi注解当作公有API使用。

inline fun foo() {
    test1() //编译报错
    test2() //编译报错
    test3() //编译报错
    test4() //编译通过
    test5() //编译通过
}
internal fun test1(){ …… }
private fun test2(){ …… }
protected fun test3(){ …… }
fun test4(){ …… }
@PublishedApi
internal fun test5(){ …… }

委托函数

在委托模式中,有两个对象参与处理同一个请求,接受请求的对象将请求委托给另一个对象来处理。Kotlin中通过关键字by实现委托。

类委托

<接口的子类定义> by <接口实例对象>

interface Base {
    fun print(msg: String)
}
class BaseImpl : Base {
    override fun print(msg: String) {
        System.out.println(msg)
    }
}
class Test1 : Base by BaseImpl()
class Test2(impl: Base) : Base by impl

编译后,被委托类的内接口方法将转为调用委托接口实例对象内的对应方法。

public final class Test1 implements Base {
   private final BaseImpl $$delegate_0 = new BaseImpl();
   public void print(@NotNull String msg) {
      Intrinsics.checkNotNullParameter(msg, "msg");
      this.$$delegate_0.print(msg);
   }
}
public final class Test2 implements Base {
   private final Base $$delegate_0;
   public Test2(@NotNull Base impl) {
      Intrinsics.checkNotNullParameter(impl, "impl");
      super();
      this.$$delegate_0 = impl;
   }
   public void print(@NotNull String msg) {
      Intrinsics.checkNotNullParameter(msg, "msg");
      this.$$delegate_0.print(msg);
   }
}

属性委托

<var/val> <属性名>:<类型> by <委托对象> 被委托的属性的get方法以及set方法将被委托给这个对象的对应的get方法或set方法。属性委托不必实现任何接口,实现方式大致有以下几种:

1、声明委托对象的类型,其内部必须含有使用operator修饰的getValuesetValue方法(val修饰的属性可省略setValue方法):
class Example {
    var name: String by Delegate()
}
class Delegate  {
    //thisRef:被委托属性所在类的实例
    //property:被委托属性的基本信息
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, 这里委托了 ${property.name} 属性"
    }
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$thisRef${property.name} 属性赋值为 $value")
    }
}

亦可使用kotlin中提供的ReadOnlyPropertyReadWriteProperty接口声明委托代理对象的类型。

2、声明委托代理对象的类型,其内部必须含有使用operator修饰的provideDelegate方法,此方法返回实际委托对象:
class Example {
    var name: String by ProvideDelegate()
}
class Delegate  {.....}//上面的class
class ProvideDelegate {
    operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): Delegate {
        System.out.println("propertyName=" + property.name)
        return Delegate()
    }
}

由于此方法与方法1的差别主要在通过provideDelegate方法创建委托对象,可在在此过程中进行预处理。亦可直接使用kotlin中提供的PropertyDelegateProvider接口。

3、使用Lazy创建延迟初始化的不可修改的属性委托:

<val> <属性名>:<类型> by lazy <Lambda表达式>

val id: Int by lazy {
    999
}

lazy是一个函数, 接受一个Lambda表达式作为参数, 返回一个Lazy <T>实例的函数,返回的实例可以作为实现延迟属性的委托。

public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
//SynchronizedLazyImpl通过volatile和synchronized关键字双重检验保证初始化时线程安全
private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE
    // final field is required to enable safe publication of constructed instance
    private val lock = lock ?: this
    override val value: T
        get() {
            val _v1 = _value
            if (_v1 !== UNINITIALIZED_VALUE) {
                @Suppress("UNCHECKED_CAST")
                return _v1 as T
            }
            return synchronized(lock) {
                val _v2 = _value
                if (_v2 !== UNINITIALIZED_VALUE) {
                    @Suppress("UNCHECKED_CAST") (_v2 as T)
                } else {
                    val typedValue = initializer!!()
                    _value = typedValue
                    initializer = null
                    typedValue
                }
            }
        }
    override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE
    override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."
    private fun writeReplace(): Any = InitializedLazyImpl(value)
}

lazy还有其他2个同名方法,可配置加锁对象以及是否使用synchronized加锁。

4、使用kotlin.properties.Delegates类内的方法创建委托对象
  • notNull:被委托属性不可为null
  • observable:监听被委托属性修改后的状态
  • vetoable:在被委托属性修改前进行校验、拦截
5、把属性储存在映射(Map)中
class User(val map: Map) {
    val name: String by map//调用name的get方法时相当于调用map.get("name").toString()
    val age: Int by map//调用age的get方法时相当于调用map.get("age").toInt()
}

这也适用于var属性,如果把只读的Map换成MutableMap

5、委托给另一属性

一个属性可以把它的getset方法委托给另一个属性,这种委托对于顶层和类的属性(成员和扩展)都可用: <var/val> <属性名>:<类型> by <委托对象/this(可省略)>::<属性名>

class Delegate {
    val key: String = "delegate"
    var value: Int = 0
}
class Example(private val delegate: Delegate) {
    val name: String by delegate::key
    val id: String by this::name
    var score: Int by delegate::value
}

此种委托方式会将被委托属性编译成KProperty0PropertyReference0Impl)或KMutableProperty0MutablePropertyReference0Impl)对象,被委托属性的setget方法将会调用对应Impl的setget方法。

6、局部委托属性

可以在局部代码块内使用属性委托,局部被委托属性不允许作为委托属性。

class Example(private val delegate: Delegate) {
    fun test() {
        val name: String by delegate::key//编译通过
        var score: Int by delegate::value//编译通过
        val id: String by ::name//编译报错
    }
}

中缀表示法

使用infix关键字修饰的函数可以使⽤中缀表示法(忽略该调⽤的点与圆括号)调⽤。

infix fun Int.max(x: Int): Int {
    return Math.max(this, x)
}
fun test() {    
    1 max 2 // ⽤中缀表示法调⽤该函数
    1.max(2) // 等同于这样
}

中缀函数必须满⾜以下要求:

  • 它们必须是成员函数或扩展函数。
  • 它们必须只有⼀个参数。
  • 其参数不得接受可变数量的参数且不能有默认值。

中缀函数调⽤的优先级低于算术操作符、类型转换以及rangeTo操作符。

尾递归函数

如果某个函数的末尾又调用了函数自身,此函数称为尾递归函数。Koltin中通过在fun前使用tailrec修饰符显示声明此函数为尾递归函数。如果被标注的函数满足所需的形式条件时,编译器会优化使用循环的方式替代递归。

  1. 在递归调⽤后有更多代码时,不能使⽤尾递归;
  2. 不能⽤在try/catch/finally块中;
  3. 也不能⽤于open的函数。