二、kotlin的函数

462 阅读10分钟

函数★

自定义一个打印集合的方法

fun <T> joinToString(
   collection: Collection<T>,
   separator: String,
   prefix: String,
   postfix: String
): String {
   val stringBuffer = StringBuilder(prefix)
   for ((index, value) in collection.withIndex()) {
      if (index > 0) {
         stringBuffer.append(separator)
      }
      stringBuffer.append(value)
   }
   stringBuffer.append(postfix)
   return stringBuffer.toString()
}

命名参数(又叫具名参数) ★

println(joinToString(list, separator = ",", prefix = "[ ", postfix = " ]"))
println(joinToString(list, separator = ",", "[ ", postfix = " ]"))

list, separator = ",", prefix = "[ ", postfix = " ]"

给参数配上了名字, 然后根据名字传递给相同名字的参数位置上

有什么用?

image.png

反着传递参数

image.png

配合默认参数值, 跳过默认参数只传递下一个参数

函数参数默认值 ★

给函数的参数添加默认值

fun <T> joinToString(
   collection: Collection<T>,
   separator: String = ", ",
   prefix: String = "[ ",
   postfix: String = " ]"
): String {
   val stringBuffer = StringBuilder(prefix)
   for ((index, value) in collection.withIndex()) {
      if (index > 0) {
         stringBuffer.append(separator)
      }
      stringBuffer.append(value)
   }
   stringBuffer.append(postfix)
   return stringBuffer.toString()
}
   separator: String = ", ",
   prefix: String = "[ ",
   postfix: String = " ]"   

使用默认参数值之后, 可以这样调用这种函数

println(joinToString(list, separator = ",", postfix = " ]"))
println(joinToString01(list, ", ", postfix = " ]"))

我们可以选择省略掉默认参数值

image.png

默认函数参数的本质

image.png

函数默认参数就是生成一个叫running$default的函数, 在函数体内添加 一堆 if 语句赋值默认值

在 java 中没有默认参数值, 不过我们可以用 @JvmOverloads, 让一个函数生成重载函数

image.png

@JvmOverloads
fun <T> joinToString03(
   collection: Collection<T>,
   separator: String = ", ",
   prefix: String = "[ ",
   postfix: String = " ]"
): String {
   val stringBuffer = StringBuilder(prefix)
   for ((index, value) in collection.withIndex()) {
      if (index > 0) {
         stringBuffer.append(separator)
      }
      stringBuffer.append(value)
   }
   stringBuffer.append(postfix)
   return stringBuffer.toString()
}

他会生成这些函数

image.png

而java中每个函数的默认参数值都被省略

反引号函数

image.png

如果函数名存在关键字或者别的不能使用的特殊字符, 可以使用 反引号

fun `fun`() {
   println("ffffff")
}

反引号函数还可以使用在, 只有 kotlin 才能拥有 java 不能调用的情况下

顶层函数和属性 ★

kotlin没有静态static关键字, 也就意味着, 没有显示的 static 函数或者属性, 很多人都说有, 那是伴生对象(companion object), 对, 那个让人看起来好像是代替java的static对象的, 不过本质上, 他仅仅定义了个静态类和一个静态对象罢了, 其中伴生对象的函数还是非静态的, 不过借助静态的伴生对象(静态单例对象)让其看起来像是静态函数

private class User01 private constructor(val nickName: String) {
	companion object {
		fun newSubscribingUser(email: String): User01 {
			return User01(email.substringBefore('@'))
		}
	}
}

反编译后

final class User01 {
    public static final Companion Companion = new Companion();
    private final String nickName;

    private User01(String nickName) {
        this.nickName = nickName;
    }

    public final String getNickName() {
        return this.nickName;
    }

    public static final class Companion {
        private Companion() {
        }

        public final User01 newSubscribingUser(String email) {
            return new User01(StringsKt.substringBefore$default((String)email, (char)'@', null, (int)2, null), null);
        }
    }
}

而真正的静态属性和静态函数在kotlin中被叫做顶层属性/函数, 只不过如果没有@file:JvmName("PrintCollection")特意的注释文件名, 则默认顶层属性/函数属于文件名+Kt

顶层函数 ★

是什么?

顶层函数和顶层属性把定义的位置放在文件中, 而不是定义在类中, 该函数或者属性属于包

翻译成java代码, 该属性通常存在于 Kt 结尾的文件中

在java源码中看出顶层函数都是 static 函数

顶层函数反编译成 java 后

