Kotlin入门系列:第二章 函数的定义与调用

1 在kotlin中创建集合

val set = hashSetOf(1, 7, 53) // 创建HashSet
val list = arrayListOf(1, 7, 53)	// 创建ArrayList
val map = hashMapOf(1 to "one", 7 to "seven", 53 to "fifty-three") // 创建HashMap,to是一个函数,比如1为key,value为one,具体函数后面说

// javaClass相当于java中的getClass()
// kotlin使用的是java的集合类    
println(set.javaClass)
println(list.javaClass)
println(map.javaClass)

输出:
class java.util.HashSet
class java.util.ArrayList
class java.util.HashMap

2 让函数更好调用

2.1 命令参数

java的集合都有一个默认的 toString 实现,但是它格式化的输出是固定的

val list = listOf(1, 2, 3)
println(list)

输出:
[1, 2, 3]

下面将使用kotlin的方式实现自定义的 toString 集合输出格式

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)
// 输出(1; 2; 3)
// 以下语句晦涩难懂
println(joinToString(list, ";", "(", ")"))

// kotlin可以在设置参数时指定参数,更明显的查看参数表示的意义
// 即根据方法参数名指定传递的参数,让代码有更好的可读性,比如这里的参数";"、"("、")"分别为separator、prefix和postfix
println(joinToString(list, separator = ";", prefix = "(", postfix = ")"))

2.2 默认参数值

// 形参可以设置默认参数值
fun <T> joinToString(
    collection: Collection<T>,
    separator: String = ";",
    prefix: String = "(",
    postfix: String = ")"): String {
    
    ...
}

// 可以省略所有或部分参数的值,让方法重载更加方便
val list = joinToString(list);

kotlin和java文件是可以互相转换的,比如在IDEA或Android Studio提供了很方便的工具一键转换。具体的如何用IDE转换参考:Kotlin和Java文件互转

比如将java文件转换为kotlin文件:

public class Demo {

    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        System.out.println(joinToString(list, ";", "(", ")"));
    }

    public static <T> String joinToString(
            Collection<T> collection,
            String separator,
            String prefix,
            String postfix) {

        StringBuilder result = new StringBuilder(prefix);
        Iterator<T> iterator = collection.iterator();
        for (int index = 0; index < collection.size(); index++) {
            if (index > 0) result.append(separator);
            if (iterator.hasNext()) {
                result.append(iterator.next());
            }
        }
        result.append(postfix);
        return result.toString();
    }
}
转换后:
object Demo {
    @JvmStatic
    fun main(args: Array<String>) {
        val list: MutableList<Int> = ArrayList()
        list.add(1)
        list.add(2)
        list.add(3)
        println(joinToString(list, ";", "(", ")"))
    }

    fun <T> joinToString(
        collection: Collection<T>,
        separator: String?,
        prefix: String?,
        postfix: String?
    ): String {
        val result = StringBuilder(prefix)
        val iterator = collection.iterator()
        for (index in collection.indices) {
            if (index > 0) result.append(separator)
            if (iterator.hasNext()) {
                result.append(iterator.next())
            }
        }
        result.append(postfix)
        return result.toString()
    }
}

在kotlin转java时如果不想一个个的写重载方法参数,可以在方法添加注解 @JvmOverloads,反编译时会生成默认参数值的重载方法:

在kotlin时:

@JvmOverloads
fun <T> joinToString(
    collection: Collection<T>,
    separator: String = ";",
    prefix: String = "(",
    postfix: String = ")"): String {
    ...
}

转换到java时(转换后的文件要做一定的删减):

@JvmOverloads
@NotNull
public static final String joinToString(@NotNull Collection collection, @NotNull String separator, @NotNull String prefix, @NotNull String postfix) {
   ...
}

public static String joinToString$default(Collection var0, String var1, String var2, String var3, int var4, Object var5) {
	  ...
}

@JvmOverloads
@NotNull
public static final String joinToString(@NotNull Collection collection, @NotNull String separator, @NotNull String prefix) {
   return joinToString$default(collection, separator, prefix, (String)null, 8, (Object)null);
}

@JvmOverloads
@NotNull
public static final String joinToString(@NotNull Collection collection, @NotNull String separator) {
   return joinToString$default(collection, separator, (String)null, (String)null, 12, (Object)null);
}

