Kotlin学习之函数

原文链接 Understanding Kotlin Functions

函数对于编程语言来说是极其重要的一个组成部分,函数可以视为是程序的执行,是真正活的代码,为啥呢?因为运行的时候你必须要执行一个函数,一般从主函数入口,开始一个套一个的函数调用。函数更能体现程序的运行。特别是近些年函数式编程的编程范式开始广泛流行,让函数的地位再次在各种语言中都得到了极大的进升。对于任何一门编程语言,如果没有学好函数,那就相当于没有学,今天就要深入的学习一下Kotlin中的函数。

首先来区分一下,什么是函数什么是方法,函数是编程语言中的一级对象,地位等同于其他Type,函数可以声明在任何地方:顶层(即在任何类任何方法的外面),类里面,另一个函数里面等。一般支持函数式编程语言更喜欢用函数。声明在类里面的函数叫作成员函数,但更准确的说是方法。比如像纯OO的编程语言Java就只会说方法(Method),而像函数式编程语言(Kotlin/Scala/Groovy)喜欢说函数。

函数的基本使用

函数(Functions)在Kotlin中的一级对象,这就意味着它能像其他类型那样,可以声明变量,可以当作参数传递,可以在函数内部定义,先从基本的使用开始。

函数的声明与定义

关键字fun来声明函数,然后是函数名字,参数列表,返回值函数体修饰符 fun 函数名(参数列表) :返回类型 {函数体}

  • 修饰符,对于类的成员函数才有,一般是权限open/private
  • fun,用于声明这是一个函数的关键字
  • 函数名,就像变量名一样,是函数的名字
  • (参数列表),要用括号约束起来,就是变量的声明,多个要用逗号分隔
  • :返回类型,注意冒号,也即是函数返回值的类型,如果很明显类型可以推断出来时,就可以省略
  • {函数体},也即函数的真实定义部分,想要执行的一些语句

如:

fun double(x: Int): Int {
     return x + x
}

这就是一个标准的函数。

函数的使用

函数的使用有三种,一是调用,另一种是声明变量,再有就是当作参数(这其实是在定义一个变量,然后当作参数)。

函数的调用

函数都是表达式,都有返回值,但可以不用管返回值,调用函数的时候用括号来标识,比如前面的函数double,可以这样来调用:

val dx = double(5)

这是函数最为常用的使用方法,因为程序最终要执行,所以所有的函数最终都是要被调用的。

声明函数变量

前面说了,函数是一级类型,它可以像其他类型那样去定义变量,比如前面的double也可以这来写:

    val myDouble = {x: Int -> x + x}
    println(myDouble(6))

这里的myDouble就是一个函数变量,它的函数类型与前面的double是一样的,它是一个变量,要想执行它的函数体要加括号。

把函数当作参数

这里会涉及高阶函数,高阶函数就是涉及函数中的函数,主要体现在函数的参数或者返回值也是一个函数。比如数组和集合的过滤(filter)和遍历(forEach)里面的参数就是一个函数:

    val asc = arrayOf(1, 2, 3, 4, 5)
    asc.filter({(it and 0x01) == 0}).map(myDouble).forEach({ println(it) })
    // output-> 4, 8

因为把函数当作参数传递时都涉及函数的类型定义,而一般情况下用lambda是最方便的,先有个印象,后面会详细讲解。

参数

函数的参数还有两种比较有用的变体,称之为命名参数和默认值,这两个通常会一起使用。

命名参数

当一个函数的参数比较多时,那么在调用时想要传递参数就比较蛋疼,特别是还有相同类型的参数的时候,一片混乱,比如:

fun log(tag: String, event: String, source: String, amount: Int, price: Float, persist: Boolean): Unit {
    println("$tag, $event, $amount $price")
    if (persist) {
        // write to file
    }
}

log("func", "Function arguments", "Hard way", 5, 2.3f, false)

这样调用,参数太多了,并且相同类型的有三个,这三个极容易传错,而且因为类型检查 不会报错,可能会引发极难调试的bug。

这时就可以使用命名参数来缓解了,命名参数,就是在调用函数,传递参数的时候,指定参数的名字,即就是在声明函数时参数的名字,用以指定具体参数,然后这时就可不用管参数的相对顺序了,比如上面的函数也可以这样调用:

   log(event = "Named arguments",
        tag = "func",
        source = "Elegant",
        amount = 5,
        persist = false,
        price = 100f
    )

不会出错,而且可读性大大加强。但需要注意的是,如果要使用命名参数,就要保持一致性,给所有的参数都要命名。所以,当参数比较多的时候还是比较蛋疼,这时就需要用到参数默认值了。