public static final String joinToString(Collection collection, String separator, String prefix, String postfix)

如果要在 java 中调用这种顶层函数

image.png

import java.util.List;
// 导入了 kotlin 的文件名(文件名 + Kt)
import function.PrintCollectionDemoKt;

public class Test01 {
    public static void main(String[] args) throws Exception {
        List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 0);
        String string = PrintCollectionDemoKt.joinToString(list, ", ", "[ ", " ]");
        System.out.println(string);
    }
}

如果不要以 文件名 + Kt 的形式在java中调用, 也可以在 kotlin 的文件顶上, 加上@file:JvmName("PrintCollection")

image.png

image.png

image.png

这里只针对顶层函数, 而不是 类

image.png

顶层属性 ★

顶层属性就是在文件顶层的属性, 不属于kotlin能看到的任何类中, 它属于包

kotlin顶层函数/顶层属性都属于全局作用域

var count = 0

fun main() {
   for (i in 1..100) {
      count++
   }
   println(count)
}

反编译成java代码看看:

public final class TopPropertiesDemoKt {
   // 看这里, static 
   private static int count;

   public static final int getCount() {
      return count;
   }

   public static final void setCount(int var0) {
      count = var0;
   }

   public static final void main(String[] args) {
      int var1 = 1;
      for(byte var2 = 100; var1 <= var2; ++var1) {
         int var10000 = count++;
      }
      var1 = count;
      System.out.println(var1);
   }
}

如果顶层属性是 val 定义的, 那么只会生成 get 方法, 我们就不能进行任何的写操作了, java源码是public static final int count, 他是一个常量

Ps: 在kotlin中 static 不再是关键字

var 定义的顶层属性, 如果setter 需要自定义, 可以这样:

var count = 0
private set

fun setCount(c: Int) {
   count = c
}

不仅仅是顶层属性可以这样, 普通类中的属于也可以, 但是主构造函数中的属性不行, 需要放在类体内定义

常量 ★

const + val修饰 顶层属性, 将会被解析成常量

var count = 0 // 顶层属性
const val INT_MAX = Int.MAX_VALUE // 常量
val INT_MIN = Int.MIN_VALUE
var INT_MID = Int.MIN_VALUE + Int.MAX_VALUE
fun main(args: Array<String>) { }

反编译后

private static int count;
public static final int INT_MAX = Integer.MAX_VALUE;
private static final int INT_MIN = Integer.MIN_VALUE;
private static int INT_MID = -1;
// var count = 0 // 顶层属性
public static final int getCount() { return count; }
// var count = 0 // 顶层属性
public static final void setCount(int var0) { count = var0; }
// val INT_MIN = Int.MIN_VALUE
public static final int getINT_MIN() { return INT_MIN; }
// var INT_MID = Int.MIN_VALUE + Int.MAX_VALUE
public static final int getINT_MID() { return INT_MID; }
// var INT_MID = Int.MIN_VALUE + Int.MAX_VALUE
public static final void setINT_MID(int var0) { INT_MID = var0; }

(1) 常量不能放在类的内部, 他只能属于顶层或者伴生对象(有点像静态代码块)

const val INT_MIN = Int.MIN_VALUE

class Test {
   
   companion object {
      const val INT_MAX = Int.MAX_VALUE
   }
}

image.png

扩展 ★

是什么? ☆

扩展包含 扩展函数扩展属性, 第三方库中的类通常不让修改, 但可以使用 扩展函数(扩展属性)为第三方库添加新的函数和属性, 不需要修改库源码

扩展函数 ★

什么是扩展函数?★

扩展函数对类进行扩展, 不需要修改类中任何源码, 仅需要给函数添加 receiver 便可

为什么需要扩展函数?

库中代码无法修改, 此时可以通过扩展函数扩展该库特定类的功能

怎么用?★

非常简单, 扩展函数说到底还是函数, 可以按照普通函数写

  1. 写个函数
fun last01(): Char { return xxx }
  1. 加上扩展的类(receiver)
fun String.last01(): Char { return this[this.length - 1] }

至此扩展函数写完了

该扩展函数给 String 类添加了 last01 函数

  1. 调用扩展函数
fun main() {
    println("${"zhazha".last01()}")
}

扩展函数的本质★

扩展函数的本质: fun String.print(): Char ==> fun print(str: String): Char

对扩展函数的本质是: fun print(str: String): Char, 记住这点, 后续遇到的任何问题都可以解释

反编译代码:

private final char last01(String $this$last01) {
    return $this$last01.charAt($this$last01.length() - 1);
}

