Chapter 21《Implicit Conversions and Parameters》

隐式转换和隐式参数

  • 如果使用别人的代码库,无法进行修改,Scala进行扩展的方法是隐式转换和隐式参数。允许省略掉冗余且明显的细节。

隐式转换
  • 隐式转换通常在两个开发完全不知道对方存在的软件或类库时非常有用。如果双方都描述了同一样概念,隐式转换可以减少从一个类型显式转换成另一个类型的需要。

隐式规则

    1. 隐式定义指的是那些我们允许编译器插入程序以解决类型错误的定义。如果x + y不能通过编译,编译器可能会改为convert(x) + y,其中convert是某种可用的隐式转换,如果convert(x)的对象支持+动作,那么这个改动就可能修复程序,让它通过类型检查并正确运行。如果convert真的是某种简单的转换函数,可以不显式地写出这个方法。
    1. 隐式转换受如下规则的约束:
      1. 标记规则:只有标记为implicit的定义才可用。implicit用来标记哪些声明可被编译器用作隐式定义。编译器只会从那些显式标记为implicit的定义中选择。
      1. 作用域规则:被插入的隐式转换必须是当前作用域的单个标识符,而且跟隐式转换的源类型或者目标类型有关联。必须将隐式转换引入到当前作用域才能使得它们可用,例如,编译器不会引入varaiable.convert这种语法,必须要引入它,使其成为单个标识符。
      • 2.1. 编译器会在隐式转换的源类型或目标类型的伴生对象中查找隐式定义。例如尝试将Dollar对象传递给一个接收Euro的函数,编译器会在DollarEuro的伴生对象中寻找隐式转换。在伴生对象中定义的隐式转换可以不引入直接使用。就是隐式定义是在当前作用域有效的,而不是全部有效。
      1. 每次一个规则:每次只能有一个隐式定义被插入。
      1. 显式优先原则:只要代码按照编写的样子能够通过类型检查,就不会启动隐式转换。
    1. 命名一个隐式转换:隐式转换可以使用任何名称,名称并不重要,只有在显式引入该转换的时候使用以及决定在程序的某个位置都有哪些隐式转换可用时用到。例如,在一个对象中有两个隐式转换,intToStringStringToStringWrapper,如果只想将String转换为StringWrapper,而不想将Int转换为String,可以只引入第二个隐式转换。
  • 4.尝试隐式转换的地方:1.转换到一个预期的类型;2.对某个选择接收端的转换;3.隐式参数
A. 隐式转换到一个预期的类型
  • 当编译器发现X而需要Y的时候,查找能够将X转换为Y的隐式转换。注意隐式转换的引入需要在使用之前,不然编译器不会发现这个隐式转换。
    scala> val i: Int = 3.5
    <console>:7: error: type mismatch;
    scala> implicit def doubleToInt(x: Double) = x.toInt
    doubleToInt: (x: Double)Int //这个隐式转换需要放在定义语句之前。
    
    doubleToInt是单个标识符,如果不是定义在当前作用域中,可以使用import或者extend或者with特质来导入。类似Double转向Int这种通用类型转向受限类型的转换会丢失精度,但是反方向的转换是定义得通的,需要自己定义。在Predef中有从IntDouble的转换。
B. 转换接收端
  • 隐式转换还能应用关于方法调用的接收端,也就是方法被调用的那个对象。

  • 这个隐式转换主要有两种用途,1.接收端转换允许我们更平滑地将新类继承到已有的类继承关系图谱中,2.支持在语言中编写领域特定语言(DSL)。假如写下obj.doIt,但是obj中并不存在名为doIt的成员,编译器会在放弃之前尝试插入转换。在本例中,这个转换需要应用于接收端,也就是obj,编译器会寻找obj到预期类型的转换,这个预期类型中拥有名为doIt的成员。

    1. 与新类型互操作
    • 接收端转换的一个主要用途是让新类型和已有类型的集成更顺滑。可以让客户端的代码不改变,就像是在使用新类型一样。1 + new Rational(3,4) 可插入一个隐式转换,最后变成intToRational(1) + new Rational(3,4),完美解决Int类型中没有+(rational: Rational)这个方法。
    2. 模拟新的语法
    • 隐式转换的另一个用途是模拟添加新的语法,例如Map中的语法:Map(1 -> "1"),整体的操作过程是这样的。编译器插入any2ArrowAssoc的转换,将1转换为带有->方法的ArrowAssoc的对象,从而可以调用->,这看起来就像是一个新的语法。如果某个对象调用了不属于自己的方法,那么很有可能是使用了隐式转换。可以使用这些富包装类模式做出以类库形式定义的内部DSL
    3. 隐式类
    • Scala 2.10引入了隐式类来简化富包装类的编写。隐式类是以implicit打头的类,对于这样的类,编译器会生成一个从类的构造方法参数到类本身的隐式转换。例如,
    case class Rectangle(width: Int, height: Int)
    

    如果经常使用这个类,可以使用富包装类来简化构造工作,定义:

    implicit class RectangleMaker(width: Int) {
    def x(height: Int) = Rectangle(width, height)
    }
    // Automatically generated
    implicit def RectangleMaker(width: Int) =
    new RectangleMaker(width)
    

    对于以上的隐式类,会自动生成类构造参数到该类对象的一个转换。可以直接使用3 x 4这样的形式。

    scala> val myRectangle = 3 x 4
    myRectangle: Rectangle = Rectangle(3,4)  
    

    并不是任何类定义前面度可以放implicit,隐式类不能是样例类,并且其构造方法必须有且仅有一个参数。隐式类必须存在于一个对象、类或者特质里面。