@JvmOverloads
@NotNull
public static final String joinToString(@NotNull Collection collection) {
   return joinToString$default(collection, (String)null, (String)null, (String)null, 14, (Object)null);
}

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

在java开发过程中总会用到一些工具类,有的是处理字符串有的是处理其他数据,往往这些方法都不会写在某个功能模块的类里,而是写在单独的util包下提供xxxUtils工具类,这样我们可以在处理相关数据的时候可以直接调用而不实例化。

在kotlin中有一种写法叫顶层函数,这类函数一般定义在一个单独的kotlin文件中

// 假设该文件为StringUtils.kt
// kotlin与java不同的地方在于,kotlin不需要函数、属性、常量都要在class类中,可以直接将它们直接写在文件上
// 写在这些文件上的函数、属性、常量都是static的,公开给所有模块使用
@file:JvmName("StringFunctions") // 提供给Java调用时使用的名称

import java.lang.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()
}

在kotlin中使用时:

fun main() {
	// 可以直接通过函数名称访问
	val list = listOf(1, 2, 3)
	println(joinToString(list))
}

在java中使用时:

StringFunctions.joinToString(...)
// 如果没有kt文件没有注明@file,要访问顶层函数,是直接使用文件名+Kt名称访问,比如StirngUtilsKt.joinToString(...)

顶层属性

// 外部变量
var opCount = 0

fun performOperation() {
	opCount++;
}

fun reportOperationCount() {
	println("Operation performed $opCount times")
}

// 该变量会被存储到一个静态的字段中,val声明的属性是有一个getter的
// 如果想要声明为一个常量,可以使用const关键字
const val UNIX_LINE_SEPERATOR = "\n"
等价于java的
public static final String UNIX_LINE_SEPERATOR = "\n";

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

扩展函数,它就是一个类的成员函数,不过定义在类的外面。

// 计算一个字符串的最后一个字符
fun String.lastChar(): Char = this.get(this.length - 1)
或
fun String.lastChar(): Char = this[this.length - 1]

你所要做的,就是把你要扩展的类或接口的名称,放到即将添加的函数前面。这个类的名称被称为接收者类型;用来调用这个扩展函数的那个对象,叫做接收者对象。

String.lastChar()String为接收者类型

this.get(this.length - 1)this为接收者对象

// 顾名思义,扩展函数就是扩展一个类,在这个类的基础上再添加新的函数。
// 比如这里扩展了String类,在调用时将扩展函数中的this都替换成了传递给扩展函数的String对象,比如"kotlin",就像在String类的内部添加了这个函数一样,下面代码的效果类似于:
// fun lastChar(): Char = "Kotlin".length - 1
println("Kotlin".lastChar())

注意,扩展函数并不允许你打破它的封装性。和在内部定义的方法不同的是,扩展函数不能访问私有的或者是受保护的成员。

简单说,扩展函数就是只要类型相同,就可以将函数的接收者类型进行替换的操作,扩展函数是一个静态方法,this就是指代这个接收者类型对象

3.1 导入和扩展函数

需要使用扩展函数时,需要显式地导入

// 在strings包下的扩展函数lastChar()
import strings.lastChar
// import strings.*
// import strings.lastChar as last	// 修改导入的类或者函数名称,使用as关键字,这里把lastChar修改为last

val c = "Kotlin".lastChar()
// val c = "Kotlin".last()	// 修改扩展函数名称后使用

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

扩展函数无非就是静态函数的一个高效语法糖,可以使用更具体的类型作为接收者类型,而不是一个类。

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

    val result = StringBuilder(prefix)
    for ((index, element) in this.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}

fun Collection<String>.join(
    separator: String = ",",
    prefix: String = "",
    postfix: String = "") = joinToString(separator, prefix, postfix)

fun main() {
	println(listOf("1", "2", "3").join())
}

3.3 扩展属性

// 这里必须定义getter函数,因为没有支持字段,因此没有默认getter实现,同理也不能初始化因为没有地方存储值
val String.lastChar: Char get() = get(length - 1)

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

fun main() {
	// 使用扩展函数打印字符串最后一个字符
    println("kotlin".lastChar)
    
    // 使用扩展函数将最后一个函数替换为!
    val sb = StringBuilder("kotlin")
    sb.lastChar = '!'
    println(sb)
}

如果你尝试给扩展属性添加默认值,会出现编译错误:扩展属性不能有初始化器:

// initializer is not allowed here because this property has not backing field
val MutableList<Int>.sumIsEven: Boolean = false
	get() = this.sum() % 2 == 0

