Scala由類的動態擴充套件想到型別類

weixin_33866037發表於2018-01-06

by 壯衣

在上一篇博文《Scala對JDBC的一些封裝》中使用了一個黑魔法: 對java.sql.ResultSet類進行擴充套件為其新增了rows方法。我們稱這種黑魔法為類的動態擴充套件,可以為已有的類新增新的方法。也許我們覺得給類新增新的方法,可以找到定義類的地方新增新的方法然後重新編譯就好了,但是假如我們需要擴充套件類是Int、String或者JDK中自帶型別或者第三方jar包中的型別我們便不好用這個方式擴充套件了。在Scala中可以通過隱式類來完成類的動態擴充套件,讓我們來看一個簡單的例子:我們想給String類新增一個sayHello方法,實現如下程式碼的效果:

  def sayHello(str: String): Unit = println(s"$str Hello")

回顧上一篇博文就知道我們可以定義一個隱式類,在隱式類中實現sayHello方法,然後在需要使用sayHello方法的地方引入隱式類,程式碼如下:

object StringUtils {

  implicit class StringOp(str: String) {

    def sayHello(): Unit = println(s"$str Hello")
  }
}

如上程式碼我們在隱式類StringOp中實現了sayHello方法,看看如何使用sayHello方法吧:

  import StringUtils._

  "zdx".sayHello()

在控制檯測試下:


7571416-c82a0e06da404c38.png
image.png

現在String型別可以呼叫sayHello函式了,假如我們想給Int型別也新增一個sayHello函式呢?這麼看來我們得重新定義一個IntOp的隱式類並在隱式類中實現sayHello方法,還有別的方法嗎?能否有一個sayHello方法對任何型別都適用?先試一下方法過載的方案:

  def sayHello(s: String): Unit = println(s"$s Hello")

  def sayHello(i: Int): Unit = println(s"Hello $i")

嗯,這個方案是ok得。但是能不能把兩個sayHello變成一個呢? 再試一下模式匹配:

  def sayHello(a: Any): Unit = a match {
    case s: String => println(s"$s Hello")
    case i: Int => println(s"Hello $i")
    case _ => println(s"not support")
  }

方法變成一個了,但是假如我們需要新增型別就得重新更改sayHello方法,還有更好的方案嗎?現在可以試一下型別類了:

trait Hello[A] {

  def hello(a: A): Unit
}

我們定義了一個Hello型別並在其中定義了一個hello方法,先不管hello方法的實現。我們來看一下如何定義一個對於任何型別都適用的sayHello的方法:

  def sayHello[A](a: A)(implicit s: Hello[A]): Unit = s.hello(a)

新的定義的sayHello方法可以接受任何型別入參a,也就是說sayHello方法適用任何型別。方法的具體實現其實是通過Hello[A]型別實現的,sayHello方法有一個隱式引數s是Hello[A],通過s的hello方法來實現sayHello方法。那麼現在的問題就是我們需要實現Hello[A]型別,先看下Hello[String]型別和Hello[Int]型別如何實現的:

object Hello {

  implicit val stringHello: Hello[String] = new Hello[String] {

    override def hello(a: String): Unit = println(s"$a Hello")

  }

  implicit val intHello: Hello[Int] = new Hello[Int] {

    override def hello(a: Int): Unit = println(s"Hello $a")

  }
}

好了, 現在我們來呼叫下sayHello方法:

  import Hello._

  sayHello("zdx")

  sayHello(1)

在控制檯測試下:


7571416-db191bf06986e468.png
image.png

我們想讓sayHello適用新的型別List[Int],使用型別類就不重新必修改sayHello方法了,只需要實現Hello[List[Int]]型別就好了:

  implicit val intListHello: Hello[List[Int]] = listSayHello(intHello)

  def listSayHello[A](s: Hello[A]) = new Hello[List[A]] {

    override def hello(a: List[A]): Unit = a.map(s.hello)

  }

對List[Int]使用sayHello函式:

  import Hello._

  sayHello(List(1, 2, 3))

到現在sayHello函式滿足我們的要求,不過我們最開始想的是任何型別都可以呼叫sayHello方法,而不是寫個sayHello方法施用到任何型別,這時候需要通過方法注入來實現:

trait HelloOp[A] {

  val h: Hello[A]

  val a: A

  def sayHello(): Unit = h.hello(a)
}


object HelloOp {

  implicit def toHelloOp[A](a1: A)(implicit h1: Hello[A]): HelloOp[A] = new HelloOp[A] {

    override val a: A = a1

    override val h: Hello[A] = h1

  }
}

現在通過toHelloOp這個隱式方法我們可以將任何型別A都轉換成HelloOp[A]型別,然後呼叫sayHello方法。看下如何使用HelloOp :

  import HelloOp._

  "zdx".sayHello()

  1.sayHello()

  List(1, 2, 3).sayHello()

同樣在控制檯測試一下:


7571416-23114e59256b2426.png
image.png

看完這些例子可能覺得型別類的技巧好像很花哨但是卻不是很實用,其實型別類type class是從Haskell中傳來的概念,當然Scala社群也有ScalaZ和Cats等庫提供了豐富的TypeClass,通過這些庫可以方便的編寫我們得程式,當然這就是一個很大的課題了,留作以後再說明。

相關文章