Scala的隐式转换和参数

Key Words: Implicit conversions, Implicit parameters, Implicit class, Implicitly, Context bound

隐式转换(Implicit conversions)

隐式定义就是允许编译器插入一段程序来解决一些类型错误(type errors)的手段

对于两个软件体,彼此设计时都没有考虑到对方,此时隐式转换通常比较有用。隐式转换可以减少代码中将一个类型转换为另一个类型的显示转换(explicit conversions)

隐式规则

标记规则(Marking rule)

只有标记为implicit的定义才可用。可以用 implicit 来标记任何变量、函数或对象定义

编译器只会在显示标识为 implicit 的定义中进行选择

作用域规则(Scope rule)

被插入的隐式转换必须是当前作用域的单个标识符(single identifier),或者跟隐式转换的源类型或目标类型有关联

Scala 编译器只会考虑那些在作用域内的隐式转换。因此,必须以某种方式将隐式转换定义引入到当前作用域才能使得它们可用

编译器还会在隐式转换的源类型或目标类型的伴生对象中查找隐式定义

对于类库而言,常见的做法是提供一个包含了一些有用的隐式转换的 Preamble 对象,这样使用这个类库的代码就可以通过一个“import Preamble._”来访问该类库的隐式转换

作用域规则有助于模块化推理(modular reasoning)。当你阅读一个代码文件,你只需要考虑被引用的或者是使用全限定名的定义。这样显示写代码的好处对于对于隐式(implicits)也同样的重要。如果隐式在系统范围(system-wide)内有效,那么想理解一个源文件就不得不了解每个被引入的隐式

每次一个规则(One-at-a-time rule)

每次只能有一个隐式定义被插入

这个规则能够减少有问题代码的编译时间,以及减少程序员编写的和程序实际做的之间的差异

可以通过在隐式定义中使用隐式参数来绕过这个规则

// 下面的代码只是为了说明绕过每次一个规则(One-at-a-time rule),没有任何实际意义
implicit vall height = 1
case class Foo(width: Int, height: Int) {
  def bar = println(s"Foo: ${width} ${height}")
}

implicit def convert2Foo(width: Int)(implicit height: Int) = Foo(width, height)

1 bar

// 这样就能让编译器插入两个隐式定义了

显示优先原则(Explicits-first rule)

只要代码按编写的样子能够通过类型检查,编译器就不会尝试隐式定义

其实,到底留多少隐式转换给编译器来插入,最终是代码风格的问题

隐式转换命名

隐式转换可以使用任意的名字。只有在两个地方对于命名需要进行注意:

1.在方法的应用中,显示的写出隐式转换
2.决定哪些隐式可以在程序的哪些地方可用

隐式应用场景

在 Scala 中隐式有三个应用场景:类型的转换(conversions to an expected type)、接受者的转换(conversions fo the receiver of a selection)以及隐式参数(implicit parameters)

类型的转换

就是在上下文中使用一种类型,实际上期待另外一种不同的类型

不论何时,编译器看见一个类型 X,但是实际需要的是 Y 类型,编译器就会寻找隐式函数来将 X 转换为 Y

implicit def double2Int(d: Double) = d.toInt
val i: Int = 3.5

此处的类型转换有一个原则:将更多约束的类型转换为一个更为普通的类型

implicit def int2Double(i: Int) = i.toDouble
val d: Double = 1

接受者的转换

可以通俗的理解,在一个对象上调用该对象并不存在的某个方法,编译器会帮你挑选合适的隐式转换,来将此对象转换为含有该方法的对象

case class Foo(f: Int)
case class Bar(b: Int) {
  def doSome = println(s"hello bar: ${b}!")
}
implicit def foo2Bar(foo: Foo) = Bar(foo.f)

Foo(1) doSome

接受者的转换主要有两个用处:第一,可以平滑的集成一个新类到一个已有的类层次中;第二,支持编写 DSL(domain-specific language)

隐式类(Implicit class)

编译器通过隐式类的类参数生成一个隐式转换,该隐式将类参数转换为该类本身

implicit class WaveArrow[A](x: A) {
  def ~> [B](y: B) = new Tuple2(x, y)
}

隐式类的定义有如下约束:
1.隐式类不能是一个样例类(case class),且构造器只能含有一个形参
2.隐式类必须位于某个对象、类或者特质当中

在实际应用当中,如果使用隐式类作为富包装来给已有的类来添加一些方法,上述的约束就无关紧要了

