zoukankan      html  css  js  c++  java
  • 【Scala笔记——道】Scala 隐式Implicit

    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 的隐式值。

    • 隐式值匹配。两个或多个隐式值可能引发歧义(例如:它们具有相同的类型),编译错误会被触发。
  • 相关阅读:
    Laya页面嵌套和Scene.destory导致的Bug
    Laya的滚动容器Panel+HBox
    Laya的对象唯一标识
    Android自带的TTS功能
    一步一步学android之控件篇——ListView基本使用
    android surfaceView 的简单使用 画图,拖动效果
    Android 数据分析系列一:sharedPreferences
    Android Service总结
    android中碰撞屏幕边界反弹问题
    Android开发:setAlpha()方法和常用RGB颜色表----颜色, r g b分量数值(int), 16进制表示 一一对应
  • 原文地址:https://www.cnblogs.com/cunchen/p/9464091.html
Copyright © 2011-2022 走看看