Scala入门(二)
函数
在编程中,为了能够做出更好的抽象,我们往往会把代码块整理成函数,这里介绍下在Scala中怎样定义一个函数
这是一个简化的定义,我们没有给出返回值的类型,编译器自动为我们做了这件事😄,现在我们试试在Ammonite中试一试这个函数
为了了解函数定义更多一点,我们再来看下add的完整写法
和简化的写法相比,上面代码中的: Int定义了返回值的类型,花括号中包含了函数的所有类型,其中最后一行(这个例子里正好只有一行)表达式的值表示函数的返回值.在编写复杂函数的时候,建议先把返回值类型写明,这样实现逻辑的时候编译器就能帮你排错啦.
关键词return通常是是不需要的
很多语言都使用return关键词表示函数的返回值,出于照顾群众习惯的考虑,Scala也保留了这个关键词.但是,这通常是不必要的.比如像下面这样定义一个函数,功能和上面两个版本一样,但是编译器会给出一个Warning: Return keyword is redundant.
定义函数的时候,如果感觉确实离不开return,也不要气馁,用就用吧.但是同时记得考虑下是不是能通过充分利用表达式的特性把return去掉.下面是比较使用和不使用return关键词来实现函数分支操作的一个例子.
高阶函数
在Scala里,函数可以作为另一个函数的参数使用,在下方的例子中,callFunc被称为高阶函数.
我们在Ammonite中操作试试
高阶函数在Scala的标准库中广泛使用,为我们编程带来了很多便利,下一节我们学习Scala内置数据结构的时候就能看到.
数据结构
Array
Scala的Array和Java中的Array底层实现是一样的,但是增加了很多便利的接口可以操作或者生成数据
上方代码展示的是一些最简单的操作,更多操作可以查看官网文档进行学习
Vector
Vector是Scala实现的一个高性能数据结构,能在接近常数复杂度的时间内实现序列的拼接,随机获取等.需要注意的是,这个数据结构是不可变(Immutable)的,一个Vector生成后就不能改变它的值.关于不可变数据结构的进一步了解可以参考这篇和这篇文章,很多同学可能不习惯使用不可变数据结构进行编程,不要担心,当您习惯用表达式进行编程时也会同时习惯不可变数据结构.
更多操作见官方文档
Seq
Seq是Scala实现的一个序列类型接口,Array和Vector都实现了Seq接口,在函数定义中把参数或者返回值类型设置为Seq,然后用Array或者Vector实现,方便以后有重构需求的时候不用变更函数签名.
Set
Set应该大家都很熟了,是保证内部每个元素都唯一的集合,下方展示一些常用操作
Map
Map是由键值对组成的集合,它保证内部每个键都是唯一的,下方展示一些常用的操作
这里需要注意的是,类似map3(3)这样的操作在没有相应值的时候会抛异常,而不是返回一个null,所以一般不推荐使用,推荐使用的方法是map3.get(3),get方法不会直接返回一个Int,而是返回一个Option[Int],这里的设计思想我们在下一节介绍
Tuple
当您需要定义一个有多个返回值的函数时,Tuple能帮您大幅简化代码量,比如说下方的例子里currentYearMonthDay定义了一个返回值为Tuple3[Int,Month,Int]的函数.如果您有需要的话,可以无痛定义返回值在Tuple2到Tuple22间的各种函数,详细介绍可以看官网文档.需要注意的是,Tuple中的每个元素类型都是独立的,建议在定义函数一开始就设计好每个元素的类型以充分使用编译器的排错能力
通用操作
Scala标准库中有许多通用的操作,Array,Vector,Set,Map等等都能用,下面以Vector为例展示这些操作.
除了上述通用的操作外,标准库还提供了便利的方法让这些数据结构相互转换,通常这种操作叫toXXX,勇敢地使用ide的自动提示就能发现他们.
对数据做聚合也是数据处理中经常用到的技巧,比较有代表性的就是reduce和fold,详细一些的介绍可以看这篇文章或者上一些主流视频平台搜索获得
Spark也好,Flink也好,它们的API几乎就是个大号的Scala标准库,也就是在标准库的基础上帮您做了一些分布式,高并发,异步处理,背压之类的工作.对于用户而言,用起来是差不多的,所以掌握好标准库对您之后操作那些设施很有帮助,建议多多练习.
Billion Dollar Mistake
如果您不知道什么是Billion Dollar Mistake,详细的来龙去脉可以看看这篇InfoQ文章.Tony Hoare在1965年将空指针功能加入了他所设计的ALGOL W编程语言中,"只是因为这样比较好实现".这一行为引起了C/C++/Java等等等等语言的效仿.Tony Hoare认为他所设计的空指针功能从那时开始到现在,至少为人民群众带来了十亿美金的损失(主要是运行时的进程崩溃带来的),进行了忏悔.
忏悔都忏悔了,损失已经造成了,那我们要怎样带着历史包袱前进,来应对空指针可能带来的损失呢?Scala给我们提供了一个工具叫Option.
举个例子,在Java中您想获得一个字符串的长度,可以这么定义一个函数
一些比较年轻,比较简单,有时还很幼稚的小朋友可能觉得这个定义没什么问题,可一到实际试用中就会出事
比如当一个空值传入的时候
为了避免这种情况,一些长者会这么写
这样确实可以避免null异常发生.可是它有一个问题,那就是它不是强制的,依据每个人的工作环境不同,没做空值检查的代码可能会获得Warning,也可能不会,(就算会有Warning,很多人也会被过多Warning伤害从而无视Warning).当项目中有稍许基础需要做空检查的时候,大部分人都能做好,可是当对空检查的需求到达几十次,几百次的时候,人就多多少少会有遗漏,那些遗漏可能带来大量的运行时损失.
Scala因为继承了JVM的设计,也是允许null值出现的,但是您在Scala原创的代码中几乎总是有很好的方法避免使用null值,需要处理null值的情况主要出现在调用Java代码的情况下.
如果我们用Option(...)包住一个null值,我们就会自动得到一个None,如果我们用Option(...)包住一个非null值,我们就会得到一个Some(...)
↑↑↑↑↑上方就是把可能为null的值变成Option的玩法啦,既然已经知道怎么把可能为null的值转成Option了,下面就展示些代码看我们怎么可以和Option玩耍吧.
Option很常见的一种应用场景是处理Map中可能不包含相应键的情况
在一般集合中,我们也能用Option来帮忙应对它们可能没有某些符合要求的值和可能为空的情况
为什么Option可以帮助我们避免运行时空指针错误呢?秘诀就是它把错误都提前到了编译时,因为Option[Int]不是Int,Option[String]也不是String,您把Option中的内容取出来前,无法正确地用Int或String或别的什么类型做出任何进一步操作,否则编译器就会报错.下面展示一下有哪些方法可以合适地操作Option.
-
给出一个默认值
-
把
Option也当做一个集合,集合通用的各种操作都能在Option中使用
一点小提示: 刚接触Option的同学可能出现一个非常依赖getOrElse的情况,这样就可以从Option的危险区回落到自己习惯的基本类型安全区了.需要这样做并不丢人,但要多思考一下自己是不是没有充分利用Scala的表达式能力,以及Option作为集合的各种特性.
温馨提示: 对各种API的功能有怀疑的时候,多查官方文档.
习题
按照材料准备里的提示把习题下载到本地,编辑src/main/scala/quiz/Quiz2.scala,更改文件中的???为您的答案.在项目根目录下执行./gradlew run --args='quiz.Quiz2',获得以下输出说明习题完成.
- 喜欢挑战的同学可以考虑不看参考代码,直接编写逻辑生成下列内容.
参考答案在src/main/scala/answer/Answer2.scala中
- 发现一套不错的线上习题集,建议尝试: https://www.scala-exercises.org/std_lib