【Scala之旅】控制結構和註解

gcusky發表於2019-02-16

本節翻譯自

綜述:本節介紹了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 = "support@coders.com")
public class MyClass extends HisClass ...

Scala 中的註解應用程式看起來就像是建構函式呼叫,用於例項化一個 Java 註釋,它必須使用命名引數:

@Source(URL = "http://coders.com/",
        mail = "support@coders.com")
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 = "support@coders.com")
public class MyClass extends HisClass ...

Scala在這方面提供了更多的靈活性

@SourceURL("http://coders.com/",
           mail = "support@coders.com")
    class MyScalaClass ...

相關文章