3.1 函数定义
kotlin的函数以关键字fun
开头,参数列表中,变量名称写前面,类型写后面,返回结果写在参数列表后面,如果没有返回结果,可以不写,或者写Unit
,例如
fun add(a:Int,b:Int):Unit{}
fun add2(a:Int,b:Int):Int{
return a + b
}
如果方法函数只有一行,可以省去{}
,用=
连接两个语句:
fun add2(a:Int,b:Int):Int = a+b
3.1.1 参数列表
3.1.1.1 默认参数
fun add(a:Int,b:Int = 10):Int = a + b //函数1
add(10)
参数可以有默认值,如果不传的话,可以使用参数列表中定义的默认值,上述例子中输出的结果是20
.
参数的默认值甚至可以来源于其他参数:
fun foo(list: List<Int>,size: Int = list.size){
}
3.1.1.2 可变长参数
在可变长参数上,kotlin的特性和java保持一致:只能有一个变长参数,且必须要放在参数列表的最后一个.
fun foo(int: Int,vararg strings: String){
}
//调用
foo(10,*arrayOf("a", "b", "c"));
3.1.2 参数命名化
我们在调用函数1时,也可以指定参数的名称
add(a = 10,b = 20)
add(a = 10)
指定参数的名称的好处在于当参数列表特别长 的时候,可以很大程度的提高可阅读性.
3.1.3 多返回值
kotlin的多返回值比较奇葩,它是通过返回对象,然后接收者用多变量来接收,这么一个语法糖
fun twoReturn():Pair<Int,String> = 18 to "Pantheon"
fun threeReturn():Triple<Int,String,Int> = Triple(18,"Pantheon",180)
fun main() {
val (age,name) = twoReturn()
val (age1,name1,height) = threeReturn()
}
两个返回值返回pair
,三个Triple
,四个应该叫Quadra
,但是似乎kotlin没有实现,我们可以自己实现,需要定义一个叫Quara
的数据类:
data class Quadra<out A, out B, out C,out D>(
public val first: A,
public val second: B,
public val third: C,
public val fourth :D
) ;
然后就可以有四个返回值,当然我们也可以定义Penta
(5个返回值)等等其他一些多返回值的数据类.
3.2 函数类型
函数在kotlin里面是头等公民,函数可以随处定义,随处引用,可以当做参数传递,也可以当做返回值.函数可以通过委托的方法来获取到引用,比如:
fun add(a: Int,b:Int):Int{
return a + b;
}
class Sub{
fun sub(a: Int,b:Int):Int{
return a - b;
}
}
fun main(args: Array<String>) {
var operator1 = ::add
var operator2 = Sub::sub
var operator3 = Sub()::sub
}
我们通过静态委托
,类委托
以及对象委托
的方式获取到了对象的引用,函数在kotlin中是有类型的,这个类型由函数的入参和返回值决定,operator1
和operator3
入参和返回都一样,所以他们可以互相赋值,而没有编译错误
operator1 = operator3
而operator2
是类委托
,我们知道执行一个类的方法,如果它不是静态方法,则必须要创建出对象才可以执行,比如我们要执行operator2的方法:
operator2(Sub(),10,10)
所以它的类型,肯定和operator1,operator3
是不一样的,我们用idea
的反编译工具来反编译一下,看看函数的类型到底是怎么回事
public static final void main(@NotNull String[] args) {
Intrinsics.checkNotNullParameter(args, "args");
KFunction operator1 = null.INSTANCE;
((Function2)operator1).invoke(10, 20);
KFunction operator2 = null.INSTANCE;
((Function3)operator2).invoke(new Sub(), 10, 10);
KFunction operator3 = new Function2(new Sub()) {
// $FF: synthetic method
// $FF: bridge method
public Object invoke(Object var1, Object var2) {
return this.invoke(((Number)var1).intValue(), ((Number)var2).intValue());
}
public final int invoke(int p1, int p2) {
return ((Sub)this.receiver).sub(p1, p2);
}
};
((Function2)operator3).invoke(3, 4);
}
可以看到operator1
和operator3
其实是一个Function2
类型,而operator2
是一个Function3
public interface Function2<in P1, in P2, out R> : Function<R> {
/** Invokes the function with the specified arguments. */
public operator fun invoke(p1: P1, p2: P2): R
}
public interface Function3<in P1, in P2, in P3, out R> : Function<R> {
/** Invokes the function with the specified arguments. */
public operator fun invoke(p1: P1, p2: P2, p3: P3): R
}
看了上面的定义,其实函数的类型是由它的入参和出参决定的,如果要用java
的语法来什么operator1
和operator3
是这样
Function2<Integer, Integer, Integer> operator1 = xxx
Function2<Integer, Integer, Integer> operator3 = xxx
所以它们两的类型当然一样
而operator3
,则是
Function3<Sub,Integer,Integer,Integer> operator2 = xxx
所以它和operator1,operator2
当然不一样.
那看到Function2
,Function3
其实就是表示入参的个数,假如入参个数很多呢?kotlin最多支持多少个呢?kotlin给出的答案是22个,也就是如果定义了一个函数的参数个数超过了22个,那它将不会被kotlin支持.
3.3 拓展函数
我们再spring里面经常需要做各种配置,spring bean的核心接口是BeanFactory
,假如我想在BeanFactory
上面添加一些整合我们自己项目的方法,比如伪代码这样
beanFactory.loadMyService();
这个loadMyService
并不是BeanFactory
的接口,而是我们项目中定义的一个方法,但是任何一个实例化的beanFactory
实例都有这个方法,这种方法叫做拓展方法.我们可以这样实现:
fun BeanFactory.loadMyService(config:String){
println("${this.toString()}")
println("---loadMyService--");
}
fun main(args: Array<String>) {
var beanFactory:BeanFactory = DefaultListableBeanFactory()
beanFactory.loadMyService("")
}
拓展方法的语法非常简单,只是在对象名称前面加了一个类名
(官方一点的说法叫做receiver
),表示是给哪一个类添加方法.
刚刚说的对象的拓展方法,调用这个方法必须要创建出实例,那类的静态拓展方法呢?
在kotlin中取消了static
关键字,实现静态属性,静态方法需要用companion
来创建一个匿名的伴生对象,比如实现一个foo
的静态方法:
class Foo{
companion object{
fun foo(){
println("static method")
}
}
}
所以类的静态拓展方法是给类的伴生对象添加拓展方法,语法如下:
fun Foo.Companion.bar() = println("bar")
拓展方法的实现非常巧妙,我们以beanFactory
的loadMyService
为例,反编译一下:
public static final void loadMyService(@NotNull BeanFactory $this$loadMyService, @NotNull String config) {
Intrinsics.checkNotNullParameter($this$loadMyService, "$this$loadMyService");
Intrinsics.checkNotNullParameter(config, "config");
String var2 = String.valueOf($this$loadMyService.toString());
System.out.println(var2);
var2 = "---loadMyService--";
System.out.println(var2);
}
kotlin
会给拓展方法生成一个静态且不可覆盖的方法,第一个参数就是调用者,并且对this
关键字做了转义,所以使用拓展函数,就像使用类内部定义的方法一样.
3.4 Infix函数
infix函数的特性是这样,我们再调用对象函数的时候,通常是对象.方法名(参数)
,kotlin中的infix
函数调用时,可以省略.
和()
,这样调用对象 方法名 参数
,如我们创建一个pair
对象:
val pair = 2 to 3
定义infix函数有这样几个要求:
- 只能有一个参数
- 必须是拓展函数
- 参数不能有默认值,且不能是可变长参数
假如我们实现以5 add 2
这样调用方式调用的函数,定义如下:
infix fun Int.add(int: Int) = this + int
这个写法其实就是在拓展函数前面加了一个infix
关键字,infix
函数可以说是拓展函数的特例.
3.5 operator函数
在python
中,我们判断一个数在不在数组中,用num in list
的语法,其实就等价于list.contains(num)
,这种函数,我们把它称之为operator 函数
.
比如,我们再kotlin
中实现一个in
的operator函数
class InOperator{
var list = mutableListOf<Int>(1,2,3,4)
operator fun contains(int: Int):Boolean{
return list.contains(int)
}
}
fun main(args: Array<String>) {
var inOperator = InOperator()
val b = 3 in inOperator // 等价于 inOperator.contains(3)
println(b)
}
可以看到operator函数
的申明和普通函数并没有什么区别,只是多了一个关键字operator
,函数的名称是固定的,只能是contains
,并且参数的个数也是固定(除了get
,set
,invoke
),也就是说用户需要实现kotlin
内置的特定方法,可以以另外一种方式来调用这个方法.
除此之外operator
函数也是可以用拓展函数实现的,语法也只是在拓展函数前面加一个operator
operator fun InOperator.get(index: Int):Int{
return list.get(index)
}
fun main(args: Array<String>) {
var inOperator = InOperator();
val num = inOperator[3] //等价于 inOperator.get(3)
println(num)
}
kotlin中operator
函数总共有这么几个类型
3.5.1 Unary operations
Unary
的意思是只有一个元素,也就是被调用者,没有参数,Unary operator
中有的需要写成前缀的形式,如+a
:
表达式 | 等价表达式 |
---|---|
+a | a.unaryPlus() |
-a | a.unaryMinus() |
!a | a.not() |
a++ | a.inc() |
a-- | a.dec() |
举个unaryPlus
的例子
data class UnaryPlusObjData(val num: Int);
operator fun UnaryPlusObjData.unaryPlus():UnaryPlusObjData = UnaryPlusObjData(this.num + 1)
fun main(args: Array<String>) {
val objData = UnaryPlusObjData(1)
val data = +objData
println(data.num)
}
3.5.2 Binary operations
3.5.2.1 Arithmetic operators
表达式 | 等价表达式 |
---|---|
a + b | a.plus(b) |
a - b | a.minus(b) |
a * b | a.times(b) |
a / b | a.div(b) |
a % b | a.rem(b) |
a..b | a.rangeTo(b) |
3.5.2.2 in operator
表达式 | 等价表达式 |
---|---|
a in b | b.contains(a) |
a !in b | !b.contains(a) |
3.5.2.3 Indexed access operator
表达式 | 等价表达式 |
---|---|
a[i] | a.get(i) |
a[i, j] | a.get(i, j) |
a[i_1, ..., i_n] | a.get(i_1, ..., i_n) |
a[i] = b | a.set(i, b) |
a[i, j] = b | a.set(i, j, b) |
a[i_1, ..., i_n] = b | a.set(i_1, ..., i_n, b) |
3.5.2.4 invoke operator
表达式 | 等价表达式 |
---|---|
a() | a.invoke() |
a(i) | a.invoke(i) |
a(i, j) |
a.invoke(i, j) |
a(i_1, ..., i_n) |
a.invoke(i_1, ..., i_n) |
3.5.2.5 Augmented assignments
表达式 | 等价表达式 |
---|---|
a += b | a.plusAssign(b) |
a -= b | a.minusAssign(b) |
a *= b | a.timesAssign(b) |
a /= b | a.divAssign(b) |
a %= b | a.remAssign(b) |
3.5.2.6 Equality and inequality operators
表达式 | 等价表达式 |
---|---|
a == b | a?.equals(b) ?: (b === null) |
a != b | !(a?.equals(b) ?: (b === null)) |
3.6 lambda表达式
在java中,我们定义lambda
函数,通常先需要定义一系列的接口,然后打上FunctionalInterface
的标识,而在kotlin中则完全不用这样.比如,定义个加法器:
fun main(args: Array<String>) {
var lambda = { a: Int,b:Int -> a + b }
var sum = lambda(1, 2)
}
当函数体只有一行的时候,编译器会根据执行的结果推测出是否需要返回结果,这个例子给出的返回结果就是3
,当然我们也可以指定lambda
的返回类型为unit
,也就是不返回结果
var lambda:(Int,Int)->Unit = { a: Int,b:Int -> a + b }
所以lambda的语法要求有这样几点:
-
整个方法体被
{}
包围 -
参数列表必须也在
{}
体内,并且如果lambda
的类型已经被定义,可以省略参数的类型,如:var lambda:(Int,Int)->Unit = { a,b -> a + b }
-
表达式的body体必须在
->
后面 -
如果方法没有被定义成返回
Unit
,则表达式的最后一行会根据结果来推测出返回值.
3.7 匿名函数
我们在lambda表达式
中看到,lambda
是不用指定返回类型的,因为它可以通过上下文自动推断出来,那如果我们需要指定返回类型,除了在lambda
定义的时候指定lambda
的类型,还可以通过匿名函数来实现:
fun(num:Int):Boolean = num > 10
我们也可以把它赋值给某一个变量,然后后面的函数可以调用它:
var a = fun(num:Int):Boolean = num > 10
val mutableListOf = mutableListOf<Int>(1, 2, 3, 11, 12)
mutableListOf.filter(a)
也可以直接在调用处定义匿名函数:
val mutableListOf = mutableListOf<Int>(1, 2, 3, 11, 12)
mutableListOf.filter (fun(item):Boolean = item>10)
当然也可以省略返回类型:
val mutableListOf = mutableListOf<Int>(1, 2, 3, 11, 12)
mutableListOf.filter (fun(item) = item>10)
匿名函数和普通函数差不多,只不过没有名字,要使用它,要么把它赋值给变量,然后调用,要么直接在高阶函数里面定义且调用.
3.8 高阶函数
高阶函数的定义不是很难,就是指入参有函数,或者返回值为函数的函数,比如集合的filter
函数,
val mutableListOf = mutableListOf<Int>(1, 2, 3, 11, 12)
mutableListOf.filter {it>10}
mutableListOf.filter {it>10}
这种调用方式需要说明三个问题:
-
当高阶函数的最后一个参数是函数时,调用的时候可以不写在
()
里面,而以lambda
的形式跟在调用处后面,这一个特性为日后实现DSL
埋下了基础比如我定义了这样的高阶函数,功能很简单就是比较
5
和一个provider
产生的一个数进行比较大小:fun compare(num:Int,provider:()->Int):Boolean = num > other();
provider
是一个函数类型,没有入参,返回值为Int
,所以调用的时候我定义了一个lambda
表达式,而又因为它是函数的最后一个参数,可以这样调用:val compare = compare(10) {kotlin.random.Random.nextInt()}
-
当参数函数只有一个参数的时候,可以用
it
来作为默认参数名称,并且可以省略lambda
的->
.当然我们也可以重命名参数的名称:mutableListOf.filter {a->a>10}
当高阶函数参数函数中的参数我们不需要的时候,可以用
_
来代替参数名称,听着有点拗口,来个例子:val mapOf = mapOf<Int, Int>() mapOf.forEach{ (_, value) -> println(value)}
是不是一目了然,我们不需要
key
嘛.高阶函数好像看着不算难,但是真正一旦把之前的函数一旦结合起来看,就飞天了,甚至后面会研究的
DSL
功能,看似合理,但是不知道写的是什么,这里我们先研究研究常用的几个高阶函数3.8.1 with函数
with
是随着的意思,with
函数表达的意思是,调用receiver
函数的方法时,就象在方法体内部调用一样,举个例子:class MyWith{ fun with1():String = "with1"; fun with2(string: String) = "${string}+with2" } fun main(args: Array<String>) { val myWith = MyWith() with(myWith){ val with1 = with1() val with2 = with2(with1) println(with2) } }
with
可以省去很多对象名称.方法
的调用方式,调用方式时,像是对象内部调用.with
当然还可以有返回值,比如我们创建一个数据类
时,也可以这样去创建:class People{ lateinit var name:String; var age by Delegates.notNull<Int>(); lateinit var from:String; lateinit var to:String; override fun toString(): String { return "People(name='$name', age=$age, from='$from', to='$to')" } } fun main(args: Array<String>) { var people = with(People()) { name = "Pantheon" age = 10 from = "JiangSu" to = "Beijing" this } }
这样的话可以不用写对象名称,很简洁,注意
main
的最后一行,只有一个this
,lambda
我们说过,如果lambda
有返回值,单独写在最后一行.我们琢磨下
with
的语法是咋实现的:public inline fun <T, R> with(receiver: T, block: T.() -> R): R { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return receiver.block() }
首先参数是两个,第一个是泛型,第二个是函数类型,无入参,返回泛型,并且这个函数还是T的拓展函数,这个是实现我们刚刚使用的语法的关键.之前讲拓展函数的时候讲过,拓展函数就像其他函数一样,在拓展函数里面给
receiver
属性赋值,就像是给this
的属性赋值,所以可以不用对象.方法
的形式调用.我们在看receiver.block()
,也就是调用了拓展函数3.8.2 let函数
kotlin中每一个变量都有
let
的函数,可以使用let
函数实现对象的转换,比如:data class People(var name:String,var age:Int); data class Student(var name: String,var age: Int,var grade:String) val people = Student("Pantheon", 30, "4th").let { People(it.name, it.age) }
也可以实现链式调用的功能:
val people = Student("Pantheon", 30, "4th").let { People(it.name, it.age) }.let { println(it.name) }.let { println("----") }
类似的还有
also
函数,和let
的区别在于also
返回的类型是this
.3.8.3 run函数
run
函数和with
意思一致,只不过调用方式不一致,with
我们这样调用:val people = with(Student("Pantheon", 30, "4th")){ People(name, age) }
run
函数只有一个参数,且也是调用者的拓展函数,所以可以这样调用val people = Student("Pantheon", 30, "4th").run { People(name,age) }
与其相似的还有一个apply
函数,apply
返回的类型是this
,别的没有什么区别
3.9 inline&noline&crossinline
inline
函数是一种优化手段,在生成字节码的时候,inline
函数会被重新编织字节码,直接在被调用处生成相应的字节码,这样的好处就在于,减少了创建函数的开销,函数在kotlin中存在的形式是以FunctionN
的对象来存在,inline
函数就直接减少了FunctionN
对象的创建,来看个demo:
inline fun gogo(){
print("gogo begin")
print("gogo")
print("gogo end")
}
fun invokeGoGo(){
print("invokeGoGo begin")
gogo()
print("invokeGoGo end")
}
gogo
被inline
修饰,并被invokeGoGo
调用,在编译阶段,会直接把gogo
的代码拷贝到invokeGoGo
方法体内,从而减少创建gogo
函数对象的开销,编译后的效果等价于:
fun invokeGoGo(){
print("invokeGoGo begin")
print("gogo begin")
print("gogo")
print("gogo end")
print("invokeGoGo end")
}
inline
函数不仅可以将函数体拷贝到被调用处,还可以将高阶函数的参数拷贝到被调用处
inline fun gogo(block:()->Unit){
print("gogo begin")
block()
print("gogo end")
}
fun invokeGoGo(){
print("invokeGoGo begin")
gogo{
println("开始了")
println("start")
}
print("invokeGoGo end")
}
编译后的效果等价于这样:
fun invokeGoGo(){
print("invokeGoGo begin")
print("gogo begin")
println("开始了")
println("start")
print("gogo end")
print("invokeGoGo end")
}
但是当我们的inline
高阶函数需要返回入参的时候,却出了个问题,因为入参函数被解析成了一行行的代码,丢掉了函数本身自己的类型,编译器不知道你要返回什么给函数调用者:
inline fun gogo(block:()->Unit):()->Unit{
print("gogo begin")
block()
print("gogo end")
return block
}
这段函数编译期就会报错,这个时候noline
就显示出了作用,noline
的作用就是在于inline
函数中,有些函数,它不能被解析成一行行的函数,就可以在参数上加noline
inline fun gogo(noinline block:()->Unit):()->Unit{
print("gogo begin")
block()
print("gogo end")
return block
}
再比如,gogo
方法需要掉另外一个方法,这个方法需要传入block
,block
函数应该作为一个整体,所以也应该加上noinline
最后在说一下crossinline
的关键字,当block
发生间接调用时,也就是将block当做参数,传递给别的高阶函数时,这个时候就会出现一个问题,就是return
的问题.之前介绍过return
,inline
函数内部是可以出现return
的,并且表示结束的是被调用方,但是如果出现间接调用,该如何处理return
的结束范围呢?
比如:
inline fun f( body: () -> Unit) {
println("go")
body()
println("end")
}
fun invokeGoGo(){
f{
return
}
}
这段话最终只会输出go
,因为invokeGoGo
生成的字节码是这样:
fun invokeGoGo(){
println("go")
return
println("end")
}
但是如果我需要把body
这个函数当做参数传递给别的函数呢?
inline fun f( body: () -> Unit) {
println("go")
go{
body()
}
body()
println("end")
}
fun go(body: () -> Unit){
println("go")
body()
}
fun invokeGoGo(){
f{
return
}
}
编译期应该就懵逼了,这个return到底是指的谁,指的是go
还是f
还是invokeGoGo
,所以对于这种间接调用的情况,kotlin无奈选择了一个很不优雅的解决方法,就是间接调用需要加crossinline
关键字,并且无法使用return
语句
inline fun f( crossinline body: () -> Unit) {
println("go")
go{
body()
}
body()
println("end")
}
fun go(body: () -> Unit){
println("go")
body()
}
fun invokeGoGo(){
f{
}
}
看似是一个调用的问题,实际是引入了inline
优化方案而引起的一系列语法上的冲突,既无奈又心酸,谁让java底层对lambda支持这么差,需要上层像各种优化办法呢?