Scala学习笔记2 - 控制结构和函数

时间:2022-06-03 05:20:29

===控制结构

      scala和其他编程语言有一个根本性差异:在scala中,几乎所有构造出来的语法结构都有值。这个特性使得程序结构更加精简。scala内建的控制结构很少,仅有if、while、for、try、match和函数调用等而已。如此之少的理由是,scala从语法层面上支持函数字面量。


if表达式

      scala的if/else语法结构与java等一样,但是在scala中if/else表达式有值,这个值就是跟在if/esle后边的表达式的值。如下:

                   val s = if(x > 0) 1 else -1

      同时注意:scala的每个表达式都有一个类型,比如上述if/esle表达式的类型是Int。如果是混合类型表达式,则表达式的类型是两个分支类型的公共超类型。String和Int的超类型就是Any。如果一个if语句没有else部分,则当if条件不满足时,表达式结果为Unit。如:

                   if(x > 0) 1

就相当于:

                   if(x > 0) 1 else ()

 

while循环

      scala拥有与java和c++中一样的while和do-while循环,while、do-while结果类型是Unit。

 

for表达式

      scala中没有类似于for(; ;)的for循环,你可以使用如下形式的for循环语句:

                  for(i <- 表达式)

该for表达式语法对于数组和所有集合类均有效。具体介绍如下:

      枚举:for(i <-1 to 10),其中“i <- 表达式”语法称之为发生器,该语句是让变量i(注意此处循环变量i是val的(但无需你指定),该变量的类型是集合的元素类型)遍历表达式中的所有值。1 to 10产生的Range包含上边界,如果不想包含上边界,可以使用until

      过滤:也叫守卫,在for表达式的发生器中使用过滤器可以通过添加if子句实现,如:for(i <- 1 to 10 if i!=5),如果要添加多个过滤器,即多个if子句的话,要用分号隔开,如:for(i <- 1 to 10 if i!=5; if i!=6)。

      嵌套枚举:如果使用多个“<-”子句,你就得到了嵌套的“循环”,如:for(i<- 1 to 5; j <- 1 to i)。

      流间变量绑定:你可以在for发生器以及过滤器等中使用变量保存计算结果,以便在循环体中使用,从而避免多次计算以得到该结果。流间变量绑定和普通变量定义相似,它被当作val,但是无需声明val关键字。

      制造新集合:for(…) yield变量/循环体,最终将产生一个集合对象,集合对象的类型与它第一个发生器的类型是兼容的。

      实际上:for表达式具有等价于组合应用map、flatMap、filter和foreach这几种高阶函数的表达能力。实际上,所有的能够yield(产生)结果的for表达式都会被编译器转译为高阶方法map、flatMap及filter的组合调用;所有的不带yield的for循环都会被转译为仅对高阶函数filter和foreach的调用。正是由于这几个高阶函数支持了for表达式,所以如果一个数据类型要支持for表达式,它就要定义这几个高阶函数。有些时候,你可以使用for表达式代替map、flatMap、filter和foreach的显式组合应用,或许这样会更清晰明了呢。

 

      scala中没有break和continue语句。如果需要类似的功能时,我们可以:

      1)使用Boolean类型的控制变量

      2)使用嵌套函数,你可以从函数当中return

      3)...

 

块表达式与赋值:

      在scala中,{}包含一系列表达式,其结果也是一个表达式,块中最后一个表达式的值就是其值

      在scala中,赋值语句本身的值是Unit类型的。因此如下语句的值为“()”:

                   {r = r * n; n -= 1}

      正是由于上述原因,scala中不能多重赋值,而java和c++却可以多重赋值。因此,在scala中,如下语句中的x值为“()”:

                   x = y = 1


