Scala 的隐式转换与隐式参数

本文详细介绍了Scala编程语言中的隐式转换和隐式参数概念。隐式转换允许编译器自动处理类型错误,减少显式转换的需要。文章讲解了隐式规则,包括标记规则、作用域规则、每次一个规则和显示优先原则,并举例说明如何应用。此外,还阐述了隐式转换在类型转换、接受者转换以及隐式参数中的应用,如上下文边界和隐式类。文章强调了正确使用隐式转换和参数的重要性,以避免混淆并提高代码可读性。
摘要由CSDN通过智能技术生成

Scala 的隐式转换与隐式参数

隐式转换(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 val 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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GettingReal

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值