参数默认值

默认值也即是在声明参数的时候指定一个默认值,在调用的时候可以省略这个参数了,比如:

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

fun afoo(x: Int = 0, y: Int): Int {
    return x - y
}

foo(3)
afoo(y = 5)

注意,如果默认参数是最后一个参数,那么可以直接省略它,如示例中的foo(3),但如果默认参数不是最后一个,想省略的话,必须要用命名参数,如afoo(y = 5)。当然了,两个参数都传也可以的:

foo(3, 5)
afoo(3, 5)

所以要把默认值和命名参数结合起来才能发挥最大的价值:

fun log(tag: String, event: String, source: String = "Elegant", amount: Int = 0, price: Float, persist: Boolean = false): Unit {
    println("$tag, $event, $amount $price")
    if (persist) {
        // write to file
    }
}

    log(event = "Named arguments",
        tag = "func",
        price = 100f
    )

把握一下使用原则:如果参数不多(4个以内),那么就把默认参数往后放,调用的时候也可以不用命名参数,直接省略默认参数就好;如果参数比较多,也要把默认参数往后放,在调用的时候尽可能的使用命名参数。

返回值

函数的返回值是在参数列表之后,函数体之前用冒号加类型来声明。

fun printHello(name: String?): Unit {
    if (name != null)
        println("Hello $name")
    else
        println("Hi there!")
    // `return Unit` or `return` is optional
}

如果函数没有返回值就用Unit来声明,相当于Java中的void,但更多的时候是可以省略的:

fun printHello(name: String?) { ... }

当函数体只有一个表达式的时候,这个时候可以省略掉函数体,而把表达式直接写在函数声明的后面,用**赋值符=**来连接,如前面的double也可以这样写:

fun double(x: Int): Int = x + x

这个时候,因为函数体只有一个表达式,所以返回类型很容易推断出来,意味着这时返回类型的声明也可以省略掉:

fun double(x: Int) = x + x

这会让代码非常的简洁,又不失可读性。

解构返回

Kotlin的函数只能有一个返回值,代表某一个类型的一个变量,如果想有多个返回值,就需要用复杂的类型,比如同一类型的多个有规律的变量可能就要用集合,如数组列表等。但如果类型不同,但逻辑上有关系的2个到3个值,如果想要一起返回,就需要用到组合类型如Pair和Triple,Pair可以把两个不同类型的变量组合成一个对象,Triple可以把三个不同类型的变量组合成一个对象,这样就可以在函数中返回了。

fun nameAge() = Pair("Alex", 50)

fun fullName() = Triple("Donald", "Jonh", "Trump")

对于函数的调用者也很麻烦,要先声明Pair或者Triple对象,然后再拆解,比如这样:

val pna = nameAge()
println("Name ${pna.first}, age ${pna.second}")

这显然比较笨拙,不够简洁。在Kotlin中有更好的做法,可以在函数调用的时候,对返回值进行拆解,称之为解构,如下写法与上面是一样的:

val (name, age) = nameAge()
println("Name $name, age $age")

而且,如果只对组合中的某几个感兴趣,可以把不想要的变量用下划线_(underscore)来表示,比如说:

val (firstName, _, lastName) = fullName()
println("This is $firstName $lastName")

尾部lambda参数传递

前面说了函数可以作为参数传递给其他函数,但我们在使用的时候,一般会直接把一个lambda传递进去,比如说:

fun execute(a: Int, f: (Int)->Int): Int {
    if (a < 0) {
        return -1
    }
    return f(a)
}

调用的时候,可以这样:

execute(3, { it * it })

但更建议的方式是把lambda放到函数调用之外:

execute(5) { it + it }

再比如像集合的函数式写法,通常也只传递一个lambda,这时一般都写在函数调用之外,并且当目标函数没有其他参数时也即除了要传入的lambda外无其他参数时,代表函数调用的括号也可以省略:

val nums = arrayOf(1, 2, 3, 4, 5)
nums.filter { it and 0x01 == 0 } // 等同于filter({ it and 0x01 == 0 })
        .map { it * it } // 等同于map({ it * it })
        .forEach { println(it) } // 等同于forEach({ println(it) })

这样写非常的简洁,但会牺牲一些可读性,因为花样多了,就会比较难识别出来函数的声明与函数的调用,甚至有时候会分不清函数与普通的变量。所以,识别函数调用有两种方式,一是看有没有括号,另外就看有没有尾部lambda

匿名函数

