Kotlin教学(二)函数

Kotlin教学(二)函数

标签: Kotlin


本文包括:
1. 介绍用于集合、字符串和一般表达式的函数
2. 如何使用命名参数,默认参数值,中缀调用语法
3. 如何通过扩展函数和扩展属性将Java库适配到Kotlin中
4. 如何使用顶层和局部的函数和属性构造你的代码

函数

本节介绍Kotlin是如何提升每个程序最重要的核心部分:声明和调用函数。也会介绍如何通过扩展函数将Java库适配成Kotlin风格的代码,以发挥Kotlin语言的强大特性。

本节会将Kotlin中的集合、字符串和一般表达式作为讲解知识点的基础。

2-1 创建Kotlin中的集合

先创建set、list或者map:

val set = setOf(1, 7, 53)
val list = listOf(1, 7, 53)
val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three")

其中mapof的to是一个一般函数(中缀调用),后面会介绍。

此外,set.javaClass等价于Java中的set.getClass()。注意Kotlin没有自己的集合,全部使用的Java库的集合。尽管如此,在Kotlin中却可以用这些Java集合拥有很多Java中原本没有的功能。

例如:

list.last()
set.max()

2-2 使函数更容易调用

我们先打印出集合的内容:

val list = listOf(1, 7, 53)
println(list)

结果:

[1, 7, 53]

会调用Java集合默认的toString方法,当然输出的格式并不一定是你所期望的。

我们实现一个函数joinToString用于改变集合输出结果的前缀,后缀和分隔符:

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()
}

结果:

println(joinToString(list, "/", "(", ")"))

(1/7/53)

1-命名参数

Java中每次使用joinToString函数都需要按顺序传入参数,但是Kotlin中可以通过参数名来制定参数(不需要按照参数顺序):

println(joinToString(list, prefix = "(", postfix = ")", separator = "/"))

一旦使用了命名参数,则需要写上所有参数的名称,不然编译器会混乱。

提示:
IntelliJ IDEA会在你更改参数名时自动更新调用函数时指定的函数名。建议你通过Rename操作来更改参数名,而不是仅仅手写。

警告:
调用Java编写的方法时不能使用命名参数,包括JDK和Android框架中的所有方法。在Java 8中增加了在.class文件中保存参数名的可选特性,然而Kotlin是兼容Java 6的,因此不能在调用Java编写的方法时使用命名参数。

2-默认参数值

Kotlin可以通过默认参数避免创造重载的函数,减少过多的重复代码。

这里改造一下joinToString函数:

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()
}

分隔符、前缀和后缀都是用了默认值。接下来调用该函数:

println(joinToString(list, ";")) //按顺序填写,并省略尾随的所有参数
println(joinToString(list, prefix = "(")) //命名参数:改变了前缀,其余采用默认值

结果:

1;7;53
(1,7,53

注意:

Java中没有默认参数的概念。因此在Java中调用Kotlin函数,需要显式制定所有参数的值。如果你想让Java调用者使用Kotlin函数更加方便,可以通过@JvmOverloads注解这些函数。内置的编译器会自动产生Java重载的所有构造器。

给joinToString加上注解@JvmOverloads:

@JvmOverloads fun <T> joinToString...

3-摆脱静态工具类:顶层函数和属性

Kotlin中你不需要创建任何无意义的工具Util类,因为你可以直接将函数放置到源代码的顶层,而不需要在任何类中。这些函数仍然是声明在文件顶部的包的成员。当你需要调用这些函数时,仍然需要导入这些内容。

顶层函数
package strings

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

必须在文件join.kt中有包strings,其中有函数joinToString。当文件编译时,仍然会产生一些类,因为JVM仅仅可以运行在类中的代码。在Kotlin中使用这些函数是很方便的。然而需要在Java中使用这些函数时,该如何处理呢?

编译器会将join.kt文件形成一个类JoinKt,其中顶层的方法会被转换成该类的静态方法。这就是在Java中调用这些函数,编译器所做的事情:

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

Java中调用:

JoinKt.joinToString(list, "/", "(", ")");

当我们需要改变Kotlin文件转为Java class时的类名:

//文件最开始通过注解指定生成的类名
@file:JvmName("StringFunctions")

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

Java中调用:

/* Java */
import strings.StringFunctions;
StringFunctions.joinToString(list, ", ", "", "");
顶层属性

属性也能和函数一样放置在文件的顶层,虽然独立于类的单独数据不总是有需要的,但是仍然是有用的。

例如,你可以使用var属性去计算一些操作执行的次数:

var opCount = 0
fun performOperation(){ 
    opCount++
    // ...
}

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

可以声明常量:

val UNIX_LINE_SEPARATOR = "\n"

默认的,顶层属性和其他属性一样,可以被Java代码通过访问器来访问get/set。如果想达到public static final的效果,可以使用const修饰符——用于原始类型的属性,例如String:

const val UNIX_LINE_SEPARATOR = "\n"

等效于Java代码:

public static final String UNIX_LINE_SEPARATOR = "\n"

2-3给其他类增加方法:扩展函数和扩展属性

Kotlin中用到了JDK等Java库,Android框架等第三方框架时,如何在不重写这些API的情况下,扩展这些API呢?扩展函数就满足了这些需求。

扩展函数本质是一种能作为类的成员被调用,但却在该类外面定义的函数。

例如,我们给String扩展一个方法(lastChar)——能获取到String的最后一个字符:

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

调用:

println("Hello".lastChar())
o

通过这种方法,就给String扩展了lastChar()函数,而不需要像Java一样继承String,并添加新方法。在该语句中String.lastChar()String被称为“接收者类型”,后面的this.get以及this.length的this被称为“接收对象”(在扩展函数内部,也可以省略this)

优点:
这种方法,甚至不需要你有该类的源码。你也不需要关心该类是用Java编写的还是Kotlin编写的,或者是其他JVM语言如Groovy语言编写的。

注意点:
扩展函数可以直接访问类的属性和方法,但是不能访问该类的private或protected修饰的成员。

1-导入和扩展函数

在Kotlin中使用扩展函数,需要提前导入该函数,无论是import strings.lastCharimport strings.*都可以导入,这样是为了防止以外的命名冲突。

可以使用别名来解决冲突:

import strings.lastChar as last
val c = "Kotlin".last()

2-在Java中调用扩展函数

比如扩展函数lastChar写在文件StringUtil.kt文件中,需要通过如下方法调用:

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

可以发现Java将StringUtil.kt转为了StringUtilKt类,而扩展方法lastChar成为该类的静态方法。

3-将工具函数作为扩展

比如我们要给集合Collection添加扩展:
fun <T> Collection<T>.joinToString(...){...}

joinToString作为工具扩展到了集合之上。

扩展函数的静态特性意味着扩展函数不能在子类中被覆盖。

4-扩展函数无法被覆盖

Kotlin中的方法覆盖一般都是成员函数,但是你不能覆盖一个扩展函数。

例如,View有click方法,Button继承自View并覆盖了click方法,可以实现多态:

open class View{
    open fun click() = println("View click")
}
fun View.showOff() = println("I'm a View")

class Button: View(){
    override fun click() = println("Button click")
}
fun Button.showOff() = println("I'm a Button")

fun main(args: Array<String>) {
    //子类覆盖父类函数
    var view: View = Button()
    view.click()
    //扩展函数
    view.showOff()
}

结果:

Button click
I’m a View

由此可见,对于View变量,调用click方法,最终会去调用Button的方法。扩展函数即使名称,参数都完全一样(showOff())也不具备这种多态性,因为扩展函数具有静态特性。

注意:
如果有扩展函数和成员函数同名,会优先使用该扩展函数。这在扩展API时需要注意。

5-扩展属性

扩展属性提供一种扩展API类的方法,但是扩展属性和类的属性是不一样的,不能有任何状态和field域(因此一定要定义getter),之前扩展String的lastChar扩展函数,也可以用扩展属性来实现:

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

当你在Java中访问扩展属性时,需要通过get方法去获取StringUtilKt.getLastChar("Java").

4-集合:varargs(可变参数),中缀调用以及库支持

本节讲解Kotlin标准库中用于集合的函数。通过这些内容,介绍语言相关的特性:

  1. vararg关键字允许你声明一个参数数量可变的函数
  2. 中缀调用使你能够更方便的调用单参数的函数
  3. 析构声明destructuring declarations,允许你拆解一个组合的值到多个变量中

1-继承Java集合API

Kotlin中使用了Java库的集合类时,有很多Java中没有的函数,比如listOF,setOf等等,这些都是作为扩展函数附加到Java库API上面的。

2-可变参数:函数接收任意数量的参数

和Java中可变参数一样,Kotlin中也有可变参数:

val list = listOf(2, 3, 5, 9)

listOf函数原型如下:

fun listOf<T>(vararg values: T): List<T> { ... }

Kotlin与Java不同之处在于,Kotlin使用修饰符varags修饰参数。
第二处不同在于,Java中你可以直接传入数组,Kotlin中需要显式解包数组,通过星号:

val array = arrayOf("Java", "C")
val list = listOf("Kotlin", *array)
list.forEach { println(it) }
}

