“=>”、“()=>”和“Unit=>”有什么区别?它们与编程有关。

161

我正在尝试表示一个不接受任何参数并且不返回任何值的函数(如果必须知道,我是在模拟JavaScript中的setTimeout函数。)

case class Scheduled(time : Int, callback :  => Unit)

无法编译,提示“`val'参数可能不是按名称调用”

case class Scheduled(time : Int, callback :  () => Unit)  

这段代码可以编译,但需要使用奇怪的方式来调用,而不是

Scheduled(40, { println("x") } )

我必须这样做。

Scheduled(40, { () => println("x") } )      

以下方式也可以达到同样的效果

class Scheduled(time : Int, callback :  Unit => Unit)

但以更不合理的方式被调用

 Scheduled(40, { x : Unit => println("x") } )

(类型为 Unit 的变量是什么?)当然我想要的是一个构造函数,可以像调用普通函数一样调用它:

 Scheduled(40, println("x") )

给婴儿喂奶瓶!


5
使用惰性参数(by-name parms)时,使用案例类(case classes)的另一种方法是将它们放在第二个参数列表中,例如 case class Scheduled(time: Int)(callback: => Unit)。这样做是可行的,因为第二个参数列表不会被公开暴露,也不会包含在生成的equals/hashCode 方法中。 - nilskp
关于按名称参数和0元函数之间的差异,还有一些有趣的方面可以在这个问题和答案中找到。实际上,当我发现这个问题时,这正是我正在寻找的内容。 - lex82
4个回答

253

按名称调用: => 类型

=>类型符号代表按名称调用,它是参数传递的众多方式之一。如果您不熟悉这些内容,我建议花些时间阅读维基百科文章,尽管现在通常使用的是按值调用和引用调用。

它意味着传递的值会被替换为函数内部的值名。例如,考虑以下函数:

def f(x: => Int) = x * x

如果我这样调用它

var y = 0
f { y += 1; y }

那么代码将会执行如下:

{ y += 1; y } * { y += 1; y }

虽然这引出了一个问题,即如果有标识符名称冲突会发生什么。在传统的按名调用中,一种叫做捕获避免替换的机制会发生,以避免命名冲突。然而,在Scala中,采用了另一种方式实现相同的结果--参数内的标识符名称不能引用或者遮盖被调用函数中的标识符。

介绍完其他两个相关点之后,还有一些与按名调用相关的内容需要讲解。

0元函数: () => Type

语法 () => Type 表示 Function0 类型的函数。也就是说,这是一个不带参数且返回某些值的函数。这类似于调用方法 size()--它没有参数并返回一个数字。

有趣的是,这个语法与匿名函数字面量的语法非常相似,这导致有些人感到混淆。例如:

() => println("I'm an anonymous function")

是一个零元匿名函数字面量,其类型

() => Unit

那么我们可以这样写:

val f: () => Unit = () => println("I'm an anonymous function")

重要的是不要混淆类型和值。

Unit => Type

实际上,这只是一个Function1函数,它的第一个参数是Unit类型。其他写法可能是(Unit) => Type或者Function1[Unit, Type]。然而,这很少是我们想要的。 Unit类型的主要作用是指示一个不感兴趣的值,所以没有意义去接收该值。

例如,考虑以下情况:

def f(x: Unit) = ...

x 只能有一个值,因此不需要接收它,那么它可能有什么用呢?一种可能的用途是链式调用返回 Unit 的函数:

val f = (x: Unit) => println("I'm f")
val g = (x: Unit) => println("I'm g")
val h = f andThen g

由于andThen只在Function1上定义,而我们要链接的函数返回的是Unit,因此我们必须将它们定义为Function1[Unit, Unit]类型才能链接它们。

混淆的原因

第一个混淆的原因是认为0元函数类型和字面量之间的相似性也存在于按名称调用中。换句话说,认为因为:

() => { println("Hi!") }

() => Unit的文字表示,然后

{ println("Hi!") }

这里的=> Unit不是字面量,而是一段代码块

另一个令人困惑的地方是,Unit类型的写作(),看起来像是一个0元参数列表(但实际上不是)。


1
@som-snytt 好吧,问题没有询问 case ... =>,所以我没有提及。悲伤但真实。 :-) - Daniel C. Sobral
1
@Daniel C. Sobral,您能否解释一下“那是一段代码块,而不是文字字面量”的部分。那么这两者的确切区别是什么? - nish1013
3
“字面量”是一个值(例如整数1,字符'a',字符串"abc"或函数() => println("here")等)。它可以作为参数传递,存储在变量中等。 “代码块”是语法限定的语句集合 - 它不是一个值,不能被传递或者类似于此的操作。 - Daniel C. Sobral
Unit => Type() => Type 有什么区别? - Incerteza
3
@Alex 这就像是 (Unit) => Type() => Type 的区别一样——前者是一个 Function1[Unit, Type],后者是一个 Function0[Type] - Daniel C. Sobral
显示剩余3条评论

39
case class Scheduled(time : Int, callback :  => Unit)

case修饰符使得每个构造函数参数都隐式转为val。因此(正如有人指出的那样),如果你去掉case,就可以使用按名调用的参数。编译器可能仍然允许这样做,但如果它创建了val callback而不是变形成lazy val callback,可能会让人感到惊讶。

当你将callback: () => Unit更改为一个函数类型时,你的case就只是接收一个函数而不是一个按名调用的参数了。显然,函数可以存储在val callback中,所以没有问题。

最简单的方法来得到你想要的结果(Scheduled(40, println("x")),其中使用按名调用的参数来传递lambda),可能是跳过case,并明确地创建你一开始无法获得的apply

class Scheduled(val time: Int, val callback: () => Unit) {
    def doit = callback()
}

object Scheduled {
    def apply(time: Int, callback: => Unit) =
        new Scheduled(time, { () => callback })
}

使用中:

scala> Scheduled(1234, println("x"))
res0: Scheduled = Scheduled@5eb10190

scala> Scheduled(1234, println("x")).doit
x

3
为什么不将它保留为Case Class并覆盖默认的Apply方法?此外,编译器无法将By-Name参数转换为Lazy Val,因为它们具有本质上不同的语义,Lazy 只会执行一次,而By-Name 在每次引用时都会执行。 - Viktor Klang
@ViktorKlang 你如何覆盖 case class 的默认 apply 方法?https://dev59.com/BnE85IYBdhLWcg3wr1ke - Sawyer
对象 ClassName { def apply(…): … = … } - Viktor Klang
四年后,我意识到我选择的答案只回答了标题中的问题,而不是我实际提出的问题(而这个答案确实回答了我的问题)。 - Michael Lorton

1
在这个问题中,您想要模拟JavaScript中的SetTimeOut函数。基于以前的答案,我编写了以下代码:
class Scheduled(time: Int, cb: => Unit) {
  private def runCb = cb
}

object Scheduled {
  def apply(time: Int, cb: => Unit) = {
    val instance = new Scheduled(time, cb)
    Thread.sleep(time*1000)
    instance.runCb
  }
}

在 REPL 中,我们可以获得类似这样的东西:
scala> Scheduled(10, println("a")); Scheduled(1, println("b"))
a
b

我们的模拟与SetTimeOut并不完全相同,因为我们的模拟是阻塞函数,而SetTimeOut是非阻塞函数。

0

我是这样做的(只是不想破坏应用):

case class Thing[A](..., lazy: () => A) {}
object Thing {
  def of[A](..., a: => A): Thing[A] = Thing(..., () => a)
}

并调用它

Thing.of(..., your_value)

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