【Scala之旅】控制结构和注解

本节翻译自

综述:本节介绍了for推导式的使用;学习如何使用Scala特有的注解,以及如何与Java注解实现互操作。

for推导式

Scala提供了一个轻量级符号 for 表示序列推导。推导式的形式为 for (enumerators) yield e,其中 enumerators 是指以分号分隔的枚举器列表。枚举器是一个引入新变量的生成器,或者是一个过滤器。推导式求解出由枚举器生成的每个绑定的主体 e,并返回这些值的序列。

这里给个例子:

case class User(val name: String, val age: Int)

val userBase = List(new User("Travis", 28),
  new User("Kelly", 33),
  new User("Jennifer", 44),
  new User("Dennis", 23))

val twentySomethings = for (user <- userBase if (user.age >=20 && user.age < 30))
  yield user.name  // i.e. add this to a list

twentySomethings.foreach(name => println(name))  // prints Travis Dennis

for 循环实际上使用 yield 语句创建了一个 List。因为我们说 yield user.name 是一个 List[String]user <- userBean 是我们的生成器,if(user.age >= 20 && user.age < 30 是一个过滤掉20多岁用户 (filters out users who are in their 20s) 的守卫。

下面是更加复杂的例子,其使用了两个生成器。它计算在 0n-1 之间的所有数值对,它们的和等于给定的值 v

def foo(n: Int, v: Int) =
   for (i <- 0 until n;
        j <- i until n if i + j == v)
   yield (i, j)

foo(10, 10) foreach {
  case (i, j) =>
    print(s"($i, $j) ")  // prints (1, 9) (2, 8) (3, 7) (4, 6) (5, 5)
}

这里 n == 10v == 10。在第一次迭代过程中 i == 0j == 0,所以 i + j != v,因此没有东西的产出。在 i 递增到1之前,j 会递增9次。如果没有 if 守卫,该方法只会简单地打印出以下内容:

(0, 0) (0, 1) (0, 2) (0, 3) (0, 4) (0, 5) (0, 6) (0, 7) (0, 8) (0, 9) (1, 1) ...

注意,推导式并不局限于列表。每一个支持 withFiltermapflapMap(用适当的类型)的数据类型,都可以使用序列推导式。

你可以在推导式中忽略 yield。在这种情况下,推导式将返回 Unit。如果你需要执行副作用,这可能很有用。下面这个程序的结果相当于上面的,但没有使用 yield

def foo(n: Int, v: Int) =
   for (i <- 0 until n;
        j <- i until n if i + j == v)
   print(s"($i, $j)")

foo(10, 10)

注解

注解将元信息与定义关联起来。例如,如果在方法之前有 @deprecated 注解,则方法被使用会导致编译器打印警告。

object DeprecationDemo extends App {
  @deprecated
  def hello = "hola"

  hello  
}

这段程序将编译,但是编译器会打印警告:“there was one deprecation warning”。

注解子句适用于它后面的第一个定义或声明。多个注解子句可能在定义和声明之前出现。这些子句的顺序无关紧要。

确保编码正确性的注解

如果条件不满足,某些注解实际上会导致编译失败。例如,@tailrec 的注解确保了一个方法是尾递归的。尾递归可以保持内存需求不变。下面展示了如何在计算阶乘的方法中使用它:

import scala.annotation.tailrec

def factorial(x: Int): Int = {

  @tailrec
  def factorialHelper(x: Int, accumulator: Int): Int = {
    if (x == 1) accumulator else factorialHelper(x - 1, accumulator * x)
  }
  factorialHelper(x, 1)
}

factorialHelper 方法有一个 @tailrec 注解,它确保了该方法确实是尾递归的。如果我们将 factorialHelper 的实现更改为以下内容,那么它就会失败:

import scala.annotation.tailrec

def factorial(x: Int): Int = {
  @tailrec
  def factorialHelper(x: Int): Int = {
    if (x == 1) 1 else x * factorialHelper(x - 1)
  }
  factorialHelper(x)
}

我们会得到一个“Recursive call not in tail position”的消息。

注解代码生成的影响

一些注解如 @inline 会影响生成的代码(也就是说,如果你没有使用注解的话,你的jar文件可能有不同的字节)。内联意味着将代码插入到调用地点的方法的主体中。结果字节码更长,但有希望运行得更快。使用 @inline 的注解并不能确保一个方法是内联的,但是当且仅当满足一些关于生成代码大小的启发式规则时,才会导致编译器做到这一点。

Java注解

在编写与 Java 交互的 Scala 代码时,注解语法有一些不同之处。注意:确保使用了 -target:jvm-1.8 选项和 Java 注解。

Java 以注解的形式拥有用户定义的元数据。注释的一个关键特征是它们依赖于指定 name-value 对来初始化它们的元素。例如,如果我们需要一个注释来跟踪某个类的源头,我们可以将它定义为

@interface Source {
  public String URL();
  public String mail();
}

然后把它应用到下面

@Source(URL = "http://coders.com/",
        mail = "[email protected]")
public class MyClass extends HisClass ...

Scala 中的注解应用程序看起来就像是构造函数调用,用于实例化一个 Java 注释,它必须使用命名参数:

@Source(URL = "http://coders.com/",
        mail = "[email protected]")
class MyScalaClass ...

如果注解只包含一个元素(没有默认值),那么这个语法就很麻烦。因此,按照约定,如果名称被指定为 value,则可以在Java中使用类似于构造器的语法:

@interface SourceURL {
    public String value();
    public String mail() default "";
}

然后把它应用到下面

@SourceURL("http://coders.com/")
public class MyClass extends HisClass ...

在这种情况下,Scala 提供了同样的可能性

@SourceURL("http://coders.com/")
class MyScalaClass ...

mail 元素是用缺省值指定的,因此我们不需要显式地为它提供一个值。但是,如果我们这样做了,我们就不能在Java中混合使用两种风格:

@SourceURL(value = "http://coders.com/",
           mail = "[email protected]")
public class MyClass extends HisClass ...

Scala在这方面提供了更多的灵活性

@SourceURL("http://coders.com/",
           mail = "[email protected]")
    class MyScalaClass ...

相关推荐