如上,这样能组合一些值和数组(“Kotlin”和array数组中的内容组合起来),Java中是不允许的。

3-成对工作:中缀调用和析构声明

创建一个map:

val map = mapOf(10 to "ten", 3 to "three", 99 to "ninty-nine")

to本质是中缀调用的方法,将函数名放置于目标对象和参数之间。10 to "ten"类似于10.to("ten")

声明中缀函数,需要infix修饰符:

infix fun Any.to(other: Any) = Pair(this, other)

Kotlin中可以将元素对分配到一组变量中:

val (number, name) = 1 to "one"

5-用于字符串和正则表达式的函数

Kotlin中字符串和Java毫无区别,互相兼容,也不需要额外的包装。但是Kotlin提供了很多强大简洁的扩展函数。

1-拆分字符串

字符串的split方法,是用于拆分字符串。但是Java中splite()方法却无法生效在.(点)上,比如"12.345-6.A".split("."),根据设想应该被拆分为12\365-6\A,但实际上却返回空数组,因为Java中.是表示任何字符的正则表达式。

Kotlin通过扩展函数解决了这些复杂的问题,如果你想要拆分正则表达式,需要Regex类型的参数,而不是String类型。这样更容易区分正则表达式和一般文本的情况:

 println("12.345-6.A".split("\\.|-".toRegex())) //正则表达式,用.和-拆分

[12, 345, 6, A]

非正则表达式:

 println("12.345-6.A".split(".", "-"))

2-正则表达式和三重引号字符串

现在有个目标,是从文件路径path中解析出目录,文件名和文件后缀,可以如下实现:

fun parsePath(path: String) {
    val directory = path.substringBeforeLast("/") //目录
    val fullName = path.substringAfterLast("/") //获得完整文件名
    val fileName = fullName.substringBeforeLast(".") //从完整文件名中解析
    val extension = fullName.substringAfterLast(".")
    println("Dir: $directory, name: $fileName, ext: $extension")
}

调用:
parsePath(“/Users/yole/kotlin-book/chapter.adoc”)
结果:Dir: /Users/yole/kotlin-book, name: chapter, ext: adoc

该实例也可以通过正则表达式实现:

fun parsePathRegexp(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")
    }
}

3-三重引号字符串

三重引号字符串不仅可以避免转义字符,还可以包含任何字符,包括换行符。

val kotlinLogo = """| //
                   .|//
                   .|/ \"""
println(kotlinLogo.trimMargin("."))     //删除.点号以及前面的空白内容            

三重引号中依旧可以使用字符串模板${...}

6-使你的代码整洁:局部函数和扩展

Java中总是会有一些重复代码,比如你想要验证用户信息合法,然后进行数据库操作,java思想代码如下:

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")
    }
    //用户保存到数据库中等操作
}

可以通过局部函数优化:

class User(val id: Int, val name: String, val address: String)
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")
    //用户保存到数据库中等操作
}

通过扩展函数优化:

class User(val id: Int, val name: String, val address: String)
//User的扩展函数
fun User.validateBeforeSave() {
    fun validate(value: String, fieldName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException(
                    "Can't save user $id: empty $fieldName")
        }
    }
    validate(name, "Name")
    validate(address, "Address")
}

fun saveUser(user: User) {
    user.validateBeforeSave() //对user合法性判断
    //用户保存到数据库中等操作
}

通过扩展函数和局部函数,使得代码非常简洁,易读。比如上例中User扩展的方法,可能在用到User的其他地方根本不需要该方法,因此扩展函数的强大之处就显现出来。这里扩展函数还可以作为saveUser()的局部函数,但是这样不易理解,我们也不建议使用超过一层的嵌套。

7-总结

  • Kotlin没有自己的集合类,而是扩展Java集合并提供更丰富的API。
  • 定义函数参数的默认值极大地减少了定义重载函数的需求,并且命名参数语法使得调用具有很多参数的函数更加易读。
  • 函数和属性可以在文件中直接声明,不仅仅作为类的成员,允许了更灵活的代码结构。
  • 扩展函数和扩展属性,使得你能扩展任何类的API,包括在外部库定义的类,而不需要修改它们的源码,并且没有运行时的额外消耗。
  • 中缀调用提供一种操作符类的简介语法,中缀调用仅仅生效于单参数的方法。
  • Kotlin提供了大量便捷的字符串处理函数,能用于处理正则表达式或者单纯文本。
  • 三重引号字符串提供了便捷的方法,用于书写需要大量转义字符的表达式。
  • 局部函数能帮助我们的代码更加简洁并且消除重复。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猎羽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值