欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

scala(1)——scala基础

程序员文章站 2022-06-14 17:00:42
...

对之前scala学习的一个总结,本篇博客主要对scala中的一些基本类型、流程控制等基本的操作进行介绍,加深自己的理解。

字符串

字符串作为编程语言中最为常用、基础的类型,其操作是十分重要的,下面介绍scala中字符串的说明以及操作。

1、scala中String操作

scala中的字符串实际上就是java中String,在java中的一些用法在scala中也是支持的,但由于scala中支持隐式转换,其实StringOps类中的所有方法,在scala中的String中也全部可以使用,在该类中基本上包括了上百字符串操作方法(毫不夸张),在scala中本质上将String看做是字符集合,单个字符的访问可以直接按下标进行访问,因此在该类中还包含了一些对于集合操作的方法,例如filter、map、reduceLeft等。另外说明一下,在scala中基本上一些常用的操作方法基本上都对应有符号化的操作符来表示,更加方便使用,示例如下:

    println("China" ++ "*")
    //这里进行比较的是字母的前后顺序,不是长度
    println("China" < "*")
    println("China" +: "*")
    println("China *".map(c => (c, 1)))
China*
true
Vector(China, T, a, i, w, a, n)
Vector((C,1), (h,1), (i,1), (n,1), (a,1), ( ,1), (T,1), (a,1), (i,1), (W,1), (a,1), (n,1))

以上是scala中String操作函数基本的应用,相比java的字符串操作,scala进行了全面的扩充,而且可以用==直接进行相等判断,用起来感觉更加顺畅,里边还包含了很多的其他的有趣有用的函数(take、drop),这里不再一一说明了。

2、多行字符串表示

在scala中可以创建多行String,

"""
This is 
a dog
"""

3、引用变量

在scala基础字符串的插值就是在字符串前加’s’,然后将变量名插入字符串中,用$进行标识,同时可以使用内嵌表达式,示例如下:

val name = "tongtong"
println(s"$name is a boy.")
println(s"${name.toUpper} is a boy.")
tongtong is a boy.
TONGTONG is a boy.

s其实是StringContext类中的方法,用以实现简单的插值操作,它里边其实调用了更高阶的函数standardInterpolator进行实现;除了s之外,还包含f和raw的插值操作,f表示格式化的输出,raw表示不会对字符进行转义,示例如下:

val name = "tongtong"
    val age = 10.001
    println("$name is a boy.")
    println(s"${name.toUpperCase} is a boy\n.")
    println(f"${name.toUpperCase} is a boy, and he is $age%.2f years\n.")
    println(raw"${name.toUpperCase} is a boy\n.")
$name is a boy.
TONGTONG is a boy
.
TONGTONG is a boy, and he is 10.00 years
.
TONGTONG is a boy\n.

更加细致的解释,可以关注一下StringContext这个类(严格的说,它是object)。

下标是格式化字符串输出时的一些规则:

格式化符号 描述
%c 字符
%d 十进制数字
%e 指数浮点数
%f 浮点数
%i 整数(十进制)
%o 八进制
%s 字符串
%u 无符号十进制
%x 十六进制
%% 打印百分号
\% 打印百分号

4、字符串中的正则表达式

scala中字符串很容易表示出正则表达式,即在字符串的后面加上“.r”就完成了正则表达式的创建,在使用正则表达式时,可以使用查找模式,也可以使用匹配模式,详细代码介绍如下 :

val regex = "[0-9]+".r
// 查找模式,支持多种查找模式,first只返回第一个匹配到的内容,all返回一个迭代器,所有匹配都包含
val context = "90 sjk 70"
println(regex.findFirstIn(context))
println(regex.findFirstMatchIn(context))
regex.findAllIn(context).foreach(println)
// 判断字符是否匹配的情况
println(context.matches(regex.regex))
Some(90)
Some(90)
90
70
false

都是一些Api的基本用法,replace的用法基本类似,也支持正则表达式,这里不再说明,在这部分最后,附赠一条正则表达式:

[-+]?(([0-9]+(\\.[0-9]*)?)|\\.[0-9]+)(e[-+]?[0-9]+)?

5、字符串中的隐式转换

