1. 程式人生 > >scala(1)——scala基礎

scala(1)——scala基礎

對之前scala學習的一個總結,本篇部落格主要對scala中的一些基本型別、流程控制等基本的操作進行介紹,加深自己的理解。

字串

字串作為程式語言中最為常用、基礎的型別,其操作是十分重要的,下面介紹scala中字串的說明以及操作。

1、scala中String操作

scala中的字串實際上就是java中String,在java中的一些用法在scala中也是支援的,但由於scala中支援隱式轉換,其實StringOps類中的所有方法,在scala中的String中也全部可以使用,在該類中基本上包括了上百字串操作方法(毫不誇張),在scala中本質上將String看做是字元集合,單個字元的訪問可以直接按下標進行訪問,因此在該類中還包含了一些對於集合操作的方法,例如filter、map、reduceLeft等。另外說明一下,在scala中基本上一些常用的操作方法基本上都對應有符號化的操作符來表示,更加方便使用,示例如下:

    println("China" ++ "Taiwan")
    //這裡進行比較的是字母的前後順序,不是長度
    println("China" < "Taiwan")
    println("China" +: "Taiwan")
    println("China TaiWan".map(c => (c, 1)))
ChinaTaiwan
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打開了新世界的大門。