Scala剖析 03: 隐式转换

Scala剖析 03: 隐式转换,第1张

目录

前言

当"隐式"不生效时

定义普通变量进行编译

单独定义隐式值,没有隐式参数与其对应时

隐式方法

类型转换

功能扩展

隐式值和隐式参数

隐式类

武魂融合技

上下文界定

隐式值是匿名内部类 

implicitly


前言

scala 同 java 一样,运行在 JVM 上.其语法的灵活和易用可以理解为都是在 java 语言的基础上封装了丰富的语法糖.那scala 不同的语言特性"翻译"为 java 又是什么样子,Scala 剖析系列将会从零开始进行探索.

本文主要讲述了 scala 中隐式转换在反编译 Java 结果中的体现,目的是从 Java 的角度充分理解隐式转换.文章适合有 scala 基础清楚 implicit 相关语法和应用场景的同学阅读.

当"隐式"不生效时

隐式值或者隐式方法都是在被需要的时候才会生效.比如我们定义了一个单独的隐式值,而整个代码的作用域内没有需要隐式值填充的方法,则该隐式值应该是不生效的.下面我们举例说明:

定义普通变量进行编译

scala 代码如下

object ImplicitTest{
    val i : Int = 1
}

反编译后的 Java 代码如下

public final class ImplicitTest$ {
    public static ImplicitTest$ MODULE$;
    private final int i;
    public int i() {
        return this.i;
    }

    private ImplicitTest$() {
        MODULE$ = this;
        this.i = 1;
    }
}

为什么编译结果是这个样子在上一章 val,var和 def 里有详细讲述,此处不再赘述.

单独定义隐式值,没有隐式参数与其对应时

scala代码:

object ImplicitTest{
    implicit val i : Int = 1
}

反编译后的 Java 代码:

public final class ImplicitTest$ {
    public static ImplicitTest$ MODULE$;
    private final int i;
    public int i() {
        return this.i;
    }

    private ImplicitTest$() {
        MODULE$ = this;
        this.i = 1;
    }
}

可以看到隐式值在没有触发其"隐式"特性的时候,字节码层面同普通的变量定义并没有区别.隐式方法也是同样的道理,不再举例说明.

隐式方法 类型转换

隐式方法的作用是在传入方法的参数类型不是其所期望的类型时,会自动寻找是否存在对应的隐式转换方法.该功能在字节码的体现上是怎样的呢?

下边的一段代码定义了一个 String2Int 的隐式函数和一个需要 Int 类型的方法,此时在 main 函数中给 needInt 方法传入一个 String 类型的参数,会触发隐式转换.

object ImplicitTest{

    implicit def implicitFunc(s: String) = {s.toInt}

    def main(args: Array[String]): Unit = {
        val s : String = "1"
        needInt(s) // int
    }

    def needInt(i : Int) = {println(i.getClass.getSimpleName)}

}

反编译的 Java 代码 ImplicitTest$.class main 方法:

public void main(String[] args) {
    String s = "1";
    needInt(implicitFunc(s));
}

可以看到 needInt 在传入 String 变量 s 时,会自动调用隐式方法.而隐式方法 implicitFunc 本身在反编译的结果中并没有其他特殊的 *** 作.只是一个简单的静态方法. ImplicitTest.class 代码如下

public final class ImplicitTest {
    public static void needInt(int paramInt) {
        ImplicitTest$.MODULE$.needInt(paramInt);
    }

    public static void main(String[] paramArrayOfString) {
        ImplicitTest$.MODULE$.main(paramArrayOfString);
    }

    public static int implicitFunc(String paramString) {
        return ImplicitTest$.MODULE$.implicitFunc(paramString);
    }
}

所以我们可以理解为 implicit 只被 scala 的编译器识别,JVM 在指令集中(粗略的翻了一下)没有其对应的 *** 作.

功能扩展

隐式方法的另一个功能是对现有类进行功能扩展.语法和概念此处不详细描述,我们主要看scala 代码案例和它反编译后的结果

scala 代码

object ImplicitTest{

    implicit def DB2Dog(db: DB) = {new Dog}

    def main(args: Array[String]): Unit = {
        val db: DB = new DB
        db.sleep //sleep
    }
}

class DB{
    def read = {println("read")}
}

class Dog{
    def sleep = {println("sleep")}
}

定义了两个简单的类 DB 和 Dog,通过隐式转换的方式给 DB 类扩展了 Dog 的 sleep 方法.那在字节码反编译为 Java 时又是如何实现的呢?Java 代码如下:

public Dog DB2Dog(DB db) {
    return new Dog();
}

public void main(String[] args) {
    DB db = new DB();
    DB2Dog(db).sleep(); // 重点
}

可以看到,还是同样简单的对代码做了自动化的嵌套. DB2Dog(db)返回的就是一个 Dog 对象,而 Dog 对象本身具有 sleep 方法所以能够完成调用.

隐式值和隐式参数

前边讲到,单独定义隐式值而 没有方法需要 该隐式值类型的隐式参数时(有点绕),隐式值是不生效的.所以隐式值一般都是同隐式参数配合使用.因为隐式值的自动装配特性,项目中大多都是柯里化函数搭配隐式参数,看个代码样例:

object ImplicitTest{

    def main(args: Array[String]): Unit = {
        implicit val s : String = "String"
        val i: Int = 1
        needIntAndStr(i)// 1 String
    }