在StringOps和StringContext中已经使用了隐式转换,下面定义一个自定义的隐式转换工具,先看代码。

implicit class StringUtils(s: String) {
    def increment = s.map(c => (c + 1).toChar)
}
println(name.increment)

在scala定义隐式转换类,“一个隐式转换类必须定义在允许定义方法的范围内(不在最高层)”,即隐式转换类必须定义在一个类或对象或包的内部,在不同的范围进行定义时,只需要相应的import相依的范围即可。

其工作原理:1、编译器找到name对应的字符串常量;2、编译器要在String上调用increment方法;3、由于String中没有,那么编译器会在当前范围内进行隐式转换的搜索,然后找到StringUtil里的increment方法

隐式转换的好处:不需要继承自一个现有类并添加相应的新方法。

“基本类型”

scala其实是没有基本类型这一说的,因为所有的基本类型都是类,这也是这个标题为什么要加上引号的原因;类型的种类和java一样,不在说明;

scala中同样支持BigInt和BigDecimal,基本和java中一样,类型之间的转换没有java中的强制转换,而是替换成了更友好的toSomething.

重载默认数值类型可以用简单的字符标识,也可以用’:’+数据类型来确认,具体代码如下:

    val num1 = 0: Byte
    val num2 = 0f
    val num3 = 0: Float

scala中没有自增、自减操作,可以用+=、-=来替代。其他内容就是数值区间的创建、随机数的生成、数值的格式化,不在说明。

流程控制

scala是函数式编程,所以其使用循环遍历过程中会和java有所不同,下面先说明for:

1、基本使用

    val list = List(1, 2, 2, 3)
    for (n <- list) println(n)
    for (i <- list.indices) println(list(i))
    // not including end
    for (i <- 0 until list.size) println(list(i))
    // including end
    for (i <- 0 to list.size - 1) println(list(i))

    val arr = for (v <- 'a' until 'd') yield v.toUpper

    val arr1 = for (v <- 'a' until 'd') yield {
      (v.toInt + 1).toChar.toUpper
    }
    println(arr + ", " + arr1)

    for ((a, c) <- list.zipWithIndex) {
      // 前面是内容,后面是下标
      println(a + ":" + c)
    }

    for ((n, z) <- list.zip("abcd")) {
      println(n + ", " + z)
    }

    val li = 'a' to 'z'
    val teLi = li.withFilter(k => k % 10 != 0)
    teLi.foreach(print)

    for {
      (n, z) <- list.zip("abcd")
      (a, b) <- list.zip("abcd")
    } {
      println(s"$n $z $a $b ")
    }

scala中for的使用与java中有较大的区别,一般遍历元素类似于java中的for each,按下标访问时可以借助until、to关键字,还可以利用集合的indices方法来进行访问;

借助yield关键字可以实现for循环值的返回,其工作原理大致可以理解为for过程中,申请临时的缓存区将产生的数据进行存放,最后统一返回集合,一般yield返回的集合和for的原始集合的类型和大小都一致 ;

在for中,循环条件可以有多个,如果有多个表达式,那么建议使用大括号来进行标识,scala中的for竟可能友好的支持了多种操作(针对java);此外,在所有集合中,scala都是支持foreach操作,面向函数式编程,使用foreach更加和谐、优雅,因此建议能够foreach的地方不用for!

for循环式被编译器如何解释:1、遍历集合的简单for循环为集合上的foreach方法调用;2、带有卫语句的for循环被解释为在foreach调用后在集合上调用withFilter方法的序列;3、带有yield表达式的for循环被解释为在集合上调用map方法;4、带有yield和卫语句的被解释为在调用withFilter后调用map。

相关接口(withFilter、map)详见scala api,备注一下<-符号被称为生成器,一个for中可以创建多个生成器。

2、for中卫语句的使用

2.1什么是卫语句(Guard Clauses)

卫语句(Guard Clauses):Replace Nested Conditional with Guard Clauses,即把复杂的条件表达式拆分成多个条件表达式,比如一个很复杂的表达式,嵌套了好几层的if - then-else语句,转换为多个if语句,实现它的逻辑,这多条的if语句就是卫语句,由此实现简化逻辑判断的效果,增加程序的可读性。

