2-基本类型

本章介绍Elixir的基本类型。Elixir主要的基本类型有:
整型(integer),浮点(float),布尔(boolean),原子(atom,又称symbol符号),
字符串(string),列表(list)和元组(tuple)等。

它们在iex中显示如下:

  1. iex> 1 # integer
  2. iex> 0x1F # integer
  3. iex> 1.0 # float
  4. iex> true # boolean
  5. iex> :atom # atom / symbol
  6. iex> "elixir" # string
  7. iex> [1, 2, 3] # list
  8. iex> {1, 2, 3} # tuple

2.1-基本算数运算

打开iex,输入以下表达式:

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

10 / 2返回了一个浮点型的5.0而非整型的5,这是预期的。
在Elixir中,/运算符总是返回浮点型数值。

如果你想进行整型除法,或者求余数,可以使用函数divrem
(rem的意思是division remainder,余数):

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

在写函数参数时,括号是可选的。(ruby程序员会心一笑)

Elixir支持用 捷径(shortcut) 书写二进制、八进制、十六进制整数。如:

  1. iex> 0b1010
  2. 10
  3. iex> 0o777
  4. 511
  5. iex> 0x1F
  6. 31

揉揉眼,八进制是0o,数字0 + 小写o。

输入浮点型数字需要一个小数点,且在其后至少有一位数字。
Elixir支持使用e来表示指数:

  1. iex> 1.0
  2. 1.0
  3. iex> 1.0e-10
  4. 1.0e-10

Elixir中浮点型都是64位双精度。

2.2-布尔

Elixir使用truefalse两个布尔值。

  1. iex> true
  2. true
  3. iex> true == false
  4. false

Elixir提供了许多用以判断类型的函数,如is_boolean/1函数可以用来检查参数是不是布尔型。

在Elixir中,函数通过名称和参数个数(又称元数,arity)来识别。
is_boolean/1表示名为is_boolean,接受一个参数的函数;
is_boolean/2表示与其同名、但接受2个参数的不同函数。(只是打个比方,这样的is_boolean实际上不存在)
另外,<函数名>/<元数>这样的表述是为了在讲述函数时方便,在实际程序中如果调用函数,
是不用注明/1/2的。

  1. iex> is_boolean(true)
  2. true
  3. iex> is_boolean(1)
  4. false

类似的函数还有is_integer/1is_float/1is_number/1
分别测试参数是否是整型、浮点型或者两者其一。

可以在交互式命令行中使用h命令来打印函数或运算符的帮助信息。
h is_boolean/1h ==/2
注意此处提及某个函数时,不但要给出名称,还要加上元数/<arity>

2.3-原子

原子(atom)是一种常量,名字就是它的值。
有些语言中称其为 符号(symbol)(如ruby):

  1. iex> :hello
  2. :hello
  3. iex> :hello == :world
  4. false

布尔值truefalse实际上就是原子:

  1. iex> true == :true
  2. true
  3. iex> is_atom(false)
  4. true

2.4-字符串

在Elixir中,字符串以 双括号 包裹,采用UTF-8编码:

  1. iex> "hellö"
  2. "hellö"

