這篇文章將為大家詳細講解有關如何理解Scala的核心規則,文章內容質量較高,因此小編分享給大家做個參考,希望大家閱讀完這篇文章后對相關知識有一定的了解。
Read Eval Print Loop (REPL)
REPL在Scala里面指的是直接運行scala.exe進入的交互式命令行模式。廣義上講,也泛指那些在線編程工具。
核心規則1:請使用REPL來熟悉Scala語言。
Scala的REPL有個好處是能夠將我們輸入的每行代碼的內部表示反饋出來。比如:
scala> def add(a:Int, b:Int):Int = a + b
add: (a: Int, b: Int)Int
我們定義一個函數,完成兩個數的加法。Scala回顯給我們的內容可以幫助我們寫代碼。
表達式與語句
表達式與語句的區別是:語句是用來執行的,而表達式是用來求值的。在程序員的世界里,表達式就是返回值,語言就是沒有返回值執行程序。
Scala是表達式導向的編程語言。但并不是100%成立,Scala代碼中還是有控制語塊,畢竟我們寫程序就是為了控制各種實體為我們服務的。
核心規則2:使用表達式,而不是語句。
這條規則主要是幫助我們簡化代碼,就像前面加法的例子,a+b就是一個表達式。相比于我們C語言寫的相同實現,簡單不好。代碼里面,像這樣的例子肯定還是存在很多的。
不要使用Return
當我們使用表達式的時候,就不需要Return了。因為表達式本身就是用來求值的,我們必要再去顯式地說我現在要返回什么。Scala編譯器自動使用***一個表達式的返回值作為函數的返回值。
我們應該記得一個編程指導意見就是函數在同一個地方返回。如果我們現在沒有Return語句了,像在Scala中,有沒有類似的編程指導呢?看個例子:
object NoReturn extends scala.App { def createErrorMessage1(errorCode : Int) : String = { val result = errorCode match { case 1 => "Network Failure" case 2 => "I/O Failure" case 3 => "Unknown Error" } return result } def createErrorMessage2(errorCode: Int) : String = { var result : String = null // not val errorCode match { case 1 => result = "Network Failure" case 2 => result = "I/O Failure" case _ => result = "Unknown Error" } return result; } def createErrorMessage3(errorCode : Int) : String = { errorCode match { case 1 => "Network Failure" case 2 => "I/O Failure" case 3 => "Unknown Error" } } println(createErrorMessage1(1)) println(createErrorMessage2(2)) println(createErrorMessage3(3)) println(1 match{case 1 => "Network Failure" case 2 => 3}) println(2 match{case 1 => "Network Failure" case 2 => 3}) }
createErrorMessage2應該是我們以往的寫法。定義一個局部變量,然后匹配errorCode,對其進行賦值。createErrorMessage1是Scala推薦的寫法(雖然還不夠簡潔),它使用的是val而不是var,來聲明臨時變量。val表示值,賦值后就不允許再更改;var是變量,可以重復賦值。createErrorMessage1的的result之后是一個表達式。求值之后直接就賦值了。createErrorMessage3就更加簡潔了,差不多到了***形態了。函數直接就返回一個表達式,少了臨時對象。
注:match case支持每個分支返回的類型不同。這個特性在函數式編程中非常有用。
Scala雖然支持所有的3中寫法,但是推薦***一種。因為它幫助簡化了代碼的復雜度,增加了程序的不可變性。不可變指的是程序在執行過程中,所有的狀態(變量)都是常量。不可變的代碼比可變代碼更加容易理解、調試和維護。
表達式導向的語言傾向與使用不可變的對象,能減少程序中的可變對象。
使用不可變對象
核心規則3:使用不可變對象可以大幅減少運行時故障。當面對可變與不可變的選擇時,選擇不可變對象無疑是最安全的。
對象等價性
Scala提供了##和==來判斷對象是不是等價,它們可以作用于AnyRef(引用)和AnyVal(值)。
對象的哈希值和equal應該成對出現。因為等價性經常使用到了hash值。
import collection.immutable.HashMap class Point2(var x: Int, var y: Int) extends Equals { def move(mx: Int, my: Int) : Unit = { x = x + mx y = y + my } override def hashCode(): Int = y + (31*x) def canEqual(that: Any): Boolean = that match { case p: Point2 => true case _ => false } override def equals(that: Any): Boolean = { def strictEquals(other: Point2) = this.x == other.x && this.y == other.y that match { case a: AnyRef if this eq a => true case p: Point2 => (p canEqual this) && strictEquals(p) case _ => false } } } object ObjecteEquality extends scala.App { val x = new Point2(1,1) val y = new Point2(1,2) val z = new Point2(1,1) println(x == y) // false println(x == z) // true val map = HashMap(x -> "HAI", y -> "ZOMG") println(map(x)) // HAI println(map(y)) // ZOMG println(map(z)) // HAI, if we remove hashCode, there will be an exception x.move(1,1) // println(map(x)) //Exception in thread "main" java.util.NoSuchElementException: key not found: Point2@40 println(map.find(_._1 == x)) }
3-22行定義了一個Point2類,它繼承自Equals。
trait Equals extends Any { def canEqual(that: Any): Boolean def equals(that: Any): Boolean }
定義了自己的move方法和hashCode方法。canEqual用來判斷是否可以在對象上應用equal方法,這里只是檢查是否類型匹配。equal包含一個內部函數strictEquals用來判斷對象的成員是否相等。equal首先檢查是不是引用了同一個Point2對象,如果是,直接返回true。否則,檢查類型是不是匹配,如果是,用strictEquals用來判斷對象的成員是否相等。
第36行:println(map(z)),它的正確執行依賴于hashCode是否定義。Map在尋找指定key的值的時候,會調用key.##。
第38行,由于move改變了x的內部狀態,hashCode計算出來的新值當做key去Map里面查找,找不到對應的值,就會報NoSuchElementException異常。
第40行,比較奇特??聪耭ind的定義:
trait IterableLike:
override /*TraversableLike*/ def find(p: A => Boolean): Option[A] = iterator.find(p)
object Iterator:
def find(p: A => Boolean): Option[A] = { var res: Option[A] = None while (res.isEmpty && hasNext) { val e = next() if (p(e)) res = Some(e) } res }
傳給find的是一個predicate。迭代器遍歷集合中的每個元素,并將該元素作為參數傳給predicate。所有我們這里傳給predicate的參數是一個鍵值對[A,B]。_就是傳給predicate的參數。_1指的是鍵值對中的***個元素(實際上是元組中的***個元素),即A,也就是作為key的Point2?,F在很容易明白這句的意思了,就是與x的hashCode一樣的元素。_1的定義位于:
trait Product2:
/** A projection of element 1 of this Product. * @return A projection of element 1. */ def _1: T1
在我們實現對象的等價判斷的時候,請遵循:
如果兩個對象相等,那它們應該有相同的hashCode。
對象的hashCode在其生命周期內不會改變。
如果將一個對象發送給其他的JVM,應該保證等價判斷依賴于對象在兩個JVM都可用的屬性。主要用于序列化。
如果我們的對象是不可變的,那么上面的條件2自行就滿足了,這會簡化等價判斷。另外,不可變性不僅僅簡化等價判斷,也會簡化并行數據的訪問,因為不存在同步互斥。
使用None而不是null
null的使用還是很受大家詬病的。null迫使大家添加了額外的處理代碼。Scala使用Option來包裝了null的處理,我們不在需要去判斷變量是否為空。我們可以將Option看成一個通用的容器,包含了一些對象的容器(Some),或者是空容器(None)。這兩周容器都需要對象的類型。
類似地,Scala還有空的列表Nil。
核心規則4:使用None而不是null
Java中,我們經常會碰到空異常。如果我們學會了正確使用Option,完全可以避免空異常的發生。
Scala的Option伴生對象(companion object)包含一個工廠方法,將Java的null自動轉換為None:var x : Option[String] = Option(null)。等價于var x : Option[String] = Null。
Scala更加高級的用法是把它當作一個集合。這意味著,你可以在Option上使用map、flatMap、foreach,甚至是for表達式。
使用Null的一些高級實例:
class HttpSession class Connection object DriverManager { def getConnection(url: String, user: String, pw: String): Connection = { println("getConnection") new Connection } } object AdvancedNull extends scala.App { //CREATE AN OBJECT OR RETURN A DEFAULT def getTemporaryDirectory(tmpArg: Option[String]): java.io.File = { tmpArg.map(name => new java.io.File(name)). filter(_.isDirectory). getOrElse(new java.io.File( System.getProperty("java.io.tmpdir"))) } //EXECUTE BLOCK OF CODE IF VARIABLE IS INITIALIZED val username1: Option[String] = Option("Sulliy") for (uname <- username1) { println("User: " + uname) } val username2: Option[String] = None for (uname <- username2) { println("User: " + uname) } def canAuthenticate(username: String, password: Array[Char]): Boolean = { println("canAuthenticate") true } def privilegesFor(username: String): Int = { println("privilegesFor") 0 } def injectPrivilegesIntoSession(session: HttpSession, privileges: Int): Unit = { println("injectPrivilegesIntoSession") } def authenticateSession(session: HttpSession, username: Option[String], password: Option[Array[Char]]) = { for (u <- username; p <- password; if canAuthenticate(u, p)) { val privileges = privilegesFor(u) injectPrivilegesIntoSession(session, privileges) } } authenticateSession(new HttpSession, None, None) //USING POTENTIAL UNINITIALIZED VARIABLES TO CONSTRUCT ANOTHER VARIABLE def createConnection(conn_url: Option[String], conn_user: Option[String], conn_pw: Option[String]): Option[Connection] = for { url <- conn_url user <- conn_user pw <- conn_pw } yield DriverManager.getConnection(url, user, pw) createConnection(None, Option("sully"), None) def lift3[A, B, C, D](f: Function3[A, B, C, D]): Function3[Option[A], Option[B], Option[C], Option[D]] = { (oa: Option[A], ob: Option[B], oc: Option[C]) => for (a <- oa; b <- ob; c <- oc) yield f(a, b, c) } lift3(DriverManager.getConnection)(Option("127.0.0.1"), Option("sulliy"), Option("sulliy")) }
11行-16行,示例了通過一個文件名獲取File對象。由于輸入參數是Option[String],該函數可以接受None,既null。map、filter、getOrElse都是Option的成員函數:
@inline final def map[B](f: A => B): Option[B] = if (isEmpty) None else Some(f(this.get)) @inline final def filter(p: A => Boolean): Option[A] = if (isEmpty || p(this.get)) this else None @inline final def getOrElse[B >: A](default: => B): B = if (isEmpty) default else this.get
前兩個函數都又返回了Option[],所以我們可以進行級聯的書寫。map返回None(Option的子類),None的filter返回None,None的getOrElse返回default,即new java.io.File( System.getProperty("java.io.tmpdir")。
我們在需要創建一個對象或者返回一個缺省對象的時候,可以使用這種方法。
18行-21行,示例了將Option放在for循環里面。由于username1賦值了,username2為空,因此20行會被執行,24行不會被執行。37行-47行,給了一個更加復雜的例子。
49行-56行,示例了通過一個可能未初始化的對象來創建新對象。用到了yield。
58行-62行,提供了一個通用方法,將普通的Java方法封裝為支持Option的新的Scala方法。這樣,我們就不需要自己去處理所有參數的null檢查。
多態環境下的等價判斷
核心規則5:在使用多態情況下,使用scala.Equals提供的模板。
前面已經提到了scala.Equals,需要注意的是:請同時重寫canEqual和equal。
關于如何理解Scala的核心規則就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。