2-基本資料型別

StraightDave發表於2014-09-09

2-基本資料型別

基本算數運算
布林
原子
字串
匿名函式
(鏈式)列表
元組
列表還是元組?

本章介紹Elixir一些基本的型別, 如:整型(integer),浮點型(float),布林(boolean),原子(atom,又稱symbol),字串(string),列表(list)和元組(tuple)等。
它們在iex中顯示如下:

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,輸入以下表示式:

iex> 1 + 2
3
iex> 5 * 5
25
iex> 10 / 2
5.0

10 / 2返回了一個浮點型的5.0而非整型的5,這是預期的。在Elixir中,/運算子總是返回浮點型數值。

如果你想進行整型除法,或者求餘數,可以使用函式divrem。(rem的意思是division remainder):

iex> div(10, 2)
5
iex> div 10, 2
5
iex> rem 10, 3
1

在寫函式引數時,括號是可選的。(ruby程式設計師會心一笑)

Elixir支援用捷徑(shortcut)書寫二進位制、八進位制、十六進位制整數,如:

iex> 0b1010
10
iex> 0o777
511
iex> 0x1F
31

揉揉眼,八進位制是0o,數字0+小寫o。

輸入浮點型數字需要一個小數點,且在其後至少有一位數字。Elixir支援使用e來表示指數:

iex> 1.0
1.0
iex> 1.0e-10
1.0e-10

Elixir中浮點型都是64位雙精度。

2.2-布林

Elixir使用truefalse兩個布林值。

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的。

iex> is_boolean(true)
true
iex> is_boolean(1)
false

類似的函式還有is_integer/1is_float/1is_number/1,分別測試引數是否是整型、浮點型或者兩者其一。

可以在互動式命令列中使用h命令來列印函式或運算子的幫助資訊。如h is_boolean/1h ==/2。注意此處提及某個函式時,不但要給出名稱,還要加上元數/<arity>

2.3-原子

原子(atom)是一種常量,變數名就是它的值。有些語言(如ruby)中稱其為符號(symbol)

iex> :hello
:hello
iex> :hello == :world
false

布林值truefalse實際上就是原子:

iex> true == :true
true
iex> is_atom(false)
true  

此外原子也支援:"原子名"的方式
大寫字母開頭的"變數"實際是:"Elixir.變數名"的別名:

iex> Hello == :"Elixir.Hello"
true  

但如果這個別名已經有Elixir.字首了 那麼就不會再有Elixir字首了:

iex> Elixir.Hello == :"Elixir.Hello"
true

模組呼叫其實也是使用到了這種原子別名:

iex> IO == :"Elixir.IO"
true
iex> :"Elixir.IO".puts "an atom"
an atom

2.4-字串

在Elixir中,字串以雙括號包裹,採用UTF-8編碼:

iex> "hellö"
"hellö"

Elixir支援字串插值(語法類似ruby):

iex> "hellö #{:world}"
"hellö world"

字串可以直接包含換行符,或者其轉義字元:

iex> "hello
...> world"
"hello\nworld"
iex> "hello\nworld"
"hello\nworld"

你可以使用IO模組(module)裡的IO.puts/1方法列印字串:

iex> IO.puts "hello\nworld"
hello
world
:ok

函式IO.puts/1列印完字串後,返回原子值:ok

字串在Elixir內部被表示為二進位制數值(binaries),也就是一連串的位元組(bytes):

iex> is_binary("hellö")
true

注意,二進位制數值(binary)是Elixir內部的儲存結構之一。字串、列表等型別在語言內部就儲存為二進位制數值,因此它們也可以被專門操作二進位制數值的函式修改。

你可以檢視字串包含的位元組數量:

iex> byte_size("hellö")
6

為啥是6?不是5個字元麼?注意裡面有一個非ASCII字元ö,在UTF-8下被編碼為2個位元組。

我們可以使用專門的函式來返回字串中的字元數量:

iex> String.length("hellö")
5

String模組中提供了很多符合Unicode標準的函式來操作字串。 如:

iex> String.upcase("hellö")
"HELLÖ"

記住,單引號和雙引號包裹的字串在Elixir中是兩種不同的資料型別:

iex> 'hellö' == "hellö"
false

我們將在之後關於“二進位制、字串與字元列表”章節中詳細講述它們的區別。

2.5-匿名函式

在Elixir中,使用關鍵字fnend來界定函式。如:

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)內的其它變數:

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變數。
但要注意,在匿名函式內修改了所引用的外部變數的值,並不實際反映到該變數上:

iex> x = 42
42
iex> (fn -> x = 0 end).()
0
iex> x
42

這個例子中匿名函式把引用了外部變數x,並修改它的值為0.這時函式執行後,外部的x沒有被影響。

2.6-(鏈式)列表

Elixir使用方括號標識列表。列表可以包含任意型別的值:

iex> [1, 2, true, 3]
[1, 2, true, 3]
iex> length [1, 2, 3]
3

兩個列表可以使用++/2拼接,使用--/2做“減法”:

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/1tl/1從原列表中取出:

iex> list = [1,2,3]
iex> hd(list)
1
iex> tl(list)
[2, 3]

嘗試從一個空列表中取出頭或尾將會報錯:

iex> hd []
** (ArgumentError) argument error

2.7-元組

Elixir使用大括號(花括號)定義元組(tuples)。
類似列表,元組也可以承載任意型別的資料:

iex> {:ok, "hello"}
{:ok, "hello"}
iex> tuple_size {:ok, "hello"}
2

元組使用連續的記憶體空間儲存資料。這意味著可以很方便地使用索引訪問元組資料,以及獲取元組大小。(索引從0開始):

iex> tuple = {:ok, "hello"}
{:ok, "hello"}
iex> elem(tuple, 1)
"hello"
iex> tuple_size(tuple)
2

也可以很方便地使用函式put_elem/3設定某個位置的元素值:

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語法表示這種模式:

iex> list = [1|[2|[3|[]]]]
[1, 2, 3]

列表方括號中的豎線(|)表示列表頭與尾的分界。

這個原理意味著獲取列表的長度是一個線性操作:我們必須遍歷完整個列表才能知道它的長度。
但是列表的前置拼接操作很快捷:

iex> [0] ++ list
[0, 1, 2, 3]
iex> list ++ [4]
[1, 2, 3, 4]

上面例子中第一條語句是前置拼接操作,執行起來很快。因為它只是簡單地新增了一個新列表單元,它指向原先列表頭部。而原先的列表沒有任何變化。
第二條語句是字尾拼接操作,執行速度較慢。這是因為它重建了原先的列表,讓原先列表的末尾元素指向那個新元素。

而另一方面,元組在記憶體中是連續儲存的。這意味著獲取元組大小,或者使用索引訪問元組元素的操作十分快速。 但是元組在修改或新增元素時開銷很大,因為這些操作會在記憶體中對元組的進行整體複製。

這些討論告訴我們當如何在不同的情況下選擇使用不同的資料結構。

函式常用元組來返回多個資訊。如File.read/1,它讀取檔案內容,返回一個元組:

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的函式,它使用索引來訪問一個元組元素。 這個函式沒有相應的列表版本,因為根據儲存機制,列表不適用通過索引來訪問:

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還提供了PortReferencePID三個資料型別(它們常用於程式互動)。 這些資料型別將在講解程式時詳細介紹。

相關文章