当我们在范围内存在多个不明确的隐式时,Scala 编译如何选择隐式
Posted
技术标签:
【中文标题】当我们在范围内存在多个不明确的隐式时,Scala 编译如何选择隐式【英文标题】:How implicit is chosen by Scala compile when we have multiple ambiguous implicit present in scope 【发布时间】:2021-10-23 06:35:41 【问题描述】:我是 Scala 新手,我指的是“Scala for Impatient - Second Edition”一书。
我正在编写一个小代码,我在其中创建了一个类 Fraction
,它作为两个 Int 字段 num
(数字)和 den
(分母)。该类有一个方法*
,它执行num
和den
的乘法并返回新的Fraction
实例。
在这里,为了理解implicit
的工作原理,我创建了一个对象FractionConversions
,它有助于两个隐式方法intToFraction
和fractionToDouble
。
在测试ImplicitConversionTester
中的代码时,我已经导入了FractionConversions._
路径,因此编译器可以使用这两种隐式方法。
现在,参考代码让图片更清晰。
package OperatorOverloadingAndImplicitConversion
class Fraction(n : Int, d : Int)
private val num = this.n
private val den = this.d
def *(other : Fraction) = new Fraction(num*other.num, den*other.den)
override def toString: String =
s"Value of Fraction is : $(num*0.1)/den"
object Fraction
def apply(n: Int, d: Int): Fraction = new Fraction(n, d)
object FractionConversions
implicit def fractionToDouble(f : Fraction): Double =
println("##### FractionConversions.fractionToDouble called ...")
(f.num*0.1)/f.den
implicit def intToFraction(n : Int) : Fraction =
println("##### FractionConversions.intToFraction called ...")
new Fraction(n,1)
object ImplicitConversionTester extends App
import FractionConversions._
/*
* CASE 1 : Here, "fractionToDouble" implicit is called.
Why "intToFraction" was eligible but not called ?
*/
val a = 2 * Fraction(1,2)
/*
* CASE 2 : Works as expected.
Here, number "2" is converted to Fraction using "intToFraction" implicit.
*/
val b = 2.den
/*
* CASE 3: Why again "intToFraction" but not "fractionToDouble" ? Why ?
*/
val c = Fraction(4,5) * 3
println(a) // output : 0.1 FractionConversions.fractionToDouble called
println(b) // output : 1 FractionConversions.intToFraction called
println(c) // output : Value of Fraction is : 0.24000000000000005. FractionConversions.intToFraction called
我在上面的代码中有查询:
案例#1:对于声明val a = 2 * Fraction(1,2)
,
为什么fractionToDouble
implicit 在这种情况下被调用,即使intToFraction
也符合条件?
case#3:对于语句val c = Fraction(4,5) * 3
,为什么调用intToFraction
?为什么没有使用fractionToDouble
?
在这里,我尝试复制书中提到的以下场景,因此出现了上述问题。
那么,我们应该总结一下,编译器避免转换左侧操作数并选择右侧进行转换,如果它们都符合转换条件?例如,对于(a*b)
,即使a
和b
都符合转换条件,a 总是被忽略,b 被转换为预期类型?
【问题讨论】:
尽可能远离隐式转换,它们会导致代码混乱,并使编译器生成较慢或错误的代码。更喜欢两种扩展方法:toFraction
on Int
和 toDouble
on Fraction
@LuisMiguelMejíaSuárez 如果OP拥有Fraction
的代码,为什么toDouble
应该是一个扩展方法?另外,implicit class IntOps(n: Int) def toFraction: Fraction = new Fraction(n,1)
只是class IntOps(n: Int) def toFraction: Fraction = new Fraction(n,1) ; implicit def intToOps(n: Int): IntOps = new IntOps(n)
的糖,所以你实际上不能完全远离隐式转换。
@DmytroMitin 是的,第一个对我来说主要是不好的措辞,是的,虽然从技术上讲,扩展方法是在后台使用隐式转换实现的,但两者都是不同的模式,你知道的。就像说一个人不能远离go-to,因为if
和while
是在字节码中实现的,或者你不能远离循环,因为map
是使用这些实现的,或者你不能远离机器代码,因为一切都将由 CPU 执行。
@DmytroMitin 我理解并完全同意,但我的观点仍然存在;虽然你在技术上是正确的,但实践却不然。由于扩展方法需要显式调用扩展方法,因此编译器搜索比从两种不相关类型的一般转换更快、更安全。无论如何,这不仅仅是我的观点,类似的观点可以在以前的 Scala 版本的 stdlib 文档中找到,其中 scala.collection.JavaConversions
对象仍然存在但已被弃用:scala-lang.org/files/archive/api/2.12.13/scala/collection/…
@GunjanShah 这就是我使用扩展方法代替的意思:scastie.scala-lang.org/BalmungSan/OUHrKHIzR8CVdaR69F7vQw - 您还可以提供 Fraction.*
的重载来接受双精度值和另一个扩展方法 *
在 IntOps
类来获得您想要的相同 API。
【参考方案1】:
您的描述是正确的:对于表达式a.f(b)
(注意a*b
只是a.*(b)
的运算符符号),适用于b
的隐式转换允许表达式进行类型检查将优先于适用于a
的隐式转换。这是因为 a
的隐式转换(在语言标准中称为“视图”)只有在 *
(在 Int
中)不适用于 Fraction
时才能尝试(参见 SLS 7.3)...但是 SLS 6.6 中的适用性定义表明隐式视图足以适用。
因此,如果想将2
隐式转换为Fraction
以使用Fraction.*
,则需要
(2: Fraction) * Fraction(1, 2)
同样,对于情况 3,如果您想先将分数隐式转换为 Double
:
(Fraction(4,5): Double) * 3
【讨论】:
以上是关于当我们在范围内存在多个不明确的隐式时,Scala 编译如何选择隐式的主要内容,如果未能解决你的问题,请参考以下文章