在Scala中,花括号和圆括号的正式区别是什么?它们应该何时使用?

371

在函数中,使用括号 () 和花括号 {} 传递参数的形式有何区别?

我从 Programming in Scala 这本书中得到的感觉是 Scala 非常灵活,我应该使用自己最喜欢的方式,但我发现有些情况下代码可以编译通过,而有些情况则不能。

例如(仅作为示例;我希望讨论一般情况,而不仅仅是这个特定的示例):

val tupleList = List[(String, String)]()
val filtered = tupleList.takeWhile( case (s1, s2) => s1 == s2 )

=> 错误:简单表达式的起始无效

val filtered = tupleList.takeWhile{ case (s1, s2) => s1 == s2 }

=> 好的。

9个回答

400

我曾经试图写过这方面的内容,但最终放弃了,因为规则有些模糊。基本上,你需要掌握它。

也许最好集中精力在大括号和小括号可以互换的地方:当传递参数给方法调用时。如果方法只期望一个参数,则可以将大括号替换为小括号。例如:

List(1, 2, 3).reduceLeft{_ + _} // valid, single Function2[Int,Int] parameter

List{1, 2, 3}.reduceLeft(_ + _) // invalid, A* vararg parameter

然而,要更好地理解这些规则,还有更多需要了解的内容。

使用圆括号增强编译检查

Spray的作者建议使用圆括号,因为它们可以提供更严格的编译检查。特别是对于像Spray这样的领域专用语言。通过使用圆括号,您告诉编译器它只应该编译一行代码;因此,如果意外输入两行或更多行代码,编译器将会报错。但是,使用花括号却不是这种情况——例如,如果您忘记在某个地方添加运算符,您的代码会编译通过,但得到的结果可能与预期不同,并且可能难以找出错误。以下示例虽有点牵强(因为表达式是纯洁的,至少会给出警告),但可以说明这一点:

method {
  1 +
  2
  3
}

method(
  1 +
  2
  3
)
第一个代码段编译成功,第二个代码段会报错:error: ')' expected but integer literal found。原作者想要写的是1 + 2 + 3
对于含默认参数的多参数方法,有人认为使用括号可以避免因忘记用逗号分隔而导致的错误。
关于冗长程度的问题,一个常被忽视的重要点是,使用花括号会不可避免地导致冗长的代码,因为Scala样式指南明确规定,闭合花括号必须位于其自己的一行上:

...闭合大括号在函数最后一行之后占据一行。

许多自动格式化程序,比如IntelliJ中的程序,会自动为您执行此重新格式化。所以尽量在可能的情况下使用圆括号。
在使用中缀符号表示法时,例如List(1,2,3) indexOf (2)如果只有一个参数,你可以省略括号并将其写成List(1, 2, 3) indexOf 2。但在点符号表示法中不适用。
还要注意的是,当您有一个单参数且为多令牌表达式的情况,例如 x + 2a => a % 2 == 0 ,您必须使用括号来表示表达式的边界。
因为有时可以省略括号,所以有时元组需要额外的括号,比如((1, 2)),有时外部的括号可以省略,比如(1, 2)。这可能会导致混淆。
Scala支持类似于以下示例的函数和偏函数字面量语法:
{
    case pattern if guard => statements
    case pattern => statements
}

你可以在使用 matchcatch 关键字时使用case语句:

object match {
    case pattern if guard => statements
    case pattern => statements
}
try {
    block
} catch {
    case pattern if guard => statements
    case pattern => statements
} finally {
    block
}

如果你想使用case语句,就必须在花括号中使用它们,因为在任何其他上下文中都不能使用。如果你想知道什么区分函数和部分函数文字,答案是:上下文。如果Scala需要一个函数,则会得到一个函数。如果期望一个部分函数,那么你将得到一个部分函数。如果两者都被期望,它会产生关于歧义的错误。

表达式和块