String.last01(): Char 变成了 char last01(String this), 这便是 kotlin 编译器为扩展函数做的处理

因此可如下使用:

@Test
public final void test01() {
    println("${last01("zhazha")}")
}

此后遇见扩展函数便可看成: Int.xxx() ==> xxx(this: Int)

由此推导: Int() -> Char <==> (Int) -> Char 可相互转换使用

fun String.last01(): Char {
   return this[this.length - 1]
}

fun main() {
   val vLast: (String) -> Char = String::last01
   vLast("zhazha")
   val extLast: String.() -> Char = vLast
   "zhazha".extLast()
}

String.() -> Char(String) -> Char 相互转换

注意:扩展函数可以以 String::last01 如此形式获取扩展函数引用

如若出现扩展函数与扩展属性同名时, String::last01默认引用成员函数

案例

效仿 with 标准函数复写一个属于自己的 with 扩展函数

参考标准库代码如下:

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

可以效仿模拟出两种写法:

private fun <A, B> A.with01(block: A.() -> B) = this.block()
private fun <A, B> A.with02(block: (A) -> B) = block(this)

@Test
fun test02() {
   println("zhazha".with01 { // this ->
      this.length
   })
   println("zhazha".with02 { // it ->
      it.length
   })
}

依据前面的总结, 在此处还可认为: A.() -B(A) -> B有着相同的效果

只不过使用的方式不同, 前者可以使用 this 后者 使用it

扩展函数没有多态性★

扩展函数的调用实际上不走底层java多态那一套, 是编译器在操作

open class Shape
class Rectangle: Shape() {}
class Triangle: Shape() {}

private fun Shape.getName() = "Shape = ${s.javaClass}"
private fun Rectangle.getName() = "Rectangle"
private fun Triangle.getName() = "Triangle"
private fun printName(s: Shape) = println(s.getName())

@Test
fun test01() {
   val s = Shape()
   printName(s)
   val r = Rectangle()
   printName(r)
   val t = Triangle()
   printName(t)
}
Shape
Shape
Shape

要记住, 扩展函数的原型是这样:

private fun getName(s: Shape) = "Shape class = ${s.javaClass}"
private fun getName(r: Rectangle) = "Rectangle"
private fun getName(t: Triangle) = "Triangle"
private fun printName(s: Shape) = println(getName(s))

在执行这段代码fun printName(s: Shape) = println(getName(s))时, getName(s)调用的是 fun getName(s: Shape) 这个函数

这是简单的函数调用不是对象的方法调用

对象的方法调用才有多态, 函数调用不存在 虚函数指针虚函数表, 对象才有

除非代码改成这样:

open class Shape {
   open fun getName(): String = "Shape"
}
class Rectangle: Shape() {
   override fun getName(): String = "Rectangle"
}
class Triangle: Shape() {
   override fun getName(): String = "Triangle"
}
private fun printName(s: Shape) = println(s.getName())

所以不要被扩展函数的 s.getName() 误导了, 其本质上是getName(s) 是函数调用

类对象的 s.getName()才会有多态

成员函数的优先级大于扩展函数★

当类中存在的一个函数和扩展函数有这相同的函数签名, 那么kotlin编译器会优先选择成员函数, 如果扩展函数和成员函数的函数签名不同时, 则不会收到任何影响

class Example {
   fun printFunctionType() = println("class Method")
}

fun Example.printFunctionType() = println("extension method")

fun main(args: Array<String>) {
   val example = Example()
   example.printFunctionType() // class Method
}

可空接收者(使用 Any? 给所有对象添加扩展函数)

fun Any?.toString(): String = if (this == null) "null" else toString()

// 上面这段代码可以简化, 看来我kotlin能力还是不行, 想到了可以用 ?: 表达式, 可是脑抽了, 用了 if 表达式, 上了 idea 才知道还能这样写, 失策失策~~~
fun Any?.toString(): String = this?.toString() ?: "null"

Any?是 kotlin 中所有类的父类, 给 Any? 添加扩展函数则是给所有类添加了一个函数

匿名扩展函数 ★

匿名扩展函数如何定义?
函数?
fun print(): Char
匿名?
fun (): Char
扩展?
fun receiver.(): Char
val c: String.() -> Char = fun String.(): Char = this[this.length - 1]
"zhazha".c()

可以变成:

val c: String.() -> Char = { this[this.length - 1] }

再加强一点点, 把他变成参数呢? 匿名扩展函数类型参数

