操作順序與關聯


+、 -、*、/...這些一般語言所謂的運算子,在Scala中其實是方法定義,如果一個運算式中混合了多個「運算子」,那麼該如何決定其優先順序呢?又或者 說,如何決定該呼叫哪個物件的哪個方法?例如 1 + 2 * 3,如何能決定為 (1).+((2).*(3))?而不是((1).+(2)).*(3)?

Scala主要是由方法名稱的第一個字元來決定運算的順序,如果方法名稱的第一個字元是*,則它比+有更高的優先順序,以下是方法第一個字元的優先順序 (由高而低),同一列表示優先順序相同:
  • 除了以下字元外的其它特殊字元
  • *、/、%
  • +、:
  • =、!
  • <、>
  • &
  • |
  • 字母
  • 指定運算(非=、<=、>=、==,且以=結尾方法)

所以運算符號一定優先於字母名稱的方法,例如 1 + 2 equals 3,結果就是 (1 + 2) equals 3。指定運算是+=、-=、*= 、/=等以=結尾,但又不是=、<=、 >=、==的運算方法,指定運算優先順序最低,所以 a *= 1 + 4,結果會是 a *= (1 + 4)。當然,最簡單的方式,還是使用括號來釐清優先順序。

當優先順序相同時,基本上由左至右進行運算,所以若1 + 2 + 3時,運算的順序是(1 + 2) + 3,+號是左邊物件的方法,運算方法到底屬於哪個物件,這稱之為「關聯性」(Associativity)。一般情況下,運算方法都是屬於左邊物件,不過,當方法名稱是以:結尾時, 則該名稱會是右運算元的方法。在Scala中的例子是List 的::,它用來在List「前端」附加物件,例如:
val x = List(1, 2)
println(0 :: x)

這會顯示List(0, 1, 2),但0是個Int,並沒有定義::方法(就算是隱式轉換後的RichInt類別也沒有定義::方法),這個操作是如何完成的?答案是::是右邊物件的 方法,也就是x所參考的List物件上的方法,所以上面這個程式,等同於:
val x = List(1, 2)
println(x.::(0))

這個程式顯示的結果也是List(0, 1, 2)。如果你這麼寫,則程式會出錯:
val x = List(1, 2)
println(x :: 3)

由於::以:結尾,所以Scala會認為要由右邊物件來呼叫::方法,但3是個Int,不會有::方法,所以會出現「value :: is not a member of Int」的錯誤訊息。

由於:結尾的方法會被Scala視為右邊物件的方法,所以如果是-1 :: 0 :: x,實際上的運算順序會是反過來從右至左,也就是(-1 :: (0 :: x))。

由於在Scala中,任何的方法都可以撰寫運算子的形式,當你嘗試定義自己的「運算子」時,就必須注意操作的優先順序與關聯性(也許像是Array的/: 這樣的方法)。