Scala中方法和函数的区别

281

我读了Scala Functions(属于Another tour of Scala)。在那篇文章中,作者提到:

方法和函数不是同一回事。

但他没有解释具体含义。他想表达什么意思呢?


2
http://jim-mcbeath.blogspot.com/2009/05/scala-functions-vs-methods.html - Marcelo Cantos
3
我认为你可以从“方法和函数有什么区别”这篇文章中获得一些东西。 - jinglining
一个带有好答案的后续问题:Scala中的函数与方法 - Josiah Yoder
12个回答

255

Jim已经在他的博客文章中详细介绍了这个问题,但为了参考,我在此发布简要概述。

首先,让我们看看Scala规范告诉我们什么。第三章(类型)告诉我们有关函数类型(3.2.9)和方法类型(3.3.1)。第四章(基本声明)讲述了值声明和定义(4.1)、变量声明和定义(4.2)和函数声明和定义(4.6)。第六章(表达式)讲述了匿名函数(6.23)和方法值(6.7)。奇怪的是,函数值只在3.2.9中提到过一次,其他地方都没有出现。

函数类型(大致上)是一个形如(T1, ..., Tn) => U的类型,它是标准库中的FunctionN trait的简写。匿名函数方法值都具有函数类型,函数类型可以用作值、变量和函数声明和定义的一部分。实际上,它可以成为方法类型的一部分。

方法类型是一种非值类型。这意味着没有值 - 没有对象、没有实例 - 具有方法类型。如上所述,方法值实际上具有函数类型。方法类型是一个def声明 - 关于def的所有内容,除了其主体。

值声明和定义以及变量声明和定义valvar声明,包括类型和值 - 可以分别是函数类型匿名函数或方法值。请注意,在JVM上,这些(方法值)是使用Java称为“方法”来实现的。

函数声明是一个def声明,包括类型和主体。类型部分是方法类型,主体是一个表达式或块。这也是在JVM上使用Java所称的“方法”来实现的。

最后,一个匿名函数是一个函数类型的实例(即,一个FunctionN特质的实例),而方法值是相同的东西!区别在于,方法值是从方法创建的,可以通过在方法后面加下划线(m _是对应于“函数声明”(def)m的方法值),或者通过称为eta扩展的过程进行,这类似于从方法到函数的自动转换。
这就是规范所说的,所以让我提前说一下:我们不使用那个术语!它会导致所谓的“函数声明”(程序的一部分(第4章——基本声明))和“匿名函数”(表达式)以及“函数类型”之间的混淆。函数类型是一种类型——特质。
下面的术语由经验丰富的Scala程序员使用,与规范的术语相比有一个变化:我们用“方法”代替“函数声明”。甚至是方法声明。此外,我们注意到,值声明和变量声明在实际目的上也是方法。
因此,鉴于上述术语的变化,以下是区别的实际解释。

函数是一个对象,包含FunctionX特质之一,例如Function0Function1Function2等。它可能还包括PartialFunction,实际上它扩展了Function1

让我们看看其中一个特质的类型签名:

trait Function2[-T1, -T2, +R] extends AnyRef

这个特质有一个抽象方法(也有一些具体实现的方法):

def apply(v1: T1, v2: T2): R

这就是关于它的全部内容。一个函数有一个apply方法,接收类型为T1、T2、...、TN的N个参数,并返回类型为R的值。它在接收到的参数上是逆变的,在结果上是协变的。
这种变化意味着Function1[Seq[T], String]是Function1[List[T], AnyRef]的子类型。作为子类型,它可以用来代替其它类型。如果我要调用f(List(1, 2, 3))并期望得到AnyRef,那么上述两种类型都可以使用。
现在,方法和函数的相似之处是什么?如果f是一个函数,m是一个局部方法,那么两者都可以像这样被调用:
val o1 = f(List(1, 2, 3))
val o2 = m(List(1, 2, 3))

这些调用实际上是不同的,因为第一个只是一种语法糖。Scala将其扩展为:

