【Scala笔记——道】Scala 隐式Implicit

版权声明:转载请注明出处,欢迎各位斧正 https://blog.csdn.net/wang135139/article/details/81228250

Scala隐式

隐式究竟是什么呢?

scala 中隐式相较于java 来说是一种全新的特性。那么隐式究竟是什么呢?
隐式存在三种基本使用方式:
- 隐式属性
- 隐式方法
- 隐式对象

隐式属性例如

implicit val int size = 5

def caculate(a : Int)(implicit val size) = a * size

在这种情况下,隐式是作为一种上下文属性进行输入,更复杂的情况就像上文中讲到的执行上下文和功能控制。其本质上是一种资源的注入,隐藏/省略资源的显示配置,而达到装载的目的。
讲到这里相信很容易联想到Spring中的IOC,在Spring/Guice的IOC中我们通过注解/配置这种形式来实现注入资源实例化。Spring/Guice中对于注入的资源,我们可以通过xml或者注解完成资源属性的配置,但是这种方法存在不足,xml或者注解配置都属于静态配置,如果我们需要一些动态特性的时候需要额外的去做很多工作。
就比如我们有一个支付Controller,在支付方式是美金的时候我们需要调用美金服务,在支付方式为人民币的时候我们需要调用人民币服务。

class PayController @Inject()settlementService: SettlementService) { // 这种情况下只能注入一种结算方式,无法实现动态结算

  def doSettle(bill: Bill) = {
    settlementService.doSettle(bill)
  }

}

如果要实现这种结算,我们就必须手动通过代码在判断 bill.getType 后手动实现对应结算功能。但这样就引入了耦合。

而隐式属性无疑是一种更好的方式,这里可以自由注入结算方式


class PayController ()(implicit val settlementService: SettlementService) {
  def doSettle(bill: Bill) = {
    settlementService.doSettle(bill)
  }
}

函数方法本质上是进行一种转化,这种转化不依赖上下文,也就是说
f(a) => b 不会影响任何其他的状态,也可以称作无副作用。
隐式函数方法,本质上也是一种函数方法,可以看做是对于元素的一种转化关系,由 a => b。
在java中 facade模式是比较常用的一种模式,facade模式提供的是对于接口信息的封装。
在系统开发或业务开发中facade模式是使用比较频繁的,在java中可能我们对应不同系统接口会提供不同facade,但对于不同facade的转化都需要在代码中手动装填。
通过方法级隐式转化我们可以方便的实现接口级的隐式转化。

例如下文中我们对于订单进行扩展,实际结算的订单可能涉及线上和线下两种订单,但最终订单信息都会被转化为内部的 BillInfo。
这里我们通过方法级隐式转化,直接实现 OnlineBillFacade/OfflineBillFacade => BillInfo
而不需要大量判断代码实现逻辑控制。

sealed trait Bill
case class OnlineBillFacade(count: Int,
                      platform: Platform,
                      currency: Currency) extend Bill

case class OfflineBillFacade(count: Int,
                      address: String,
                      shop: Shop,
                      currency: Currency) extend Bill

case class BillInfo(
                      flowNumber: Long,
                      createTime: Long,
                      state: State,
                      count: Int,
                      platform: Platform,
                      address: String,
                      shop: Shop,
                      currency: Currency) extend Bill


object BillConverter {

  implicit def onlineBill2BillInfo(facade: OnlineBillFacade) : BillInfo = ...
  implicit def offlineBillFacade(facade: OfflineBillFacade) : BillInfo  = ...
}

class PayController ()(implicit val settlementService: SettlementService) {

  def doSettle[T <: Bill](bill: T) = {
    settlementService.doSettle((BillInfo)bill)
  }

}

对于隐式对象,无疑是对于AOP思想的进一步的探索。在AOP中我们想要不改变源码还要增加功能,AOP中我们通过动态代理实现功能的扩展。
通过动态代理,我们可以方便的实现切面控制。面向切面编程实际上有一个前提,就是我们的一切其实都得围绕接口进行设计,切面所能控制的最小粒度是就是方法级。
并且由于是泛型配置,事实上如果要在切面中使用通知时,还需要对于输入参数进行筛选判断而完成泛型管理,这部分工作很不利于扩展,事实上这里我们没有办法对于泛型进行类型强约束。

而隐式对象为我们带来了全新的可能,由于隐式对象是面向POJO,因此隐式对象相较AOP拥有更细的粒度控制。并且由于是针对POJO,隐式对象不需要进行边界界定。
通过隐式对象,我们可以真正在不改变原有代码基础上实现功能的扩展。

 应用

  • 执行上下文
  • 功能控制
  • 限定可用实例
  • 隐式证据
  • 类型擦除
  • 改善报错
  • 虚类型

执行上下文

通用的上下文信息通过隐式默认实现,降低耦合

编写事务、数据库连接、线程池以及用户会话时隐式参数上下文也同样适合使用。使用方法参数能组合行为,而将方法参数设置为隐式参数能够使 API 变得更加简洁。

