以下两种在Scala中编写函数的方式有何区别?

8

我是Scala的新手,看到很多定义函数的方式,但找不到明确的解释它们之间的区别,以及何时使用哪种形式。

以下函数定义有什么主要区别?

  1. With '='

    def func1(node: scala.xml.Node) = {
        print(node.label + " = " + node.text + ",")
    }
    
  2. Without '='

    def func2 (node: scala.xml.Node) {
        print(node.label + " = " + node.text + ",")
    }
    
  3. With '=>'

    def func3 = (node: scala.xml.Node) => {
        print(node.label + " = " + node.text + ",")
    }
    
  4. As a var

    var func4 = (node: scala.xml.Node) => {
        print(node.label + " = " + node.text + ",")
    }
    
  5. Without a block

    def func5 (node: scala.xml.Node) = print(node.label + " = " + node.text + ",")  
    

当作为回调函数使用时,它们看起来都会编译和呈现相同的结果。

    xmlNodes.iterator.foreach(...)
  • 它们生成的字节码有任何区别吗?
  • 在什么情况下使用哪种形式有指导方针吗?
4个回答

19
每个问题在本网站的其他地方都有答案,但我认为没有什么可以将它们全部处理。因此:
花括号和等号
使用等号定义的方法返回一个值(任何最后一件事情评估)。只使用花括号定义的方法返回Unit。如果您使用等号,但最后一件事评估为Unit,则没有区别。如果是等号后的单个语句,则不需要花括号;这对字节码没有影响。因此,1、2和5本质上是相同的:
def f1(s: String) = { println(s) }     // println returns `Unit`
def f2(s: String) { println(s) }       // `Unit` return again
def f5(s: String) = println(s)         // Don't need braces; there's only one statement

函数与方法

一个函数通常被写作 A => B,是 Function 类的子类之一,例如 Function1[A,B]。由于这个类有一个 apply 方法,在使用括号而没有方法名时,Scala 会自动调用它,看起来像是一个方法调用,实际上它是在那个 Function 对象上进行的调用!所以如果你写下:

def f3 = (s: String) => println(s)

那么你的意思是"f3应该创建一个Function1[String,Unit]实例,它具有一个类似于def apply(s: String) = println(s)apply方法"。因此,如果你调用f3("Hi"),这首先会调用f3来创建函数对象,然后调用apply方法。

每次使用时都创建函数对象非常浪费资源,因此将函数对象存储在var中更为合理:

val f4 = (s: String) => println(s)

这个对象保存了一个相同函数对象的实例,它与def(方法)返回的一样,因此你不必每次重新创建它。
何时使用什么:
人们对于: Unit = ...{ }的约定有所不同。个人而言,我所有返回Unit的方法都不带等号——这对我来说是一个指示,表明该方法几乎肯定没有用,除非它具有某种副作用(改变变量,执行IO等)。此外,我通常只在必要时使用大括号,因为有多个语句或单个语句过于复杂,我希望有一个视觉辅助工具告诉我它在哪里结束。
当你想要一个方法时,应该使用方法。每当你想将函数对象传递到其他方法中使用它们时(或每当你想能够应用函数时),都应创建函数对象(或应将其指定为参数)。例如,假设你想要能够缩放值:
class Scalable(d: Double) {
  def scale(/* What goes here? */) = ...
}

您可以提供一个常数乘数。或者您可以提供要添加和要乘的内容。但最灵活的是,您只需请求从 DoubleDouble 的任意函数:

def scale(f: Double => Double) = f(d)

现在,你可能对于“默认”的比例有了一个想法。那可能根本没有缩放。因此,您可能需要一个函数,它接受 Double 并返回完全相同的 Double

val unscaled = (d: Double) => d

我们将函数存储在一个val中,因为我们不想一遍又一遍地创建它。现在我们可以将这个函数用作默认参数:

class Scalable(d: Double) {
  val unscaled = (d: Double) => d
  def scale(f: Double => Double = unscaled) = f(d)
}

现在我们可以调用 x.scalex.scale(_*2) 以及 x.scale(math.sqrt),它们都可以正常工作。

5