val o1 = f.apply(List(1, 2, 3))

当然,这是对象f的方法调用。函数还有其他语法糖,如函数字面量(实际上有两个)和(T1, T2) => R类型签名。例如:
val f = (l: List[Int]) => l mkString ""
val g: (AnyVal) => String = {
  case i: Int => "Int"
  case d: Double => "Double"
  case o => "Other"
}

方法和函数之间的另一个相似之处在于,前者可以很容易地转换为后者:

val f = m _

如果假设m的类型为(List[Int])AnyRef,Scala将会扩展that,并转化为以下形式(在Scala 2.7中):

val f = new AnyRef with Function1[List[Int], AnyRef] {
  def apply(x$1: List[Int]) = this.m(x$1)
}

在Scala 2.8中,它实际上使用一个AbstractFunction1类来减小类的大小。
注意,不能反过来将函数转换为方法。
然而,方法有一个巨大的优势(好吧,两个——它们可以稍微快一点):它们可以接收类型参数。例如,虽然上面的f可以必须指定它接收的List的类型(在示例中是List[Int]),但m可以对其进行参数化:
def m[T](l: List[T]): String = l mkString ""

我认为这基本上涵盖了所有内容,但是如果还有任何问题需要回答,我很乐意补充。


28
很遗憾,Odersky/Venners/Spoon 编写的 Scala 书籍以及 Scala 规范文件都有一些混用 "函数" 和 "方法" 的情况。(它们在可能应该使用 "方法" 的地方使用 "函数",但有时候也会反过来。例如,规范文件的第 6.7 节涉及将方法转换为函数,但却被命名为 "方法值"。呃)我认为这些词语的不严谨使用导致了很多人在学习 Scala 语言时感到困惑。 - Seth Tisue
4
我知道,我知道——《Programming in Scala》是教我学习Scala的书。我是靠走弯路学会的,也就是说,Paulp纠正了我的偏差。 - Daniel C. Sobral
5
讲解得非常好!我有一点要补充:当你引用编译器扩展val f = m的方式为val f = new AnyRef with Function1[List[Int], AnyRef] {def apply(x$1: List[Int]) = this.m(x$1)}时,你应该指出apply方法内的this并不是指向AnyRef对象,而是指向在其方法中评估val f = m _的对象(可以说是_outer_ this),因为this是闭包捕获的值之一(就像下面指出的return一样)。 - Holger Peine
5
@tldr 《Scala编程》(http://www.artima.com/shop/programming_in_scala_2ed)是Odersky等人合著的书,@tldr是它的常用缩写(他们告诉我他们不太喜欢PiS这个缩写,因为某些原因!:)) - Daniel C. Sobral
2
如果您在SO答案中写了一篇文章,请在其顶部添加TL;DR。 - MaxNevermind
显示剩余5条评论

69

方法和函数之间的一个重要实际区别是return的含义。 return仅从方法中返回。例如:

scala> val f = () => { return "test" }
<console>:4: error: return outside method definition
       val f = () => { return "test" }
                       ^

在方法中定义的函数返回时,相当于进行了非局部返回:

scala> def f: String = {                 
     |    val g = () => { return "test" }
     | g()                               
     | "not this"
     | }
f: String

scala> f
res4: String = test

在本地方法中返回仅返回该方法。

scala> def f2: String = {         
     | def g(): String = { return "test" }
     | g()
     | "is this"
     | }
f2: String

scala> f2
res5: String = is this

9
因为闭包捕获了返回值。 - Daniel C. Sobral
6
我不认为有一个场景需要从函数返回到非本地作用域。实际上,如果一个函数可以随意决定向上返回堆栈,这可能会成为严重的安全问题。感觉有点像longjmp,只是更容易意外出错。我注意到scalac不允许我从函数中返回,这是否意味着这种做法已经被该语言废弃了? - root
2
@root - 如果在 for (a <- List(1, 2, 3)) { return ... } 内部返回,会怎么样?这将被解糖为一个闭包。 - Ben Lings
嗯... 好吧,那是一个合理的使用情况。仍然存在导致难以调试的可怕问题的潜力,但这将使它更加明智。 - root
非本地返回也是在需要“短路”折叠时的更好方法之一,例如:https://dev59.com/jGcs5IYBdhLWcg3wYzBU#12897950 - Todd Owen
2
说实话,我会使用不同的语法。让return从函数中返回一个值,并使用某种形式的escapebreakcontinue来从方法中返回。 - Ryan Leach

44

函数 可以通过一系列参数的调用来产生结果。一个函数包含参数列表、主体和返回类型。 属于类、特质或单例对象的函数被称为方法。在其他函数内部定义的函数称为本地函数。返回Unit类型结果的函数称为过程。 源代码中的匿名函数称为函数字面量。在运行时,函数字面量实例化为称为函数值的对象。

《Scala编程第二版》Martin Odersky - Lex Spoon - Bill Venners


1
一个函数可以作为def或val/var属于一个类。只有def才是方法。 - Josiah Yoder

32

假设你有一个列表

scala> val x =List.range(10,20)
x: List[Int] = List(10, 11, 12, 13, 14, 15, 16, 17, 18, 19)

定义一个方法

scala> def m1(i:Int)=i+2
m1: (i: Int)Int

定义一个函数

scala> (i:Int)=>i+2
res0: Int => Int = <function1>

scala> x.map((x)=>x+2)
res2: List[Int] = List(12, 13, 14, 15, 16, 17, 18, 19, 20, 21)

接受参数的方法

scala> m1(2)
res3: Int = 4

使用val定义函数

scala> val p =(i:Int)=>i+2
p: Int => Int = <function1>

函数的参数是可选的

 scala> p(2)
    res4: Int = 4

scala> p
res5: Int => Int = <function1>

方法的参数是必需的

scala> m1
<console>:9: error: missing arguments for method m1;
follow this method with `_' if you want to treat it as a partially applied function

请检查以下教程,该教程通过示例解释了传递其他差异,例如方法与函数之间的其他差异、将函数用作变量、创建返回函数的函数。


13

函数不支持参数默认值,而方法支持。将方法转换为函数会丢失参数默认值。(Scala 2.8.1)


6
这是有原因的吗? - corazza

12

这里有一篇很好的文章(链接),我大部分描述都是参考它的。 简单比较一下Scala中函数和方法的区别,供您参考:

函数: 它们基本上就是一个对象。更准确地说,函数是带有apply方法的对象;因此,它们比方法慢一点,因为会产生开销。它类似于静态方法,因为它们独立于要调用的对象。 下面是一个简单的函数示例:

val f1 = (x: Int) => x + x
f1(2)  // 4

上面的代码行仅仅是将一个对象赋值给另一个对象,例如object1 = object2。实际上,我们例子中的object2是一个匿名函数,因此左侧获取了一个对象(Function)的类型。因此,现在f1是一个对象(Function)。这个匿名函数实际上是Function1[Int, Int]的一个实例,表示一个参数类型为Int,返回值类型为Int的函数。 调用不带参数的f1将给出匿名函数的签名(Int => Int = )

方法(Methods): 它们不是对象,而是分配给类的实例,即对象。与Java中的方法或C++中的成员函数完全相同(正如Raffi Khatchadourian这个问题的评论中指出的一样)等等。 一个简单的方法示例如下:

def m1(x: Int) = x + x
m1(2)  // 4

上面的那一行不是简单的值赋值,而是一个方法的定义。当你像第二行那样使用值2调用这个方法时,x将被替换为2并计算结果,你将得到4作为输出。如果只写m1,你会在这里出现错误,因为它是一个方法,需要输入值。通过使用 _,你可以将一个方法分配给一个函数,如下所示:

val f2 = m1 _  // Int => Int = <function1>

“将方法分配给函数”是什么意思?这是否意味着您现在拥有一个与该方法行为相同的对象? - K. M
@K.M: val f2 = m1 _ 相当于 val f2 = new Function1[Int, Int] { def m1(x: Int) = x + x }; - sasuke

4
这里有一篇由Rob Norris撰写的文章,解释了Scala中方法和函数的区别,以下是简短摘要:
Scala中的方法不是值,而函数是值。通过η-扩展(由尾随下划线触发),您可以构造一个委托给方法的函数。
根据以下定义:
方法是使用def定义的内容,而值是可以分配给val的内容。
简而言之,当我们定义一个方法时,我们无法将其分配给val。
scala> def add1(n: Int): Int = n + 1
add1: (n: Int)Int

scala> val f = add1
<console>:8: error: missing arguments for method add1;
follow this method with `_' if you want to treat it as a partially applied function
       val f = add1

注意 add1 的类型,它看起来不正常;你无法声明一个类型为 (n: Int)Int 的变量。方法不是值。
然而,通过添加η扩展后缀运算符(η的发音为“eta”),我们可以将方法转换为函数值。请注意f的类型。
scala> val f = add1 _
f: Int => Int = <function1>

scala> f(3)
res0: Int = 4
< p > _ 的效果是执行以下等效操作:我们构造一个 Function1 实例,该实例代理到我们的方法。

scala> val g = new Function1[Int, Int] { def apply(n: Int): Int = add1(n) }
g: Int => Int = <function1>

scala> g(3)
res18: Int = 4

2
实际上,Scala程序员只需要了解以下三个规则,就可以正确地使用函数和方法:
- 用def定义的方法和用=>定义的函数文字是函数。这在《Programming in Scala, 4th edition》第8章的第143页中有定义。 - 函数值是可以传递的对象。函数文字和部分应用的函数都是函数值。 - 如果在代码中需要一个函数值,则可以省略部分应用的函数的下划线。例如:someNumber.foreach(println) 在四版的《Programming in Scala》中,人们仍然难以区分两个重要概念:函数和函数值,因为���有版本都没有给出清晰的解释。语言规范太过复杂。我发现上述规则简单而准确。

1
在Scala 2.13中,与函数不同的是,方法可以接受/返回
  • 类型参数(多态方法)
  • 隐式参数
  • 依赖类型
然而,在dotty(Scala 3)中,这些限制被解除,例如多态函数类型#4672,dotty版本0.23.0-RC1启用了以下语法 类型参数
def fmet[T](x: List[T]) = x.map(e => (e, e))
val ffun = [T] => (x: List[T]) => x.map(e => (e, e))

隐式参数(上下文 参数)

def gmet[T](implicit num: Numeric[T]): T = num.zero
val gfun: [T] => Numeric[T] ?=> T = [T] => (using num: Numeric[T]) => num.zero

依赖类型
class A { class B }
def hmet(a: A): a.B = new a.B
val hfun: (a: A) => a.B = hmet

更多示例,请参见tests/run/polymorphic-functions.scala


0

方法属于一个对象(通常是您定义它的classtraitobject),而函数本身是一个值,因为在Scala中每个值都是一个对象,因此,函数是一个对象

例如,给定下面的一个方法和一个函数:

def timesTwoMethod(x :Int): Int = x * 2
def timesTwoFunction = (x: Int) => x * 2

第二个def是一个类型为Int => Int的对象(Function1[Int, Int]的语法糖)。

Scala将函数作为对象,以便它们可以作为一等实体使用。这样,您可以将函数作为参数传递给其他函数。

但是,Scala也可以通过称为Eta扩展的机制将方法视为函数。

例如,在List上定义的高阶函数map接收另一个函数f:A => B作为其唯一参数。下面两行是等价的:

List(1, 2, 3).map(timesTwoMethod)
List(1, 2, 3).map(timesTwoFunction)

当编译器在需要函数的地方看到一个def时,它会自动将该方法转换为等效的函数。

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