Java转Kotlin:函数基础

684 阅读6分钟

1 认识Kotlin的函数

1.1 一等公民

在Kotlin中,函数有自己的类型,与同级,是"一等公民"。

可以比较一下Java中的方法加深理解:在Java中,没有函数,都称为方法(二者区别见后文),方法只能定义在里面,不能脱离单独存在;在Kotlin中,函数方法都存在,脱离定义的叫函数,定义在里面的叫方法

在Kotlin中,函数fun与类class级别相同。可以在合适的条件下赋值、传递和调用

1.2 Kotlin函数的学习路线图

函数路线图

2 Kotlin函数基础

2.1 函数定义

Kotlin中的函数定义由fun关键字开头;接函数名与小括号对,小括号中是形参列表,多个形参用逗号隔开;小括号后面接冒号,用于隔开函数的返回类型;函数的返回类型后面接大括号对,大括号对里面则是函数体,如下所示。

函数的定义

  • Kotlin中的空返回值不是void而是Unit
  • 函数如果返回值是Unit,则可以省略:Unit

Unit相当于void

2.2 函数与方法

  • 方法可以认为是函数的一种特殊类型,可以说函数包含了方法;
  • 从形式上,有receiver的函数即为方法,所谓receiver,是指调用方法的对象或运行时类
  • 脱离类定义的叫函数,定义在类里面的叫方法
//Kotlin
class people {
    //TODO:方法(Method)
    fun sayHi() {
        println("Hi!")
    }
}

//TODO:函数(function)
fun doNothing(anything:Any):Any {
    return anything
}

fun main() {
    //方法的调用者叫receiver
    val p = people()
    p.sayHi()
    //函数没有receiver
    doNothing(123)
}

2.3 函数类型

2.3.1 函数类型基础

先看一张图:

函数类型_1

再思考一个问题,上面说到Kotlin中的函数可以传递,也就是可以作为函数形参,这个形参如何定义,是什么类型?

  • Kotlin综合形参类型列表返回值类型,给出了函数的类型形如(形参列表)->返回值类型

  • 函数类型中的->Unit不能省略

  • 形参类型列表不同于形参列表,形参列表是arg1:Type1,arg2:Type2,对应的形参类型列表不包含形参名,是Type1,Type2

  • 定义在类里面的函数(方法)呢?

函数类型_2

对于定义在类里面的函数(方法),其函数类型有三种写法:

  • 第一种,Receiver.(Type1,Type2)->Type3
  • 第二种,(Receiver,Type1,Type2)->Type3
  • 第三种,见2.3.3

其实像第二种这样将receiver写成第一个形参的,在面向对象的语言中比较多见,例如在Python中,定义在类中的所有函数的第一个参数都是self,即为receiver。只是,在Java中,编译器帮我们省略了这一点,以至于现在猛地一看有些不适应。

#Python
class People:
    def sayWords(self, words):
        print(words)

2.3.2 receiver具体化

对于以下这个函数(方法):

//Kotlin
class Person(var name: String, var age: Int) {
    fun sayHi() {
        println("Hello! How are you doing?")
    }
}

fun main() {
    val mike = Person("Mike", 23)
    val func1 = Person::sayHi
    val func2 = mike::sayHi
}

Person::sayHimike::sayHi是两个不同的函数,如果将receiver写成函数(方法)的第一个传入参数,则

  • Person::sayHi的函数类型是(Person)->Unit
  • mike::sayHi的函数类型是()->Unit

receiver具体化之前

receiver具体化之后

receiver具体化之后的报错

这种receiver具体化的情况与数学中的变量降维类似:

f(x, y, z) = (x + y) * z
f(2, y, z) = (2 + y) * z
#当x具体化为2后,不再是变量,函数可以写成
g(y, z) = (2 + y) * z

2.3.3 函数类型的三种写法

对于Person类中的sayHi函数(方法),其函数类型有三种表示方式:

  1. (Person) -> Unit:将receiver作为第一个形参写到括号里面;
  2. Person.() -> Unit:将receiver放到括号前面,用调用符分隔;
  3. Function1<Person, Unit>:使用Kotlin内置的函数类型容器,其中Function11表示<>中只有前1个是传入参数,之后的是传出参数;有多少传入参数,就使用对应的函数类型容器
    • Function0<Out>
    • Function2<In1, In2, Out>
    • Function3<In1, In2, In3, Out>
    • ... ...
//Kotlin
class Person(var name: String, var age: Int) {
    fun sayHi() {
        println("Hello! How are you doing?")
    }
}

val f1: (Person) -> Unit = Person::sayHi
val f2: Person.() -> Unit = Person::sayHi
val f3: Function1<Person, Unit> = Person::sayHi