// 导入了可供编译器使用的全局默认值
import scala.concurrent.ExecutionContext.Implicits.global

apply[T](body: => T)(implicit executor: ExecutionContext): Future[T]

功能控制

通过引入授权令牌,我们可以控制某些特定的 API 操作只能供某些用户调用,我们也可以使用授权令牌决定数据可见性,而隐式用户会话参数也许就包含了这类令牌信息。

def createMenu(implicit session: Session): Menu = {
  val defaultItems = List(helpItem, searchItem)
  val accountItems =
  if (session.loggedin()) List(viewAccountItem, editAccountItem)
  else List(loginItem)
  Menu(defaultItems ++ accountItems)
}

限定可用实例

对具有参数化类型方法中的类型参数进行限定,使该参数只接受某些类型的输入

package implicits

object Implicits {
    import implicits.javadb.JRow


    implicit class SRow (jrow: JRow){

        def get[T](colName: String)(implicit toT: (JRow, String) => T): T =
            toT(jrow, colName)
    }


    implicit val jrowToInt: (JRow, String) => Int = (jrow: JRow, colName: String) => jrow.getInt(colName)
    implicit val jrowToDouble: (JRow, String) => Double = (jrow: JRow, colName: String) => jrow.getDouble(colName)
    implicit val jrowToString: (JRow, String) => String = (jrow: JRow, colName: String) => jrow.getText(colName)

    def main(args: Array[String]) = {
        val row = javadb.JRow("one" -> 1, "two" -> 2.2, "three" -> "THREE!")
        val oneValue1: Int = row.get("one")
        val twoValue1: Double = row.get("two")
        val threeValue1: String = row.get("three")
//                 val fourValue1: Byte = row.get("four")
        // 不编译该行
        println(s"one1 -> $oneValue1")
        println(s"two1 -> $twoValue1")
        println(s"three1 -> $threeValue1")
        val oneValue2 = row.get[Int]("one")
        val twoValue2 = row.get[Double]("two")
        val threeValue2 = row.get[String]("three")

//         val fourValue2 = row.get[Byte]("four")
        // 不编译该行
        println(s"one2 -> $oneValue2")
        println(s"two2 -> $twoValue2")
        println(s"three2 -> $threeValue2")
    }
}


package database_api {

    case class InvalidColumnName(name: String)
        extends RuntimeException(s"Invalid column name $name")

    trait Row {
        def getInt      (colName: String): Int
        def getDouble   (colName: String): Double
        def getText     (colName: String): String
    }
}

package javadb {
    import database_api._

    case class JRow(representation: Map[String, Any]) extends Row {
        private def get(colName: String): Any =
            representation.getOrElse(colName, throw InvalidColumnName(colName))

        def getInt      (colName: String): Int      = get(colName).asInstanceOf[Int]
        def getDouble   (colName: String): Double   = get(colName).asInstanceOf[Double]
        def getText     (colName: String): String   = get(colName).asInstanceOf[String]
    }

    object JRow {
        def apply(pairs: (String, Any)*) = new JRow(Map(pairs :_*))
    }
}

隐式证据

有时候,我们只需要限定允许的类型,并不需要提供额外的处理。换句话说,我们需要
“证据”证明提出的类型满足我们的需求。现在我们将讨论另外一种被称为隐式证据的相
关技术来对允许的类型进行限定,而且这些类型无需继承某一共有的超类。

trait TraversableOnce[+A] ... {
...
def toMap[T, U](implicit ev: <:<[A, (T, U)]): immutable.Map[T, U]
...
}