为什么不能编译通过?其实,这与扩展函数一样,其本质也是对应java中的静态方法。由于扩展没有实际地将成员插入类中,因此对扩展属性来说幕后字段是无效的。

public final class ExampleTestKt {
	// 扩展属性实际上也是一个静态方法
   public static final boolean getSumIsEven(@NotNull List $this$sumIsEven) {
      Intrinsics.checkParameterIsNotNull($this$sumIsEven, "$this$sumIsEven");
      return CollectionsKt.sumOfInt((Iterable)$this$sumIsEven) % 2 == 0;
   }
}

扩展属性只能由显式提供的getter和setter定义。

3.4 扩展函数的实现机制

扩展函数如此方便,会不会对性能产生影响?我们可以写一个扩展函数然后将它编译为java查看:

fun MutableList<Int>.exchange(fromIndex: Int, toIndex: Int) {
	val temp = this[fromIndex]
	this[fromIndex] = this[toIndex]
	this[toIndex] = temp
}

@Metadata(
   mv = {1, 1, 15},
   bv = {1, 0, 3},
   k = 2,
   d1 = {"\u0000\u0012\n\u0000\n\u0002\u0010\u0002\n\u0002\u0010!\n\u0002\u0010\b\n\u0002\b\u0003\u001a \u0010\u0000\u001a\u00020\u0001*\b\u0012\u0004\u0012\u00020\u00030\u00022\u0006\u0010\u0004\u001a\u00020\u00032\u0006\u0010\u0005\u001a\u00020\u0003¨\u0006\u0006"},
   d2 = {"exchange", "", "", "", "fromIndex", "toIndex", "app_debug"}
)
public final class ExampleTestKt {
   public static final void exchange(@NotNull List $this$exchange, int fromIndex, int toIndex) {
      Intrinsics.checkParameterIsNotNull($this$exchange, "$this$exchange");
      int temp = ((Number)$this$exchange.get(fromIndex)).intValue();
      $this$exchange.set(fromIndex, $this$exchange.get(toIndex));
      $this$exchange.set(toIndex, temp);
   }
}

我们可以发现,扩展函数是一个静态方法。静态方法的特点:它独立于该类的任何对象,且不依赖类的特定实例,被该类的所有实例共享。此外,被 public 修饰的静态方法本质上也就是全局方法。

所以,扩展函数不会带来额外的性能消耗。

3.5 扩展函数的作用域

既然扩展函数是全局的,一般情况下我们可以定义在包内,比如 com.example.extension 包下:

// Extension.kt
package com.example.extension

fun MutableList<Int>.exchange(fromIndex: Int, toIndex: Int) {
	val tmp = this[fromIndex]
	this[fromIndex] = this[toIndex]
	this[toIndex] = tmp
}

但如果你尝试将扩展函数放在一个类中管理,就会出现问题:

class Extension {
	// 即使添加了修饰符为public也一样无法访问
	public fun MutableList<Int>.exchange(fromIndex: Int, toIndex: Int) {
		val tmp = this[fromIndex]
		this[fromIndex] = this[toIndex]
		this[toIndex] = tmp
	}	
}

你会发现无法调用扩展函数 mutableList.exchange() 了,方法修饰为 public 也一样,我们可以看下编译代码:

public final class Extension {
   public final void exchange(@NotNull List $this$exchange, int fromIndex, int toIndex) {
      Intrinsics.checkParameterIsNotNull($this$exchange, "$this$exchange");
      int temp = ((Number)$this$exchange.get(fromIndex)).intValue();
      $this$exchange.set(fromIndex, $this$exchange.get(toIndex));
      $this$exchange.set(toIndex, temp);
   }
}

扩展函数变成类的方法了,只能提供给类或子类使用。

3.6 父类和子类相同名称扩展函数

open class Animal
class Dog : Animal()

fun Animal.name() = "animal"
fun Dog.name() = "dog"

fun Animal.printName(anim: Animal) {
	println(anim.name())
}

fun main(args: Array<String>) {
	Dog().printName(Dog())
}

animal

上面有一个扩展函数,Dog 是 Animal 的子类,它们都提供了相同名称的扩展函数,使用了 Dog 子类调用最终却显示了父类扩展函数的输出结果,原因和上面分析一样,因为扩展函数是一个静态函数,它是不具备运行时的多态。可以看下 java 反编译上面示例的结果:

public static final String name(Animal receiver) {
	return "animal"
}

public static final String name(Dog receiver) {
	return "dog"
}

// 扩展函数在反编译为 java 或在 java 调用时,第一个函数会是调用函数的类
// 可以看到第二个参数为 Animal 对象,而不是 Dog 对象
public static final void printName(Animal r, Animal a) {
	String str = name(a);
	System.out.println(str);
}

public static final void main(String[] args) {
	// 在 kotlin 的 Dog 对象被强转为 Animal 类型
	printName((Animal) new Dog()(Animal) new Dog());
}

3.7 成员方法优先级总高于扩展函数

// 类中定义成员函数foo()
class Son {
	fun foo() = println("son called member foo")
}

// 定义扩展函数foo
fun Son.foo() = println("son called extension foo")

Son().foo() 

输出:
son called member foo

当扩展函数和现有类的成员方法同时存在时,kotlin将会默认使用类的成员方法。

这么设计其实是有意义的,在多人开发的时候,如果每个人都对 Son 扩展了 foo(),那就很容易造成混淆,对于第三方库来说甚至是异常灾难:我们把不应该更改的方法改变了。

3.7 标准库中的扩展函数:run、let、also、takeIf

3.7.1 run

@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

run 是任何类型 T 的通用扩展函数,run 中执行了返回类型为 R 的扩展函数 block,最终返回该扩展函数的结果。

run 扩展函数的作用域是独立的:

fun testFoo() {
	val nickanme = "prefert"

	run {
		val nickname = "test"
		println(nickname)
	}
	println(nickname)
}

这个范围函数本身似乎不是很有用。但相比范围,还有一点不错的是,它返回范围内最后一个对象。

// 如果没有登录的话,显示登录对话框,否则显示其他对话框
run {
	if (!islogin) loginDialog else getNewAccountDialog
}.show()

3.7.2 let

// let
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this) // 返回闭包结果
}

// apply
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this // 返回本身对象T
}

letapply 类似,唯一不同的是返回值:apply 返回的是原来的对象,而 let 返回的是闭包里面的值。

apply 的使用可以将同一个变量的处理全都放在一个语句块中方便管理:

import java.lang.StringBuilder

fun main() {
    val sb = StringBuilder()
    val result = sb.apply {
        append("I'm")
        append("Using")
        append("apply")
        append("function")
    }
    // hash值是相同的
    println(sb.hashCode())
    println(result.hashCode())
}

let 的使用很多时候都和可空类型判断结合处理:

fun main() {
    val kot = Kot()
    kot.dealStudent()
}

class Kot {
    val student: Student? = Student(10)

    fun dealStudent() {
    	// student不为null的时候调用扩展函数let
        val result = student?.let {
            println(it.age)
            it.age // let返回闭包的结果
        }
        println(result)
    }
}

data class Student(val age: Int)

3.7.3 also

@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.also(block: (T) -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    return this
}

also 像是 letapply 函数的加强版。

class Kot {
	val student: Student? = getStudent()
	var age = 0
	fun dealStudent() {
		val result = student?.also { stu ->
			// this.age是外部类Kot的成员变量var age
			// stu是指定的名称,默认会提供it使用
			this.age += stu.age
			println(this.age)
			println(stu.age)
			this.age
		}
	}
}

值得注意的是:如果使用 apply,由于它内部是一个扩展函数,this 将指向 studuent 本身而不是外部类 Kot,而且也无法调用到 Kot.age

3.7.4 takeIf

@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? {
    contract {
        callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
    }
    return if (predicate(this)) this else null
}

如果我们不仅仅只想判空,还想加入条件,这时 let 可能就显得有点不足,就可以使用 takeIf

 val result = student?.takeIf { it.age >= 18 }.let { ... }

takeIf 是接收满足条件的情况下执行后续操作,还有相反的 takeUnless

3.7.5 标准函数使用示例

data class User(var name: String)