匿名函数就是不指定函数的名字,但保留关键字fun,通常用于把函数当作 参数传递给高阶函数时使用。如:

list.forEach(fun (item) { println(item) })

但,因为对lambda支持的非常好,所以凡事用到匿名函数的地言都可以用lambda来替换(当然了,lambda也是匿名函数的一个实现方式)。匿名函数的一个用处就 可以加代码标签(label),以方便终止语句的精准控制,可以参见另一篇文章中关于局部控制的讨论,可能也只有这个地方适用匿名函数,其他情况都应该直接用lambda。

内部函数

就是定义在另外一个函数内部的函数,通常用作于传递参数或者返回值,如:

fun foo(): ()->Unit {
   fun bar() {
   	println("Func bar inside fun foo")
   }
   return bar
}

事实上,随处可见的lambda都可以算上是内部函数。

高阶函数

高阶函数就是函数的函数,也就是说函数的参数或者返回值是一个函数的函数,也即把函数像其他类型那样使用。函数在Kotlin中一级类型(first class type),因此从语义层面支持了函数式编程范式,当然也就支持了高阶函数以及lambdas。比如像集合的操作filter/map/fold都是高阶函数,因为它们接受一个函数作为参数。

函数类型

高阶函数是把函数作为参数或者返回值,但显然并不是所有的函数都能当作高阶函数的参数或者返回值,换句话说,函数本身其实也是有类型之别的,两个函数不见得就是一样的。函数是用来针对其参数,然后在函数体内进行一些运算最终返回一个值,所以区分不同的函数最关键的是输入参数和返回值,与其名字其实没有关系,因此输入参数一致,返回值一致就可以视为同一种函数。

函数的类型用参数和返回值来表示,如**(A, B) -> R**形式,A和B是参数,R是返回值,需要注意的是括号不能省略,常见的具体形式有:

  • () -> Unit 无参数无返回值
  • () -> R 无参数有返回值
  • (A) -> Unit 有一个参数,无返回值
  • (A) -> R 一个参数,一个返回值
  • (A, B) -> Unit 两个参数,无返回值
  • (A, B) -> R 两个参数,一个返回值

函数的类型与方法签名类似(method signature),代表着某一类的函数。在高阶函数的函数参数或者返回函数就需要用函数类型来声明。

实例化一个函数类型

有很多种途径可以实例化一个函数类型,比较常见的有:

  • 通过lambda表达式,如{ a, b -> a + b },这就是一个函数类型(A, B) -> R的实例
  • 匿名函数,如fun(a: Int, b: Int): Int { return if (a > 0 && b > 0) a + b else -1 }
  • 引用现存的某一个函数,函数签名(参数相同,返回值相同)就视为同一种函数类型,那么已定义好的函数中有能匹配的就可以直接引用过来,**顶级函数和构造函数用:😗*来引用,**类成员函数用类名:😗*来引用,如::isOdd, String::toInt, ::Tripple

lambda表达式

就是匿名隐式函数体,匿名是不用指定函数的名字,连参数的类型和返回值的类型也都省略,有时甚至连参数都可以省略,只有一个函数体,是最为简洁的一种函数定义方式,通常用于传递给高阶函数的参数,lambda力求简洁,所以但凡能推断出来的都可以省略。最简洁的lambda只有函数体,如val asc = IntArray(5) { it * it } // 创建一个长度为5的整数数组并初始化为[0,1,4,9,16]。

lambda的形式是**{ A, B -> expressions }**,外面的花括号不可省略,这是lambda的标识,然后是参数列表,->用于分隔参数和函数体,除了函数体,其余的都可以省略掉,只要能推断出来。

Trailing lambdas(尾部lambda)

这个前面讲过了,再复习一下,当一个函数的最后一个参数是一个函数时,就可以在函数的调用外部写lambda,比如:

val product = items.fold(1) { acc, e -> acc * e }

run { println("...") }

隐式参数

如果lambda表达式只有一个参数,那么这个参数也可以省略,只写函数体就可以,并且可以用隐式参数it,比如:

ints.filter { it > 0 } // this literal is of type '(it: Int) -> Boolean'

val asc = IntArray(5) { it * it } 

lambda的返回值

lambda力求简洁,所以函数体的最后一个表达式的值即是此lambda的返回值,一般不用显式的return:

ints.filter { it > 0 } // boolean result of 'it > 0' is returned

val asc = IntArray(5) { it * it } // it * it is the return

ints.filter {
    val shouldFilter = it > 0
    shouldFilter
}