针对“每个函数只能有一个入口和一个出口”的观念,根深蒂固于某些程序员的脑海里。现今的编程语言都会强制保证每个函数只有一个入口,至于“单一出口”规则,其实不是那么有用。保持代码清晰才是最关键的:如果单一出口能使这个函数更清晰易读,那么就使用单一出口;否则就不必这么做。

做法:1、对于每个检查,放进一个卫语句。卫语句要不就从函数返回,要不就抛出一个异常;2、每次将条件检查替换成卫语句后,编译并测试。如果所有卫语句都导致相同的结果,请使用 Consolidate Conditional Expression (合并条件表达式)。

2.2卫语句使用

在for的循环条件中使用卫语句,具体如下,一看就会:

    for (i <- 0 to 10 if i % 2 == 0) println(i)
    for {
      i <- 0 to 10
      if i % 2 == 0
      if i > 3
      if i < 7
    } println(i)

卫语句的使用旨在使得程序代码清晰、可读。

3、实现break和continue

scala中没有break、continue关键字,但在scala.util.control.Breaks类中提供了类似的用法。

    import util.control.Breaks._
    println("BREAK  ------")
    breakable {
      for (i <- 0 to 10) {
        println(i)
        if (i % 5 == 0) break
      }
    }
    println("CONTINUE   ----------")
    for (i <- 0 to 10) {
      breakable {
        if (i % 5 == 0) break
        println(i)
      }
    }

下面对break和continue的例子进行解释:break就是在if满足条件时,申明的break方法抛出了一个异常(抛出BreakControl, 而BreakControl extends ControlThrowable),而在外层的breakable对这个异常进行了捕获,继续执行循环外的其他代码;同理,相应的continue也是这个原理,只不过这个抛出异常实在本次循环的循环内部,所以如果满足条件它会跳过本次循环,进入下次循环。

更高级的用法:可以在嵌套循环中,或者多个条件判断中使用标签break(一看就会)。

    import util.control.Breaks._
    val outter = new Breaks
    val inner = new Breaks
    outter.breakable{
      for (i <- 0 to 5){
        inner.breakable{
          for (j <- 0 to 10){
            if (i == 0 && j == 0) inner.break
            if (i == 5 && j == 5) outter.break
            // function
          }
        }
      }
    }

当然,使用标记位、标记值, 或者为函数设置多个出口、递归函数等都可以实现跳出循环的功能,不一定非要用该种方式。

4、if和switch

4.1基本功能

在scala中没有三木运算符,但是scala中if else可以用作显示的“三木云算法”,并且scala还比较提倡这用用法。下面重点介绍一下switch。

    val i = 1
    val num = i match {
      case 1 => println("num 1")
      case 2 => println("num 2")
      case 3 => println("num 3")
      case _ => println("default")
    }
    println("**" + num)

    import scala.annotation.switch
    val number = (i: @switch) match {
      case 1 => println("num 1")
      case 2 => println("num 2")
      case 3 => println("num 3")
      case _ => println("default")
    }
    println("**" + number)

先上一段代码,和java相比,有一些符号的变化,基本上还是相似的,特别需要说明的是,在scala中可以使用@switch注解来进行switch的优化,使用该注解的优点:1、使用注解可以将被表达式编译为tableswitch或者lookupswitch,性能会更好,因为其生成分支表而不是决策树,当表达式获取值时,它可以直接跳到结果处而不是通过决策树区决定,其中两种方式对应不同算法,当case中的值连续时,编译成tableswitch,解释执行时从table数组根据case值计算下标来取值,从数组中取到的便是要跳转的行数,当case中的值不连续时,编译成lookupswitch,解释执行时需要从头到尾遍历找到case对应的代码行,由于case对应的值不是连续的,如果仍然用表来保存case对应的行号,会浪费大量空间;2、对表达式进行检查,如果不能被编译成table或者lookup,会引发警告;

    val Two = 2
    val number = (i: @switch) match {
      case 1 => println("num 1")
      case Two => println("num 2")
      case 3 => println("num 3")
      case _ => println("default")
      // 如果关心默认值,则可以通过这种方式来对默认值进行操作
      case oops => println(oops)
    }
    println("**" + number)