可以使用括号来创建子表达式。花括号可以用来创建代码块(这不是函数文字,所以小心不要试图像使用函数文字一样使用它)。代码块由多个语句组成,每个语句都可以是导入语句、声明或表达式。其格式如下:

{
    import stuff._
    statement ; // ; optional at the end of the line
    statement ; statement // not optional here
    var x = 0 // declaration
    while (x < 10) { x += 1 } // stuff
    (x % 5) + 1 // expression
}

( expression )

所以,如果你需要声明、多个语句、import 或者其他类似的东西,你就需要用花括号来包裹。因为表达式是一种语句,所以括号可能会出现在花括号中。但有趣的是,代码块也是表达式,因此您可以将它们用在表达式的任何位置:

( { var x = 0; while (x < 10) { x += 1}; x } % 5) + 1

因此,既然表达式是语句,代码块也是表达式,那么下面的所有内容都是有效的:

1       // literal
(1)     // expression
{1}     // block of code
({1})   // expression with a block of code
{(1)}   // block of code with an expression
({(1)}) // you get the drift...

它们不可互换的情况

基本上,你不能在任何其他地方将{}替换为()或反之亦然。例如:

while (x < 10) { x += 1 }

这不是一个方法调用,所以你不能以任何其他方式编写它。嗯,你可以在 condition 的括号中间放置花括号,也可以在代码块的花括号中使用括号:

while ({x < 10}) { (x += 1) }

67
这就是人们争论Scala复杂的原因了。我自己也称自己为Scala爱好者。 - andyczerwonka
2
不需要为每个方法引入作用域,我认为这使得Scala代码更简单!理想情况下,没有任何方法应该使用 {} - 一切都应该是一个单一的纯表达式。 - samthebest
3
@andyczerwonka 我完全赞同,但这是你为获得灵活性和表达能力所付出的自然而不可避免的代价。Scala并不过度昂贵。当然,对于任何特定情况来说,这是否是正确的选择就是另外一回事了。 - Ashkan Kh. Nazary
你好,当你说 List{1, 2, 3}.reduceLeft(_ + _) 是无效的时候,是指它有语法错误吗?但我发现这段代码可以编译。我把我的代码放在了这里 - calvin
你在所有的例子中都使用了 List(1, 2, 3),而不是 List{1, 2, 3}。然而,在 Scala 的当前版本(2.13)中,这会导致一个不同的错误信息(意外的逗号)。你可能需要回到 2.7 或 2.8 版本才能得到原始的错误信息。 - Daniel C. Sobral

63
这里涉及到几个不同的规则和推断:首先,当参数是函数时,Scala会自动推断大括号,例如在list.map(_ * 2)中,大括号被推断了,这只是list.map({_ * 2})的一种缩写形式。其次,如果最后一个参数列表只有一个参数且是函数,则可以省略括号,因此list.foldLeft(0)(_ + _)可以写成list.foldLeft(0) { _ + _ }(或者list.foldLeft(0)({_ + _}),如果你想特别明确)。
但是,如果你加了case,就会得到一个部分函数而不是函数,正如其他人所提到的那样,Scala不会为部分函数推断大括号,所以list.map(case x => x * 2)不起作用,但是list.map({case x => 2 * 2})list.map { case x => x * 2 }都可以。

4
不仅限于最后一个参数列表。例如,list.foldLeft{0}{_+_} 是有效的。 - Daniel C. Sobral
1
啊,我确信我读过只有最后一个参数列表,但显然我错了!知道这点很好。 - Theo

23

社区正在努力标准化花括号和圆括号的用法,请参阅Scala风格指南(第21页):http://www.codecommit.com/scala-style-guide.pdf

建议使用高阶方法调用的语法是始终使用花括号,并跳过点号:

val filtered = tupleList takeWhile { case (s1, s2) => s1 == s2 }

对于“普通”的方法调用,您应该使用点和括号。

val result = myInstance.foo(5, "Hello")

