从Java到Kotlin(五)

函数与Lambda表达式

目录

一、函数声明与调用
二、参数和返回值
三、单表达式函数
四、函数作用域
五、泛型函数
六、尾递归函数
七、中缀表示法
八、Lambda表达式的语法
九、高阶函数与Lambda表达式
十、匿名函数
十一、内联函数


一、函数声明与调用

Java 中的方法使用 void 关键字声明:

void foo(){}

Kotlin 中的函数使用 fun 关键字声明:

fun foo(){}

用法相似,加入有一个 User 类,里面有一个 foo() 函数,调用函数的代码如下:
Java代码

new User().foo();

Kotlin代码

User().foo()

二、参数和返回值

声明有参数的函数,代码如下:
Java代码

void foo(String str, int i) {}

Kotlin代码

fun foo(str: String, i: Int) {}

Java先定义类型,后命名;Kotlin先命名,后定义类型,中间用冒号:分隔。两者都是多个参数中间用逗号,分隔。
如函数有返回值,代码如下:
Java代码

String foo(String str, int i) {
    return "";
}

Kotlin代码

fun foo(str: String, i: Int): String {
   return ""
}

Java是把void替换成返回值的类型,而Kotlin是把返回值声明在函数的末尾,并用冒号:分隔。
两种语言声明参数和返回值的方式有点相似,而Kotlin还有更强大的功能,例如默认参数命名参数,如下所示:
函数参数可以有默认值,当没有给参数指定值的时候,使用默认值

//给i指定默认值为1
fun foo(str: String, i: Int = 1) {
    println("$str  $i")
}
//调用该函数,这个时候可以只传一个参数
foo("abc")
//运行代码,得到结果为: abc  1

如果有默认值的参数在无默认值的参数之前,要略过有默认值的参数去给无默认值的参数指定值,要使用命名参数来指定值,有点绕我们看代码:

//有默认值的参数在无默认值的参数之前
fun foo(i: Int = 1, str: String) {
    println("$str  $i")
}
//foo("hello")  //编译错误
foo(str = "hello")  //编译通过,要使用参数的命名来指定值
//运行代码,得到结果为: hello  1
  • 可变数量的参数
    函数的参数可以用 vararg 修饰符标记,表示允许将可变数量的参数传递给函数,如下所示:
//用 vararg 修饰符标记参数
fun <T> asList(vararg ts: T): List<T> {
    val result = ArrayList<T>()
    for (t in ts) // ts is an Array
        result.add(t)
    return result
}

val a = arrayOf(1, 2, 3)
//*a代表把a里所有元素
val list = asList(-1, 0, *a, 4)
//运行代码,得到结果为: [-1, 0, 1, 2, 3, 4]

三、单表达式函数

在Kotlin中,如果函数的函数体只有一条语句,并且有返回值,那么可以省略函数体的大括号,变成单表达式函数。如下所示:

//函数体内只有一条语句,且有返回值
fun foo(): String{
    return "abc"
}
//这时可以省略大括号,变成单表达式函数
fun foo() = "abc"

四、函数作用域

在 Kotlin 中函数可以在文件顶层声明,这意味着你不需要像一些语言如 Java 那样创建一个类来保存一个函数。此外除了顶层函数,Kotlin 中函数也可以声明在局部作用域、作为成员函数以及扩展函数。

1. 成员函数

成员函数是指在类或对象里定义的函数。

Java代码:

class User {
    //在类里定义函数。
    void foo() {}
}
//调用
new User().foo();

Kotlin代码:

class User() {
    //在类里定义函数。
    fun foo() {}
}
//调用
User().foo()

2. 局部函数

Kotlin支持在函数内嵌套另一个函数,嵌套在里面的函数成为局部函数,如下所示:

fun foo() {
    println("outside")
    fun inside() {
        println("inside")
   }
   inside()
}

//调用foo()函数
foo()

运行代码,得到结果

而Java中没有局部函数这一概念。

五、泛型函数

泛型参数使用尖括号指定,如下所示:
Java代码

<T> void print(T t) {
}

<T> List<T> printList(T t) {
}

Kotlin代码

fun <T> printList(item: T) {
}