fun main(args: Array<String>) {
	val user = User("user")

	// let 与 run 都会返回闭包的执行结果,区别在于 let you闭包参数,而 run 没有闭包参数
	// let 的闭包参数是调用的对象,即 it 是 User 对象
	val letResult = user.let { "let::${it.javaClass}"}
	println(letResult)
	// this 是调用的对象,即 this 是 User 对象
	val runResult = user.run { "run::${this.javaClass}" }
	println(runResult)

	// also 与 apply 都不返回闭包的执行结果,区别在于 also 有闭包参数,而 apply 没有闭包参数
	user.also {
		println("also:${it.javaClass}")
	}
	user.apply {
		println("apply:${this.javaClass}")
	}
	
	// takeIf 的闭包返回一个判断结果,为 false 时 takeIf 函数会返回空
	// takeUnless 与 takeIf 相反,闭包的判断结果为 true 时函数会返回空
	user.takeIf {
		it.name.length > 0
	}?.also {
		println("姓名为${it.name}")
	} ?: println("姓名为空")
	user.takeUnless {
		it.name.length > 0
	}?.also {
		println("姓名为空")
	} ?: println("姓名为${user.name}")
}

3.8 扩展不是万能的

kotlin是一种静态类型语言,我们创建的每个对象不仅具有运行时,还具有编译时类型。在使用扩展函数时,要清楚的了解静态和动态调度之间的区别。

3.8.1 静态与动态调度

以一个例子来说明什么是静态和动态调度。

class Base {
	public void fun() {
		System.out.println("I'm Base foo!");
	}
}

class Extended extends Base {
	@Override
	public void fun() {
		System.out.println("I'm Extended foo!");
	}
}

Base base = new Extended();
base.fun();

输出:
I'm Extended foo!

我们声明一个名为 base 的变量,它具有编译时类型 Base 和运行时类型 Extended。当我们调用时,base.fun() 将动态调度该方法,这意味着运行时类型 Extendedfun() 被调用。

当我们调用重载方法时,调度变为静态并且仅取决于编译时类型。

void foo(Base base) {... }
void foo(Extended extended) { ... }

public static void main(String[] args) {
	Base base = new Extended();
	foo(base); // 即使base是Extended实例,但是会调用foo(Base base)
}

3.8.2 扩展函数始终静态调度

扩展函数都有一个接收器(receiver),由于接收器实际上只是字节代码中编译方法的参数,因此你可以重载它,但不能覆盖它。这可能是成员和扩展函数之间最重要的区别:前者是动态调度的,后者总是静态调度的

open class Base
class Extended: Base()

fun Base.foo() = "I'm Base.foo!"
fun Extended.foo() = "I'm Extended foo!"

fun main(args: Array<String>) {
	val instance: Base = Extended()
	val instance2 = Extended()
	println(instance.foo()) // I'm Base.foo!
	println(instance2.foo()) // I'm Extended foo!
}

3.8.3 类中的扩展函数

如果我们在类的内部声明扩展函数,那么它将不是静态的。如果该扩展函数加上 open 关键字,我们可以在子类中进行重写。这是否意味着它将被动态调度?

当在类内部声明扩展函数时,它同时具有调度接收器和扩展接收器。

  • 调度接收器(dispatch receiver):扩展被声明为成员时存在的一种特殊接收器,它表示声明扩展名的类的实例

  • 扩展接收器(extension receiver):与kotlin扩展密切相关的接收器,表示我们为其定义扩展的对象

// X是调度接收器,Y是扩展接收器
class X {
	fun Y.foo() = "I'm Y.foo"
}

如果将扩展函数声明为 open,则它的调度接收器只能是动态的,而扩展接收器总是在编译时解析。

open class Base
class Extended : Base()

open class X {
	open fun Base.foo() {
		println("I'm Base.foo in X")
	}
	
	open fun Extended.foo() {
		println("I'm Extended.foo in X")
	}
}

class Y : X() {
	override fun Base.foo() {
		println("I'm Base.foo in Y")
	}

	override fun Extended.foo() {
		println("I'm Extended.foo in Y")
	}
}

