【问题标题】:Why does the Scala compiler disallow overloaded methods with default arguments?为什么 Scala 编译器不允许使用默认参数的重载方法?
【发布时间】:2011-06-06 19:54:52
【问题描述】:

虽然在某些情况下此类方法重载可能会变得模棱两可,但为什么编译器不允许在编译时和运行时都没有模棱两可的代码?

例子:

// This fails:
def foo(a: String)(b: Int = 42) = a + b
def foo(a: Int)   (b: Int = 42) = a + b

// This fails, too. Even if there is no position in the argument list,
// where the types are the same.
def foo(a: Int)   (b: Int = 42) = a + b
def foo(a: String)(b: String = "Foo") = a + b

// This is OK:
def foo(a: String)(b: Int) = a + b
def foo(a: Int)   (b: Int = 42) = a + b    

// Even this is OK.
def foo(a: Int)(b: Int) = a + b
def foo(a: Int)(b: String = "Foo") = a + b

val bar = foo(42)_ // This complains obviously ...

这些限制有什么不能放宽一点的原因吗?

特别是当将重载的 Java 代码转换为 Scala 时,默认参数非常重要,并且在用一个 Scala 方法替换大量 Java 方法后发现规范/编译器施加了任意限制并不好。

【问题讨论】:

  • “任意限制” :-)
  • 看起来你可以使用类型参数来解决这个问题。编译:object Test { def a[A](b: Int, c: Int, d: Int = 7): Unit = {}; def a[A](a:String, b: String = ""): Unit = {}; a(2,3,4); a("a");}
  • @user1609012:你的把戏对我不起作用。我使用 Scala 2.12.0 和 Scala 2.11.8 进行了尝试。
  • 恕我直言,这是 Scala 中最强的痛点之一。每当我尝试提供灵活的 API 时,我经常会遇到这个问题,尤其是在重载伴随对象的 apply() 时。虽然我更喜欢 Scala 而不是 Kotlin,但在 Kotlin 中你可以进行这种重载...
  • 在此记录的票是github.com/scala/bug/issues/8161

标签: scala methods default overloading


【解决方案1】:

我想引用 Lukas Rytz(来自 here):

原因是我们想要一个确定性的命名方案 生成的方法返回默认参数。如果你写

def f(a: Int = 1)

编译器生成

def f$default$1 = 1

如果您有两个在同一个参数上具有默认值的重载 位置,我们需要一个不同的命名方案。但我们想保留 生成的字节码在多个编译器运行时稳定。

未来 Scala 版本的解决方案可能是将非默认参数(方法开头的那些,消除重载版本的歧义)的 类型名称 合并到命名模式中,例如在这种情况下:

def foo(a: String)(b: Int = 42) = a + b
def foo(a: Int)   (b: Int = 42) = a + b

应该是这样的:

def foo$String$default$2 = 42
def foo$Int$default$2 = 42

有人愿意write a SIP proposal吗?

【讨论】:

  • 我认为您在这里的提议很有意义,而且我不认为指定/实施它会如此复杂。本质上,参数类型是函数 ID 的一部分。编译器当前对 foo(String) 和 foo(Int) 做了什么(即没有默认值的重载方法)?
  • 在从 Java 访问 Scala 方法时,这不会有效地引入强制性匈牙利表示法吗?似乎它会使接口变得非常脆弱,迫使用户在函数的类型参数发生变化时要小心。
  • 另外,复杂类型呢?例如A with B
【解决方案2】:

对于重载决议与默认参数的交互,很难获得可读且精确的规范。当然,对于许多个别情况,例如此处介绍的情况,很容易说出应该发生的情况。但这还不够。我们需要一个规范来决定所有可能的极端情况。重载分辨率已经很难指定。在混合中添加默认参数会使其更难。这就是我们选择将两者分开的原因。

【讨论】:

  • 感谢您的回答。可能让我感到困惑的是,基本上在其他任何地方,编译器只会在实际存在歧义时才会抱怨。但是在这里编译器会抱怨,因为可能会出现类似的情况会出现歧义。因此,在第一种情况下,编译器仅在存在已证实的问题时才会抱怨,但在第二种情况下,编译器的行为不太精确,并且会为“看似有效”的代码触发错误。以最不惊讶的原则来看这件事,这有点可惜。
  • “很难获得可读和精确的规范 [...]”是否意味着如果有人提出一个好的规范,当前的情况可能会得到改善和/或实施?恕我直言,目前的情况相当多地限制了命名/默认参数的可用性......
  • 有一个提议对规范进行更改的过程。 scala-lang.org/node/233
  • I have some comments (请参阅链接答案下方的我的 cmets)关于 Scala 使重载不受欢迎和二等公民。如果我们继续故意削弱 Scala 中的重载,我们正在用名称代替打字,这在 IMO 是一个倒退的方向。
  • 如果 Python 可以做到,我看不出 Scala 做不到的任何充分理由。复杂性的论据是一个很好的论据:从用户的角度来看,实现此功能将使 Scale 变得不那么复杂。阅读其他答案,您会看到人们发明非常复杂的东西只是为了解决从用户的角度来看甚至不应该存在的问题。