如果要用显式的return语句,要注意scope,在这篇文章有深入讨论,用隐式label来限定scope:

ints.filter {
    val shouldFilter = it > 0
    return@filter shouldFilter
}

丢弃参数

有时候,参数有多个,但可能并不会全都使用,仅使用了其中一个,这时不使用的参数就可以用**下划线_(underscore)**来代替,以表示这个参数不会被使用:

map.forEach { (_, value) -> println("$value!") }

Kotlin的lambda可以写出非常简洁的函数式链式语句,一气呵成可读性又非常的好,比如:

val headers = fetchHeaders()
headers.filter { it.length == 5 }
	.sortedBy { it }
	.map { it.uppercase() }
	.forEach { println(it) }

未完待续

函数是Kotlin中最最重要的一个类型,博大精深,用法多样,但万变不离其宗,重点理解函数就是行为,是可以运行的一组语句和表达式的小集合;区分开函数的声明和定义就可以了。

Kotlin的函数类型很多,这里只介绍了一些基础,后面将会继续介绍一些高级的函数如内联函数(inline functions),infix notation以及Extensions和Scope functions。

参考资料

  • Kotlin functions
  • Kotlin | Default and Named argument
  • Kotlin——高级篇(一):Lambda表达式详解
  • Lambda Expressions in Kotlin
  • Kotlin lambda表达式

原创不易,打赏点赞在看收藏分享 总要有一个吧

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/169048.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

《微信小程序开发从入门到实战》学习二十二

3.3 开发创建投票页面 3.3.10 使用switch开关组件 用switch开关组件增加一个设置是否匿名投票的功能。 switch常用属性如下&#xff1a; checked 开还是关&#xff0c;默认false关 disabled 是否禁用&#xff0c;默认false不禁用&#xff0…

应试教育导致学生迷信标准答案惯性导致思维僵化-移动机器人

移动机器人课程群实践创新的困境与突围 一、引言 随着科技的快速发展&#xff0c;工程教育变得越来越重要。然而&#xff0c;传统的应试教育模式往往侧重于理论知识的传授&#xff0c;忽视了学生的实践能力和创新精神的培养。这在移动机器人课程群的教学中表现得尤为明显。本文…

win10手机投屏到电脑的操作方法

工具/原料&#xff1a; 系统版本&#xff1a;iOS 15.3,HarmonyOS 2.0.0&#xff0c;windows10系统 品牌型号&#xff1a;iPhone 13,HUAWEI Mate 40 Pro&#xff0c;联想小新air14 方法/步骤&#xff1a;方法一&#xff1a;安卓手机使用无线投屏功能投屏到win10电脑 1、保持手…

sapjco3.dll has version “721.619“, but required is at least version “721.913“

