当我们在范围内存在多个不明确的隐式时,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(分母)。该类有一个方法*,它执行numden 的乘法并返回新的Fraction 实例。

在这里,为了理解implicit 的工作原理,我创建了一个对象FractionConversions,它有助于两个隐式方法intToFractionfractionToDouble

在测试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), 为什么fractionToDoubleimplicit 在这种情况下被调用,即使intToFraction 也符合条件?

case#3:对于语句val c = Fraction(4,5) * 3,为什么调用intToFraction?为什么没有使用fractionToDouble

在这里,我尝试复制书中提到的以下场景,因此出现了上述问题。

那么,我们应该总结一下,编译器避免转换左侧操作数并选择右侧进行转换,如果它们都符合转换条件?例如,对于(a*b),即使ab 都符合转换条件,a 总是被忽略,b 被转换为预期类型?

【问题讨论】:

尽可能远离隐式转换,它们会导致代码混乱,并使编译器生成较慢或错误的代码。更喜欢两种扩展方法:toFraction on InttoDouble 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,因为ifwhile是在字节码中实现的,或者你不能远离循环,因为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 编译如何选择隐式的主要内容,如果未能解决你的问题,请参考以下文章

自动回滚多个语句的隐式事务?

Scala中的隐式转换|理解

Scala:从泛型类型到第二泛型类型的隐式转换

Scala的隐式转换

找不到参数 flash 的隐式值

25.scala的隐式转换