这个scalac编译,就会进行警告,那么在什么情况下使用该注解呢?

1、匹配的值一定是一个已知的整数(但应该不限于整数,用连续字符测试并没有出现相应警告)。

2、匹配表达式必须“简单”。即不能包含任何的类型检测,if语句或者抽取器

3、保证表达式在编译时的值可用。

4、至少包含两个case语句

4.2、辅助功能

case可以匹配多个值,可以匹配字符串(从java7开始,可以直接对字符串进行switch操作,网上一部分说java 8 不行,但我使用的java 8 的版本是可以的,可能是低版本的java 8里边不支持吧,所以稳定的新版本更换一下jdk还是很有必要的

import scala.annotation.switch
val number = (i: @switch) match {
  // 冒号之后必须要有空格
  case 1 | 10 | 30 => println("num 1*")
  case 2 | 4 | 8 => println("num 2e")
  case _ => println("default")
}
println("**" + number)

2.1 在switch中使用模式匹配

可以在该语句中进行模式匹配,范围包含序列模式,常量模式,变量模式,类型模式,元组模式,构造函数模式,各种模式可以使用变量,和变量进行绑定,可以在右面语句中使用该变量,具体代码如下:

def test(x: Any) = x match {
    case List(0, _, _) => "匹配有三个元素并且以0开头的list"
    case Vector(0, _*) => "匹配有多个元素并且以0开头的Vector"
    case (0, _) => "匹配有两个元素并且以0开头的tuple"
    case m: Map[_, _] => s"$m"
    case l: List[_, _] => s"$l"
    // 对类型进行绑定如上,如果要对内容进行变量绑定如下
    case list @ List(0, _*) => s"$list"
    csee list @ Some(_) => s"$list"
  }

在switch中使用Option(Some、None)

def toInt(s:String): Option[Int] = {
    //感觉已经不是switch的范畴了 
    try {
      Some(Integer.parseInt(s.trim))
    }catch {
      case e:Exception => None
    }
  }

在表达式中,使用case类:

  trait Animal
  case class Dog(name: String, age: Int) extends Animal
  case class Cat(name: String) extends Animal
  def pirntInfo(x: Animal) = x match {
    case Dog(name, age) => s"$name, $age"
    case Cat(name) => s"cat named $name"
  }

在表达式中使用卫语句:

  trait Animal
  case class Dog(name: String, age: Int) extends Animal
  case class Cat(name: String) extends Animal
  def pirntInfo(x: Animal) = x match {
    case Dog(name, age) if name.startsWith("wang") && age > 10 => s"$name, $age"
    case Cat(name) => s"cat named $name"
  }

使用匹配表达式替换isInstanceOf

def isInt(x:Any):Boolean = x match {
  case Int => true
  case _ => false
}       

try catch finally

这一部分和java相比感觉基本没有变化,唯一区别就是写法上的区别,在catch中用case语句进行异常的捕获,如果要在finally中对前面的连接或者其他东西操作,可以在try外申明Option变量(该变量用法和java中的optional有着相同的效果,这里不再说明),在try中对其进行赋值操作,然后在finally中进行关闭或者其他操作。

自定义控制结构

自定义控制结构起始是定义一个函数,例如循环控制结构,可以将其参数分别为设置为循环的条件和循环体内要执行的代码块,这样即可实现一个自定义循环,如下:

def defWhile(isTrue: => Boolean)(codeBlock: => Unit):   Unit ={
      while (isTrue){
        codeBlock
      }
    }

这种自定义只能在函数式编程语言中成为可能,因为你的参数可以是具体的代码块或者其他的函数,其实,scala里边避免定义很多的关键字,取而代之的是定义很多可用的函数,通过自定义一些操作,使得语言的可操作的空间变得更大。

心得体会:本博客内容主要来自《scala in action》这本书,scala给我最直观的感受就是很“随意”,“*”,写出的代码棱角变少了,也许这就是优雅吧,目前都是用java,当然java现在也进化到了函数级,在实践中我也在慢慢摸索java的函数化,真实感觉scala打开了新世界的大门。