// 上面的隐式类定义,编译器会做如下等效的工作
implicit def WaveArrow[A](x: A) = new WaveArrow[A](x)

Map(1 ~> 2)

隐式参数

隐式参数给调用者提供更多被调用的函数的信息,通常用在泛型函数中

隐式参数用于最后一个柯里化后的参数列表,而不是最后一个参数

由于隐式参数的匹配是通过参数的类型来进行的,所以要求隐式参数的类型尽可能的罕见与特别("rare" or "special"),以此来防止匹配的失误

隐式参数还经常被用来提供关于之前参数列表中显示提到的类型的信息(Implicit parameters is that they are perhaps most often used to provide information about a type mentioned explicitly in an earlier parameter list)

通过下面例子来理解上述语句,隐式参数 ordering 的类型是 Ordering[T],在此例中,该参数提供了更多关于类型 T 的信息,也就是如何对 T 进行排序。类型 T 在 List[T] 中被提及,也就是列表中元素的类型,而 List[T] 出现在 Ordering[T] 之前

def max[T](xs: List[T])(implicit ordering: Ordering[T]): T = {
  xs match {
    case List() => throw IllegalArgumentException
    case List(x) => x
    case x :: last => {
      val m = max(last)(ordering)
      if (ordering.gt(x, m)) x else m
    }
  }
}
隐式参数的风格规则

1.使用定制化的命名类型,也就是说不要使用通用类型

// 使用定制化的命名类型
class Drink(s: String)
implicit val drink: Drink = new Drink("maotai")
def drinkSome(implicit drink: Drink) = {}

// 不要使用通用的类型
implicit val drink: String = "maotai"
def drinkSome(implicit drink: String) = {}

2.在隐式参数类型中至少使用一个能够定义角色的名称

def maxListPoorStyle[T](elems: List[T])(implicit orderer: (T, T) => Boolean): T

上述隐式参数的表达方式就会给读者带来困扰,也不能提供关于类型 T 的更多信息(比如函数式判断相等、大于还是小于)

上下文边界(Context bounds)

当在一个函数的形参上面使用隐式,编译器不仅试着用隐式值来补充该形参,而且在方法体中把该形参当做可用的隐式

def max[T](xs: List[T])(implicit ordering: Ordering[T]): T = {
  xs match {
    case List() => throw IllegalArgumentException
    case List(x) => x
    case x :: last => {
      val m = max(last)                        // (ordering) is implicit
      if (ordering.gt(x, m)) x else m     // this ordering is still explicit
    }
  }
}

有种方式能够减少代码中对于 ordering 的使用

def implicitly[T](implicit t: T) = t  // 存在于标准库中

def max[T](xs: List[T])(implicit ordering: Ordering[T]): T = {
  xs match {
    case List() => throw IllegalArgumentException
    case List(x) => x
    case x :: last => {
      val m = max(last)                                            // (ordering) is implicit
      if (implicitly[Ordering[T]].gt(x, m)) x else m     // this ordering is implicit now
    }
  }
}

// 这时你可以不用关注隐式参数具体的名字了
def max[T](xs: List[T])(implicit comparator: Ordering[T]): T = {
  xs match {
    case List() => throw IllegalArgumentException
    case List(x) => x
    case x :: last => {
      val m = max(last)                                            // (ordering) is implicit
      if (implicitly[Ordering[T]].gt(x, m)) x else m     // this ordering is implicit now
    }
  }
}

通过如上的表述,隐式参数的形参名字并不重要。Scala 可以使用上下文边界来省略隐式参数的定义精简方法头

def max[T : Ordering](xs: List[T]): T = {
  xs match {
    case List() => throw IllegalArgumentException
    case List(x) => x
    case x :: last => {
      val m = max(last)                                            // (ordering) is implicit
      if (implicitly[Ordering[T]].gt(x, m)) x else m     // this ordering is implicit now
    }
  }
}

上下文边界,可以把它理解为隐式参数使用的一个语法糖

隐式转换适用情况

多转换同时适用

在同一个作用域下,有多个转换同时能够工作,编译器为了不制造困惑,会拒绝插入转换

但是在 Scala 2.8 时放松了这个规则。如果一个可用的转换比另外一个转换更加严格且具体,编译器会选择更加具体的转换

怎样才能认为更加具体呢?
1.前者参数的类型是后者的子类
2.如果转换都是方法,前者封装的类扩展子后者封装的类

调试

scalac Xprint:typer xxx.scala

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

推荐阅读更多精彩内容