fun last03(str: String, last: String.() -> Char) {
   last(str)
   str.last()
}

fun main () {
    val toC: String.() -> Char = fun String.(): Char = this[this.length - 1]
    "zhazha".toC()
    last03("zhazha", fun String.(): Char = this[this.length - 1])
}

java源码:

public static final void last03(String str, Function1<? super String, Character> last) {
    // ...
}

Function1 toC2 = main.toC.1.INSTANCE;
toC2.invoke((Object)"zhazha");
ExtensionFuncPropertyDemoKt.last03("zhazha", (Function1<? super String, Character>)((Function1)main.1.INSTANCE));

匿名扩展函数对象不论作为变量还是参数都被kotlin定义成了 Function1

记住仅仅是对扩展函数的 变量 和 参数, 和定义的扩展函数无关

image.png

Function1 接口, 看到 inout 了么? 有进, 有出. p1 进, R 出

如果有学过 java 的话 , 你就把 last 当作接口Function1的匿名对象(Function1的子类), 而匿名对象里面有个 invoke 函数

上面那段代码还可以这样写

fun last03(str: String, last: String.() -> Char) {
   str.last()
}

既是的匿名对象 也是 扩展函数, 这点需要注意

回到 last03 函数的调用

fun last03(str: String, last: String.() -> Char) {
   str.last()
}

它的调用:

last03("zhazha", fun String.(): Char = this[this.length - 1])

第二个参数是 匿名扩展函数, 那要怎么把第二个参数变成 传递 lambda 呢?

在 kotlin 中, lambda 表达式是以 {} 标识作用域, 使用 -> 区分 参数 和 函数体 的表达式, 所以先写上 {}

发现idea报错提示

image.png

你听了它的话 last03 函数声明就变成:

fun last(str: String, param: String.() -> Unit) = str.param()

Char 变成 Unit

然后函数调用就变成这样:

last("xixi") {}

懂了吧? 改回原先的代码, 试试返回个 Char 看看

last("xixi") { 'a' }

没报错

fun last(str: String, param: String.() -> Char) = str.param()

fun main(args: Array<String>) {
    last("xixi") { // this: String // 这里隐藏着一个 this
        this.last()
    }
}

★ 总结: 这说明了 String.() -> Char 转成 lambda 表达式的话, 可以 不用管 接收者 是怎样, 相当于写一个 () -> Char 要怎么转成lambda就行, 只不过 lambda 作用域内多了个 this, 该对象由 我们主动以 str.param() 形式调用,被 kotlin 编译器识别注入了receiver 罢了

如果上面的解释看不懂, 理解不了

我们可以将其还原成: fun last(str: String, param: String.() -> Char) = str.param()

这样我们可以这么使用:

last("xixi") { it -> // 这是自带的
    it.last()
}

匿名扩展函数 String.(): Char 也就是 (String): Char

扩展属性 ★

它是一个属性, 有 get/set 方法, 没有字段(field)

怎么写???

既然是扩展的属性, 核心是属性

(1) 先写个字段

val count: Int = 0

(2) 加上扩展

这里的扩展我选择 List<Int>

val List<Int>.count: Int

(3) 最后加上自定义访问器(get/set)

由于这里是 val 修饰, 所以只有 get 没有 set 访问器

val List<Int>.count: Int
    get() = this.size

完成

怎么用?

val list = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
println(list.count)

翻译成java源码

public final class ExtensionPropertiesDemoKt {
    public static final int getCount(List<Integer> count) {
        return count.size();
    }

