2-基本類型
==========
本章介紹Elixir的基本類型。Elixir主要的基本類型有:
整型(integer),浮點(float),布爾(boolean),原子(atom,又稱symbol符號),
字符串(string),列表(list)和元組(tuple)等。
它們在iex中顯示如下:
```elixir
iex> 1 # integer
iex> 0x1F # integer
iex> 1.0 # float
iex> true # boolean
iex> :atom # atom / symbol
iex> "elixir" # string
iex> [1, 2, 3] # list
iex> {1, 2, 3} # tuple
```
## 2.1-基本算數運算
打開```iex```,輸入以下表達式:
```elixir
iex> 1 + 2
3
iex> 5 * 5
25
iex> 10 / 2
5.0
```
>```10 / 2```返回了一個浮點型的5.0而非整型的5,這是預期的。
在Elixir中,```/```運算符總是返回浮點型數值。
如果你想進行整型除法,或者求余數,可以使用函數```div```和```rem```。
(rem的意思是division remainder,余數):
```elixir
iex> div(10, 2)
5
iex> div 10, 2
5
iex> rem 10, 3
1
```
>在寫函數參數時,括號是可選的。(ruby程序員會心一笑)
Elixir支持用 **捷徑(shortcut)** 書寫二進制、八進制、十六進制整數。如:
```elixir
iex> 0b1010
10
iex> 0o777
511
iex> 0x1F
31
```
>揉揉眼,八進制是```0o```,數字0 + 小寫o。
輸入浮點型數字需要一個小數點,且在其后至少有一位數字。
Elixir支持使用```e```來表示指數:
```elixir
iex> 1.0
1.0
iex> 1.0e-10
1.0e-10
```
Elixir中浮點型都是64位雙精度。
## 2.2-布爾
Elixir使用```true```和```false```兩個布爾值。
```elixir
iex> true
true
iex> true == false
false
```
Elixir提供了許多用以判斷類型的函數,如```is_boolean/1```函數可以用來檢查參數是不是布爾型。
>在Elixir中,函數通過名稱和參數個數(又稱元數,arity)來識別。
如```is_boolean/1```表示名為```is_boolean```,接受一個參數的函數;
而```is_boolean/2```表示與其同名、但接受2個參數的**不同**函數。(只是打個比方,這樣的is_boolean實際上不存在)
另外,```<函數名>/<元數>```這樣的表述是為了在講述函數時方便,在實際程序中如果調用函數,
是不用注明```/1```或```/2```的。
```elixir
iex> is_boolean(true)
true
iex> is_boolean(1)
false
```
類似的函數還有```is_integer/1```,```is_float/1```,```is_number/1```,
分別測試參數是否是整型、浮點型或者兩者其一。
>可以在交互式命令行中使用```h```命令來打印函數或運算符的幫助信息。
如```h is_boolean/1```或```h ==/2```。
注意此處提及某個函數時,不但要給出名稱,還要加上元數```/<arity>```。
## 2.3-原子
原子(atom)是一種常量,名字就是它的值。
有些語言中稱其為 **符號(symbol)**(如ruby):
```elixir
iex> :hello
:hello
iex> :hello == :world
false
```
布爾值```true```和```false```實際上就是原子:
```elixir
iex> true == :true
true
iex> is_atom(false)
true
```
## 2.4-字符串
在Elixir中,字符串以 **雙括號** 包裹,采用UTF-8編碼:
```elixir
iex> "hell?"
"hell?"
```
Elixir支持字符串插值(和ruby一樣使用```#{ ... }```):
```elixir
iex> "hell? #{:world}"
"hell? world"
```
字符串可以直接包含換行符,或者其轉義字符:
```elixir
iex> "hello
...> world"
"hello\nworld"
iex> "hello\nworld"
"hello\nworld"
```
你可以使用```IO```模塊(module)里的```IO.puts/1```方法打印字符串:
```elixir
iex> IO.puts "hello\nworld"
hello
world
:ok
```
函數```IO.puts/1```打印完字符串后,返回原子值```:ok```。
字符串在Elixir內部被表示為二進制數值(binaries),也就是一連串的字節(bytes):
```elixir
iex> is_binary("hell?")
true
```
>注意,二進制數值(binary)是Elixir內部的存儲結構之一。
字符串、列表等類型在語言內部就表示為二進制數值,因此它們也可以被專門操作二進制數值的函數修改。
你可以查看字符串包含的字節數量:
```elixir
iex> byte_size("hell?")
6
```
>為啥是6?不是5個字符么?注意里面有一個非ASCII字符```?```,在UTF-8下被編碼為2個字節。
我們可以使用專門的函數來返回字符串中的字符數量:
```elixir
iex> String.length("hell?")
5
```
[String模塊](http://elixir-lang.org/docs/stable/elixir/String.html)中提供了
很多符合Unicode標準的函數來操作字符串。如:
```elixir
iex> String.upcase("hell?")
"HELL?"
```
記住,單引號和雙引號包裹的字符串在Elixir中是兩種不同的數據類型:
```elixir
iex> 'hell?' == "hell?"
false
```
我們將在之后關于“二進制、字符串與字符列表”章節中詳細講述它們的區別。
## 2.5-匿名函數
在Elixir中,使用關鍵字```fn```和```end```來界定函數。如:
```elixir
iex> add = fn a, b -> a + b end
#Function<12.71889879/2 in :erl_eval.expr/5>
iex> is_function(add)
true
iex> is_function(add, 2)
true
iex> is_function(add, 1)
false
iex> add.(1, 2)
3
```
在Elixir中,函數是 **一等公民**。你可以將函數作為參數傳遞給其他函數,就像整型和浮點型一樣。
在上面的例子中,我們向函數```is_function/1```傳遞了由變量```add```表示的匿名函數,
結果返回```true```。
我們還可以調用函數```is_function/2```來判斷該參數函數的元數(參數個數)。
注意,在調用一個匿名函數時,在變量名和寫參數的括號之間要有個 **點號(.)**。
匿名函數是閉包,意味著它們可以保留其定義的作用域(scope)內的其它變量值:
```elixir
iex> add_two = fn a -> add.(a, 2) end
#Function<6.71889879/1 in :erl_eval.expr/5>
iex> add_two.(2)
4
```
這個例子定義的匿名函數```add_two```它內部使用了之前在同一個iex內定義好的```add```變量。
但要注意,在匿名函數內修改了所引用的外部變量的值,并不實際反映到該變量上:
```elixir
iex> x = 42
42
iex> (fn -> x = 0 end).()
0
iex> x
42
```
這個例子中匿名函數把引用了外部變量x,并修改它的值為0。這時函數執行后,外部的x沒有被影響。
## 2.6-(鏈式)列表
Elixir使用方括號標識列表。列表可以包含任意類型的值:
```elixir
iex> [1, 2, true, 3]
[1, 2, true, 3]
iex> length [1, 2, 3]
3
```
兩個列表可以使用```++/2```拼接,使用```--/2```做“減法”:
```elixir
iex> [1, 2, 3] ++ [4, 5, 6]
[1, 2, 3, 4, 5, 6]
iex> [1, true, 2, false, 3, true] -- [true, false]
[1, 2, 3, true]
```
本教程將多次涉及列表頭(head)和尾(tail)的概念。
列表的頭指的是第一個元素,而尾指的是除了第一個元素以外,其它元素組成的列表。
它們分別可以用函數```hd/1```和```tl/1```從原列表中取出:
```elixir
iex> list = [1,2,3]
iex> hd(list)
1
iex> tl(list)
[2, 3]
```
嘗試從一個空列表中取出頭或尾將會報錯:
```elixir
iex> hd []
** (ArgumentError) argument error
```
## 2.7-元組
Elixir使用大括號(花括號)定義元組(tuples)。類似列表,元組也可以承載任意類型的數據:
```elixir
iex> {:ok, "hello"}
{:ok, "hello"}
iex> tuple_size {:ok, "hello"}
2
```
元組使用 ***連續的內存空間*** 存儲數據。
這意味著可以很方便地使用索引訪問元組數據,以及獲取元組大小(索引從0開始):
```elixir
iex> tuple = {:ok, "hello"}
{:ok, "hello"}
iex> elem(tuple, 1)
"hello"
iex> tuple_size(tuple)
2
```
也可以很方便地使用函數```put_elem/3```設置某個位置的元素值:
```elixir
iex> tuple = {:ok, "hello"}
{:ok, "hello"}
iex> put_elem(tuple, 1, "world")
{:ok, "world"}
iex> tuple
{:ok, "hello"}
```
注意函數```put_elem/3```返回一個新元組。原來那個由變量tuple標識的元組沒有被改變。
這是因為Elixir的數據類型是 **不可變的**。
這種不可變性使你永遠不用擔心你的數據會在某處被某些代碼改變。
在處理并發程序時,這種不可變性有利于減少多個程序實體同時修改一個數據結構時引起的競爭以及其他麻煩。
## 2.8-列表還是元組?
列表與元組的區別:列表在內存中是以鏈表的形式存儲的,一個元素指向下一個元素,
然后再下一個...直到到達列表末尾。
我們稱這樣的一對數據(元素值 和 指向下一個元素的指針)為列表的一個單元(cons cell)。
用Elixir語法表示這種模式:
```elixir
iex> list = [1|[2|[3|[]]]]
[1, 2, 3]
```
>列表方括號中的豎線(|)表示列表頭與尾的分界。
這個原理意味著獲取列表的長度是一個線性操作:我們必須遍歷完整個列表才能知道它的長度。
但是列表的前置拼接操作很快捷:
```elixir
iex> [0] ++ list
[0, 1, 2, 3]
iex> list ++ [4]
[1, 2, 3, 4]
```
上面例子中第一條語句是 __前置__ 拼接操作,執行起來很快。
因為它只是簡單地添加了一個新列表單元,它的尾指針指向原先列表頭部。而原先的列表沒有任何變化。
第二條語句是 __后綴__ 拼接操作,執行速度較慢。
這是因為它 **重建** 了原先的列表,讓原先列表的末尾元素指向那個新元素。
另一方面,元組在內存中是連續存儲的。
這意味著獲取元組大小,或者使用索引訪問元組元素的操作十分快速。
但是元組在修改或添加元素時開銷很大,因為這些操作會在內存中對元組的進行整體復制。
這些討論告訴我們當如何在不同的情況下選擇使用不同的數據結構。
函數常用元組來返回多個信息。如```File.read/1```,它讀取文件內容,返回一個元組:
```elixir
iex> File.read("path/to/existing/file")
{:ok, "... contents ..."}
iex> File.read("path/to/unknown/file")
{:error, :enoent}
```
如果傳遞給函數```File.read/1```的文件路徑有效,那么函數返回一個元組,
其首元素是原子```:ok```,第二個元素是文件內容。
如果路徑無效,函數也將返回一個元組,其首元素是原子```:error```,第二個元素是錯誤信息。
大多數情況下,Elixir會引導你做正確的事。
比如有個叫```elem/2```的函數,它使用索引來訪問一個元組元素。
這個函數沒有相應的列表版本,因為根據存儲機制,列表不適用通過索引來訪問:
```elixir
iex> tuple = {:ok, "hello"}
{:ok, "hello"}
iex> elem(tuple, 1)
"hello"
```
當需要計算某數據結構包含的元素個數時,Elixir遵循一個簡單的規則:
如果操作在常數時間內完成(答案是提前算好的),這樣的函數通常被命名為 ```*size```。
而如果操作需要顯式計數,那么該函數通常命名為 ```*length```。
例如,目前講到過的4個計數函數:```byte_size/1```(用來計算字符串有多少字節),```tuple_size/1```
(用來計算元組大小),```length/1```(計算列表長度)
以及```String.length/1```(計算字符串中的字符數)。
按照命名規則,當我們用```byte_size```獲取字符串所占字節數時,開銷較小。
但是當我們用```String.length```獲取字符串unicode字符個數時,需要遍歷整個字符串,開銷較大。
除了本章介紹的數據類型,Elixir還提供了 **Port**,**Reference** 和 **PID** 三個數據類型(它們常用于進程交互)。這些數據類型將在講解進程時詳細介紹。