Elixir支持字符串插值(和ruby一样使用#{ ... }):

  1. iex> "hellö #{:world}"
  2. "hellö world"

字符串可以直接包含换行符,或者其转义字符:

  1. iex> "hello
  2. ...> world"
  3. "hello\nworld"
  4. iex> "hello\nworld"
  5. "hello\nworld"

你可以使用IO模块(module)里的IO.puts/1方法打印字符串:

  1. iex> IO.puts "hello\nworld"
  2. hello
  3. world
  4. :ok

函数IO.puts/1打印完字符串后,返回原子值:ok

字符串在Elixir内部被表示为二进制数值(binaries),也就是一连串的字节(bytes):

  1. iex> is_binary("hellö")
  2. true

注意,二进制数值(binary)是Elixir内部的存储结构之一。
字符串、列表等类型在语言内部就表示为二进制数值,因此它们也可以被专门操作二进制数值的函数修改。

你可以查看字符串包含的字节数量:

  1. iex> byte_size("hellö")
  2. 6

为啥是6?不是5个字符么?注意里面有一个非ASCII字符ö,在UTF-8下被编码为2个字节。

我们可以使用专门的函数来返回字符串中的字符数量:

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

String模块中提供了
很多符合Unicode标准的函数来操作字符串。如:

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

记住,单引号和双引号包裹的字符串在Elixir中是两种不同的数据类型:

  1. iex> 'hellö' == "hellö"
  2. false

我们将在之后关于“二进制、字符串与字符列表”章节中详细讲述它们的区别。

2.5-匿名函数

在Elixir中,使用关键字fnend来界定函数。如:

  1. iex> add = fn a, b -> a + b end
  2. #Function<12.71889879/2 in :erl_eval.expr/5>
  3. iex> is_function(add)
  4. true
  5. iex> is_function(add, 2)
  6. true
  7. iex> is_function(add, 1)
  8. false
  9. iex> add.(1, 2)
  10. 3

在Elixir中,函数是 一等公民。你可以将函数作为参数传递给其他函数,就像整型和浮点型一样。
在上面的例子中,我们向函数is_function/1传递了由变量add表示的匿名函数,
结果返回true
我们还可以调用函数is_function/2来判断该参数函数的元数(参数个数)。

注意,在调用一个匿名函数时,在变量名和写参数的括号之间要有个 点号(.)

匿名函数是闭包,意味着它们可以保留其定义的作用域(scope)内的其它变量值:

  1. iex> add_two = fn a -> add.(a, 2) end
  2. #Function<6.71889879/1 in :erl_eval.expr/5>
  3. iex> add_two.(2)
  4. 4

这个例子定义的匿名函数add_two它内部使用了之前在同一个iex内定义好的add变量。
但要注意,在匿名函数内修改了所引用的外部变量的值,并不实际反映到该变量上:

  1. iex> x = 42
  2. 42
  3. iex> (fn -> x = 0 end).()
  4. 0
  5. iex> x
  6. 42

这个例子中匿名函数把引用了外部变量x,并修改它的值为0。这时函数执行后,外部的x没有被影响。

2.6-(链式)列表

Elixir使用方括号标识列表。列表可以包含任意类型的值:

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

两个列表可以使用++/2拼接,使用--/2做“减法”:

  1. iex> [1, 2, 3] ++ [4, 5, 6]
  2. [1, 2, 3, 4, 5, 6]
  3. iex> [1, true, 2, false, 3, true] -- [true, false]
  4. [1, 2, 3, true]

本教程将多次涉及列表头(head)和尾(tail)的概念。
列表的头指的是第一个元素,而尾指的是除了第一个元素以外,其它元素组成的列表。
它们分别可以用函数hd/1tl/1从原列表中取出:

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

尝试从一个空列表中取出头或尾将会报错:

  1. iex> hd []
  2. ** (ArgumentError) argument error

2.7-元组

Elixir使用大括号(花括号)定义元组(tuples)。类似列表,元组也可以承载任意类型的数据:

  1. iex> {:ok, "hello"}
  2. {:ok, "hello"}
  3. iex> tuple_size {:ok, "hello"}
  4. 2

元组使用 连续的内存空间 存储数据。
这意味着可以很方便地使用索引访问元组数据,以及获取元组大小(索引从0开始):

  1. iex> tuple = {:ok, "hello"}
  2. {:ok, "hello"}
  3. iex> elem(tuple, 1)
  4. "hello"
  5. iex> tuple_size(tuple)
  6. 2

也可以很方便地使用函数put_elem/3设置某个位置的元素值:

  1. iex> tuple = {:ok, "hello"}
  2. {:ok, "hello"}
  3. iex> put_elem(tuple, 1, "world")
  4. {:ok, "world"}
  5. iex> tuple
  6. {:ok, "hello"}

注意函数put_elem/3返回一个新元组。原来那个由变量tuple标识的元组没有被改变。
这是因为Elixir的数据类型是 不可变的
这种不可变性使你永远不用担心你的数据会在某处被某些代码改变。
在处理并发程序时,这种不可变性有利于减少多个程序实体同时修改一个数据结构时引起的竞争以及其他麻烦。

2.8-列表还是元组?

列表与元组的区别:列表在内存中是以链表的形式存储的,一个元素指向下一个元素,
然后再下一个…直到到达列表末尾。
我们称这样的一对数据(元素值 和 指向下一个元素的指针)为列表的一个单元(cons cell)。

用Elixir语法表示这种模式:

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

列表方括号中的竖线(|)表示列表头与尾的分界。

这个原理意味着获取列表的长度是一个线性操作:我们必须遍历完整个列表才能知道它的长度。
但是列表的前置拼接操作很快捷:

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

上面例子中第一条语句是 前置 拼接操作,执行起来很快。
因为它只是简单地添加了一个新列表单元,它的尾指针指向原先列表头部。而原先的列表没有任何变化。

第二条语句是 后缀 拼接操作,执行速度较慢。
这是因为它 重建 了原先的列表,让原先列表的末尾元素指向那个新元素。

另一方面,元组在内存中是连续存储的。
这意味着获取元组大小,或者使用索引访问元组元素的操作十分快速。
但是元组在修改或添加元素时开销很大,因为这些操作会在内存中对元组的进行整体复制。

这些讨论告诉我们当如何在不同的情况下选择使用不同的数据结构。

函数常用元组来返回多个信息。如File.read/1,它读取文件内容,返回一个元组:

  1. iex> File.read("path/to/existing/file")
  2. {:ok, "... contents ..."}
  3. iex> File.read("path/to/unknown/file")
  4. {:error, :enoent}

如果传递给函数File.read/1的文件路径有效,那么函数返回一个元组,
其首元素是原子:ok,第二个元素是文件内容。
如果路径无效,函数也将返回一个元组,其首元素是原子:error,第二个元素是错误信息。

大多数情况下,Elixir会引导你做正确的事。
比如有个叫elem/2的函数,它使用索引来访问一个元组元素。
这个函数没有相应的列表版本,因为根据存储机制,列表不适用通过索引来访问:

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

当需要计算某数据结构包含的元素个数时,Elixir遵循一个简单的规则:
如果操作在常数时间内完成(答案是提前算好的),这样的函数通常被命名为 *size
而如果操作需要显式计数,那么该函数通常命名为 *length

例如,目前讲到过的4个计数函数:byte_size/1(用来计算字符串有多少字节),tuple_size/1
(用来计算元组大小),length/1(计算列表长度)
以及String.length/1(计算字符串中的字符数)。

按照命名规则,当我们用byte_size获取字符串所占字节数时,开销较小。
但是当我们用String.length获取字符串unicode字符个数时,需要遍历整个字符串,开销较大。

除了本章介绍的数据类型,Elixir还提供了 PortReferencePID 三个数据类型(它们常用于进程交互)。这些数据类型将在讲解进程时详细介绍。