函数类型的三种表示形式

2.4 函数引用

函数引用这个词应该是没有听说过的,但是类的引用在Java中不少。

//Java
class Person {
    String name;
    int age;
    Person(String name, int age){
        this.name = name;
        this.age = age;
    }
    public void greet(Person p) {
        System.out.println("Hello!"+p.name);
    }
}

public static void main(String[] args) {
    //Person类的引用
    Person mike = new Person("Mike", 22);
    Person jack = new Person("Jack", 23);
    //引用可以赋值给相同类型的变量
    Person aBoy = mike;
    //引用可以作为实参传递给形参
    mike.greet(jack);
}

函数引用也是类似的,前面的函数类型给函数定好了框架,没有规定函数的名字和函数体,因此同一个函数类型之下可以有无数种具体的函数实现。我们可以定义一个类型相同的变量来接收该类型的函数实现:

//Kotlin
fun print(content:String) {
    println("print println : $content")
}

//函数引用给变量赋值
val p:(String)->Unit = ::print

//设置函数类型的形参
fun printAndReturn(func:(String)->Unit):String {
    val str = "Hello"
    func(str)
    return str
}

fun main() {
    //引用传递
    val str = printAndReturn(p)
    println("main println : $str")
}

以上代码中,函数print是函数类型(String)->Unit的一个具体实现,可以定义一个该类型的变量p来接收函数print的引用,该函数的引用表示为::print,可以理解双冒号::为C++中的命名空间符号,只是这里的函数print没有receiver,所以::之前是空的。

函数的引用

注意!函数引用的写法仅仅取决于receiver函数名,写作receiver::funcName,当然,没有receiver的就写作像上面的::funcName。若存在类似于函数重载的情况,一旦编译器无法推导,则报错。

2.5 函数调用

任何位置的函数都有两种调用方式:

  1. funcName(In1, In2)
  2. funcName.invoke(In1, In2)

第一种是常见的写法,第二种的invoke函数(方法)是所有函数类型的内置方法,顾名思义,就是调用的意思。

//Kotlin
class Person(var name: String, var age: Int) {
    fun communicate(selfIntroduction: (Person) -> Unit, ask: (Person) -> Unit, p: Person) {
        selfIntroduction(this)//TODO:第一种函数调用方式
        ask.invoke(p)//TODO:第二种函数调用方式
    }
}

fun selfIntroduction(p: Person) {
    println("I am ${p.name} and I am ${p.age} years old.")
}

fun ask(p: Person) {
    println("Are you ${p.name}? Are you ${p.age} years old?")
}

fun main() {
    val mike = Person("Mike", 23)
    val jack = Person("Jack", 22)
    mike.communicate(::selfIntroduction, ::ask, jack)
}

2.5 变长参数

在Java中的main函数(方法)的写法有两种:

//Java
//1
public static void main(String[] args){}
//2
public static void main(String...args){}

第一种是String数组的形参接受控制台的传入参数;第二种是String类型的变长参数接受控制台的传入参数。

与之类似,Kotlin也有变长参数,例如写一个函数希望接收多个Int

//Kotlin
//1
fun multiParam(ints: IntArray) {}
//2
fun multiParam(vararg ints:Int) {}

借助vararg关键字,指定紧随其后的形参是一个可变长度的参数。

传进来怎么用?其实传进来的ints就是一个IntArray类型的变量:

变长参数的实际类型

//Kotlin
fun multiParam(vararg ints: Int) {
    println(ints.size)
    println(ints[0])
    ints.forEach {
        println(it)
    }
}

2.6 多返回值

借助PairTriple等内置引用类型与()解构运算实现函数的伪多返回值效果。

//Kotlin
fun multiOutput(): Triple<Int, Long, String> {
    return Triple(1, 9L, "Hello")
}

fun main() {
    val (i, l, str) = multiOutput()
    println("i = $i\nl = $l\nstr = $str")
    //i = 1
    //l = 9
    //str = Hello
}

2.7 默认参数与具名参数

在定义函数的时候,可以给形参赋值默认参数,在调用时,若不传入该参数,则在函数内部使用该参数的默认值:

//Kotlin
fun defaultParam(x: Int, y: String, z: Long = 0L) {
    println("x = $x\ny = $y\nz = $z\n")
}

fun main() {
    defaultParam(10, "hello")
}

一般而言,将带有默认值的形参放在形参列表的最后,避免混淆编译器

默认参数一般放到最后

调用时,指定具名参数,也可以不考虑上面的一般而言,这里的具名值得是形参的名字

具名参数

3 总结

总结