implicit 对 Scala 新手来说,一个问题似乎是:编译器在哪里寻找隐式?我的意思是含蓄的,因为这个问题似乎永远不会完全形成,就好像没有文字一样。 :-) 例如,下面 integral
的值来自哪里?
scala> import scala.math._
import scala.math._
scala> def foo[T](t: T)(implicit integral: Integral[T]) {println(integral)}
foo: [T](t: T)(implicit integral: scala.math.Integral[T])Unit
scala> foo(0)
scala.math.Numeric$IntIsIntegral$@3dbea611
scala> foo(0L)
scala.math.Numeric$LongIsIntegral$@48c610af
对于那些决定学习第一个问题的答案的人来说,另一个问题是编译器如何选择使用哪个隐式,在某些明显模棱两可的情况下(但无论如何编译)?
例如,scala.Predef
定义了从 String
的两个转换:一个到 WrappedString
,另一个到 StringOps
。然而,这两个类共享很多方法,那么为什么 Scala 在调用 map
时不抱怨歧义呢?
注意:此问题的灵感来自 this other question,希望以更笼统的方式陈述问题。该示例是从那里复制的,因为它在答案中被引用。
隐式类型
Scala 中的隐式指的是可以“自动”传递的值,可以这么说,或者是从一种类型到另一种类型的自动转换。
隐式转换
简单说一下后一种类型,如果在类 C
的对象 o
上调用方法 m
,并且该类不支持方法 m
,那么 Scala 将寻找从 { 3} 到确实支持m
的东西。一个简单的示例是 String
上的方法 map
:
"abc".map(_.toInt)
String
不支持方法 map
,但 StringOps
支持,并且存在从 String
到 StringOps
的隐式转换可用(请参阅 Predef
上的 implicit def augmentString
)。
隐式参数
另一种隐式是隐式参数。它们像任何其他参数一样传递给方法调用,但编译器会尝试自动填充它们。如果不能,它会抱怨。一个可以显式传递这些参数,这就是一个使用 breakOut
的方式,例如(请参阅关于 breakOut
的问题,在您准备迎接挑战的那一天)。
在这种情况下,必须声明需要隐式,例如 foo
方法声明:
def foo[T](t: T)(implicit integral: Integral[T]) {println(integral)}
查看边界
在一种情况下,隐式既是隐式转换又是隐式参数。例如:
def getIndex[T, CC](seq: CC, value: T)(implicit conv: CC => Seq[T]) = seq.indexOf(value)
getIndex("abc", 'a')
方法 getIndex
可以接收任何对象,只要存在从其类到 Seq[T]
的隐式转换可用。因此,我可以将 String
传递给 getIndex
,它会起作用。
在幕后,编译器将 seq.IndexOf(value)
更改为 conv(seq).indexOf(value)
。
这非常有用,以至于有语法糖来编写它们。使用这个语法糖,getIndex
可以这样定义:
def getIndex[T, CC <% Seq[T]](seq: CC, value: T) = seq.indexOf(value)
这种语法糖被描述为 view bound,类似于 upper bound (CC <: Seq[Int]
) 或 lower bound (T >: Null
) .
上下文边界
隐式参数中的另一个常见模式是类型类模式。这种模式可以为没有声明它们的类提供通用接口。它既可以用作桥接模式(获得关注点分离),也可以用作适配器模式。
您提到的 Integral
类是类型类模式的经典示例。 Scala 标准库的另一个示例是 Ordering
。有一个库大量使用了这种模式,称为 Scalaz。
这是它的使用示例:
def sum[T](list: List[T])(implicit integral: Integral[T]): T = {
import integral._ // get the implicits in question into scope
list.foldLeft(integral.zero)(_ + _)
}
它还有一个语法糖,称为上下文绑定,由于需要引用隐式,它变得不那么有用。该方法的直接转换如下所示:
def sum[T : Integral](list: List[T]): T = {
val integral = implicitly[Integral[T]]
import integral._ // get the implicits in question into scope
list.foldLeft(integral.zero)(_ + _)
}
当您只需要将它们传递到使用它们的其他方法时,上下文边界会更有用。例如,Seq
上的方法 sorted
需要一个隐式 Ordering
。要创建方法 reverseSort
,可以编写:
def reverseSort[T : Ordering](seq: Seq[T]) = seq.sorted.reverse
由于 Ordering[T]
已隐式传递给 reverseSort
,因此它可以将其隐式传递给 sorted
。
隐含从何而来?
当编译器发现需要隐式时,无论是因为您正在调用对象类中不存在的方法,还是因为您正在调用需要隐式参数的方法,它都会搜索适合需要的隐式.
这种搜索遵循某些规则,这些规则定义了哪些隐式可见,哪些不可见。下表显示了编译器将在何处搜索隐式,取自 Josh Suereth 撰写的关于隐式的优秀 presentation(时间戳 20:20),我衷心推荐给任何想要提高 Scala 知识的人。从那时起,它得到了反馈和更新的补充。
下面数字 1 下可用的隐式优先于数字 2 下的隐式。除此之外,如果有几个符合条件的参数与隐式参数的类型匹配,则将使用静态重载决议的规则选择一个最具体的参数(参见 Scala规范§6.26.3)。更多详细信息可以在我在此答案末尾链接到的问题中找到。
首先查看当前范围内定义的隐式在当前范围内显式导入通配符导入其他文件中的相同范围现在查看类型的伴随对象中的关联类型参数类型的隐式范围 (2.9.1) 类型参数的隐式范围 (2.8.0 ) 嵌套类型的外部对象 其他维度
让我们为他们举一些例子:
当前范围中定义的隐式
implicit val n: Int = 5
def add(x: Int)(implicit y: Int) = x + y
add(5) // takes n from the current scope
显式导入
import scala.collection.JavaConversions.mapAsScalaMap
def env = System.getenv() // Java map
val term = env("TERM") // implicit conversion from Java Map to Scala Map
通配符导入
def sum[T : Integral](list: List[T]): T = {
val integral = implicitly[Integral[T]]
import integral._ // get the implicits in question into scope
list.foldLeft(integral.zero)(_ + _)
}
其他文件中的相同范围
编辑:似乎这没有不同的优先级。如果您有一些演示优先级区别的示例,请发表评论。否则,不要依赖这个。
这与第一个示例类似,但假设隐式定义位于与其用法不同的文件中。另请参阅如何使用 package objects 来引入隐含。
类型的伴随对象
这里有两个值得注意的对象伴侣。首先,查看“源”类型的对象伴侣。例如,在对象 Option
内部存在到 Iterable
的隐式转换,因此可以在 Option
上调用 Iterable
方法,或将 Option
传递给期望 Iterable
的东西。例如:
for {
x <- List(1, 2, 3)
y <- Some('x')
} yield (x, y)
该表达式由编译器翻译为
List(1, 2, 3).flatMap(x => Some('x').map(y => (x, y)))
但是,List.flatMap
需要一个 TraversableOnce
,而 Option
不是。然后编译器查看 Option
的对象伴侣并找到到 Iterable
的转换,这是一个 TraversableOnce
,使这个表达式正确。
二、预期类型的伴生对象:
List(1, 2, 3).sorted
方法 sorted
采用隐式 Ordering
。在这种情况下,它查看对象 Ordering
内部,与类 Ordering
相伴,并在那里找到一个隐含的 Ordering[Int]
。
请注意,超类的伴生对象也会被研究。例如:
class A(val n: Int)
object A {
implicit def str(a: A) = "A: %d" format a.n
}
class B(val x: Int, y: Int) extends A(y)
val b = new B(5, 2)
val s: String = b // s == "A: 2"
顺便说一句,这就是 Scala 在您的问题中找到隐式 Numeric[Int]
和 Numeric[Long]
的方式,因为它们是在 Numeric
中找到的,而不是在 Integral
中找到的。
参数类型的隐式范围
如果您有一个参数类型为 A
的方法,那么还将考虑类型 A
的隐式范围。通过“隐式范围”,我的意思是所有这些规则都将被递归应用——例如,将根据上述规则搜索 A
的伴随对象以查找隐式。
请注意,这并不意味着将搜索 A
的隐式范围以查找该参数的转换,而是搜索整个表达式的转换。例如:
class A(val n: Int) {
def +(other: A) = new A(n + other.n)
}
object A {
implicit def fromInt(n: Int) = new A(n)
}
// This becomes possible:
1 + new A(1)
// because it is converted into this:
A.fromInt(1) + new A(1)
这从 Scala 2.9.1 开始可用。
类型参数的隐式范围
这是使类型类模式真正起作用所必需的。例如,考虑 Ordering
:它的伴随对象中带有一些隐式,但您不能向它添加东西。那么如何为您自己的类创建一个自动找到的 Ordering
?
让我们从实现开始:
class A(val n: Int)
object A {
implicit val ord = new Ordering[A] {
def compare(x: A, y: A) = implicitly[Ordering[Int]].compare(x.n, y.n)
}
}
所以,考虑一下当你打电话时会发生什么
List(new A(5), new A(2)).sorted
正如我们所见,方法 sorted
需要一个 Ordering[A]
(实际上,它需要一个 Ordering[B]
,其中 B >: A
)。 Ordering
中没有任何这样的东西,也没有可供查看的“源”类型。显然,它在 A
中找到它,它是 Ordering
的一个类型参数。
这也是期望 CanBuildFrom
的各种收集方法的工作方式:在 CanBuildFrom
的类型参数的伴随对象中找到隐式。
注意:Ordering
定义为 trait Ordering[T]
,其中 T
是类型参数。之前我说过Scala看里面的类型参数,没有多大意义。上面隐式查找的是 Ordering[A]
,其中 A
是实际类型,而不是类型参数:它是 Ordering
的 类型参数。请参阅 Scala 规范的第 7.2 节。
这从 Scala 2.8.0 开始可用。
嵌套类型的外部对象
我实际上还没有看到这样的例子。如果有人可以分享一个,我将不胜感激。原理很简单:
class A(val n: Int) {
class B(val m: Int) { require(m < n) }
}
object A {
implicit def bToString(b: A#B) = "B: %d" format b.m
}
val a = new A(5)
val b = new a.B(3)
val s: String = b // s == "B: 3"
其他尺寸
我很确定这是一个笑话,但这个答案可能不是最新的。所以不要把这个问题作为正在发生的事情的最终仲裁者,如果你确实注意到它已经过时了,请通知我,以便我修复它。
编辑
感兴趣的相关问题:
上下文和视图边界
链接隐式
Scala:隐式参数解析优先级
我想找出隐式参数解析的优先级,而不仅仅是它寻找的位置,所以我写了一篇博文revisiting implicits without import tax(以及一些反馈后的implicit parameter precedence again)。
这是列表:
1) 通过本地声明、导入、外部范围、继承、可以不带前缀访问的包对象隐含对当前调用范围可见。
2)隐式作用域,它包含所有类型的伴随对象和包对象,它们与我们搜索的隐式类型有某种关系(即类型的包对象、类型本身的伴随对象、其类型构造函数(如果有的话))它的参数(如果有的话),以及它的超类型和超特征)。
如果在任一阶段我们发现不止一个隐式的静态重载规则用于解决它。