21
实际上,惯例是使用圆括号,该链接是非官方的。这是因为在函数式编程中,所有函数都只是一级公民,因此不应该有不同的对待方式。其次,Martin Odersky表示,您应该尝试仅将中缀用于类似于操作符的方法(例如 +--),而不是普通方法,例如 takeWhile。中缀符号表示法的整个重点是允许DSL和自定义运算符,因此应该在这种情况下使用它,而不是一直使用。 - samthebest

21
我认为在Scala中,花括号并没有什么特别或复杂的地方。要掌握它们在Scala中看似复杂的用法,只需要记住以下几点:
  1. 花括号形成一个代码块,其计算结果为最后一行代码(几乎所有语言都是这样做的)
  2. 如果需要,可以使用代码块生成函数(遵循规则1)
  3. 单行代码可以省略花括号,但除了case子句之外(Scala的选择)
  4. 当代码块作为参数时,调用函数时可以省略括号(Scala的选择)
现在让我们根据上述三条规则来解释一些示例:
val tupleList = List[(String, String)]()
// doesn't compile, violates case clause requirement
val filtered = tupleList.takeWhile( case (s1, s2) => s1 == s2 ) 
// block of code as a partial function and parentheses omission,
// i.e. tupleList.takeWhile({ case (s1, s2) => s1 == s2 })
val filtered = tupleList.takeWhile{ case (s1, s2) => s1 == s2 }

// curly braces omission, i.e. List(1, 2, 3).reduceLeft({_+_})
List(1, 2, 3).reduceLeft(_+_)
// parentheses omission, i.e. List(1, 2, 3).reduceLeft({_+_})
List(1, 2, 3).reduceLeft{_+_}
// not both though it compiles, because meaning totally changes due to precedence
List(1, 2, 3).reduceLeft _+_ // res1: String => String = <function1>

// curly braces omission, i.e. List(1, 2, 3).foldLeft(0)({_ + _})
List(1, 2, 3).foldLeft(0)(_ + _)
// parentheses omission, i.e. List(1, 2, 3).foldLeft(0)({_ + _})
List(1, 2, 3).foldLeft(0){_ + _}
// block of code and parentheses omission
List(1, 2, 3).foldLeft {0} {_ + _}
// not both though it compiles, because meaning totally changes due to precedence
List(1, 2, 3).foldLeft(0) _ + _
// error: ';' expected but integer literal found.
List(1, 2, 3).foldLeft 0 (_ + _)

def foo(f: Int => Unit) = { println("Entering foo"); f(4) }
// block of code that just evaluates to a value of a function, and parentheses omission
// i.e. foo({ println("Hey"); x => println(x) })
foo { println("Hey"); x => println(x) }

// parentheses omission, i.e. f({x})
def f(x: Int): Int = f {x}
// error: missing arguments for method f
def f(x: Int): Int = f x

1
  1. 这并不是所有语言都适用的真理。
  2. 在Scala中,这并不是真实的。例如:def f(x:Int) = f x