    def needIntAndStr(i : Int)(implicit s: String) = {
        print(i + "\t" + s)
    }
}

needIntAndStr需要两个参数,只显示的传递一个i,隐式值 s 会自动装配.方法输出结果为 "1 String".反编译后的 Java 代码如下:

ImplicitTest$.class的 main 方法中定义的 s 直接去掉了 implicit 关键字

public void main(String[] args) {
    String s = "String";
    int i = 1;
    needIntAndStr(i, s);
}

needIntAndStr方法也直接被编译为了需要两个参数

public void needIntAndStr(int i, String s) {
    scala.Predef$.MODULE$.print((new StringBuilder(1)).append(i).append("\t").append(s).toString());
}

代码里的一个小细节: String 在执行加运算时,实际底层会创建一个 StringBuilder 进行不断的 append.这一点不光在 scala 的反编译场景下,在 Java 中也是通用的.使用StringBuilder会大大提高代码运行效率.

隐式类

隐式类实际上是隐式方法的另一种方便形式.当我们需要对一个类行进多项功能的扩展,而这些功能又分布在不同的功能类中时,就需要定义多个隐式方法,而隐式类可以将所有方法写在一个类中并省略 implict 关键字.样例如下:

object ImplicitTest{
    def main(args: Array[String]): Unit = {
        1.add(1)
        1.inc()
    }

    implicit class IntPlus(i : Int) {
        def add(o : Int) = {i + o}
        def inc() = {i+=1}
    }
}

而隐式类进行多种功能扩展和多种类型转换时,反编译的结果同隐式方法的情况相同,就不再举例.

武魂融合技 上下文界定

Scala 的上下文界定中,要求必须有期界定类型XXX[DB]的隐式值.scala 代码如下

上图的代码中,needDB 使用了上下文界定(T: DB),所以在调用 needDB 方法的时候必须存在DB[T], 所以上边代码中增加一个对应的隐式值就可以正常运行了.

object ImplicitTest{
    def main(args: Array[String]): Unit = {
        implicit val db: DB[Int] = new DB[Int] // 隐式值
        needDB[Int]
    }

    //上下文界定
    def needDB[T: DB] = {println("needDB exec")}
}

class DB[A]{
    def read(a: A) : String = "String"
}

反编译为 Java 代码:

public void main(String[] args) {
    DB db = new DB();
    needDB(db);
}

public  void needDB(DB evidence$1) {
    scala.Predef$.MODULE$.println("needDB exec");
}

视界(view bounds)是对隐式转换在泛型上的简单应用,就是当泛型设置为 class A[B <% C]时,C 应该是 B 的子类,如果找不到子类,就去隐式转换里找有没有可以把其他对象转换为 C 的方法,如果有,则调用隐式转换.比较简单,不展开讲啦. 

隐式值是匿名内部类 

当隐式值是一个匿名内部类时,好像显得稍稍复杂一点,但其实同隐式值和隐式参数小节讲的没有区别.

scala 代码

object ImplicitTest{
    def main(args: Array[String]): Unit = {
        implicit val db: DB[Int] = new DB[Int]{
            def read(i: Int) = i.toString
        }
        needDB
    }

    def needDB(implicit db: DB[Int]) = {db.read(1)}
}

trait DB[A]{
    def read(a: A) : String
}

不管DB[Int]是匿名内部类还是直接创建对象,翻译为 Java 代码,main 方法中needDB 方法都会被编译为需要一个 db 参数. ImplicitTest$$anon$1就是实现于DB[Int]的匿名类的名字.Java 反编译代码如下:

public void main(String[] args) {
    DB db = new ImplicitTest$$anon$1();
    needDB(db);
}

public  void needDB(DB evidence$1) {
    scala.Predef$.MODULE$.println("needDB exec");
} 

结合 1.2 两点,可以发现上下文限定和隐式参数的形式是十分类似的,只是上下文限定可以动态指定要哪个类型的隐式参数.

一般上下文限定都会跟 implicitly 方法结合使用.

implicitly

implicitly是 scala 的 Predef 包里的方法.方法签名如下

def implicitly[T](implicit e: T) = e

定义了一个T 类型的隐式参数 e,方法返回值就是这个 e. 所以这个方法的功能就是寻找作用域内对应的隐式参数.如果我们定义 val db = implicitly[DB[Int]], 变量 db 的结果就是一个 DB[Int] 类的对象.

综合以上三点,我们可以写出如下的代码

object ImplicitTest{
    def main(args: Array[String]): Unit = {
        implicit val db: DB[Int] = new DB[Int]
        needDB[Int](1)
    }

    def needDB[T: DB](i: T) = {
        val db: DB[T] = implicitly[DB[T]]
        db.read[T](i)
    }
}

class DB[A]{
    def read[A](a: A) : String = a.toString
}

needDB 中没有使用隐式参数和显式参数,通过 上线文界定+ implicitly 方法就可以得到想要的 db 对象进行功能调用.该功能的应用场景主要是在函数 needDB 已经是柯里化且传递了一个 implicit 参数的情况下.因为函数柯里化只能有一个隐式参数.

以上就是 scala 中跟隐式转换相关的内容.感谢您的耐心阅读.

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/756900.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-30
下一篇 2022-04-30

发表评论

登录后才能评论

评论列表(0条)