Scala 隐式转换和隐式参数
隐式转换和隐式参数
1. 隐式转换
隐式转换函数(implicit conversion function) 指的是以implicit
关键字声明的带 单个参数
的函数.
这样的函数将会被自动应用, 将值从一种类型转化为另一种类型
import scala.math._
import scala.language.implicitConversions
class Fraction(n: Int, d: Int) {
private val num: Int = if (d == 0) 1 else n * sign(d) / gcd(n, d);
private val den: Int = if (d == 0) 0 else d * sign(d) / gcd(n, d);
override def toString = num + "/" + den
def sign(a: Int) = if (a > 0) 1 else if (a < 0) -1 else 0
def gcd(a: Int, b: Int): Int = if (b == 0) abs(a) else gcd(b, a % b) // 最大公约数 4/8 = 1/2
def *(other: Fraction) = new Fraction(num * other.num, den * other.den)
}
object Fraction {
def apply(n: Int, d: Int) = new Fraction(n, d) }
object Main extends App {
implicit def int2Fraction(n: Int) = Fraction(n, 1)
val result = 3 * Fraction(4, 5) // Calls int2Fraction(3)
println(result)
}
你可以给转换函数起任何名字, 由于你不显示的调用它, 因此, 你可能或想用比较短的名字, 比如 i2f
. 不过有时我们也需要显示的引入转换函数, 建议坚持使用 source2Target
这种约定俗成的命名方式
scala 给了程序员很大的控制权在什么时候应用这些转换
1.1 隐式转换规则
隐式转换在如下 3 种情况下会被考虑:
- 当表达式的类型和预期的类型不同时;
- 当对象访问一个不存在的成员时;
- 当对象调用某个方法, 而该方法的参数声明和传入参数不匹配时;
另一方面, 又下面3中情况, 编译器 不会
尝试隐式转换
- 如果代码能够在不使用隐式转换的前提下通过编译, 则不会使用隐式转换. 比如说, 如果
a*b
能编译, 那么编译器不会尝试convert(a)*b
或者a*convert(b)
. - 编译器不会尝试同时执行多个转换, 比如
convert2(convert1(a))*b
. - 存在二义性的转换是个错误. 比如
convert1(a)*b
和convert2(a)*b
都是合法的, 编译器就会报错.
上述二义性规则只适用于被尝试转换的对象, 即 a 对应两个隐式转换 , 如果 convert(a)*b
和 a*convert(b)
都是合法的, 并不存在二义性
如果你想知道编译器用了哪些隐式转换, 可以通过下面的命令编译程序
scalac -Xprint:typer YourProgram.scala
你将会看到加入隐式转化后的源码
1.2 引入隐式转换
Scala会考虑如下的隐式转换函数:
- 位于源或者目标类型的伴生对象的隐式函数
- 位于当前作用域可以以
单个标识符
指代的隐式函数
比如:
int2Fraction
函数, 我们可以将它放到 Fraction 伴生对象中, 这样它就能被用来将整数转换成分数了. 上面的代码就是这样做的
我们将int2Fraction
函数放到 FractionConversions 对象当中, 而这个对象位于 com.hosrst.impatient 包中. 如果想要这个转换, 就需要引入 FractionConversions 对象到当前作用域
import com.horstmann.impatient.FractionConversions._
单凭如下语句是不行的
import com.horstmann.impatient.FractionConversions
上面的语句引入的是 FractionConversions 对象的本身. 而 int2Fraction
方法只能以 FractionConversions.int2Fraction 的形式被任何想要被想要显示调用它的人使用. 但是, 当前作用域中, Scala只会考虑可以以单个标识符
指代的隐式函数, 而 FractionConversions.int2Fraction 有两个标识符. 如果int2Fraction
不能通过单个标识符
int2Fraction访问到, 编译器是不会自动使用它的.
REPL中, 键入:implicit以查看所有除了Predef外被引入的隐式成员, 或者 :implicit -v 查看全部
还可以将引入局部化以避免不想要的转换发生 比如
object Main extends App {
import com.horstmann.impatient.FractionConversions._
val result = 3 * Fraction(4, 5)
println(result) // 12/5
}
甚至可以选择你想要的特性转化, 通过import导入
object FractionConversions {
implicit def fraction2Double(f: Fraction) = f.num * 1.0 / f.den
}
import com.horstmann.impatient.FractionConversions.fraction2Double
val result = 3 * Fraction(4, 5) // 结果为2.4
如果某个特定的隐式转换给你带来麻烦, 也可以将其排除在外;
import com.horstmann.impatient.FractionConversions.{fraction2Double=>_, _ }
2. 隐式参数
函数或者方法可以带有一个标记为 implicit
的参数列表
这种情况下, 编译器会查找缺省值隐式值, 提供给该函数或者方法.
case class Delimiters(left: String, right: String)
def quote(what: String)(implicit delims: Delimiters) = delims.left + what + delims.right
说明: quote是一个有 两个 参数的方法, 必须接受 2 个参数, 隐式参数可以通过显示给出, 也可以隐式的给出, 如下
对于给定数据类型, 只能有一个隐式的值. 因此, 使用常用类型的隐式参数并不是一个好主意. 例如:
def quote(what: String)(implicit left:String, right: String)
别这样做, 上面的代码行不通, 因为调用者没法给两个不同的字符串
可以通过一个显示的 Delimiters 对象来调用 quote 方法, 就像这样:
quote("Hello")(Delimiters("<<", ">>")) // <<Hello>> quote 是一个柯里化 函数
可以省去隐式参数列表, 不过隐式参数必须给出(定义)
quote("Hello")
这时, 编译器会查找一个类型为 Delimiters 的隐式值. 这必须是一个被声明为 implicit 的值.
编译器将会在两个地方查找这样的一个对象:
- 在当前作用域所有可以用 单个标识符 指代的满足类型要求的 val 和 def
- 与所要求类型相关联的类型的伴生对象, 相关联的类型要求包括所要求的类型本身, 以及它的类型的参数(如果它是一个参数化类型的话)
说到底, 都是在当前作用域与能以 单个标识符 来访问到该 以 implicit 定义的 val (或者 def定义的方法, 函数)
来一个单例对象
object Hello {
implicit val quoteDelimiters = Delimiters("(", ")")
}
import Hello._
quote("Hello")
直接在当前作用域定义
implicit val quoteDelimiters = Delimiters("(", ")")
quote("Hello")
3. 利用隐式参数进行隐式转换
隐式的函数参数也可以被用作隐式转换.
举个例子来说明下,
def smaller[T](a: T, b: T) = if (a<b) a else b
这实际行不通, 编译器不会接受这一个函数, 因为它并不知道 a, b 所属类型是否是有 <
操作符
我们可以提供一个转换函数来达到目的:
def smaller[T](a: T, b: T)(implicit ord: T=>Ordered[T])
= if (Ordered(a)<b) a else b
由于 Ordered[T] 特质有一个接受 T 作为参数的 < 操作符, 因此这个版本正确.
ord 是一个带有单个参数的函数, 被打上了 implicit 标签, 并且有一个以 单个标识符 出现的名称. 因此, 它不仅是一个隐式参数, 它还是一个隐式转换. 正因为这样, 我们可以在函数体略去对 ord 显示的调用:
// 如果 a 没有操作符 < 的话, 将调用 order(a) < b
def smaller[T](a: T, b: T)(implicit ord: T=>Ordered[T])
= if (a<b) a else b
Scala中 Predef对象对大量已知类型都定义了 T=>Ordered[T] ,包括已经实现了 Order[T] 和 Comparable[T] 的类型. 正因为如此, 才可以调用 smaller(40 , 32), smaller("hello", "world") 等
如果你选哪个调用 smaller (Fraction(1, 7), Fraction(2, 9))
那你需要定义一个 Fraction => Ordered[Fraction] 的函数, 要么在调用时, 显示的写出; 要么做成一个 implicit val
4. 上下文界定
类型参数有一个形式为 T:M
的上下文界定, 其中 M 是另一个泛型类型. 它要求作用域中存在一个类型为 M[T] 的隐式值. 比如
class pair[T: Ordering]
要求存在一个类型为 Ordering[T] 的隐式值. 该隐式值可被用于该类的方法中, 考虑如下示例
class Pair[T: Ordering](val first: T, val second: T) {
def smaller(implicit ord: Ordering[T]) =
if (ord.compare(first, second) < 0) first else second
}
val p = new Pair(1, 3)
编译器会推断我们需要一个 Pair[Int]. 由于 Predef 作用域中有一个 Ordering[Int] 的隐式值, 因此 Int 满足上下文界定. 这个 Ordering[Int]就成为该类的一个字段, 会传入需要该值的隐式转换函数中
Scala语言里, Predef 会被默认引入到当前作用域
如果你愿意, 你也可以用 Predef 类的 implicitly 方法获取 Ordering[Int] :
class Pair[T: Ordering](val first: T, val second: T) {
def smaller =
if (implicitly[Ordering[T]].compare(first, second) < 0) first else second
}
implicitly 函数在 Predef.scala中定义如下:
def implicitly[T](implicit e: T) = e
同样由于 Predef 作用域中有一个 Ordering[Int] 的隐式值, 也是通过隐式转换获取 Ordering[Int]隐式值
你也可以利用 Ordered 特质中定义的 Ordering 到 Ordered 的隐式转换.
一旦引入这个转换, 你就可以使用关系操作符 <
class Pair[T: Ordering](val first: T, val second: T) {
def smaller = {
import Ordered._
if (first < second) first else second
}
}
问题: 不是说不能执行两次转换吗? 还是可以这样理解:
- 没有引入 Ordered._ 时, 需要显示的引用 Ordering[T]对象, 用来调用 compare 方法比较 两个 T类型的值, 这是不是就说明我们隐式引入参数的是 Ordering[T] 对象而并不是存在隐式转换, 将 T类型 转化为 Ordering[T]
- 而引入 Ordered._ 之后, 发生隐式转换 Ordering[T] => Ordered[T] , 然后 Ordered有
<
操作符, 就可以直接直接用<
操作符来使用 Ordered[T]对象比较大小了. 这样也不用显示的引用 Ordered[T] 对象了.
这些只是细微的变化; 重要的是你可以随时实例化 Pair[T], 只要满足存在类型为 Ordering[T] 的隐式值的条件即可.
举例来说, 如果你想要一个 Pair[Point], 则可以组织一个隐式的的 Ordering[Point]值
implicit object PointOrdering extends Ordering[Potint] {
def compare(a: Point, b: Point) = ...
}
类型证明
- T =:= U
- T <:< U
- T <%< U
这些约束将检验 T 是否等于 U, 是否是 U 的子类型, 是否一颗被隐式转换为 U.
要使用这样的类型约束, 做法是提供一个隐式参数, 比如
def firstLast[A, C] (it: C) (implicit ev: C <:< Iterable[A]) = (it.head, it.last)
=:=; <:<; <%< 是带隐式值的类
, 定义与 Predef 对象中, <:< 从本质讲就是:
abstract class <;< [-From, +To] extends Function1[From, To]
object <:< {
implicit def conforms[A] = new (A <:< A) {
def apply(x: A) = x
}
}
例子
假定编译器需要处理 implicit ev: String<:<AnyRef. 它会在伴生对象中查找类型为String <:< AnyRef 的隐式对象.
由于 <:< 相对于 From 是逆变的, 而相对于 To 是协变的 ,所以如下对象
<:<.conforms(String) 即 String<:<String 可以被当做String<:<AnyRef 的实例使用
<:<.conforms[AnyRef]也可以当做 String <:< AnyRef 实例使用, 但是相对而言, 它太笼统, 因而不会考虑这样做
ev
称作 类型证明对象
, 它的存在证明了如下事实:
本例而言. String是AnyRef的子类型
类型证明对象
类型证明对象是一个 恒等函数( 即永远返回参数原值的函数)
我们来看看为何这个恒等函数是必需的
def firstLast[A, C](it: C) (implicit ev: C<:<Iterable[A]) = (it.head, it.last)
编译器实际和是哪个不知道 C 是一个 Iterable[A](<:<只是一个类)
因此, 像it.head 和it.last 调用时不合法的. 但 ev 是一个带单个参数的函数, 因此也是一个从 C 到 Iterable[A]的隐式转换. 编译器将会应用这个隐式转换, 计算 ev(it).head 和 ev(it).last
在REPL中查看类型证明
为了检查一个泛型参数是否存在, 可以在 REPL 中调用 implicitly函数
比如: 在 REPL 中键入 implicitly[Stirng<;<Any], 将会得到一个结果
scala> implicitly[String<:<Any]
res1: String <:< Any = <function1>
@implicitNotFound注解
该注解作用是给出错误提示
@implicitNotFound
注解
告诉编译器在不能构造出带有该注解的类型的参数的时给出错误提示.
这样做的目的是个程序员有意义的错误提示.
比如说 <:<
被注解为
@implicitNotFound(msg = "cannot prove that ${From} <:< ${to}.")
abstract class <;< [-From, +To] extends Function1[From, To]
def firstLast[A, C](it: C) (implicit ev: C<:<Iterable[A]) = (it.head, it.last)
如果你调用 firstLast[String, List[Int]](List(1, 2, 3))
错误提示为:
Cannot prove that List[Int] <:< Iterable[String]
'
'缺省提示为:
Could not find implicit value for parameter ev: <:<[List[Int], Iterable[String]]
这比起缺省错误提示更有可能给程序员有价值的信息
CanBuildFrom 解读
CanBuildFrom[From , E, To]将提供类型证明, 可以创建一个类型为 To 的集合,握有类型为 E 的值, 并且和类型From兼容.
在Scala中, map定义于 TraversableLike[A, Repr] 特质中
def map[B, That](f: A => B)(implicit bf: CanBuildFrom[Repr, B, That]): That = {
def builder = {
val b = bf(repr)
b.sizeHint(this)
b
}
val b = builder
for (x <- this) b += f(x)
b.result
}
CanBuildFrom 特质带有一个 apply 方法, 产出类型为 Builder[E, to] 的对象. Builder类型带有一 +=
方法用来将元素添加到一个内部缓冲, 还有一个 result 方法产生所要求的集合
trait builder[-E, +To] {
def += (e: E): Unit
def result(): To
}
trait CanBuildFrom[-From, -E, +To] {
def apply(): Builder[E, To]
}
map 方法只是构造出一个目标类型的构造器, 然后向构造器中填充 f(_), 然后产出结果的集合
每个集合都在即伴生对象中提供能隐式的 CanBuildFrom 对象
这里 Repr 含义是 "展现类型", 该参数将让我们可以选择合适的构造器工厂来构建诸如 Range 和 String 这样的 非常规
集合
隐式参数 CanBuildFrom[Repr, B, That] 将会定位到一个可以产生目标集合的构造器工厂对象. 这个构造器工厂定义在 Repr 伴生对象中的一个隐式值.
Array ++ 方法
def ++[B >: A, That](that: GenTraversableOnce[B])(implicit bf: CanBuildFrom[Array[T], B, That]): That
B: the element type of the returned collection.
That: the class of the returned collection. Where possible, That is the same class as the current collection class Repr, but this depends on the element type B being admissible for that class, which means that an implicit instance of type CanBuildFrom[Repr, B, That] is found.
bf: an implicit value of class CanBuildFrom which determines the result class That from the current representation type Repr and the new element type B.
returns: a new collection of type That which contains all elements of this mutable indexed sequence followed by all elements of that.
Implicit: This member is added by an implicit conversion from Array[T] to ArrayOps[T] performed by method genericArrayOps in scala.Predef.