是的,字节码存在差异。而且,也有相应的指导方针。

  1. 带有=:声明一个方法,该方法接受一个参数并返回右侧块中的最后一个表达式,其类型在此处为Unit

  2. 不带=:声明一个没有返回值的方法,即返回类型始终为Unit,而不管右侧块中的最后一个表达式的类型是什么。

  3. 带有=>:声明一个返回类型为scala.xml.Node => Unit函数对象。每次调用此方法func3时,都会在堆上构造一个新的函数对象。如果您编写func3(node),则将首先调用func3以返回函数对象,然后在该函数对象上调用apply(node)。这比直接调用普通方法慢,如情况1和2。

  4. 作为var:声明一个变量,并像第3点一样创建一个函数对象,但是函数对象只创建一次。使用它来调用函数对象在大多数情况下比直接调用普通方法慢(可能无法由JIT内联),但至少您不会重新创建对象。如果您想避免其他人重新分配变量func4的危险,请改用vallazy val

  5. 这是当块只包含单个表达式时的语法糖,相当于1。

请注意,如果您使用表单1、2和5与高阶foreach方法,Scala仍将创建一个函数对象,隐式调用func1func2func5,并将其传递给foreach(至少在当前版本中不会使用方法句柄或类似的东西)。 在这些情况下,生成的代码大致对应于:
xmlNodes.iterator.foreach((node: scala.xml.Node) => funcX(node))

因此,指南是 - 除非您每次都使用相同的函数对象,否则只需创建普通方法,如1、2或5。它将被提升为函数对象,需要时会自动执行。
如果您意识到这会生成许多对象,因为经常调用此类方法,您可能希望通过使用形式4来微调优化,以确保仅创建foreach的函数对象一次。
在选择1、2和5之间进行决策时,一个指南是 - 如果您有单个语句,请使用形式5。
否则,如果返回类型为Unit,则对于公共API,请使用def foo(): Unit = {形式,以便查看您的代码的客户端可以快速清楚地看到返回类型。对于返回类型为Unit的私有方法,请使用def foo() {形式,以获得更短代码的方便性。但这只是关于样式的一个特定指南。
更多信息,请参见:http://docs.scala-lang.org/style/declarations.html#methods

2

嗯,1、2和5根本不是函数,它们是方法,方法与函数有根本的区别:方法属于对象而不是对象本身,而函数则是对象。

1、2和5也完全相同:如果只有一个语句,则不需要花括号来组合多个语句,因此5与1相同。省略=符号是声明返回类型Unit的语法糖,但Unit也是1和5的推断返回类型,因此2与1和5相同。

3是一个方法,当调用时返回一个函数。4是指向函数的变量。


我认为在Scala中一切都是对象 :) 是这样吗? - Eran Medan
@EranMedan:这取决于您对“对象”和“一切”的定义。“对象”可以意味着“程序可以操作的实体”(我现在将其称为object),或者是“对象系统的成员值”(我现在将其称为Object)。在Scala中,所有可以被程序操纵的内容(即每个object)也都是一个Object,也就是一个类的实例(不像Java中,原始类型虽然可以被程序操纵(即属于“对象”),但它们不是Object)。在Scala中,这种区别不存在:每个object都可以视为是一个Object - Jörg W Mittag
也是一个“对象”,每个“对象”也都是一个对象。然而,在语言中有些东西不能被程序操作,也不是类的实例,例如方法、类型、类、特质、变量、语法和参数列表等。例如,如果类是“Object”,那么它们就可以像任何其他对象一样具有方法和字段,我们就不需要伴生对象了。注意:您可以使用Scala的反射API来为您提供代表类、特质或类型的对象。但是,这仅是表示,而不是实际的类或类型的实例。 - Jörg W Mittag
对象只是一个代理,它并不是真正的东西。因此,当我们说“一切都是对象”时,我们真正意思是“每个对象都是Object”,也就是说,程序可以操作的所有东西也是对象系统的成员,或者换句话说,在对象系统之外没有值(不像Java中的原始类型)。我们并不是指语言中存在的所有东西都可以在运行时由程序操纵。请注意,对于方法,这种区别有些模糊,因为Scala会自动将方法转换为函数。 - Jörg W Mittag
1
在某些情况下,这就是为什么你的示例中1、2和5有效的原因:编译器看到foreach期望一个函数,但你正在传递一个方法,但方法的签名与期望的函数相匹配,所以它会为你创建一个函数。 - Jörg W Mittag

1

1-2. 当你去掉等号时,你的函数就变成了过程(返回Unit或什么都不返回)。
3. 在第三种情况下,你定义了一个返回函数的函数scala.xml.Node => Unit
4. 相同,但你将一些函数scala.xml.Node => Unit分配给变量。这个区别在Scala中定义函数的这三种方式之间的区别中有解释。
5. 与1相比没有区别。但你不能像那样写多行语句。


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