背景
上一節,我介紹了scala中的面向對象相關概念,還有一個特色功能:模式匹配。本文,我會介紹另外一個特別強大的功能隱式轉換,并在最后介紹scala中泛型的使用
1. 隱式轉換
Scala提供的隱式轉換和隱式參數功能,是非常有特色的功能,是Java等編程語言所沒有的功能。它可以允許手動指定將某種類型的對象轉換成其他類型的對象。
Scala的隱式轉換,其實最核心的就是定義隱式轉換函數,即implicit conversion function。定義的隱式轉換函數,只要在編寫的程序內引入,就會被Scala自動使用。Scala會根據隱式轉換函數的簽名,在程序中使用到隱式轉換函數接收的參數類型定義的對象時,會自動將其傳入隱式轉換函數,轉換為另外一種類型的對象并返回。這就是“隱式轉換”
隱式轉換函數叫什么名字是無所謂的,因為通常不會由用戶手動調用,而是由Scala進行調用。但是如果要使用隱式轉換,則需要對隱式轉換函數進行導入。因此通常建議將隱式轉換函數的名稱命名為“one2one的形式。
Spark源碼中有大量的隱式轉換和隱式參數。學好隱式轉換會對讀懂spark源碼有很大的幫助。
1.1. 隱式轉換的概念
隱式轉換是將類型A轉換成類型B,但并不是A真的就成了B,而是A本來的屬性仍存在的同時又擁有了B的屬性,這使得A本身不發生變化的同時擴大了功能,此屬于蒙面設計模式。又因為A直接使用了B的功能而不需要對A進行修改,因此此轉換是隱式的,使用implicit修飾。簡言之,隱式轉換就是增強類型、擴展功能。
1.2. 隱式轉換的適用情況
隱式轉換主要適用于以下兩種情況:
- 如果表達式e是類型S,并且S不符合表達式的期望類型T。
- 在具有類型S的e的e.m表達中,如果m不表示S的成員。
1.3. 隱式轉換的原理
當編譯器第一次編譯失敗的時候,會在當前的環境中查找能讓代碼編譯通過的方法,用于將類型進行轉換,實現二次編譯。當想調用對象功能時,如果編譯錯誤,那么編譯器會嘗試在當前作用域范圍內查找能調用對應功能的轉換規則,這個調用過程是由編譯器完成的,所以稱之為隱式轉換,或稱之為自動轉換。
1.4 隱式轉換的作用域
Scala編譯器僅考慮作用域之內的隱式轉換。要使用某種隱式操作,必須以單一標識符的形式(一種情況例外)將其帶入作用域之內。例如:
object TestImplicit {implicit def doubleToInt(x: Double) = x.toInt
}object Test {def main(args: Array[String]): Unit = {// 以單一標識符引進doubleToInt的隱式轉換import TestImplicit._val i: Int = 2.3}
}
單一標識符有一個例外,編譯器還將在源類型和目標類型的伴生對象中尋找隱式定義。
1.5. 隱式轉換的規則
- 顯示定義規則:在使用帶有隱式參數的函數時,如果沒有明確指定與參數類型匹配相同的隱式值,編譯器不會通過額外的隱式轉換來確定函數的要求。
- 作用域規則:不管是隱式值、隱式對象、隱式類還是隱式轉換函數,都必須在當前的作用域使用才能起作用。
- 無歧義規則:不能存在多個隱式轉換使代碼合法。例如,代碼中不應該存在兩個隱式轉換函數能夠同時使某一類型轉換為另一類型,也不應該存在相同的兩個隱式值、主構造函數參數類型以及成員方法等同的兩個隱式類。
- 一次性轉換規則:隱式轉換從源類型到目標類型只會經過一次轉換,不會經過多次隱式轉換達到。
1.6. 常見的隱式轉換類型
1.6.1. 隱式轉換函數
隱式轉換函數的格式通常為:
implicit def 函數名(參數): 目標類型 = {// 函數體// 返回值
}
例如:
package com.wanlong.next
//引入隱式轉換函數import com.wanlong.next.iTestImplicitConversionFunction._object iTestImplicitConversionFunction2 {def main(args: Array[String]): Unit = {val jack = new Man("jack")jack.fly
}/*** 隱式轉換強大之處就是可以在不知不覺中加強現有類型的功能。也就是說,可以為某個類定義-個加強版的類,并定義互相之間的隱式轉換,* 從而讓源類在使用加強版的方法時,由Scala自動進行隱式轉換為加強類,然后再調用該方法。*/
implicit def man2SuperMan(man: Man): SuperMan = {new SuperMan(man.name)
}
}class Man(val name: String)
class SuperMan(val name: String) {
def fly = println("SuperMan:" + name + " is flying")
}
//SuperMan:jack is flying
1.6.2. 隱式類
Scala 2.10后提供了隱式類,可以使用implicit聲明類。隱式類的主構造函數只能有一個參數,且這個參數的類型就是將要被轉換的目標類型。允許開發者在不修改現有類的原始定義的情況下,為該類添加新的功能。在集合中隱式類會發揮重要的作用。
隱式類的格式通常為:
implicit class 類名(參數){
// 類體,可以定義新的方法
}
需要注意的是,隱式類必須定義在另一個類、對象或者包對象(package object)內部,并且其構造函數只能有一個非隱式參數。此外,隱式類的命名雖然沒有嚴格的語法規定,但通常會采用“Rich”前綴加上原始類型名的方式(如RichInt、RichString等)來命名,以清晰地表明這個類是用于擴展某個原始類型的。
object Helpers {
implicit class MyRichInt(arg: Int) {def myMax(i: Int): Int = if (arg < i) i else arg
}
}// 使用隱式類
import Helpers._
println(1.myMax(3)) // 輸出: 3
在這個例子中,我們定義了一個隱式類MyRichInt,并為Int類型添加了一個myMax方法。當我們調用1.myMax(3)時,Scala編譯器會自動將1轉換為MyRichInt類型,并調用myMax方法
1.6.2.2 限制與注意事項
- 定義位置:隱式類必須定義在另一個類、對象或包對象內部。即隱式類不能是頂級的。
- 構造函數參數:隱式類的構造函數只能有一個非隱式參數。這是因為隱式轉換是將一種類型轉換為另外一種類型,源類型與目標類型是一一對應的。
- 命名規范:雖然命名沒有嚴格規定,但建議使用“Rich”前綴加上原始類型名的方式來命名隱式類。
- 避免濫用:過度使用隱式類和隱式轉換可能會導致代碼的可讀性變差。因為隱式轉換是在編譯器自動進行的,對于閱讀代碼的人來說,可能不容易發現代碼中實際發生的轉換。
- 與其他隱式機制的沖突:在Scala中,還有其他隱式機制(如隱式參數、隱式轉換函數等),使用時需要注意避免沖突。
- 隱式類的運作方式是:隱式類將包裹目標類型,隱式類的所有方法都會自動“附加”到目標類型上。
1.7 注意事項
- 隱式轉換函數的函數名可以是任意的,與函數名稱無關,只與函數簽名(函數參數和返回值類型)有關。即隱式函數的入參要是編譯不通過的類型,返回值要是能正確編譯的類型。
- 如果當前作用域中存在函數簽名相同但函數名稱不同的兩個隱式轉換函數,則在進行隱式轉換時會報錯。
- 在同一作用域內,不能有任何方法、成員或對象與隱式類同名。
- 隱式類不能是case class。
- Scala的隱式轉換是一種靈活且強大的功能,但使用時需要謹慎,以避免引入難以調試的隱式行為。 通常建議,僅僅在需要進行隱式轉換的地方,比如某個函數或者方法內,用immport導入隱式轉換函數,這樣可以縮小隱式轉換函數的作用域,避免不需要的隱式轉換。
2. 泛型
Scala的泛型是一種強大的特性,它允許在定義類、特質(Traits)或方法時使用類型參數。這種機制使得代碼更加通用和靈活,可以適應不同的數據類型而無需重復編寫代碼。以下是對Scala泛型的詳細介紹:
2.1 泛型的基本語法
在Scala中,使用方括號[]
來定義類型參數。例如,定義一個名為Box
的泛型類,它有一個類型參數T
:
class Box[T](val item: T) {def getValue(): T = item
}
在這里,T
可以是任何合法的Scala類型。在創建Box
類的實例時,可以指定具體的類型參數,如Box[Int]
或Box[String]
。
2.2 泛型的應用場景
- 泛型類:如上所述的
Box
類就是一個泛型類的例子。泛型類允許在類的定義中引入類型參數,從而在創建對象時指定具體的類型。 - 泛型方法:泛型方法允許方法的參數類型或返回類型根據調用時的實際情況來確定。例如,定義一個泛型方法
printList
,它可以接受任何類型的列表并打印出列表中的元素:
def printList[T](list: List[T]): Unit = {list.foreach(println)
}
在調用printList
方法時,可以傳入不同類型的列表,如List[Int]
或List[String]
。
- 泛型特質:泛型特質允許在特質的定義中引入類型參數。這樣,在定義特質的子類或子單例對象時,可以指定具體的類型參數。例如,定義一個泛型特質
Logger
,它有一個變量a
和一個show
方法,它們都使用Logger
特質的泛型:
trait Logger[T] {val a: Tdef show(b: T): Unit = println(b)
}
然后,可以定義一個單例對象ConsoleLogger
,它繼承Logger
特質并指定具體的類型參數為String
:
object ConsoleLogger extends Logger[String] {override val a: String = "張三"
}
2.3 泛型的上下界
在使用泛型時,有時需要限制泛型參數的類型范圍。這時,可以使用泛型的上下界。
- 上界:使用
T <: 類型名
表示給類型添加一個上界,即泛型參數T
必須是該類型或其子類。例如,定義一個泛型方法demo
,它接受一個Array
參數,并限定該Array
的元素類型只能是Person
或其子類:
class Person
class Student extends Persondef demo[T <: Person](arr: Array[T]): Unit = println(arr)
- 下界:使用
T >: 類型名
表示給類型添加一個下界,即泛型參數T
必須是該類型或其父類。例如,定義一個泛型類Shelter
,它接受一個類型參數T
,并限定T
必須是Dog
或其父類:
class Animal
class Dog extends Animalclass Shelter[T >: Dog](val animal: T)
如果泛型既有上界又有下界,下界應寫在前面,上界寫在后面,即[T >: 類型1 <: 類型2]
。
2.4 協變、逆變與非變
在Scala中,泛型參數還可以聲明為協變(Covariance)、逆變(Contravariance)或非變(Invariance)。
- 非變:默認情況下,泛型類是非變的。這意味著,如果
B
是A
的子類型,那么Pair[A]
和Pair[B]
之間沒有任何從屬關系。 - 協變:如果類型
B
是類型A
的子類型,那么Pair[B]
可以認為是Pair[A]
的子類型。這稱為協變關系。在Scala中,可以通過在類型參數前加上+
符號來聲明協變關系(但在Scala的類聲明中通常不顯式聲明,而是通過類型系統的規則來隱式處理)。 - 逆變:如果類型
B
是類型A
的子類型,那么Pair[A]
可以認為是Pair[B]
的子類型(這在實際中較少見,因為通常函數參數的逆變和返回類型的協變是通過函數類型來處理的,而不是簡單的泛型類)。這稱為逆變關系。在Scala中,可以通過在類型參數前加上-
符號來聲明逆變關系(同樣,在Scala的類聲明中不顯式聲明逆變)。
需要注意的是,協變和逆變主要影響泛型類型的子類型關系,它們在Scala的類型系統中有著復雜而重要的應用,特別是在處理函數類型和集合類型時。
2.5 泛型的類型擦除
Scala的泛型系統在編譯時會將泛型類型信息擦除,生成的字節碼中不包含泛型類型的具體信息。這個過程被稱為擦除(Type Erasure)。擦除機制是為了保持與Java的互操作性并減少運行時的開銷。由于擦除機制,運行時無法獲取泛型類型的具體信息,因此不能直接在泛型代碼中執行某些類型特定的操作(如創建泛型類型的實例或檢查泛型類型的參數類型)。然而,Scala提供了一些反射相關的工具和方法(如Manifest
、TypeTag
和ClassTag
等)來在一定程度上恢復泛型類型的信息。
Scala的泛型是一種強大的特性,它允許編寫更加通用和靈活的代碼。通過合理使用泛型的上下界、協變、逆變和非變等特性以及理解泛型的類型擦除機制,可以編寫出更加健壯和可維護的Scala程序。
以上,如有錯誤,請不吝指正!