隐式定义有多糟糕?

How bad are implicit definitions?

我喜欢隐式定义。它们使代码看起来不错,它们让用户觉得某些功能在 class 上自然可用,而它只是一个隐式定义。然而,我在考虑 JS 原型,你基本上可以在你没有写的 class 上定义一个方法。但是如果在下一个版本中这个 class 定义了一个具有相同签名的方法并对它的行为做出假设,那你就完蛋了。

Scala 的隐式能够做几乎相同的事情,但有一个主要区别:隐式定义是有范围的,因此 class 的作者没有风险通过隐式定义将代码注入某人别人的代码。但是用户的代码呢?它是否不受 class 的更改的影响,他正在向 ?

添加隐式

让我们考虑一下这段代码:

class HeyMan {
    def hello = println("Hello")
}

object Main extends App {
    val heyMan = new HeyMan

    implicit class ImplicitHeyMan(heyMan: HeyMan) {
        def hello = println("What's up ?")
    }
    heyMan.hello // prints Hello
}

很糟糕,不是吗?对我来说,正确的行为应该是隐式定义总是隐藏真实定义,这样用户代码就不会在他调用的 API 中出现新方法。

你怎么看?有没有办法让它安全,或者我们应该停止以这种方式使用隐式?

关于隐式转换的语言行为定义得非常清楚:

if one calls a method m on an object o of a class C, and that class does not support method m, then Scala will look for an implicit conversion from C to something that does support m.

http://docs.scala-lang.org/tutorials/FAQ/finding-implicits.html

换句话说,如果 heyMan 的(静态已知的)class/trait 已经定义,则隐式转换将永远不会应用于表达式 heyMan.hello 中的 heyMan方法 hello—仅当您调用它 尚未定义的方法时才会尝试隐式转换。


To me, the correct behaviour should be that the implicit definition always shades the real definition, so that the user code is protected from the apparition of new methods in the API he's calling into.

相反的情况不也一样吗?如果隐式转换确实优先,那么用户将面临他们已经存在了 5 年的长期定义的方法突然被新版本的库依赖项中的新隐式转换所掩盖的危险。

这种情况似乎 比用户 显式 优先定义新方法的情况更隐蔽和难以调试。


Is there a way to make it safe or should we stop using implicits this way ?

如果获得隐式行为真的很重要,也许您应该强制使用显式类型进行隐式转换:

object Main extends App {
    val heyMan = new HeyMan

    implicit class ImplicitHeyMan(heyMan: HeyMan) {
        def hello = println("What's up ?")
    }

    heyMan.hello // prints Hello

    val iHeyMan: ImplicitHeyMan // force conversion via implicit
    iHeyMan.hello // prints What's up
}

从我们在评论中的(扩展)对话来看,您似乎想要一种方法来检查基础 class 是否不会通过隐式转换定义您正在使用的方法。

我认为 Łukasz 在下面的评论是正确的 — 这是您应该在测试中发现的问题。具体来说,您可以为此使用 ScalaTest's assertTypeError。只需尝试在隐式范围之外调用该方法,它应该无法进行类型检查(并通过测试):

// Should pass only if your implicit isn't in scope,
// and the underlying class doesn't define the hello method
assertTypeError("(new HeyMan).hello")