Scala中的“后缀操作符”

64

我已经搜索了半个小时,仍然无法弄清楚。

SIP: 模块化语言功能中,有许多功能需要在Scala 2.10中显式“启用”(import language.feature)。 其中有一个叫做postfixOps的功能,但我无法在任何地方找到相关引用。这个功能确切地允许什么?


最后似乎编译器尽管产生警告,但仍然可以进行编译。因此,像我这样的人可能只需忽略那些烦人的警告。 - dmitry
我担心2.11版本之后的计划是,与其只是警告,结果将会是编译错误。据我所了解,目前的状态只是为了“缓解”过渡期间的问题... - user500592
太糟糕了,我还是不明白为什么“后缀操作”与像隐式参数或存在类型这样真正强大的功能放在一起。我尝试在SIP网页上提问,但没有得到答案。 - dmitry
6
在 scalac 命令行中添加 -language:postfixOps,一切都将恢复到原来的状态,无需导入任何内容,也不会出现警告。在 build.sbt 中将它添加到 scalacOptions 序列中即可。 - AmigoNico
你不应该在现代程序中使用它,不要忽略这个警告,我已经解释了原因。请查看一下。 - Alex
3个回答

64

它允许您在后缀位置使用运算符语法。例如:

List(1,2,3) tail

与其

List(1,2,3).tail

在这个无害的例子中并没有问题,但它可能会导致歧义。以下代码无法编译:

val appender:List[Int] => List[Int] = List(1,2,3) ::: //add ; here
List(3,4,5).foreach {println}

错误信息并不是很有帮助:

    value ::: is not a member of Unit

它试图在foreach调用的结果上调用:::方法,该结果的类型为Unit。这可能不是程序员想要的。为了获得正确的结果,您需要在第一行后插入一个分号。


7
天哪...我非常喜欢它,但现在我被迫要么加上愚蠢的import language.postfixOps,要么到处使用点号。为什么会这样?!! - dmitry
2
实际上,SIP 18 是 Scala 中真正令人讨厌和有争议的第一件事。如果我能想象到结构类型、存在类型甚至隐式转换在日常代码中并不经常使用,但后缀语法……我会尽可能地在任何地方使用它。 - dmitry
4
你确定后缀操作的那个例子不正确吗?我可以很容易地提供一个有用的例子。比如说:List(1,2,3) map { _ + 1 } reverseList(1,2,3).map( _ + 1).reverse 更易读(当然这只是我的想法)。 - dmitry
2
@KimStebel用分号的例子会产生警告,但没有错误。这只是一种不同类型的混淆。仅仅因为有人想在参数的位置上使用带有通配符的柯里方法。这几乎不能解释为何后缀操作是一个坏主意。 - dmitry
3
感谢您在appender中提供有趣的示例,这应该是一个很好的Scala编程难题。 - som-snytt
显示剩余11条评论

37

最简单的回答:

在没有参数的方法中省略点号是不推荐使用的!

List(1,2,3) reverse //is bad style and will lead to unpredicted behaviour
List(1,2,3) map(_*2) reverse //bad too, because reverse can take first method call from the next line (details below)

对于只有一个 高阶函数 参数的方法,比如 map、filter、count,可以省略参数外的圆点,这样做也是安全的!同时,像 zip 这样的纯函数式方法也是如此。

List(1,2,3) map(_*2) filter(_>2)
(List(1,2,3) map(_*2)).reverse //safe and good
List(1,3,5) zip List(2,4,6)

为什么需要长篇回答

case class MyBool(x: Boolean) {
  def !!! = MyBool(!x) //postfix
  def or(other: MyBool): MyBool = if(x) other else this //infix
  def justMethod0() = this //method with empty parameters
  def justMethod2(a: MyBool, b: MyBool) = this //method with two or more
  override def toString = if(x) "true" else "false"
}

1) 后缀操作符“-”实际上是一个没有参数(a!==a.!)且没有括号的方法调用。(被认为是不安全和已弃用)

val b1 = MyBool(false) !!!
List(1,2,3) head

2) 后缀运算符是一种方法,应该以行结尾,否则它将被视为中缀运算符。

val b1 = MyBool(true) no! no! //ERROR
//is actually parsed like
val b2 = MyBool(true).no!(no!) //(no!) is unknown identifier
//as bad as
Vector(1,2,3) toList map(_*2) //ERROR

3) 中缀运算符是一种只有一个参数的方法,可以在没有句点和括号的情况下被调用。仅适用于纯函数式方法。

val c1 = MyBool(true) or b1 or MyBool(true)
val c2 = MyBool(true).or(b1).or(MyBool(true))
c1 == c2

4) 如果你在调用带有参数的方法时使用参数,它将不需要点链接。例如: def a(), def a(x), def a(x,y)。但是你只应该对使用高阶函数作为参数的方法进行此操作!

val d1 = MyBool(true) justMethod2(b1, c1) or b1 justMethod0() justMethod2(c1, b1)
//yes, it works, but it may be confusing idea
val d2 = MyBool(true).justMethod2(b1,c1).or(b1).justMethod0().justMethod2(c1, b1)
d1 == d2
//looks familiar? This is where it should be used:
List(1,2,3) filter(_>1) map(_*2)

样例警告:

警告:存在一个过时的警告;使用-deprecation重新运行程序可以查看详情。 警告:后缀操作符 tail 应该通过使隐式值 scala.language.postfixOps 可见来启用。您可以通过添加“import scala.language.postfixOps”导入语句或设置编译器选项 -language:postfixOps 来实现此目标。请参阅Scala文档中关于值scala.language.postfixOps的讨论,了解为什么应该明确启用该特性。


5

它指的是能够将一个无参方法(不带参数列表或空参数列表)作为后缀运算符调用的能力:

例如:

case class MyBool(value: Boolean) {
    def negated = new MyBool(!value)
}
val b1 = MyBool( true )
val b2 = b1 negated // Same as b1.negated

请查看:http://www.scala-lang.org/node/118

1
正如我的第二个例子所示,它不仅限于零元方法。 - Kim Stebel
1
我不确定你所指的示例是什么。无论如何,我猜想如果你想吹毛求疵的话,这也适用于所有参数都有默认值的非空方法。无论如何,我只是在转述官方的Scala文档(见链接):“正如此代码的第一行所示,也可以将无参数方法用作后缀运算符”。 - Régis Jean-Gilles
1
不需要默认值。我指的是我的答案中的示例,特别是 val appender:List[Int] => List[Int] = List(1,2,3) ::: 这一行。::: 方法不是无参数的,也没有默认值。 - Kim Stebel
2
好的。我已经检查过了,似乎postfixOps也包含了这种情况,尽管对我来说它们是非常不同的(你的例子展示了myObject.myMethod可以被默默地视为myObject.myMethod _)。 - Régis Jean-Gilles
1
比起回答更能引人入胜的评论区会得到一个+1。 - som-snytt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接