context with path [] threw exception [org.glassfish.jersey.server.ContainerException: java.lang.ExceptionInInitializerError: Native library sapjco3 is too old. Found library C:\Windows\System32\sapjco3.dll has version “721.619”, but required is at least …

网络参考模型与标准协议(一)

OSI参考模型 OSI 模型(Open Systems Interconnection Model)&#xff0c;由国际化标准组织ISO (TheInternational Organization for Standardization )收录在ISO 7489标准中并于1984年发布。 OSI参考模型又被称为七层模型&#xff0c;由下至上依次为: 物理层: 在设备之间传输比…

碰到一个逆天表中表数据渲染

1. 逆天表中表数据问题 我有一个antd-table组件&#xff0c;他的编辑可以打开一个编辑弹窗打开弹窗里面还会有一个表格&#xff0c;如果这个表格的column是在外层js文件中保存的话&#xff0c;那么第一次打开会正常渲染数据&#xff0c;再次打开就不会渲染&#xff0c;即使是已…

svn问题集

被锁定&#xff0c;无法拉取提交 解决方法

选择java商城开发商需要注意哪些方面?

Java商城开发是一项庞大而复杂的任务&#xff0c;选择一家合适的开发商至关重要。那么&#xff0c;我们在选择Java商城开发商时&#xff0c;需要注意哪些方面呢&#xff1f; 1、专业经验 选择具有丰富经验的开发商是至关重要的。开发商应该拥有多年的Java开发经验&#xff0c;…

【开源】基于Vue.js的衣物搭配系统的设计和实现

项目编号&#xff1a; S 016 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S016&#xff0c;文末获取源码。} 项目编号&#xff1a;S016&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、研究内容2.1 衣物档案模块2.2 衣物搭配模块2.3 衣…

【数据结构】树与二叉树(廿一):树和森林的遍历——先根遍历(递归算法PreOrder、非递归算法NPO)

文章目录 5.1 树的基本概念5.1.1 树的定义5.1.2 森林的定义5.1.3 树的术语 5.2 二叉树5.3 树5.3.1 树的存储结构1. 理论基础2. 典型实例3. Father链接结构4. 儿子链表链接结构5. 左儿子右兄弟链接结构 5.3.2 获取结点的算法5.3.3 树和森林的遍历1. 先根遍历&#xff08;递归&am…

强化学习笔记

这里写自定义目录标题 参考资料基础知识16.3 有模型学习16.3.1 策略评估16.3.2 策略改进16.3.3 策略迭代16.3.3 值迭代 16.4 免模型学习16.4.1 蒙特卡罗强化学习16.4.2 时序差分学习Sarsa算法&#xff1a;同策略算法&#xff08;on-policy&#xff09;&#xff1a;行为策略是目…

INFINI Labs 产品更新 | 发布 Easysearch Java 客户端,Console 支持 SQL 查询等功能

近年来&#xff0c;日志管理平台越来越流行。使用日志管理平台可以实时地、统一地、方便地管理和查看日志&#xff0c;挖掘日志数据价值&#xff0c;驱动运维、运营&#xff0c;提升服务管理效率。 方案架构 Beats 是轻量级采集器&#xff0c;包括 Filebeat、Metricbeat 等。E…

开发者生态:共享知识,携手共进,共创技术辉煌

开发者生态&#xff1a;共享知识&#xff0c;携手共进&#xff0c;共创技术辉煌 在数字化时代&#xff0c;开发者是推动技术进步和创新的重要力量。他们创造着改变世界的软件和应用&#xff0c;推动着技术的边界不断向前。而在这个快速发展的时代&#xff0c;建立一个健康、活跃…

【Q2—30min】

1.socket服务端创建过程 socket是应用层与TCP/IP协议族通信的中间软件抽象层&#xff0c;它是一组接口。在设计模式中&#xff0c;Socket其实就是一个门面模式&#xff0c;它把复杂的TCP/IP协议族隐藏在Socket接口后面&#xff0c;对用户来说&#xff0c;一组简单的接口就是全部…

mysql面试题——存储引擎相关

一&#xff1a;MySQL 支持哪些存储引擎? MySQL支持多种存储引擎&#xff0c;比如InnoDB&#xff0c;MyISAM&#xff0c; MySQL大于等于5.5之后&#xff0c;默认存储引擎是InnoDB 二&#xff1a;InnoDB 和 MyISAM 有什么区别? InnoDB支持事务&#xff0c;MyISAM不支持InnoD…

前端入门(二)Vue2到Vue3

文章目录 Vue简介Vue的特点Hello, Vue Vue基本语法模板语法数据绑定&#xff08;v-bind、v-model&#xff09;el与data的两种写法 数据代理实现原理Object.defineProperty()数据代理 事件处理&#xff08;v-on:click / click&#xff09;事件修饰符键盘事件&#xff08;略&…

思伟老友记 | 厦门路桥翔通海砼建材有限公司与思伟软件携手走过23年

23年 感恩相伴 携手成长 2001年-2023年&#xff0c;厦门路桥翔通海砼建材有限公司已携手上海思伟软件有限公司走过23年。从最初的半手动生产模式到如今的自动生产一体化系统&#xff0c;海砼公司通过思伟软件生产混凝土累计超过1000万m&#xff0c;思伟软件则借助海砼公司的实…

C语言——函数的嵌套调用

#define _CRT_SECURE_NO_WARNINGS 1#include<stdio.h>void new_line() {printf("Hello\n"); }void three_line() {int i0;for(i0;i<3;i){new_line();} }int main() {three_line();return 0; }

测试用例的设计方法(全):正交实验设计方法|功能图分析方法|场景设计方发

正交实验设计方法 一.方法简介 利用因果图来设计测试用例时, 作为输入条件的原因与输出结果之间的因果关系,有时很难从软件需求规格说明中得到。往往因果关系非常庞大,以至于据此因果图而得到的测试用例数目多的惊人&#xff0c;给软件测试带来沉重的负担&#xff0c;为了有效…

Vue3 常用组件

一、Fragment组件 Vue2 的template 模板中必须要有一个根标签&#xff0c;而我们在Vue3 的模板中不需要使用根标签就能渲染&#xff0c;因为Vue3 在内部会将多个标签包含在一个Fragment 虚拟元素中。 好处就在于可以减少标签的层级&#xff0c;减小内存占用。 二、Teleport组…