- aij
@aij,感谢您的评论。对于第一点,我建议Scala提供的“{}”行为的熟悉度。我已经更新了措辞以提高精确性。至于第四点,由于“()”和“{}”之间的交互有些棘手,因此“def f(x:Int):Int = f {x}”是有效的,这就是我提出第五点的原因。 :) - lcn
1
在Scala中,我倾向于认为()和{}基本上是可以互换的,除了它们解析内容的方式不同。我通常不会写f({x}),所以f{x}并不感觉像省略括号,而更像是用花括号替换它们。其他语言确实允许您省略括号,例如,在SML中,'fun f(x) = f x'是有效的。 - aij
@aij,将f {x}视为f({x})似乎对我来说是更好的解释,因为认为(){}是可互换的不太直观。顺便说一下,f({x})的解释在Scala规范(第6.6节)中得到了一定的支持:ArgumentExprs :: =‘(’[Exprs]‘)’|‘(’[Exprs','] PostfixExpr':' '_' '*' ')'| [nl] BlockExp - lcn

14

我认为值得解释一下它们在函数调用中的用法以及为什么会发生各种事情。就像其他人已经说过的那样,花括号定义了一个代码块,它也是一个表达式,因此可以放在需要表达式的地方进行评估。在评估时,将执行其语句,并且最后一个语句的值是整个块评估的结果(类似于Ruby中的一些内容)。

有了这个,我们可以做到:

2 + { 3 }             // res: Int = 5
val x = { 4 }         // res: x: Int = 4
List({1},{2},{3})     // res: List[Int] = List(1,2,3)

最后一个例子只是一个带有三个参数的函数调用,其中每个参数首先被评估。

现在来看看如何使用函数调用定义接受另一个函数作为参数的简单函数。

def foo(f: Int => Unit) = { println("Entering foo"); f(4) }
为了调用它,我们需要传递一个参数类型为Int的函数,因此我们可以使用函数字面量并将其传递给foo:

要调用它,我们需要传递一个接受Int类型参数的函数,这样我们就可以使用函数字面量并将其传递给foo:

foo( x => println(x) )

现在如前所述,我们可以使用一段代码块来代替表达式,因此让我们使用它。


foo({ x => println(x) })

这里发生的是,大括号内的代码会被求值,并将函数值作为块求值的返回值,然后将该值传递给foo。这在语义上与以前的调用相同。

但我们可以添加更多内容:

foo({ println("Hey"); x => println(x) })

现在我们的代码块包含两个语句,因为它在执行foo之前被评估,所以会先打印"Hey",然后将我们的函数传递给foo,打印"Entering foo",最后打印"4"。

虽然这看起来有点丑陋,但Scala允许我们在这种情况下跳过括号,因此我们可以写成:

foo { println("Hey"); x => println(x) }
或者
foo { x => println(x) }

这看起来更好,等同于以前的方法。这里依然会先评估代码块,然后将评估结果(即 x => println(x))作为参数传递给foo函数。


2
只有我一个人觉得这样,但我实际上更喜欢foo({ x => println(x) })的明确性。也许是我太固执了... - dade

8
因为你使用了case,所以你定义了一个部分函数,而部分函数需要使用大括号。

1
我是在一般情况下询问答案,而不仅仅是针对这个例子的答案。 - Marc-François

5

使用圆括号增加编译检查

Spray的作者建议使用圆括号来增加编译检查。对于像Spray这样的DSL,这一点尤为重要。通过使用圆括号,您告诉编译器它应该只给出一行代码,因此如果您无意中给出两个或更多,编译器会报错。但是,如果您使用花括号,在某些情况下,如忘记操作符之类的情况下,您的代码将被编译,但结果可能不是您期望的,这可能导致非常难以发现的错误。下面的示例很简单(因为表达式是纯粹的,并且至少会有警告),但可以说明问题。

method {
  1 +
  2
  3
}

method(
  1 +
  2
  3
 )

第一个编译通过,第二个报错:error: ')' expected but integer literal found. 作者想要写的是1 + 2 + 3
有人可能会认为对于具有默认参数的多参数方法来说也是类似的;使用括号时不可能意外忘记用逗号分隔参数。 冗长性 关于冗长性,一个经常被忽视的重要问题。使用花括号不可避免地会导致冗长的代码,因为Scala风格指南明确规定关闭花括号必须在它们自己的行上:http://docs.scala-lang.org/style/declarations.html“...闭合括号在函数的最后一行之后另起一行。”许多自动重新格式化工具(如Intellij)将自动为您执行此重新格式化。因此,尽可能使用圆括号。例如:List(1, 2, 3).reduceLeft{_ + _}变成了:
List(1, 2, 3).reduceLeft {
  _ + _
}

-1
在理想的编码风格中,括号基本上用于单行代码。 但是,如果特定的代码片段是多行的,则使用大括号是更好的方式。

-3

在使用大括号时,分号会自动添加,而括号则不会。考虑 takeWhile 函数,由于它期望部分函数,因此只有 {case xxx => ??? } 是有效的定义,而不是在 case 表达式周围加上括号。


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