【解决方案3】:

我无法回答您的问题,但这里有一个解决方法:

implicit def left2Either[A,B](a:A):Either[A,B] = Left(a)
implicit def right2Either[A,B](b:B):Either[A,B] = Right(b)

def foo(a: Either[Int, String], b: Int = 42) = a match {
  case Left(i) => i + b
  case Right(s) => s + b
}

如果你有两个很长的 arg 列表,它们只有一个 arg 不同,那么这可能是值得的……

【讨论】:

  • 好吧,我尝试使用默认参数来使我的代码更加简洁和易读……实际上,我在一个类中添加了一个隐式转换,它只是将替代类型转换为接受的类型.就是觉得丑。使用默认参数的方法应该可以工作!
  • 您应该小心此类转换,因为它们适用于 Either 的所有用途,而不仅仅是 foo - 这样,每当请求 Either[A, B] 值时,AB 被接受。如果你想朝这个方向发展,应该定义一种只被具有默认参数的函数接受的类型(如foo 这里);当然,这是否是一个方便的解决方案变得更加不清楚。
【解决方案4】:

对我有用的是重新定义(Java 风格)重载方法。

def foo(a: Int, b: Int) = a + b
def foo(a: Int, b: String) = a + b
def foo(a: Int) = a + "42"
def foo(a: String) = a + "42"

这可以确保编译器根据当前参数获得您想要的分辨率。

【讨论】:

    【解决方案5】:

    这是@Landei 答案的概括:

    你真正想要的:

    def pretty(tree: Tree, showFields: Boolean = false): String = // ...
    def pretty(tree: List[Tree], showFields: Boolean = false): String = // ...
    def pretty(tree: Option[Tree], showFields: Boolean = false): String = // ...
    

    解决方法

    def pretty(input: CanPretty, showFields: Boolean = false): String = {
      input match {
        case TreeCanPretty(tree)       => prettyTree(tree, showFields)
        case ListTreeCanPretty(tree)   => prettyList(tree, showFields)
        case OptionTreeCanPretty(tree) => prettyOption(tree, showFields)
      }
    }
    
    sealed trait CanPretty
    case class TreeCanPretty(tree: Tree) extends CanPretty
    case class ListTreeCanPretty(tree: List[Tree]) extends CanPretty
    case class OptionTreeCanPretty(tree: Option[Tree]) extends CanPretty
    
    import scala.language.implicitConversions
    implicit def treeCanPretty(tree: Tree): CanPretty = TreeCanPretty(tree)
    implicit def listTreeCanPretty(tree: List[Tree]): CanPretty = ListTreeCanPretty(tree)
    implicit def optionTreeCanPretty(tree: Option[Tree]): CanPretty = OptionTreeCanPretty(tree)
    
    private def prettyTree(tree: Tree, showFields: Boolean): String = "fun ..."
    private def prettyList(tree: List[Tree], showFields: Boolean): String = "fun ..."
    private def prettyOption(tree: Option[Tree], showFields: Boolean): String = "fun ..."
    

    【讨论】:

      【解决方案6】:

      一种可能的情况是

      
        def foo(a: Int)(b: Int = 10)(c: String = "10") = a + b + c
        def foo(a: Int)(b: String = "10")(c: Int = 10) = a + b + c
      

      编译器会对调用哪一个感到困惑。为了防止其他可能的危险,编译器最多允许一个重载方法具有默认参数。

      只是我的猜测:-)

      【讨论】:

        【解决方案7】:

        我的理解是,在编译的类中使用默认参数值可能会发生名称冲突。我已经在几个线程中看到过类似的内容。

        命名参数规范在这里: http://www.scala-lang.org/sites/default/files/sids/rytz/Mon,%202009-11-09,%2017:29/named-args.pdf

        它说:

         Overloading If there are multiple overloaded alternatives of a method, at most one is
         allowed to specify default arguments.
        

        所以,无论如何,它暂时是行不通的。

        你可以做一些你可能在 Java 中做的事情,例如:

        def foo(a: String)(b: Int) =  a + (if (b > 0) b else 42)
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2011-09-17
          • 1970-01-01
          • 2018-04-30
          • 1970-01-01
          • 2012-02-14
          • 2020-08-04
          • 1970-01-01
          相关资源
          最近更新 更多