将参数传递给括号 ()
和大括号 {}
中的函数之间的形式区别是什么?
我从 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 }
=> 很好。
我曾经尝试过写这篇文章,但最后我放弃了,因为规则有些分散。基本上,你必须掌握它。
也许最好专注于花括号和圆括号可以互换使用的地方:将参数传递给方法调用时。当且仅当方法需要单个参数时,您可以用花括号替换括号。例如:
List(1, 2, 3).reduceLeft{_ + _} // valid, single Function2[Int,Int] parameter
List{1, 2, 3}.reduceLeft(_ + _) // invalid, A* vararg parameter
但是,为了更好地掌握这些规则,您还需要了解更多信息。
使用括号增加编译检查
Spray 的作者推荐使用圆括号,因为它们增加了编译检查。这对于像 Spray 这样的 DSL 来说尤其重要。通过使用括号,您告诉编译器它应该只给出一行;因此,如果您不小心给了它两个或更多,它会抱怨。现在,大括号就不是这种情况了——例如,如果你在某处忘记了一个运算符,那么你的代码将编译,你会得到意想不到的结果,并且可能是一个很难找到的错误。下面是人为的(因为表达式是纯粹的,并且至少会给出警告),但重点是:
method {
1 +
2
3
}
method(
1 +
2
3
)
第一个编译,第二个给出 error: ')' expected but integer literal found
。作者想写1 + 2 + 3
。
有人可能会争辩说,对于具有默认参数的多参数方法,它是相似的。使用括号时,不可能不小心忘记用逗号分隔参数。
冗长
关于冗长的一个重要的经常被忽视的注释。使用花括号不可避免地会导致代码冗长,因为 Scala style guide 明确指出右花括号必须在自己的行上:
…右大括号位于函数最后一行之后的单独一行。
许多自动重新格式化程序,如 IntelliJ 中的,会自动为您执行重新格式化。所以尽量坚持使用圆括号。
中缀符号
使用中缀表示法时,如 List(1,2,3) indexOf (2)
,如果只有一个参数,则可以省略括号并将其写为 List(1, 2, 3) indexOf 2
。这不是点符号的情况。
另请注意,当您有一个参数是多标记表达式时,例如 x + 2
或 a => a % 2 == 0
,您必须使用括号来指示表达式的边界。
元组
因为有时可以省略括号,所以有时元组需要额外的括号,例如 ((1, 2))
,有时可以省略外括号,例如 (1, 2)
。这可能会引起混淆。
带大小写的函数/部分函数文字
Scala 具有函数和部分函数字面量的语法。它看起来像这样:
{
case pattern if guard => statements
case pattern => statements
}
您可以使用 case
语句的唯一其他地方是使用 match
和 catch
关键字:
object match {
case pattern if guard => statements
case pattern => statements
}
try {
block
} catch {
case pattern if guard => statements
case pattern => statements
} finally {
block
}
您不能在任何其他上下文中使用 case
语句。因此,如果您想使用 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
或类似的东西,你需要花括号。而且因为表达式是一个语句,括号可能出现在花括号内。但有趣的是代码块是 also 表达式,所以你可以在任何inside 表达式中使用它们:
( { 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 }
这不是一个方法调用,所以你不能以任何其他方式编写它。好吧,您可以将花括号 inside 放在 condition
的括号中,也可以将括号 inside 放在代码块的花括号中:
while ({x < 10}) { (x += 1) }
所以,我希望这会有所帮助。
这里有几个不同的规则和推论:首先,当参数是函数时,Scala 会推断大括号,例如,在 list.map(_ * 2)
中,大括号是推断出来的,它只是 list.map({_ * 2})
的更短形式。其次,Scala 允许您跳过最后一个参数列表上的括号,如果该参数列表有一个参数并且它是一个函数,那么 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 }
会。
list.foldLeft{0}{_+_}
有效。
社区正在努力标准化大括号和圆括号的使用,请参阅 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")
+
、--
)使用中缀,而不是像 takeWhile
这样的常规方法。中缀符号的全部意义在于允许 DSL 和自定义运算符,因此不应一直在这种情况下使用它。
我不认为 Scala 中的花括号有什么特别或复杂的地方。要掌握它们在 Scala 中看似复杂的用法,只需记住几件简单的事情:
花括号形成一个代码块,它计算最后一行代码(几乎所有语言都这样做)如果需要,可以使用代码块生成一个函数(遵循规则 1) 单行代码可以省略花括号除了 case 子句(Scala 选择),括号可以在以代码块作为参数的函数调用中省略(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
{}
行为提供熟悉度。我已经更新了精确的措辞。对于 4,由于 ()
和 {}
之间的交互,它有点棘手,因为 def f(x: Int): Int = f {x}
有效,这就是我有 5 个的原因。 :)
fun f(x) = f x
在 SML 中有效。
f {x}
视为 f({x})
对我来说似乎是一个更好的解释,因为将 ()
和 {}
互换的想法不太直观。顺便说一句,f({x})
解释在某种程度上得到了 Scala 规范(第 6.6 节)的支持:ArgumentExprs ::= ‘(’ [Exprs] ‘)’ | ‘(’ [Exprs ‘,’] PostfixExpr ‘:’ ‘_’ ‘*’ ’)’ | [nl] BlockExp
我认为值得解释它们在函数调用中的用法以及为什么会发生各种事情。正如有人已经说过的,花括号定义了一个代码块,它也是一个表达式,因此可以放在期望表达式的位置并对其进行评估。评估时,它的语句被执行,最后一个语句的值是整个块评估的结果(有点像 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:
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。
foo({ x => println(x) })
的显式性质。也许我太拘泥于自己的方式了...
因为您使用的是 case
,所以您定义了一个偏函数,偏函数需要花括号。
使用括号增加编译检查
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 {
_ + _
}
理想编码风格中的括号基本上用于单行代码。但是如果特定的代码是多行的,那么使用大括号是更好的方法。
使用大括号,您会得到分号,而括号则不会。考虑 takeWhile
函数,因为它需要偏函数,所以只有 {case xxx => ??? }
是有效定义,而不是 case 表达式周围的括号。
{}
- 一切都应该是一个单一的纯表达式List{1, 2, 3}.reduceLeft(_ + _)
无效时,你的意思是它有语法错误吗?但我发现代码可以编译。我把我的代码 hereList(1, 2, 3)
,而不是List{1, 2, 3}
。唉,在 Scala 的当前版本(2.13)上,这会失败并显示不同的错误消息(意外的逗号)。您可能必须回到 2.7 或 2.8 才能得到原始错误。