Dart:會用但不會講的運算子

img

前言

**運算子(Operator)可以說是程式語言最重要的功能之一,但是回想起來一開始學程式時卻是有點痛苦的回憶,因為它跟我們所學的數學邏輯有點像又有些差異,好像懂但又說不出個所以然,最後的結果就是圖法煉鋼,多寫幾次程式慢慢有感覺的就“好像”**懂了,但是有時候又會碰到無法解釋的邏輯,例如下面 JavaScript 的例子:
img

資料型別(Data Types)

DateTime、bool、num、String 幾乎我們在開發過程中都一定會使用到,但是這些資料型別到底是什麼?
查看官方文件 dart:core library 可以發現其實它們都被歸在**類別(Class)**裡面。
img
點進每個資料型別的葉面就可以看到,在最上方就標明它們是類別物件,甚至連 Null 都是一個類別。
img

運算子 (Operator)

那什麼是運算子(Operator)?以數值型別(num) 為例,目前我們可以知道它就是一個類別,觀看官方文件就可以看到頁面內有一個 Operators 的說明區段,再仔細一看就可以發現我們常用的運算子都列在上面。
img
從說明其實不難看出
運算子其實就是函式
,也就是說每種資料型別都是一個個 Class,而運算子就是這些 Class 內的 Function,現在開始回想我們所撰寫運算式到底是什麼?
我們以最常見的加法為例:1+2
第一個 1 會判定為數值,所以會有個 num class 來儲存它。
第二個 + 會判定為 1 這個 Class 的加法 Function,它還需要一個 num 當作參數。
第三個 2 會判定為數值,所以也會有個 num class 來儲存它,同時它會被當作 + 的參數帶入計算。
img
+ 函式內它會將自己這個類別(1)的值與參數類別(2)的值轉換成二進制,做完加總之後再將結果轉換成 num 型別並回傳,所以 1+2 我們會得到一個值為3的 num 型別,到這邊我們應該可以了解運算式就是不斷的拆解值並呼叫對應的函式來作處理,跟我們一般的數學運算觀念上不太一樣。
DartPad 上執行 Run 可以看到可以正常運行,只是看不到任何變化,因為我們不知道回傳的 3 放在哪裡。
img

嘗試把 2 改為字串,就會發現編譯器發出錯誤警告,原因很簡單 + 函式只允許傳入 num 型別,所以與其說 Dart 的強型別定義讓程式可以更加嚴謹,不如說 Dart 只提供相同型的運算函式,所以我們無法撰寫不同型別的運算。
img

變數 (Variable)

上面提到我們無法得到回傳結果,如果把記憶體當作是一本筆記本,當我們運算 1+2 時它回傳的 3 其實有寫在這本筆記本內,只是當我們要取值時卻不知道它原本寫在哪一頁內,因此程式語言包含一種可以讓我們取值的機制-變數(Variable),我們可以將變數想像成書籍的目錄頁,在目錄頁內會有章節名稱以其它所在的頁數。
img
變數使用方式也是相同的,我們可以給予每個變數一個的名稱,然後用這個變數來紀錄資料的位置(頁數),但是要怎麼紀錄呢?Dart 提供一個 = 的函式,可以協助我們
將右邊資料所在的頁數寫入到左邊變數的頁次
,接下來我們宣告一個 a 變數,並透過 = 將來右邊的 1+2 的回傳值寫入到左邊的 a 變數,接著將 a 帶入到 print 來輸出到畫面上,print 在讀取 a 這個變數時,Dart 會自動幫我們搜尋到 a 所對應的值 3,因此最後我們可以從畫面上看到一個 3
img

運算子優先序 (Operator precedence)

Dart 是 Google 所開發的程式語言,後來由 ECMA 制定標準規範(ECMA-408),我們可以從 ECMA-408網站 下載文件-ECMA-408.pdf(目前版本為第4版),開啟文件瀏覽到最後一個章節 20.2 Operator Precedence,我們可以看到一個運算子優先序的表格,內容如下:

運算子名稱 運算子 相依性 優先性
Unary postfix ., ?., e++, e–, e1[e2], e1(), () 16
Unary prefix -e, !e, ˜e, ++e, –e 15
Multiplicative *, /,˜/, % 從左至右 14
Additive +, - 從左至右 13
Shift <<, >> 從左至右 12
Bitwise AND & 從左至右 11
Bitwise XOR ˆ 從左至右 10
Bitwise Or | 從左至右 9
Relational <, >, <=, >=, as, is, is! 8
Equality ==, != 7
Logical AND && 從左至右 6
Logical Or || 從左至右 5
If-null ?? 從左至右 4
Conditional e1? e2: e3 從右至左 3
Cascade .. 從左至右 2
Assignment =, *=, /=, +=, -=, &=, ˆ= etc. 從右至左 1
上面除了列出 Dart 的運算子還多個優先性(Precedence)相依性(Associativity)優先性就像我們數學講的先乘除後加減,每一行程式碼都會先解析出所有的運算子,再依運算子的優先性依序處理,而當運算子優先性相同時則依相依性來決定運算的方向。

所以說程式處理順序不是一律由左至右,只是因為大部分的運算子的相依性都是由左至右,所以才容易有這種錯覺。

接下來我們在連續等號的邏輯,範例如下:
img
a=b=c=5 使用2個 =,因為優先性相等所以依照由右到左的原則處理。
首先執行 c=5c 變數會被賦予一個值 5,接著運算函式會回傳 5,所以運算式就會變成 a=b=5
接著執行 b=5b 變數會被賦予一個值 5,接著運算函式會回傳 5,所以運算式就會變成 a=5
最後執行 a=5a 變數會被賦予一個值 5,接著運算函式會回傳 5,所以將 abc 輸出時都會得到 5 的結果。
img
接著我們把運算式改成 a=b=c>5,並賦予 c 初始值 1,我們可以看到結果是 afalsebfalsec1 的結果,雖然看似有點奇怪,但是依照運算子優先序的邏輯來思考就會迎刃而解。
img

因為 Dart 的 嚴謹,所以我們很難寫出特殊的運算式,但是我們拿比較貼近 Dart 的 JavaScript 來看就會發現比較好玩的例子,以一開始的範例 console.log(1<2<3)console.log(3>2>1)來說,console.log 與剛剛使用的 print 一樣,可以將值輸出到畫面上。
接著我們查詢 JavaScript 的運算子優先序,可以看到當運算子優先性一樣時它處理順序是由左到右。
img
我們先看 1<2<3,運算式使用2個 <,因為優先性相等所以依照由左到右的原則處理。
首先先執行 1<2,在此我們會得到一個 true 的布林值,因此運算式會變成 true<3
在 JavaScript 內當布林值與數值型別要比較時會先將布林值轉換為數值型別,true 會變成 1false 會變成0,因此運算式會變成 1<3
最後由 1<3 可以得到一個 true 的布林值。
img
同樣的邏輯我們在思考 3>2>1 就可以發現為什麼會得到 false 的結過。
img

後記

深入了解運算子就會發現概念很簡單但是事情變複雜了,最好的方式應該是避免些出易讀性不高的程式碼,因為別人在維護時不只很難理解,要是誤解可能衍生出不必要的 BUG。