X().deal(Base())    // I'm Base.foo in X
Y().deal(Base())    // I'm Base.foo in Y - dispatch receiver被动态处理
X().deal(Extended() // I'm Base.foo in X - extension receiver被静态处理
Y().deal(Extended() // I'm Base.foo in Y 

可以发现 Extended 的扩展函数始终没有被调用。决定两个 Base 类扩展函数执行哪一个,直接因素是执行 deal() 的类的运行时类型。

扩展函数使用注意事项:

  • 如果该扩展函数是顶级函数或成员函数,则不能被覆盖

  • 我们无法访问其接收器的非公共属性

  • 扩展接收器总是被静态调度

3.8.4 被滥用的扩展函数

fun Context.loadImage(url: String, imageView: ImageView) {
	Glide.with(this)
		.load(url)
		.placeholder(R.mipmap.img_default)
		.error(R.mipmap.ic_error)
		.into(imageView)
}

ImageActivity.kt
this.loadImage(url, imageView)

上面的扩展函数其实是滥用的,实际上并没有以任何方式扩展现有类。

Context 作为上下文已经承担了很多责任,基于 Context 扩展还很可能产生 ImageView 于传入上下文周期不一致导致的问题。

正确做法是在 ImageView 进行扩展:

fun ImageView.loadImage(url: String) {
	Glide.with(this.context)
		.load(url)
		.placeholder(R.mipmap.img_default)
		.error(R.mipmap.ic_error)
		.into(this)
}

// 图片请求框架扩展
object ImageLoader {
	fun with(context: Context, url: String, imageView: ImageView) {
		Glide.with(context)
		.load(url)
		.placeholder(R.mipmap.img_default)
		.error(R.mipmap.ic_error)
		.into(imageView)
	}
}

3.9 高阶函数

高阶函数指的是将函数用作一个函数的参数或者返回值的函数(如果你觉得高阶函数好难看不懂,你可以简单的把它看成是一个callback回调【大多数时候可以作为callback用】,这样会好理解一些)。

如下一个高阶函数:

fun test(block: () -> Unit) {
	block()
}

test {
	...
}

一个高阶函数格式如下:

高阶函数名称: [类型].(参数列表) -> 返回值
block: () -> Unit
block: (Int, Int) -> Int

3.9.1 将函数作为参数的高阶函数

public inline fun CharSequence.sumBy(selector: (Char) -> Int): Int {
	var sum: Int = 0
	// 遍历字符串字符
	for (element in this) {
		sum += selector(element) // 高阶函数的操作由外部处理
	}
	return sum
}

val str = "abc"
val sum = str.sumBy { 
	// 遍历字符串每个字符转为int值返回
	it.toInt() 
}
println(sum)

3.9.2 将函数作为返回值的高阶函数

fun <T> lock(lock: Lock, body: () -> T): T {
	lock.lock()
	try {
		return body() // 高阶函数操作后返回类型T
	} finally {
		lock.unlock()
	}
}

val result = lock(lock) {
	// 不需要写return关键词
	sharedResource.operation()
}

3.9.3 自定义高阶函数

fun operation(num1: Int, num2: Int, result: (Int, Int) -> Int) {
	return result(num1, num2) 
}

val plus = operation(10, 10) { num1, num2 ->
  	num1 + num2
}
val reduce = operation(10, 10) { num1, num2 ->
    num1 - num2
}
val ride = operation(10, 10) { num1, num2 ->
    num1 * num2
}
val except = operation(10, 10) { num1, num2 ->
    num1 / num2
}

3.9.4 run()和T.run()的区别

run()T.run() 两个函数差不多,两者之间的区别可以通过源码查看:

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

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

可以发现 T.run() 的高阶函数 block() 是一个扩展在 T 类型下的函数,说明这个高阶函数可以使用当前对象 T 的上下文。当我们想要使用当前对象的上下文的时候,可以使用这个函数。

"kotlin".run {
	// 可以使用this对象,它代表对象"kotlin"
	println("length=${this.length}")
	println("first=${first()}")
	println("last=${last()}")
}

上面说明的扩展函数 runletalsotakeIf 都是有使用到高阶函数。

4 处理集合:可变参数、中缀调用和库支持

4.1 扩展java集合的API

val strings: List<String> = listOf("first", "second", "fourteenth")
strings.last()	// 输出fourteenth

val numbers: Collection<Int> = setOf(1, 14, 2)
numbers.max()	// 输出14

last和max都是扩展函数
fun <T> List<T>.last(): T { .. }
fun Collection<Int>.max(): Int { .. }

4.2 可变参数:让函数支持任意数量的参数

val list = listOf(2, 3, 5, 7, 11)

// vararg是可变参数修饰符,相当于java的可变参数符号"..."
fun listOf<T>(vararg values: T): List<T> { .. }

// kotlin和java的另一个区别是,当需要传递的参数已经包装在数组中时,调用该函数的语法
// 在java中,可以按原样传递数组,而kotlin则要求你显式地解包数组,以便每个数组元素在函数中能作为单独的参数来调用
// 这个功能被称为展开运算符,而使用的时候,不过是在对应的参数前面放一个*
fun main(args: Array<String>) {
	val list = listOf("args:", *args);	// 展开运算符展开数组内容
	println(list)
}

4.3 键值对的处理:中缀调用和解构声明

4.3.1 中缀调用

// to是一种特殊的函数,称为中缀调用
val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three")

// 下面是等价的
1.to("one")
1 to "one"

// 中缀调用可以与只有一个参数的函数一起使用,无论是普通的函数还是扩展函数
// 要允许使用中缀符号调用函数,需要使用infix修饰符标记
// Pair是kotlin标准库中的类,用来表示一对元素
infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

4.3.2 解构声明

我们想要从对象获取数值存储到临时变量时经常会这样做:

val b1 = Bird(20.0, 1, "blue")
val weight = b1.weight
val age = b1.age
val color = b1.color

kotlin为我们提供了解构声明,我们就可以这样做:

val b1 = Bird(20.0, 1, "blue")
val (weight, age, color) = b1 
println("$weight, $age, $color")

但是kotlin对于数组的解构也有一定限制,在数组中它默认最多允许赋值5个变量。

解构内部的原理是通过 componentN 的方法来实现,N代表类中属性的顺序:

// 数据类Bird反编译为java代码
public final double component1() { return this.weight; }
public final int component2() { return this.age; }
@NotNull
public final String component3() { return this.color; }

5 字符串和正则表达式的处理

kotlin的正则表达式和java相同

5.1 分割字符串

// 可以指定多个分隔符,且在java中"."是需要做转义操作的而在kotlin不用
"12.345-6.A".split(".", "-")	// 输出[12, 345, 6, A]

5.2 正则表达式和三重引号的字符串

// val path = "/Users/yole/kotlin-book/chapter.adoc"
fun parsePath(path: String) {
	val directory = path.substringBeforeLast("/")	// /Users/yole/kotlin-book
	val fullname = path.substringAfterLast("/")		// chapter.adoc
	
	val filename = fullname.substringBeforeLast(".")	// chapter
	val extension = fullname.substringAfterLast(".")	// adoc

	println("Dir:$directory, name:$filename, ext:$extension")
}

fun parsePath(path: String) {
	val regex = """(.+)/(.+)\.(.+)""".toRegex()	
	val matchResult = regex.matchEntire(path)
	if (matchResult != null) {
		val (directory, filename, extension) = matchResult.destructured
		println("Dir:$directory, name:$filename, ext:$extension")
	}
}

6 让你的代码更整洁:局部函数和扩展

在java中减少重复代码时会使用 Extract Method抽取分解函数来实现重用代码,但是这样可能让代码更费解,因为你以一个包含许多小方法的类告终,而且它们之间并没有明确关系。

在kotlin提供了一个更整洁的方案:可以在函数中嵌套这些提取的函数。这样既可以获得所需的结构,也无需额外的语法开销。

class User(val id: Int, val name: String, val address: String)

fun saveUser(user: User) {
	if (user.name.isEmpty()) {
		throw IllegalArgumentException("Can't save user ${user.id}:empty name")
	}

	if (user.address.isEmpty() {
		throw IllegalArgumentException("Can't save user ${user.id}:empty address")
	}
	...
}

上面判断 isEmpty() 的代码是重复的,在kotlin中可以将验证代码放到局部函数中,可以摆脱重复,并保持清晰的代码结构:

class User(val id: Int, val name: String, val address: String)

fun saveUser(user: User) {
	// 在saveUser()方法中声明一个validate()局部函数验证有效性
	// 局部函数可以访问所在函数中的所有参数和变量
	// 即validate()局部函数可以直接使用saveUser()函数的User参数
	fun validate(value: String, 
				 fieldName: String) {
		if (value.isEmpty()) {
			throw IllegalArgumentException("Can't save user ${user.id}:empty $fieldName")
		}
	} 

	// 调用局部函数验证代码有效性
	validate(user.name, "Name")
	validate(user.address, "Address")

	// 执行后续操作
	...
}

将上面代码放到扩展函数中:

class User(val id: Int, val name: String, val address: String)

fun User.validateBeforeSave() {
	fun validate(value: String, fieldName: String) {
		if (value.isEmpty()) {
			throw IllegalArgumentException("Can't save user id $id, empty $fieldName")
		}
	}
	
	validate(name, "Name")
	validate(address, "Address")
}

fun saveUser(user: User) {
	user.validateBeforeSave()
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值