fun <T> printList(item: T): List<T> {
}

六、尾递归函数

尾递归函数是一个递归函数,用关键字tailrec来修饰,函数必须将其自身调用作为它执行的最后一个操作。当一个函数用tailrec修饰符标记并满足所需的形式时,编译器会优化该递归,留下一个快速而高效的基于循环的版本,无堆栈溢出的风险,举个例子:
先看一段代码

fun count(x: Int = 1): Int = if (x == 10) x else count(x - 1)

上面的count()函数是一个死循环,当我们调用count()函数后,会报StackOverflowError。这时可以用tailrec修饰符标记该递归函数,并将其自身调用作为它执行的最后一个操作,如下所示:

tailrec fun count(x: Int = 1): Int = if (x == 10) x else count(x - 1)

再次运行代码,无堆栈溢出。

七、中缀表示法

中缀表示法是调用函数的另一种方法。如果要使用中缀表示法,需要用infix 关键字来修饰函数,且要满足下列条件:

  • 它们必须是成员函数或扩展函数;
  • 它们必须只有一个参数;
  • 其参数不得接受可变数量的参数。

下面来举个例子:

//扩展函数
infix fun String.removeLetter(str: String): String {
    //this指调用者
    return this.replace(str, "")
}

//调用
var str = "hello world"
//不使用中缀表示法
println(str.removeLetter("h")) //输出ello world
//使用中缀表示法
println(str removeLetter "d")  //输出hello worl
//使用中缀表示法调用str removeLetter "d"等同于调用str.removeLetter("d")

//还可以连续调用
println(str.removeLetter("h").removeLetter("d").removeLetter("l")) // 输出 eo wor
println(str removeLetter "h" removeLetter "d" removeLetter "l") // 输出 eo wor

八、Lambda表达式的语法

Lambda表达式的语法如下:

  • Lambda 表达式总是括在大括号中;
  • 其参数(如果有的话)在 -> 之前声明(参数类型可以省略);
  • 函数体(如果存在的话)在 -> 后面。

举个例子:

//这是一个Lambda表达式的完整语法形式
val sum = { x: Int, y: Int -> x + y }
//Lambda表达式在大括号中
//参数 x 和 y 在 -> 之前声明
//参数声明放在大括号内,并有参数类型标注
//函数体 x + y 在 -> 后面

val i: Int = sum(1, 2)
println(i) //输出结果为 3

如果Lambda表达式自动推断的返回类型不是Unit,那么在Lambda表达式函数体中,会把最后一条表达式的值当做是返回值。所以上面的常量sum 的返回值是Int类型。如果要指定常量sum的返回值为Int类型,可以这样写:

val sum: (Int, Int) -> Int = { x, y -> x + y }

val i: Int = sum(1, 2)
println(i) //输出结果为 3

当Lambda表达式只有一个参数的时候,那么它将可以省略这个唯一的参数的定义,连同->也可以省略。如下所示:

//当Lambda表达式只有一个参数的时候
val getInt: (Int) -> Int = { x -> x + 1 }
val int = getInt(2)
println(int)  //输出结果为:3

//可以省略这个参数的定义
//并且将隐含地奖这个参数命名为 it
val sum: (Int) -> Int = { it + 1 }
val int = sum(2)
println(int)  //输出结果为:3

上面说到如果Lambda表达式自动推断的返回类型不是Unit,那么在Lambda表达式函数体中,会把最后一条表达式的值当做是返回值。举个例子:

var sum: (Int) -> Int = {
      val i: Int = it + 1
      val j: Int = i + 3
      val k: Int = it + j - i
      i
      k
      j
  }
println(sum(1)) 
//输出结果为 5,也就是 j 的值

九、高阶函数与Lambda表达式

高阶函数是将函数用作参数或返回值的函数,如下所示:

fun getName(name: String): String {
    return name
}

fun printName(a: String, name: (str: String) -> String): String {
    var str = "$a${name("Czh")}"
    return str
}

//调用
println(printName("Name:", ::getName))
//运行代码,输出 Name:Czh