C. 隐式参数
  • 编译器会插入隐式定义的最后一个地方是参数列表,编译器有时候会将someCall(a)替换为someCall(a)(b),通过追加一个参数列表来完成某个函数的调用。隐式参数提供的是整个最后一组currying的参数列表,而不仅仅是最后一个参数。例如,someCall(a)可能根据实际情况被替换为someCall(a)(b, c, d)。如果要让编译器隐式地填充隐式参数,首先需要定义这样一个符合预期类型的变量。填充的变量也必须声明为implicit的,如果不是这样,编译器不会使用它来填充缺失的列表。如果变量不是当前作用域内的单个标识符,也不会被采纳。implicit关键字是应用到整个参数列表而不是单个参数的。
    def greet(name: String)(implicit prompt: PreferredPrompt, drink: PreferredDrink) = {} 
    
  • 由于编译器是在作用域内通过类型匹配来填充隐式参数,所以一般希望类型能够特殊从而避免误匹配的出现。隐式参数最常使用的场景是提供关于在更靠前的参数列表中已经显式提到的类的信息。
    def maxListOrdering[T](elements: List[T])(implicit ordering: Ordering[T]): T 
    
    隐式参数ordering说明了已知类型T更多的信息。有了Tordering的类型就已知,可以使用隐式参数隐式插入。
  • 隐式参数的代码风格,最好是对隐式参数使用定制名称的类型,比如PreferredPromptPreferredDrink,而不是StringString,而且这个类型命名时,至少使用一个能明确其智能的名字,比如Ordering。如果定义成为implicit (T,T) => T,该参数并没有透露出任何关于该烈性用途的信息,范围太广,很容易误使用。

上下文界定
  • 当一个参数是隐式定义的时候,在函数体内又使用了这个隐式参数作为参数传递,这时可以将不用写这个参数。
    def maxListOrdering[T](elements: List[T])(ordering: Ordering[T]): T = {
        val maxRest = maxListOrdering(rest)(ordering)  //这个ordering可以省略
    }
    def maxListOrdering[T](elements: List[T])(ordering: Ordering[T]): T = {
        if (ordering.gt(x, maxRest)) x else maxRest //为了避免使用ordering,
    //可以使用库函数  > implicit[Ordering[T]],该函数返回的是Ordering[T]的隐式对象。
    //这样ordering就可以随意命名。
    }
    
    由于上文中提到的模式很常用,Scala允许声调这个参数的名称并使用上下文界定来缩短方法签名。[T: Ordering]是一个上下文界定,context bound,做了两件事情。1. 引入类型参数T,2.添加一个Ordering[T]的隐式参数,并不需要知道这个参数的名字。最后的代码如下:
    def maxList[T : Ordering](elements: List[T]): T =
    elements match {
    case List() =>
    throw new IllegalArgumentException("empty list!")
    case List(x) => x
    case x :: rest =>
    val maxRest = maxList(rest)
    if (implicitly[Ordering[T]].gt(x, maxRest)) x
    else maxRest
    }
    

当有多个转换可选时

  • 如果有多个转换可选,Scala会拒绝插入,隐式转换在这个转换是显而易见的并且纯粹是样板代码的时候最好用。Scala目前采取的措施是使用可用转换中更具体的转换,具体体现在,该转换的入参类型是别的转换的子类型;两者都是方法,具体转换所在的类扩展自通用转换所在的类。

调试

  • 调试的时候可以显示地写出来转换用以明确编译器插入的转换到底是哪一个。也可以对编译器设置选项-Xprint:typer,使用这个选项来运行scalac,可以看到添加了隐式转换之后的代码。使用的方法:scalac -Xprint:typer test.scala。隐式转换还是不能滥用的。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352

推荐阅读更多精彩内容