对于Scala新手来说,一个隐含的问题似乎是:编译器从哪里寻找隐式函数?我指的是含蓄,因为这个问题似乎从来没有完全形成过,好像没有词来形容它。:-)例如,下面的积分值是从哪里来的?

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时不抱怨歧义呢?

注意:这个问题是受到另一个问题的启发,希望以更一般的方式说明问题。示例是从那里复制的,因为答案中引用了它。


隐式的类型

Scala中的隐式指的是可以“自动”传递的值,或者从一种类型自动转换为另一种类型。

隐式转换

简单地说一下后一种类型,如果在类C的对象o上调用方法m,而这个类不支持方法m,那么Scala将寻找从C到支持m的方法的隐式转换。一个简单的例子是String上的方法映射:

"abc".map(_.toInt)

String不支持方法映射,但StringOps支持,并且有一个从String到StringOps的隐式转换可用(参见Predef上的隐式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)

这个语法糖被描述为一个视图边界,类似于上界(CC <: Seq[Int])或下界(T >: Null)。

上下文范围

隐式参数的另一个常见模式是类型类模式。此模式允许向没有声明公共接口的类提供公共接口。它既可以用作桥接模式(获得关注点分离),也可以用作适配器模式。

您提到的Integral类是类型类模式的一个经典示例。Scala标准库的另一个例子是有序。有一个库大量使用这种模式,称为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上排序的方法需要隐式排序。要创建一个方法reverseSort,可以这样写:

def reverseSort[T : Ordering](seq: Seq[T]) = seq.sorted.reverse

因为ordered [T]是隐式传递给reverseSort的,所以它可以隐式传递给sorted。

隐式是怎么来的?

当编译器看到需要隐式参数时,无论是因为你调用的方法在对象的类中不存在,还是因为你调用的方法需要隐式参数,它都会搜索一个符合需要的隐式参数。

这个搜索遵循某些规则,这些规则定义了哪些隐式是可见的,哪些是不可见的。下表显示了编译器搜索隐式函数的位置,摘自Josh Suereth关于隐式函数的精彩演示(时间戳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)(_ + _)
}

在其他文件中相同的范围

编辑:这似乎没有不同的优先级。如果你有一些例子,说明优先级的区别,请作出评论。否则,不要依赖这个。

这类似于第一个示例,但假设隐式定义位于与其用法不同的文件中。另请参阅包对象如何用于引入隐式。

类型的伴生对象

这里有两个值得注意的对象同伴。首先,研究“源”类型的对象伙伴。例如,在对象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)))

然而,清单。flatMap需要一个TraversableOnce,而Option不是。编译器然后查看Option的对象伙伴,并找到到Iterable的转换,这是一个TraversableOnce,使这个表达式正确。

第二,期望类型的伴生对象:

List(1, 2, 3).sorted

sorted方法采用隐式ordered。在本例中,它在order类的同伴order对象中查找,并在那里找到一个隐式的order [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开始提供的。

类型参数的隐式作用域

这是使类型类模式真正工作所必需的。例如,考虑ordered:它在其伴生对象中带有一些隐式,但您不能向它添加东西。那么,如何为自动找到的自己的类创建一个order呢?

让我们从实现开始:

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方法期望一个ordered [A](实际上,它期望一个ordered [B],其中B >: A)。在ordered内部没有任何这样的东西,也没有“source”类型可供查看。显然,它是在A中找到它,A是order的类型参数。

这也是期望CanBuildFrom的各种收集方法的工作方式:隐式在CanBuildFrom的类型参数的伴生对象中找到。

注:ordered定义为trait ordered [T],其中T为类型参数。在前面,我说过Scala在类型参数内部进行检查,这没什么意义。上面隐式查找的是orders [A],其中A是实际类型,而不是类型参数:它是orders的类型参数。请参见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"

其他维度

我很确定这是一个笑话,但这个答案可能不是最新的。因此,不要把这个问题作为正在发生的事情的最终仲裁者,如果你注意到它已经过时了,请通知我,以便我可以修复它。

EDIT

相关问题:

上下文和视图边界 值得一提的是链接 Scala:隐式参数解析优先级


我想要找出隐式参数解析的优先级,而不仅仅是它在哪里查找,所以我写了一篇博客文章,在没有进口税的情况下重新审视隐式(以及在一些反馈后再次审视隐式参数优先级)。

以下是名单:

1)通过本地声明、导入、外部作用域、继承、无需前缀即可访问的包对象对当前调用作用域可见。 2)隐式作用域,它包含所有类型的伴随对象和包对象,这些对象与我们搜索的隐式类型有某种关系(即类型的包对象,类型本身的伴随对象,它的类型构造函数(如果有),它的形参(如果有),以及它的超类型和超特征)。

如果在任何一个阶段,我们发现一个以上的隐式,静态重载规则被用来解决它。