上面代码中name: (str: String) -> String是一个函数,拥有函数类型() -> String,接收一个String参数,当我们执行var str = "$a${name("Czh")}"这行代码的时候,相当于执行了var str = "$a${getName("Czh")}",并返回了字符串"Czh"。当我们调用printName("Name:", ::getName)时,将函数作为参数传入高阶函数,需要在该函数前加两个冒号::作为标记。

Kotlin提供了Lambda表达式来让我们更方便地传递函数参数值。Lambda表达式总是被大括号括着;如果有参数的话,其参数在 -> 之前声明,参数类型可以省略;如果存在函数体的话,函数体在-> 后面,如下所示:

println(printName("Name:", { name -> getName("Czh") }))
//运行代码,输出 Name:Czh

如果函数的最后一个参数是一个函数,并且你传递一个Lambda表达
式作为相应的参数,你可以在圆括号()之外指定它,如下所示:

println(printName("Name:") { name -> getName("Czh") })
//运行代码,输出 Name:Czh

十、匿名函数

匿名函数与常规函数一样,只是省略了函数名称而已。举个例子

fun(x: Int, y: Int): Int = x + y

匿名函数函数体是表达式,也可以是代码段,如下所示:

fun(x: Int, y: Int): Int {
    return x + y
}

上面高阶函数的例子中的printName函数的第二个参数也可以传入一个匿名函数,如下所示:

println(printName("Name:", fun(str: String): String { return "Czh" }))
//运行代码,输出 Name:Czh

十一、内联函数

1.内联函数

使用高阶函数会带来一些运行时的效率损失。每一个函数都是一个对象,并且会捕获一个闭包。 即那些在函数体内会访问到的变量。 内存分配(对于函数对象和类)和虚拟调用会引入运行时间开销。这时可以通过内联函数消除这类的开销。举个例子:

fun printName(a: String, name: (str: String) -> String): String {
    var str = "$a${name("Czh")}"
    return str
}

println(printName("Name:", { name -> getName("Czh") }))

上面代码中,printName函数有一个函数类型的参数,通过Lambda表达式向printName函数传入参数值,Kotlin编译器会为Lambda表达式单独创建一个对象,再将Lambda表达式转换为相应的函数并调用。如果这种情况出现比较多的时候,就会很消耗资源。这是可以在函数前使用inline关键字,把Lambda函数内联到调用处。如下所示:

inline fun printName(a: String, name: (str: String) -> String): String {
    var str = "$a${name("Czh")}"
    return str
}

println(printName("Name:", { name -> getName("Czh") }))

2.禁用内联

通过inline关键字,编译器将Lambda函数内联到调用处,消除了运行时消耗。但内联可能导致生成的代码增加,所以需要避免内联比较大的Lambda表达式。如果想禁用一些Lambda函数的内联,可以使用noinline修饰符禁用该Lambda函数的内联,如下所示:

inline fun printName(name1: (str1: String) -> String
                     , noinline name2: (str2: String) -> String): String {
    var str = "${name1("Name:")}${name2("Czh")}"
    return str
}

3.内联属性

inline关键字除了可以使函数内联之外,还能内联没有幕后字段(field)的属性,如下所示:

val foo: Foo
    inline get() = Foo()

var bar: Bar
    get() = ……
    inline set(v) { …… }

总结

本篇文章对比了Java方法和Kotlin函数在写法上的区别,也认识了Lambda函数和还列举了一些Kotlin函数中比较特别的语法,如中缀表示法等。可见Kotlin中的函数内容还是很多的,用法也相对复杂,但运用好Kotlin的函数,能使开发变得更简单。

参考文献:
Kotlin语言中文站、《Kotlin程序开发入门精要》

推荐阅读:
从Java到Kotlin(一)为什么使用Kotlin
从Java到Kotlin(二)基本语法
从Java到Kotlin(三)类和接口
从Java到Kotlin(四)对象与泛型
从Java到Kotlin(六)扩展与委托
从Java到Kotlin(七)反射和注解
从Java到Kotlin(八)Kotlin的其他技术
Kotlin学习资料汇总


更多精彩文章请扫描下方二维码关注微信公众号"AndroidCzh":这里将长期为您分享原创文章、Android开发经验等!
QQ交流群: 705929135

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249

推荐阅读更多精彩内容