我们曾提及过,可以使用中缀表示法表示由两个类型参数所组成的类型,因此下列两种表
达式是等价的:
<:<[A, B]
A <:< B
在 toMap 中, B 实际上是一个 pair:
<:<[A, (T, U

类型擦除

object M {
implicit object IntMarker
implicit object StringMarker
def m(seq: Seq[Int])(implicit i: IntMarker.type): Unit = println(s"Seq[Int]: $seq")
def m(seq: Seq[String])(implicit s: StringMarker.type): Unit =
println(s"Seq[String]: $seq")
}

虚类型 Phantom Type

类似于类型擦除,虚类型仅用于标记。

虚类型本身实际上不属于隐式转换的范畴,但这里其实和类型擦除在使用之上有一定的相似之初。

虚类型主要有以下两个优点:
- 使无效状态无法代表。最好的体现就是 List、Cons 以及 Nil的关系
- 携带类型级别的一些信息

例如通过虚类型限制距离单位

case class Distance[A](x: Double) extends AnyVal

case object Kilometer
case object Mile

def marathonDistance: Distance[Kilometer.type] = Distance[Kilometer.type](42.195)

def distanceKmToMiles(kilos: Distance[Kilometer.type]): Distance[Mile.type] =
    Distance[Mile.type](kilos.x * 0.621371)

def marathonDistanceInMiles: Distance[Mile.type] = distanceKmToMiles( marathonDistance )

隐式报错

@implicitNotFound(msg =
"Cannot construct a collection of type ${To} with elements of type ${Elem}" +
" based on a collection of type ${From}.")
trait CanBuildFrom[-From, -Elem, +To] {...}

类型类模式

不同于java 子类型多态, 这一功能也被成为 特设多态(ad hoc polymorphism)
scala java 共有 参数化多态(paremetric polymorphism)

case class Address(street: String, city: String)
case class Person(name: String, address: Address)
trait ToJSON {
def toJSON(level: Int = 0): String
val INDENTATION = " "
def indentation(level: Int = 0): (String,String) =
(INDENTATION * level, INDENTATION * (level+1))
}
implicit class AddressToJSON(address: Address) extends ToJSON {
def toJSON(level: Int = 0): String = {
val (outdent, indent) = indentation(level)
s"""{
|${indent}"street": "${address.street}",
|${indent}"city":
"${address.city}"
|$outdent}""".stripMargin
}
}
implicit class PersonToJSON(person: Person) extends ToJSON {

探究隐式

正如上述使用场景所述,隐式在scala中给我们带来很多惊喜。通过隐式,我们也可以更好的解决上下文处理、边界处理、类型擦除等问题。

隐式不足

为何不适用简单类型 + 类型类模式

  • 额外时间编写隐式代码
  • 编译会花费额外时间
  • 运行开销,隐式代码本质是反射
  • 隐式特征与其他 Scala 特征,尤其是子类型特征发生交集时,会产生一些技术问题 [scala-debate email 邮件组]
trait Stringizer[+T] {
def stringize: String
}
implicit class AnyStringizer(a: Any) extends Stringizer[Any] {
def stringize: String = a match {
case s: String => s
case i: Int => (i*10).toString
case f: Float => (f*10.1).toString
case other =>
throw new UnsupportedOperationException(s"Can't stringize $other")
}
}
val list: List[Any] = List(1, 2.2F, "three", 'symbol)
list foreach { (x:Any) =>
try {
println(s"$x: ${x.stringize}")
} catch {
case e: java.lang.UnsupportedOperationException => println(e)
}
}

我们定义了一个名为 Stringizer 的抽象体。如果按照之前 ToJSON 示例的做法,我们会为所有我们希望能字符串化的类型创建隐式类。这本身就是一个问题。如果我们希望处理一组不同的类型实例,我们只能在 list 类型的 map 方法内隐式地传入一个 Stringizer 实例。
因此,我们就必须定义一个 AnyStringerize 类,该类知道如何对我们已知的所有类型进行处理。这些类型甚至还包含用于抛出异常的 default 子句。这种实现方式非常不美观,同时也违背了面向对象编程中的一条核心规则——你不应该使用 switch 语句对可能发生变化的类型进行判断。相反,你应该利用多态分发任务,这类似于 toString 方法在 Scala 和 Java 语言中的运作方式。

隐式使用注意

  • 无论何时都要为隐式转换方法指定返回类型。否则,类型推导推断出的返回类型可能会导致预料之外的结果。
  • 虽然编译器会执行一些“方便”用户的转换。但是目前来看这些转换带来的麻烦多过益处(以后推出的 Scala 也许会修改这些行为)。

例如:
假如你为某一类型定义方法 + ,并试图将该方法应用到某一不属于该类型的实例上,
那么编译器会调用该实例的 toString 方法,这样一来便能执行 String 类型的 + 操作(合
并字符串操作)。这可以解释某些特定情况下出现像 String 是错误类型的奇怪错误

与此同时,如果有必要的话,编译器会将方法的输入参数自动组合成一个元组。有时候这
一行为会给人造成困扰。幸运的是,Scala 2.11 现在会抛出警告信息。

scala> def m(pair:Tuple2[Int,String]) = println(pair)
scala> m(1, "two")
<console>:9: warning: Adapting argument list by creating a 2-tuple:
this may not be what you want.
signature: m(pair: (Int, String)): Unit
given arguments: 1, "two"
after adaptation: m((1, "two"): (Int, String))
m(1,"two")

隐式解析规则

  • Scala 会解析无须输入前缀路径的类型兼容隐式值。换句话说,隐式值定义在相同作用域中。例如:隐式值定义在相同代码块中,隐式值定义在相同类型中,隐式值定义在伴生对象中(如果存在的话),或者定义在父类型中。
  • Scala 会解析那些导入到当前作用域的隐式值(这也无须输入前缀路径)。

  • 隐式类 Scala匹配。 将挑选匹配度最高的隐式。举个例子,如果隐式参数类型是 Foo 类型,而当前作用域中既存在 Foo 类型的隐式值又存在AnyRef 类型的隐式值,那么 Scala 会挑选类型为 Foo 的隐式值。

  • 隐式值匹配。两个或多个隐式值可能引发歧义(例如:它们具有相同的类型),编译错误会被触发。

猜你喜欢

转载自blog.csdn.net/wang135139/article/details/81228250