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")elseprintln("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 returnedval asc = IntArray(5) { it * it } // it * it is the returnints.filter {val shouldFilter = it > 0shouldFilter
}

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

ints.filter {val shouldFilter = it > 0return@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表达式

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vue3学习记录之内置组件TransitionGroup基于状态变化的过渡和动画

文章目录 前言说明进入 / 离开动画移动动画渐进延迟列表动画和 <Transition> 的区别 前言 本文参考vue3 Vue 提供了两个内置组件&#xff0c;可以帮助我们制作基于状态变化的过渡和动画 <Transition > 会在一个元素或组件进入和离开 DOM 时应用动画。 <Transit…

设计模式-13-职责链(责任链)模式

经典的设计模式有23种&#xff0c;但是常用的设计模式一般情况下不会到一半&#xff0c;我们就针对一些常用的设计模式进行一些详细的讲解和分析&#xff0c;方便大家更加容易理解和使用设计模式。 1-原理和实现 职责链模式的英文翻译是Chain Of Responsibility Design Patter…

网络运维与网络安全 学习笔记2023.11.20

网络运维与网络安全 学习笔记 第二十一天 今日目标 交换网路径选择、Eth-Trunk原理、动态Eth-Trunk配置 Eth-Trunk案例实践、MUX VLAN原理、MUX VLAN配置 交换网路径选择 STP的作用 在交换网络中提供冗余/备份路径 提供冗余路径的同时&#xff0c;防止环路的产生 影响同网…

大数据基础设施搭建 - ZooKeeper

文章目录 一、上传压缩包二、解压压缩包三、本机安装3.1 修改配置文件3.1.1 创建ZooKeeper数据存储目录3.1.2 修改配置文件名3.1.2 修改配置文件内容 3.3 启动/停止服务端3.4 测试&#xff08;1&#xff09;启动客户端&#xff08;2&#xff09;测试客户端操作 四、集群安装4.1…

【C#】字符串拼接相关

目录 1.字符串拼接方式1 用号进行字符串拼接 复合运算符 2.字符串拼接方式2 3.控制台打印拼 4.例子 1.字符串拼接方式1 之前的算数运算符 只是用来数值类型变量进行数学运算的而 string 不存在算数运算符 不能计算 但是可以通过号来进行字符串拼接 用号进行字符串拼接 …

链表:C++实现

引言&#xff1a; 链表是一种常见的数据结构&#xff0c;它由一系列节点组成&#xff0c;每个节点包含一个数据元素和一个指向下一个节点的指针。相比于数组&#xff0c;链表具有动态性和灵活性&#xff0c;可以高效地进行插入和删除操作&#xff0c;但是查找操作的时间复杂度较…

Docker中的RabbitMQ已经启动运行,但是管理界面打不开

文章目录 前言一、解决方法方法一方法二 总结 前言 肯定有好多小伙伴在学习RabbitMQ的过程中&#xff0c;发现镜像运行&#xff0c;但是我的管理界面怎么进不去&#xff0c;或者说我第一天可以进去&#xff0c;怎么第二天进不去了&#xff0c;为什么每次重新打开虚拟机都进不去…

C#访问修饰符

C#中的访问修饰符用于控制类型成员&#xff08;如字段、属性、方法等&#xff09;的访问级别。以下是C#中常用的访问修饰符&#xff1a; public&#xff1a;公共访问级别&#xff0c;没有任何访问限制。在任何其他类或程序集中都可以访问标记为 public 的成员。 private&#…

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

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

JavaScript的学习,就这一篇就OK了!(超详细)

目录 Day27 JavaScript(1) 1、JS的引入方式 2、ECMAScript基本语法 3、ECMAScript 基本数据类型​编辑 3.1 数字类型 3.2 字符串 3.3 布尔值 3.4 空值&#xff08;Undefined和Null&#xff09; 3.5 类型转换 3.6 原始值和引用值 4、运算符 5、流程控制语句 5.1 分…

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

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

给大伙讲个笑话:阿里云服务器开了安全组防火墙还是无法访问到服务

铺垫&#xff1a; 某天我在阿里云上买了一个服务器&#xff0c;买完我就通过MobaXterm进行了ssh&#xff08;这个软件是会保存登录信息的&#xff09; 故事开始&#xff1a; 过了n天之后我想用这个服务器来部署流媒体服务&#xff0c;咔咔两下就部署好了流媒体服务器&#x…

基于SSM的供电公司安全生产考试系统设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;Vue 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#xff1a;是 目录…

云桌面 node_modules 切换艰辛历程记录 rebuild失败记录

拿到node_modules后更换 执行npm rebuild 重新构建 报错 node版本不一致 nvm切换 版本 不成功 换个窗口又变回原来版本号了 设置默认版本 nvm alias default 14.16.1 发现下面还有一个stable的还指向原来版本 nvm alias stable 14.16.1 rebuild 还是失败 逐个rebuild 每个依赖单…

2.FastRunner定时任务Celery+RabbitMQ

注意&#xff1a;celery版本和Python冲突问题 不能用高版本Python 用3.5以下&#xff0c;因为项目的celery用的django-celery 3.2.2 python3.7 async关键字 冲突版本 celery3.x方案一&#xff1a; celery3.xpython3.6方案二 &#xff1a; celery4.xpython3.7 解决celery执…

Python---PyCharm调试技巧--Step over(F8)、Step into(F7)

Step over&#xff08;F8&#xff09;&#xff1a;代码一步一步向下执行&#xff0c;但是遇到了函数以后&#xff0c;不进入函数体内部&#xff0c;直接返回函数的最终的执行结果。------------遇到函数跳过&#xff0c;直接执行最后的结果。 Step into&#xff08;F7&#xf…

UDP网络套接字编程

先来说说数据在网络上的传输过程吧&#xff0c;我们知道系统其实终究是根据冯诺依曼来构成的&#xff0c;而网络数据是怎么发的呢&#xff1f; 其实很简单&#xff0c;网络有五层。如下&#xff1a; 如上图&#xff0c;我们知道的是&#xff0c;每层对应的操作系统中的那些地方…

ESP32-BLE基础知识

一、存储模式 两种存储模式&#xff1a; 大端存储&#xff1a;低地址存高字节&#xff0c;如将0x1234存成[0x12,0x34]。小端存储&#xff1a;低地址存低字节&#xff0c;如将0x1234存成[0x34,0x12]。 一般来说&#xff0c;我们看到的一些字符串形式的数字都是大端存储形式&a…

龙芯 Loongson 架构 UOS 系统编译 Qt 5.15.2 源码

背景 需要在龙芯&#xff08;Loongson&#xff09;CPU&#xff0c;UOS 系统下&#xff0c;进行国产化项目适配&#xff0c;UOS 自带 Qt 5.11&#xff0c;但是版本过老&#xff0c;与目前基于 Qt 5.15.2 项目存在不兼容情况&#xff0c;故需要自行编译 Qt 5.15.2开发环境。 软…

基于SSM的高校毕业选题管理系统设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…