match表达式模式匹配

      scala中没有switch,但有更强大的match。它们的主要区别在于:

      ①  任何类型的常量/变量,都可以作为比较用的样本;

      ②  在每个case语句最后,不需要break,break是隐含的;

      ③  更重要的是match表达式也有值;

      ④  如果没有匹配的模式,则MatchError异常会被抛出。

      match表达式的形式为:选择器 match{备选项 }。一个模式匹配包含了一系列备选项,每个都开始于关键字case。每个备选项都包含了一个模式以及一到多个表达式,它们将在模式匹配过程中被计算。箭头符号“=>”隔开了模式和表达式。按照代码先后顺序,一旦一个模式被匹配,则执行“=>”后边的表达式((这些)表达式的值就作为match表达式的值),后续case语句不再执行。示例如下:

                   a match {

                            case1 => "match 1"

                            case_ => "match _"

                   }

      match模式的种类如下:

      ①  通配模式:可以匹配任意对象,一般作为默认情况,放在备选项最后,如:

                   case _ =>

      ②  变量模式:类似于通配符,可以匹配任意对象,不同的是匹配的对象会被绑定在变量上,之后就可以使用这个变量操作对象。所谓变量就是在模式中临时生成的变量,不是外部变量,外部变量在模式匹配时被当作常量使用,见常量模式。注意:同一个模式变量只能在模式中出现一次。

      ③  常量模式:仅匹配自身,任何字面量都可以作为常量,外部变量在模式匹配时也被当作常量使用,如:

                   case "false" =>"false"

                   case true=> "truth"

                   case Nil=> "empty list"

      对于一个符号名,是变量还是常量呢?scala使用了一个简单的文字规则对此加以区分:用小写字母开始的简单名被当作是模式变量,所有其他的引用被认为是常量。如果常量是小写命名的外部变量,那么它就得特殊处理一下了:如果它是对象的字段,则可以加上“this.”或“obj.”前缀;或者更通用的是使用字面量标识符解决问题,也即用反引号“`”包围之。

      ④  抽取器模式:抽取器机制基于可以从对象中抽取值的unapplyunapplySeq方法,其中,unapply用于抽取固定数量的东东,unapplySeq用于抽取可变数量的东东,它们都被称为抽取方法,抽取器正是通过隐式调用抽取方法抽取出对应东东的。抽取器中也可以包含可选的apply方法,它也被称作注入方法,注入方法使你的对象可以当作构造器来用,而抽取方法使你的对象可以当作模式来用,对象本身被称作抽取器,与是否具有apply方法无关。样本类会自动生成伴生对象并添加一定的句法以作为抽取器,实际上,你也可以自己定义一个任意其他名字的单例对象作为抽取器使用,以这样的方式定义的抽取器对象与样本类类型是无关联的。你可以对数组、列表、元组进行模式匹配,这正是基于抽取器模式的。

      ⑤  类型模式:你可以把类型模式当作类型测试和类型转换的简易替代,示例如下:

                   case s: String => s.length

      ⑥  变量绑定:除了独立的变量模式之外,你还可以把任何其他模式绑定到变量。只要简单地写上变量名、一个@符号,以及这个模式。

      模式守卫:模式守卫接在模式之后,开始于if,相当于一个判断语句。守卫可以是任意的引用模式中变量的布尔表达式。如果存在模式守卫,只有在守卫返回true的时候匹配才算成功。

      Option类型:scala为可选值定义了一个名为Option的标准类型,一个Option实例的值要么是Some类型的实例,要么是None对象。分离可选值最通常的办法是通过模式匹配,如下:

                   case Some(s) => s

                   case None => “?”

      模式无处不在:在scala中,模式可以出现在很多地方,而不单单在match表达式里。比如:

      ①  模式使用在变量定义中,如下:

                   val myTuple = (123, “abc”)

                   val (number, string) = myTuple

      ②  模式匹配花括号中的样本序列(即备选项)可以用在能够出现函数字面量的任何地方,实质上,样本序列就是更普遍的函数字面量,函数字面量只有一个入口点和参数列表,样本序列可以有多个入口点,每个都有自己的参数列表,每个样本都是函数的一个入口点,参数被模式所特化。如下:

                   val withDefault: Option[Int] => String = {

                         case Some(x) => "is int"

                         case None=> "?"

                   }

      ③  for表达式里也可以使用模式。示例如下:

                   for((number, string) <- myTuple) println(number +string)

      模式匹配中的中缀标注:带有两个参数的方法可以作为中缀操作符使用,使用中缀操作符时实际上是其中一个操作数在调用操作符对应的方法,而另一个操作数作为方法的参数。但对于模式来说规则有些不同:如果被当作模式,那么类似于p op q这样的中缀标注等价于op(p,q),也就是说中缀标注符op被用做抽取器模式。

 

===函数

函数定义

      定义函数时,除了递归函数之外,你可以省略返回值类型声明,scala会根据=号后边的表达式的类型推断返回值类型,同时=号后边表达式的值就是函数的返回值,你无需使用return语句(scala推荐你使用表达式值代替return返回值,当然根据你的需要,也可以显式使用return返回值)。示例如下:

                   def abs(x: Double) = if(x >= 0) x else -x

                   def fac(n: Int) = {

                            var r = 1

                            for(i <- 1 to n)r = r * i

                            r

                   }

对于递归函数必须指定返回值类型,如下:

                   def fac(n: Int) : Int = if(n <= 0 ) 1else  n * fac(n-1)

但你要知道的是:声明函数返回类型,总是有好处的,它可以使你的函数接口清晰。因此建议不要省略函数返回类型声明

      函数体定义时有“=”时,如果函数仅计算单个结果表达式,则可以省略花括号。如果表达式很短,甚至可以把它放在def的同一行里。

      去掉了函数体定义时的“=”的函数一般称之为“过程”,过程函数的结果类型一定是Unit。因此,有时定义函数时忘记加等号,结果常常是出乎你的意料的。

      没有返回值的函数的默认返回值是Unit。

 

函数调用

      scala中,方法调用的空括号可以省略。惯例是如果方法带有副作用就加上括号,如果没有副作用就去掉括号。如果在函数定义时,省略了空括号,那么在调用时,就不能加空括号。另外,函数作为操作符使用时的调用形式参见相应部分。

 

函数参数

      一般情况下,scala编译器是无法推断函数的参数类型的,因此你需要在参数列表中声明参数的类型。对于函数字面量来说,根据其使用环境的不同,scala有时可以推断出其参数类型。

      scala里函数参数的一个重要特征是它们都是val(这是无需声明的,在参数列表里你不能显式地声明参数变量为val),不是var,所以你不能在函数里面给参数变量重新赋值,这将遭到编译器的强烈反对。

 

重复参数

      在scala中,你可以指明函数的最后一个参数是重复的,从而允许客户向函数传入可变长度参数列表。要想标注一个重复参数,可在参数的类型之后放一个星号“*”。例如:

                   def echo(args: String*) = for(arg <-args) println(arg)

这样的话,echo就可以被零至多个String参数调用。在函数内部,重复参数的类型是声明参数类型的数组。因此,echo函数里被声明为类型“String*”的args的类型实际上是Array[String]。然而,如果你有一个合适类型的数组,并尝试把它当作重复参数传入,会出现编译错误。要实现这个做法,你需要在数组名后添加一个冒号和一个_*符号,以告诉编译器把数组中的每个元素当作参数,而不是将整个数组当作单一的参数传递给echo函数,如下:

                   echo(arr: _*)

 

默认参数命名参数

      函数的默认参数与java以及c++中相似,都是从左向右结合。另外,你也可以在调用时指定参数名。示例如下:

                   def fun(str: String, left: String = “[”,right: String = “]”) = left + str + right

                   fun(“hello”)

                   fun(“hello”, “<<<”)

                   fun(“hello”, left =“<<<”)

 

函数与操作符

      从技术层面上来说,scala没有操作符重载,因为它根本没有传统意义上的操作符。诸如“+”、“-”、“*”、“/”这样的操作符,其实调用的是方法。方法被当作操作符使用时,根据使用方式的不同,可以分为:中缀标注(操作符)、前缀标注、后缀标注。

      中缀标注:中缀操作符左右分别有一个操作数。方法若只有一个参数(实际上是两个参数,因为有一个隐式的this),调用的时候就可以省略点及括号。实际上,如果方法有多个显式参数,也可以这样做,只不过你需要把参数用小括号全部括起来。如果方法被当作中缀操作符来使用(也即省略了点及括号),那么左操作数是方法的调用者,除非方法名以冒号“:”结尾(此时,方法被右操作数调用)。另外,scala的中缀标注不仅可以在操作符中存在,也可以在模式匹配类型声明中存在,参见相应部分。

      前缀标注:前缀操作符只有右边一个操作数。但是对应的方法名应该在操作符字符上加上前缀“unary_”。标识符中能作为前缀操作符用的只有+、-、!和~。

      后缀标注:后缀操作符只有左边一个操作数。任何不带显式参数的方法都可以作为后缀操作符。

 

      在scala中,函数的定义方式除了作为对象成员函数的方法之外,还有内嵌在函数中的函数函数字面量和函数值

 

嵌套定义的函数

      嵌套定义的函数也叫本地函数,本地函数仅在包含它的代码块中可见。

 

函数字面量

      在scala中,你不仅可以定义和调用函数,还可以把它们写成匿名的字面量,也即函数字面量,并把它们作为值传递。函数字面量被编译进类,并在运行期间实例化为函数值(任何函数值都是某个扩展了scala包的若干FunctionN特质之一的类的实例,如Function0是没有参数的函数,Function1是有一个参数的函数等等。每一个FunctionN特质有一个apply方法用来调用函数)。因此函数字面量和值的区别在于函数字面量存在于源代码中,而函数值作为对象存在于运行期。这个区别很像类(源代码)和对象(运行期)之间的关系。

      以下是对给定数执行加一操作的函数字面量:

                  (x: Int) => x + 1

      其中,=>指出这个函数把左边的东西转变为右边的东西。在=>右边,你也可以使用{}来包含代码块。

      函数值是对象,因此你可以将其存入变量中,这些变量也是函数,你可以使用通常的括号函数调用写法调用它们。如:

                   val fun = (x: Int) => x + 1

                   val a = fun(5)

      有时,scala编译器可以推断出函数字面量的参数类型,因此你可以省略参数类型,然后你也可以省略参数外边的括号。如:

                   (x) => x + 1

                   x => x + 1

      如果想让函数字面量更简洁,可以把通配符“_”当作单个参数的占位符。如果遇见编译器无法识别参数类型时,在“_”之后加上参数类型声明即可。如:

                   List(1,2,3,4,5).filter(_ > 3)

                   val fun = (_: Int) + (_: Int)

 

部分应用函数

      你还可以使用单个“_”替换整个参数列表。例如可以写成:

                   List(1,2,3,4,5).foreach(println(_))

      或者更好的方法是你还可以写成:

                   List(1,2,3,4,5).foreach(println _)

      以这种方式使用下划线时,你就正在写一个部分应用函数。部分应用函数是一种表达式,你不需要提供函数需要的所有参数,代之以仅提供部分,或不提供所需参数。如下先定义一个函数,然后创建一个部分应用函数,并保存于变量,然后该变量就可以作为函数使用:

                   def sum(a: Int, b: Int, c: Int) = a + b + c

                   val a = sum _

                   println(a(1,2,3))

      实际发生的事情是这样的:名为a的变量指向一个函数值对象,这个函数值是由scala编译器依照部分应用函数表达式sum _,自动产生的类的一个实例。编译器产生的类有一个apply方法带有3个参数(之所以带3个参数是因为sum _表达式缺少的参数数量为3),然后scala编译器把表达式a(1,2,3)翻译成对函数值的apply方法的调用。你可以使用这种方式把成员函数和本地函数转换为函数值,进而在函数中使用它们。不过,你还可以通过提供某些但不是全部需要的参数表达一个部分应用函数。如下,此变量在使用的时候,可以仅提供一个参数:

                   val b = sum(1, _: Int, 3)

      如果你正在写一个省略所有参数的部分应用函数表达式,如println _或sum _,而且在代码的那个地方正需要一个函数,你就可以省略掉下划线(不是需要函数的地方,你这样写,编译器可能会把它当作一个函数调用,因为在scala中,调用无副作用的函数时,默认不加括号)。如下代码就是:

                   List(1,2,3,4,5).foreach(println)

 

闭包

      闭包是可以包含*(未绑定到特定对象)变量的代码块;这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)。比如说,在函数字面量中使用定义在其外的局部变量,这就形成了一个闭包。如下代码foreach中就创建了一个闭包:

                   var sum = 0

                   List(1,2,3,4,5).foreach(x=> sum += x)

      在scala中,闭包捕获了变量本身,而不是变量的值。变量的变化在闭包中是可见的,反过来,若闭包改变对应变量的值,在外部也是可见的。

 

尾递归

      递归调用这个动作在最后的递归函数叫做尾递归。scala编译器可以对尾递归做出重要优化,当其检测到尾递归就用新值更新函数参数,然后把它替换成一个回到函数开头的跳转。

      你可以使用开关“-g:notailcalls”关掉编译器的尾递归优化。

      别高兴太早,scala里尾递归优化的局限性很大,因为jvm指令集使实现更加先进的尾递归形式变得困难。尾递归优化限定了函数必须在最后一个操作调用本身,而不是转到某个“函数值”或什么其他的中间函数的情况。

      在scala中,你不要刻意回避使用递归,相反,你应该尽量避免使用while和var配合实现的循环。

 

高阶函数

      带有其他函数作为参数的函数称为高阶函数

 

柯里化

      柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。如下就是一个柯里化之后的函数:

                   def curriedSum(x: Int)(y: Int) = x + y

      这里发生的事情是当你调用curriedSum时,实际上接连调用了两个传统函数。第一个调用的函数带单个名为x的参数,并返回第二个函数的函数值;这个被返回的函数带一个参数y,并返回最终计算结果。你可以使用部分应用函数表达式方式,来获取第一个调用返回的函数,也即第二个函数,如下:

                   val onePlus = curriedSum(3)_

 

      高阶函数柯里化配合使用可以提供灵活的抽象控制,更进一步,当函数只有一个参数时,在调用时,你可以使用花括号代替小括号,scala支持这种机制,其目的是让客户程序员写出包围在花括号内的函数字面量,从而让函数调用感觉更像抽象控制,不过需要注意的是:花括号也就是块表达式,因此你可以在其中填写多个表达式,但是最后一个表达式的值作为该块表达式的值并最终成为了函数参数。如果函数有两个以上的参数,那么你可以使用柯里化的方式来实现函数。

 

传名参数

      对于如下代码,myAssert带有一个函数参数,该参数变量的类型为不带函数参数的函数类型:

                   myAssert(predicate: () => Boolean) = {

                         if(!predicate())

                                  throw new AssertionError

                   }

在使用时,我们需要使用如下的语法:

                   myAssert(() => 5 > 3)

这样很麻烦,我们可以使用如下称之为“传名参数”的语法简化之:

                   myAssert(predicate: => Boolean) = {

                          if(!predicate)

                                  throw new AssertionError

                   }

以上代码在定义参数类型时是以“=>”开头而不是“() =>”,并在调用函数(通过函数类型的变量)时,不带“()”。现在你就可以这样使用了:

                   myAssert(5 > 3)

其中,“predicate: =>Boolean”说明predicate是函数类型,在使用时传入的是函数字面量。注意与“predicate: Boolean”的不同,后者predicate是Boolean类型的(表达式)。

 

偏函数

      偏函数部分应用函数是无关的。偏函数是只对函数定义域的一个子集进行定义的函数。scala中用scala.PartialFunction[-T,+S]来表示。偏函数主要用于这样一种场景:对某些值现在还无法给出具体的操作(即需求还不明朗),也有可能存在几种处理方式(视乎具体的需求),我们可以先对需求明确的部分进行定义,以后可以再对定义域进行修改。PartialFunction中可以使用的方法如下:

      isDefinedAt:判断定义域是否包含指定的输入。

      orElse:补充对其他域的定义。

      compose:组合其他函数形成一个新的函数,假设有两个函数f和g,那么表达式f _ compose g _则会形成一个f(g(x))形式的新函数。你可以使用该方法对定义域进行一定的偏移。

      andThen:将两个相关的偏函数串接起来,调用顺序是先调用第一个函数,然后调用第二个,假设有两个函数f和g,那么表达式f _ andThen g _则会形成一个g(f(x))形式的新函数,刚好与compose相反。