    public static final void main(String[] args) {
        Object[] arrobject = new Integer[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        List list = CollectionsKt.listOf((Object[])arrobject);
        int n = ExtensionPropertiesDemoKt.getCount(list);
        System.out.println(n);
    }
}

学完扩展函数, 再学扩展属性就简单过下完事

可变参数、中缀调用

可变参数: vararg

fun arrOf(vararg arr: Int): Array<Int> {
   return arr.toTypedArray()
}

val arr = arrOf(1, 2, 3)
arr.forEach { println(it) }

中缀表达式: infix标记的函数

我们在初始化 map 的时候也会见到:

mapOf("key1" to value1, "key2" to value2);

这才是中缀表达式

这背后都有一个 infix 中缀表达式函数在负责

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

映射在上面的 to 就是 A to B(key to value)

可以自己实现一个,比如 函数名叫 vs

class Score(val name: String, var score: Double) { }

private infix fun Score.vs(other: Score): String = when {
   this.score > other.score -> "congratulations!!! ${this.name}: ${this.score} win!!!"
   this.score == other.score -> "${this.name} and ${other.score} have the same score!!!"
   else -> "congratulations!!!${other.name}: ${other.score} win!!!"
}

@Test
fun test() {
   val xiaoMing = Score("xiangming", 98.5)
   val xiaoHong = Score("xiaoHong", 99.0)
   println(xiaoMing vs xiaoHong)
}

还可以这么写

class Score(val name: String, var score: Double) {
   infix fun vs(other: Score): String = when {
      this.score > other.score -> "congratulations!!! ${this.name}: ${this.score} win!!!"
      this.score == other.score -> "${this.name} and ${other.score} have the same score!!!"
      else -> "congratulations!!!${other.name}: ${other.score} win!!!"
   }
}

fun main() {
   val xiaoMing = Score("xiangming", 98.5)
   val xiaoHong = Score("xiaoHong", 99.0)
   println(xiaoMing vs xiaoHong)
}

经常会看到 我们在遍历 map 的时候会这样:

for((index, value) in map) {
    println("index = ${index}, value = ${value}")
}

不对, 这是解构,不是中缀, 虽然他有 toPair 函数

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

三重引号字符串: 推荐使用 trimMargin 带边界的

"""zhazha""" 这就是三重引用字符串

  1. 三重引号中的字符串不需要进行字符串转义
fun parsePath(path: String) {
   // 文件目录
   val directory = path.substringBeforeLast("""""")
   // 文件名+扩展名
   val fullName = path.substringAfterLast("""""")
   // 获取文件名
   val fileName = fullName.substringBefore(""".""")
   // 获取扩展名
   val extension = fullName.substringAfter(""".""")
   println("文件目录: $directory \nfullName: $fullName \n文件名: $fileName \n扩展名: $extension")
}

fun parsePathRegex(path: String) {
   val regex = """(.+)\(.+).(.+)""".toRegex()
   val matchResult = regex.matchEntire(path)
   if (null != matchResult) {
      val (directory, fileName, extension) = matchResult.destructured
      println(matchResult.destructured.toList().toString())
      println("文件目录: $directory \nfileName: $fileName\n扩展名: $extension")
   }
}

fun main(args: Array<String>) {
   val path = """D:\programs\codes\java\autoShutdown\src\test\java\com\zhazha\test\FooBar.java"""
   parsePathRegex(path)
}
  1. 三重字符串的第二个功能

被包围的字符串, 保留换行符, 各种奇奇怪怪的符号

val str = """
         ddddddddd
         ggg
         
      hhhhhhhhh
            wwwwwwwww
"""

println(str)
				ddddddddd
			   ggg
			   
			hhhhhhhhh
			      wwwwwwwww

去除每一行前面共同的缩进

str = """            ddddddddd
         ggg\n
         
      hhhhhhhhh
            wwwwwwwww""".trimIndent()

println(str)
	ddddddddd
   ggg\n
   
hhhhhhhhh
      wwwwwwwww

去除边界前缀之前的所有字符

str = """            .ddddddddd
         .ggg\n
         .
     .hhhhhhhhh
            .wwwwwwwww""".trimMargin(".")
println(str)
ddddddddd
ggg\n

hhhhhhhhh
wwwwwwwww

trimMargin函数默认使用|做边界前缀, 上面代码使用的.做边界前缀

局部函数和扩展

什么是局部函数?

在函数中写上函数

fun saveUser(user: User) {
   fun validate(user: User, value: String, fieldName: String) {
      if (value.isEmpty()) {
         throw IllegalArgumentException("can't save user ${user.id}: empty $fieldName")
      }
   }
   validate(user, user.name, "name")
   validate(user, user.address, "address")
   println(user)
}

反编译成 java

public static final void saveUser(User user) {
    Intrinsics.checkNotNullParameter((Object)user, (String)"user");
    LocalFuncDemo01Kt.saveUser$validate(user, user.getName(), "name");
    LocalFuncDemo01Kt.saveUser$validate(user, user.getAddress(), "address");
    boolean bl = false;
    System.out.println(user);
}

private static final void saveUser$validate(User user, String value, String fieldName) {
    CharSequence charSequence = value;
    boolean bl = false;
    if (charSequence.length() == 0) {
        throw new IllegalArgumentException("can't save user " + user.getId() + ": empty " + fieldName);
    }
}

没找到应用场景, 看起来也挺麻烦的, 目前对我来说没啥用