第 2 章 基础构建块(Building blocks)
本章涵盖内容
- 使用交互式shell
- 使用变量
- 组织代码
- 理解类型系统
- 使用运算符
- 理解运行时
是时候开始学习Elixir了。本章将介绍该语言的基本构建块,例如模块、函数和类型系统。这将是一次有些冗长且可能并不特别令人兴奋的语言特性之旅,但这里介绍的内容非常重要,因为它为探索更有趣、更高级的主题奠定了基础。
开始之前,请确保您已安装Elixir 1.15版本和Erlang 26版本。安装Elixir有几种方式,最好遵循官方Elixir网站上的说明:https://elixir-lang.org/install.html。
准备工作完成后,让我们开始探索Elixir。您首先需要了解的是交互式shell。
详细信息 本书不会提供任何语言或平台特性的详细参考手册。那样会占用太多篇幅,而且材料很快就会过时。您可以查阅以下其他参考资料:
- 若需快速了解语法,可查阅Elixir官方网站的入门指南:https://mng.bz/NVRn。
- 更详细的参考可以在在线文档中找到:https://hexdocs.pm/elixir。
- 针对具体问题,您可以访问Elixir论坛(https://elixirforum.com/)或Slack频道(https://elixir-lang.slack.com/)。
- 最后,对于许多内容,您可能需要查阅Erlang文档:https://www.erlang.org/doc。如果您不熟悉Erlang语法,可能还需要阅读Elixir的Erlang速成课程(https://elixir-lang.org/crash-course.html)。
2.1 交互式shell(The interactive shell)
实验和学习语言特性最简单的方法是通过交互式shell。您可以通过运行iex命令从命令行启动Elixir交互式shell:
$ iex
Erlang/OTP 26 [erts-14.0] [source] [64-bit] [smp:20:20] [ds:20:20:10]
Interactive Elixir (1.15.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>
运行iex会启动一个BEAM实例,然后在其中启动一个交互式Elixir shell。会打印运行时信息,例如Erlang和Elixir版本号,然后提供提示符,以便您可以输入Elixir表达式:
iex(1)> 1 + 2
3
输入表达式后,它会被解释并执行,然后将返回值打印到屏幕上。
注意 Elixir中的所有内容都是具有返回值的表达式。这不仅包括函数调用,还包括
if和case等结构。
提示 您将在本书中广泛使用
iex,尤其是在前几章。表达式的结果通常不特别重要,为了减少干扰会被省略。但请记住,每个表达式都会返回一个结果,当您在shell中输入表达式时,其结果将会显示。
您几乎可以输入任何构成有效Elixir代码的内容,包括相对复杂的多行表达式:
iex(2)> 2 * (3 + 1 ) / 4
2.0
请注意,shell直到您在最后一行完成表达式后才对其进行求值。在Elixir中,您不需要特殊字符(例如分号)来表示表达式的结束。相反,如果表达式是完整的,则换行表示表达式的结束。否则,解析器会等待更多输入,直到表达式变得完整。如果您卡住了(例如,忘记了右括号),可以通过在新的一行输入#iex:break来中止整个表达式:
iex(3)> 1 + (2
...(3)> #iex:break
** (TokenMissingError) iex:1: incomplete expression
iex(3)>
退出shell最快的方法是连续按两次Ctrl-C。这样做会强制终止操作系统进程和所有正在执行的后台作业。由于shell主要用于实验,不应用于运行实际的生产系统,因此通常可以这种方式终止它。但如果您想以更优雅的方式停止系统,可以调用System.stop。
注意 有多种方式可以启动Elixir和Erlang运行时以及运行Elixir程序。在本章结束时,您将对这些方式有所了解。在本书的第一部分,您将主要使用
iexshell,因为它是实验语言的简单而有效的方式。
您可以用shell做很多事情,但最常用的是输入表达式并检查其结果。您可以自行研究在shell中还能做些什么。基本帮助可以通过h命令获取:
iex(3)> h
在shell中输入此命令将输出一整屏与iex相关的说明。您也可以查阅负责shell工作的IEx模块的文档:
iex(4)> h IEx
您可以在在线文档中找到相同的帮助信息:https://hexdocs.pm/iex。
现在您有了一个基本的实验工具,可以开始研究语言特性了。您将从变量开始。
2.2 使用变量(Working with variables)
Elixir是一门动态编程语言,这意味着您不需要显式声明变量或其类型。相反,变量的类型由它当前包含的数据决定。用Elixir的术语来说,赋值被称为绑定。当您用一个值初始化变量时,该变量就被绑定到那个值:
iex(1)> monthly_salary = 10000
10000
Elixir中的每个表达式都有一个结果。对于 = 操作符,结果就是操作符右侧的内容。表达式求值后,shell会将这个结果打印到屏幕上。
现在,您可以引用这个变量:
iex(2)> monthly_salary
10000
当然,变量可以用于复杂的表达式:
iex(3)> monthly_salary * 12
120000
在Elixir中,变量名总是以小写字母或下划线开头。之后,可以包含字母、数字和下划线的任意组合。通常的惯例是只使用小写ASCII字母、数字和下划线:
valid_variable_name
also_valid_1
validButNotRecommended
NotValid
变量名也可以以问号 ( ?) 或感叹号 ( ! ) 结尾:
valid_name?
also_ok!
变量可以重新绑定到不同的值:
iex(1)> monthly_salary = 10000 # 绑定变量
10000 # 最后一个表达式的结果
iex(2)> monthly_salary # 返回变量值的表达式
10000 # 变量的值
iex(3)> monthly_salary = 11000 # 重新绑定
11000
iex(4)> monthly_salary
11000
重新绑定并不会改变现有的内存位置。它会预留新的内存,并将符号名重新分配给新的位置。
注意 您应该始终记住数据是不可变的。一旦内存位置被数据占用,在释放之前就不能被修改。但是变量可以重新绑定,这使它们指向不同的内存位置。因此,变量是可变的,但它们所指向的数据是不可变的。
Elixir是一门垃圾回收的语言,这意味着您不必手动释放内存。当变量超出作用域时,相应的内存就有资格被垃圾回收,并将在未来垃圾回收器清理内存时被释放。
2.3 组织代码(Organizing your code)
作为一种函数式语言,Elixir 高度依赖函数。由于数据的不可变性,一个典型的 Elixir 程序由许多小型函数组成。在接下来的第 3 章和第 4 章中,当您开始使用一些典型的函数式惯用法时,就会亲眼见证这一点。多个函数可以进一步组织到模块中。
2.3.1 模块(Modules)
模块是函数的集合,有点像一个命名空间。每一个 Elixir 函数都必须在某个模块内部定义。
Elixir 自带一个标准库,提供了许多有用的模块。例如,IO 模块可用于完成各种输入/输出操作。IO 模块中的 puts 函数可以用来向屏幕打印消息:
iex(1)> IO.puts("Hello World!")
Hello World!
:ok
如示例所示,要调用模块中的函数,您需要使用语法 ModuleName.function_name(args)。
要定义您自己的模块,您需要使用 defmodule 表达式。在模块内部,使用 def 表达式来定义函数。代码清单 2.1 演示了模块的定义。
代码清单 2.1 定义一个模块 (geometry.ex)
defmodule Geometry do
def rectangle_area(a, b) do
a * b
end
end
有两种方法可以使用这个模块。首先,您可以直接将这个定义复制并粘贴到 iex 中——如前所述,几乎任何内容都可以输入到 shell 中。第二种方法是在启动时告诉 iex 解释该文件:
$ iex geometry.ex
使用任何一种方法都会产生相同的效果。代码被编译,生成的模块被加载到运行时中,并可以在 shell 会话中使用。我们来试试看:
$ iex geometry.ex
iex(1)> Geometry.rectangle_area(6, 7)
42
很简单!您创建了一个 Geometry 模块,将其加载到 shell 会话中,并用它来计算矩形的面积。
注意 您可能已经注意到,文件名有 .ex 扩展名。这是 Elixir 源文件的常见约定。
在源代码中,一个模块必须在单个文件中定义。一个文件可以包含多个模块定义:
defmodule Module1 do
...
end
defmodule Module2 do
...
end
模块名称必须遵循某些规则。它以大写字母开头,通常采用驼峰式命名法。模块名称可以由字母数字字符、下划点和点 (.) 字符组成。点号通常用于以层次结构组织模块:
defmodule Geometry.Rectangle do
...
end
defmodule Geometry.Circle do
...
end
您也可以嵌套定义模块:
defmodule Geometry do
defmodule Rectangle do
...
end
...
end
内部模块可以通过 Geometry.Rectangle 来引用。
请注意,点号字符本身并没有什么特殊之处。它只是模块名称中允许使用的字符之一。编译后的版本不会记录模块之间的任何层次关系。
这通常用于将模块组织成有意义的层次结构,使阅读代码时更容易浏览。此外,这种非正式的命名空间划分可以消除可能的命名冲突。例如,考虑两个库,一个实现了 JSON 编码器,另一个实现了 XML 编码器。如果两个库都定义了一个名为 Encoder 的模块,那么您将无法在同一个项目中同时使用它们。然而,如果模块被命名为 Json.Encoder 和 Xml.Encoder,那么命名冲突就避免了。因此,通常会在项目中为所有模块名称添加一个共同的前缀。通常,应用程序或库的名称被用于此目的。
2.3.2 函数(Functions)
函数必须是模块的一部分。函数命名遵循与变量相同的规范:以小写字母或下划线开头,后跟字母、数字和下划线的组合。
与变量类似,函数名也可以以 ? 和 ! 字符结尾。? 字符通常用于表示返回 true 或 false 的函数。在名称末尾添加 ! 字符表示可能引发运行时错误的函数。这两者都是约定而非硬性规则,但最好遵循它们并尊重社区风格。
函数可以使用 def 宏来定义:
defmodule Geometry do
def rectangle_area(a, b) do
...
end
end
定义以 def 表达式开始,后跟函数名、参数列表以及包含在 do…end 块中的函数体。由于您使用的是动态语言,因此参数没有类型声明。
注意 请注意,defmodule 和 def 并不被称为关键字。这是因为它们确实不是!相反,它们是 Elixir 宏的示例。您现在无需担心这是如何工作的;本章稍后会对此进行一些解释。如果有助于理解,您可以将 def 和 defmodule 视为关键字,但要知道这并不完全正确。
如果函数没有参数,可以省略括号:
defmodule Program do
def run do
...
end
end
那么返回值呢?回想一下,在 Elixir 中,一切有返回值的都是表达式。函数的返回值是其最后一个表达式的返回值。Elixir 中没有显式的 return 语句。
注意 既然没有显式的 return,您可能会想知道复杂的函数是如何工作的。这将在第 3 章详细讨论,届时您将学习分支和条件逻辑。通常的规则是保持函数简短明了,这样便于计算结果并通过最后一个表达式将其返回。
您在代码清单 2.1 中已经看到了返回值的示例,但让我们在此重复一下:
defmodule Geometry do
def rectangle_area(a, b) do
a * b
end
end
现在您可以验证这一点。再次启动 shell,然后尝试调用 rectangle_area 函数:
$ iex geometry.ex
iex(1)> Geometry.rectangle_area(3, 2)
6
如果函数体只包含一个表达式,可以使用简写形式,在一行内完成定义:
defmodule Geometry do
def rectangle_area(a, b), do: a * b
end
要调用定义在另一个模块中的函数,请使用模块名后跟函数名:
iex(1)> Geometry.rectangle_area(3, 2)
6
当然,您总是可以将函数结果存储到变量中:
iex(2)> area = Geometry.rectangle_area(3, 2)
6
iex(3)> area
6
在Elixir中,括号是可选的,因此您可以省略它们:
iex(4)> Geometry.rectangle_area 3, 2
6
就个人而言,我认为省略括号会使代码产生歧义,因此我的建议是在调用函数时始终加上括号。
使用代码格式化工具
自 1.6 版本起,Elixir 内置了一个代码格式化工具,您可以用它来使代码风格保持一致,无需担心诸如代码布局或括号使用等底层风格细节。
例如,将以下代码片段格式化后:
defmodule Client do
def run do
Geometry.rectangle_area 3,2
end
end您会得到如下整洁美观的代码:
defmodule Client do
def run do
Geometry.rectangle_area(3, 2)
end
end您可以通过
mix format任务来格式化代码,也可以在您常用的编辑器中安装格式化插件。
如果要调用的函数位于同一个模块内,您可以省略模块前缀:
defmodule Geometry do
def rectangle_area(a, b) do
a * b
end
def square_area(a) do
rectangle_area(a, a)
end
end
鉴于Elixir是一门函数式语言,您经常需要组合函数,将一个函数的结果作为参数传递给下一个函数。Elixir内置了一个称为管道操作符的 |> 操作符,专门用于此目的:
iex(5)> -5 |> abs() |> Integer.to_string() |> IO.puts()
5
这段代码在编译时会被转换为以下形式:
iex(6)> IO.puts(Integer.to_string(abs(-5)))
5
更一般地说,管道操作符会将前一个调用的结果作为下一个调用的第一个参数。因此,以下代码:
prev(arg1, arg2) |> next(arg3, arg4)
在编译时会转换为:
next(prev(arg1, arg2), arg3, arg4)
可以说,管道版本更具可读性,因为执行顺序是从左到右阅读的。管道操作符在源文件中看起来特别优雅,因为您可以将管道布局在多行上:
-5
|> abs()
|> Integer.to_string()
|> IO.puts()
Shell中的多行管道 如果您将前面的管道链粘贴到
iex会话中,您会注意到每个中间结果都会被打印到控制台:iex(1)> -5
-5
iex(2)> |> abs()
5
iex(3)> |> Integer.to_string()
"5"
iex(4)> |> IO.puts()
5
回忆一下,iex会在Elixir表达式完整且有效时立即对其进行求值。在这个例子中,每一行都构成了一个有效的Elixir表达式,例如 -5 或 -5 |> abs(),因此每个中间结果都被打印出来了。
2.3.3 函数元数(Function arity)
元数描述函数接收的参数数量。一个函数由其所在的模块、名称和元数唯一标识。请看以下函数:
defmodule Rectangle do
def area(a, b) do
...
end
end
函数 Rectangle.area 接收两个参数,因此可以说它是一个元数为 2 的函数。在 Elixir 领域,这个函数常被称为 Rectangle.area/2,其中 /2 表示函数的元数。
为什么这很重要?因为两个名称相同但元数不同的函数是两个不同的函数,如下例所示。
代码清单 2.2 同名但元数不同的函数 (arity_demo.ex)
defmodule Rectangle do
def area(a), do: area(a, a)
def area(a, b), do: a * b
end
将这个模块加载到 shell 中,然后尝试以下操作:
iex(1)> Rectangle.area(5)
25
iex(2)> Rectangle.area(5, 6)
30
如您所见,这两个函数的行为完全不同。名称可能被重载,但元数不同,因此我们将它们视为两个不同的函数,各自有自己的实现。
同名但实现完全不同的函数通常没有意义。更常见的情况是,元数较低的函数会委托给元数较高的函数,并提供一些默认参数。这就是代码清单 2.2 中的情况,Rectangle.area/1 委托给了 Rectangle.area/2。让我们看另一个例子。
代码清单 2.3 同名函数、不同元数与默认参数 (arity_calc.ex)
defmodule Calculator do
def add(a), do: add(a, 0)
def add(a, b), do: a + b
end
同样,一个元数较低的函数通过一个元数较高的函数来实现。这种模式非常常见,以至于 Elixir 允许您使用 \\ 操作符后跟参数的默认值来指定参数的默认值:
defmodule Calculator do
def add(a, b \\ 0), do: a + b
end
这个定义会生成两个函数,与代码清单 2.3 中完全一样。
您可以为任意参数组合设置默认值:
defmodule MyModule do
def fun(a, b \\ 1, c, d \\ 2) do
a + b + c + d
end
end
请始终记住,默认值会生成多个同名但元数不同的函数。前面的代码生成了三个函数:MyModule.fun/2、MyModule.fun/3 和 MyModule.fun/4,其实现如下:
def fun(a, c), do: fun(a, 1, c, 2)
def fun(a, b, c), do: fun(a, b, c, 2)
def fun(a, b, c, d), do: a + b + c + d
因为元数区分了同名的多个函数,所以不可能让一个函数接受可变数量的参数。Elixir 中没有 C 语言的 … 或 JavaScript 的 arguments 的对应物。
2.3.4 函数可见性(Function visibility)
使用 def 宏定义函数时,该函数是公开的——可以被其他任何代码调用。用 Elixir 的术语来说,这个函数被导出了。你也可以使用 defp 宏来定义私有函数。私有函数只能在其定义的模块内部使用。以下示例展示了这一点。
代码清单 2.4 包含公共函数和私有函数的模块 (private_fun.ex)
defmodule TestPrivate do
def double(a) do
sum(a, a)
end
defp sum(a, b) do
a + b
end
end
模块 TestPrivate 定义了两个函数。函数 double 是导出的,可以从外部调用。在内部,它依赖私有函数 sum 来完成工作。让我们在 shell 中尝试一下。加载该模块,然后执行以下操作:
iex(1)> TestPrivate.double(3)
6
iex(2)> TestPrivate.sum(3, 4)
** (UndefinedFunctionError) function TestPrivate.sum/2 ...
如你所见,私有函数不能在模块外部调用。
2.3.5 导入与别名(Imports and aliases)
调用其他模块的函数有时可能很繁琐,因为你需要引用模块名。如果你的模块经常调用另一个模块的函数,你可以将该模块导入到你的模块中。导入一个模块允许你在调用其公共函数时省略模块名前缀:
defmodule MyModule do
import IO
def my_function do
puts "Calling imported function."
end
end
当然,你可以导入多个模块。实际上,标准库的 Kernel 模块会自动导入到每个模块中。Kernel 包含了许多常用函数,自动导入使它们更易于访问。
注意 你可以通过查阅在线文档(https://hexdocs.pm/elixir/Kernel.html)来查看 Kernel 模块中有哪些可用的函数。
另一个表达式 alias,可以让你使用不同的名称来引用一个模块:
defmodule MyModule do
alias IO, as: MyIO
def my_function do
MyIO.puts("Calling imported function.")
end
end
当模块名称很长时,别名会很有用。例如,如果你的应用程序深度划分到多层模块层次结构中,使用完全限定名引用模块会很繁琐。别名可以帮助解决这个问题。例如,假设你有一个 Geometry.Rectangle 模块。你可以在客户端模块中为其设置别名并使用较短的名称:
defmodule MyModule do
alias Geometry.Rectangle, as: Rectangle
def my_function do
Rectangle.area(...)
end
end
在前面的例子中,Geometry.Rectangle 的别名是其名称的最后一部分。这是 alias 最常见的用法,因此 Elixir 允许你在这种情况下省略 as 选项:
defmodule MyModule do
alias Geometry.Rectangle
def my_function do
Rectangle.area(...)
end
end
别名可以帮助你减少一些干扰,尤其是当你需要多次调用一个长名称模块中的函数时。
2.3.6 模块属性(Module attributes)
模块属性具有双重用途:它们既可作为编译时常量使用,也可以注册任何属性,以便在运行时进行查询。让我们来看一个例子。
下面的模块提供了处理圆的基本函数:
iex(1)> defmodule Circle do
@pi 3.14159
def area(r), do: r*r*@pi
def circumference(r), do: 2*r*@pi
end
iex(2)> Circle.area(1)
3.14159
iex(3)> Circle.circumference(1)
6.28318
请注意你如何在 shell 中直接定义模块。这是允许的,使得实验成为可能,而无需在磁盘上存储任何文件。
关于常量 @pi 的重要一点是,它仅在模块编译期间存在,此时对它的引用会被内联。
此外,属性可以被注册,这意味着它将被存储在生成的二进制文件中,并可在运行时访问。Elixir 默认会注册一些模块属性。例如,属性 @moduledoc 和 @doc 可用于为模块和函数提供文档:
defmodule Circle do
@moduledoc "Implements basic circle functions"
@pi 3.14159
@doc "Computes the area of a circle"
def area(r), do: r*r*@pi
@doc "Computes the circumference of a circle"
def circumference(r), do: 2*r*@pi
end
然而,要尝试这个,你需要生成一个编译文件。这里有一个快速的方法:将此代码保存到某处的 circle.ex 文件中,然后运行 elixirc circle.ex。这将生成文件 Elixir.Circle.beam。接下来,从同一文件夹启动 iex shell。
现在你可以在运行时检索该属性:
iex(1)> Code.fetch_docs(Circle)
{:docs_v1, 2, :elixir, "text/markdown",
%{"en" => "Implements basic circle functions"}, %{},
[
{{:function, :area, 1}, 5, ["area(r)"],
%{"en" => "Computes the area of a circle"}, %{}},
{{:function, :circumference, 1}, 8, ["circumference(r)"],
%{"en" => "Computes the circumference of a circle"}, %{}}
]}
值得注意的是,Elixir 生态系统中的其他工具知道如何处理这些属性。例如,你可以使用 iex 的帮助功能来查看模块的文档:
iex(2)> h Circle
Circle
Implements basic circle functions
iex(3)> h Circle.area
* def area(r)
Computes the area of a circle
此外,你可以使用 ex_doc 工具为你的项目生成 HTML 文档。这是生成 Elixir 文档的方式,如果你计划构建更复杂的项目,特别是那些将被许多不同客户端使用的东西,你应该考虑使用 @moduledoc 和 @doc。
其根本在于,注册属性可用于将元信息附加到模块,这些元信息随后可被其他 Elixir(甚至 Erlang)工具使用。还有许多其他预注册属性,你也可以注册自己的自定义属性。更多详细信息,请查看 Module 模块的文档。
类型规范 类型规范(通常称为 typespecs)是另一个基于属性的重要特性。它们允许你为函数提供类型信息,随后可以使用名为 dialyzer 的静态分析工具对这些信息进行分析。
以下是我们扩展 Circle 模块以包含类型规范的方法:
defmodule Circle do
@pi 3.14159
@spec area(number) :: number
def area(r), do: r*r*@pi
@spec circumference(number) :: number
def circumference(r), do: 2*r*@pi
end
在这里,你使用 @spec 属性来指示这两个函数都接受并返回一个数字。
类型规范提供了一种弥补缺乏静态类型系统的方式。结合 dialyzer 工具,这对于执行程序的静态分析非常有用。此外,类型规范可以让你更好地记录你的函数。请记住,Elixir 是一门动态语言,因此函数输入和输出不能通过查看函数签名轻易推断出来。类型规范在这方面可以提供显著帮助,我可以证明,当提供类型规范时,理解他人的代码会容易得多。
例如,看看 Elixir 函数 List.insert_at/3 的类型规范:
@spec insert_at(list, integer, any) :: list
即使不看代码或不阅读文档,你也能合理地猜出这个函数将任意类型的项(第三个参数)插入到列表(第一个参数)中的给定位置(第二个参数),并返回一个新列表。
本书中将不会使用类型规范,主要是为了使代码尽可能简短。但如果你计划构建更复杂的系统,我的建议是认真考虑使用类型规范。你可以在官方文档中找到详细参考。
2.3.7 注释(Comments)
Elixir 中的注释以 # 字符开头,表示该行的其余部分是注释:
# 这是一个注释
a = 3.14 # 这也是一个注释
不支持块注释。如果你需要注释多行,请为每一行添加 # 字符前缀。
至此,我们已经完成了函数和模块的基础知识。你现在已经了解了主要的代码组织技术。在此基础上,是时候来看看 Elixir 的类型系统了。
2.4 理解类型系统 (Understanding the type system)
Elixir 的核心采用 Erlang 的类型系统。因此,与 Erlang 库的集成通常很简单。该类型系统本身相当简单,但如果您来自传统的面向对象语言,您会发现它与您习惯的方式有很大不同。本节将介绍 Elixir 的基本类型,并讨论不可变性的一些影响。首先,我们来看数字。
2.4.1 数字 (Numbers)
数字可以是整数或浮点数,它们的使用方式基本符合预期:
iex(1)> 3
3
iex(2)> 0xFF
255
iex(3)> 3.14
3.14
iex(4)> 1.0e-2
0.01
支持标准的算术运算符:
iex(5)> 1 + 2 * 3
7
除法运算符 / 的工作方式可能与您的预期不同。它总是返回浮点值:
iex(6)> 4/2
2.0
iex(7)> 3/2
1.5
要执行整数除法或计算余数,可以使用自动导入的 Kernel 函数:
iex(8)> div(5,2)
2
iex(9)> rem(5,2)
1
为了增加语法糖,可以使用下划线字符作为视觉分隔符:
iex(10)> 1_000_000
1000000
整数的大小没有上限,可以使用任意大的数字:
iex(11)> 999999999999999999999999999999999999999999999999999999999999 999999999999999999999999999999999999999999999999999999999999
如果您担心内存占用问题,最好查阅 http://mng.bz/QREv 上的官方 Erlang 内存指南。整数占用容纳该数字所需的空间大小,而浮点数则占用 32 位或 64 位,具体取决于虚拟机的构建架构。浮点数内部采用 IEEE 754-1985(二进制精度)格式表示。
2.4.2 原子 (Atoms)
原子(Atoms)就是字面意义上的命名常量。它们类似于 Ruby 中的符号(symbols)或 C/C++ 中的枚举(enumerations)。原子常量以冒号开头,后跟字母数字和/或下划线的组合:
:an_atom
:another_atom
也可以使用以下语法在原子名称中包含空格:
:"an atom with spaces"
一个原子由两部分组成:文本和值。原子文本是您放在冒号后面的任何内容。在运行时,此文本保存在原子表中。值是放入变量中的数据,它仅仅是对原子表的一个引用。 这正是原子最适合用作命名常量的原因。它们在内存和性能方面都很高效。当您写下
variable = :some_atom
时,变量并不包含整个文本——只包含对原子表的引用。因此,内存消耗低,比较速度快,代码仍然可读。
别名(ALIASES) 原子常量还有另一种语法。您可以省略开头的冒号,而以大写字母开头:
AnAtom
这被称为别名(alias),在编译时,它会被转换为 :"Elixir.AnAtom"。我们可以在 shell 中轻松验证这一点:
iex(1)> AnAtom == :"Elixir.AnAtom"
true
当您使用别名时,编译器会隐式地将 Elixir. 前缀添加到其文本中并生成原子。但如果别名已经包含了 Elixir. 前缀,则不会再添加。 因此,以下操作也同样有效:
iex(2)> AnAtom == Elixir.AnAtom
true
您可能还记得前面提到过,也可以使用别名为模块提供替代名称:
iex(3)> alias IO, as: MyIO
iex(4)> MyIO.puts("Hello!")
Hello!
在这两种情况下都使用"别名"一词并非偶然。当您编写 alias IO, as: MyIO 时,您指示编译器将 MyIO 转换为 IO。进一步解析后,生成的二进制文件中发出的最终结果是 :Elixir.IO。因此,设置别名后,以下等式也成立:
iex(5)> MyIO == Elixir.IO
true
所有这些可能看起来有些奇怪,但它有一个重要的潜在目的。别名支持模块的正确解析。我们将在本章末尾重新讨论模块并查看它们在运行时如何加载时再详细讨论这一点。
原子作为布尔值(ATOMS AS BOOLEANS) Elixir 没有专门的布尔类型,这可能会让人感到惊讶。相反,它使用原子 :true 和 :false。作为语法糖,Elixir 允许您在不带起始冒号的情况下引用这些原子:
iex(1)> :true == true
true
iex(2)> :false == false
true
在 Elixir 中,仍然使用"布尔"一词来表示值为 :true 或 :false 的原子。标准逻辑运算符适用于布尔原子:
iex(1)> true and false
false
iex(2)> false or true
true
iex(3)> not false
true
iex(4)> not :an_atom_other_than_true_or_false
** (ArgumentError) argument error
请始终记住,布尔值就是值为 true 或 false 的原子。
Nil 与真值(NIL AND TRUTHY VALUES) 另一个特殊的原子是 :nil,它的工作方式与其他语言中的 null 有些类似。您可以不带冒号引用 nil:
iex(1)> nil == :nil
true
原子 nil 在 Elixir 对"真值"(truthiness)的额外支持中发挥作用,其工作方式类似于 C/C++ 和 Ruby 等主流语言中的用法。原子 nil 和 false 被视为假值(falsy values),而其他所有值都被视为真值(truthy value)。
此属性可与 Elixir 的短路运算符 ||、&& 和 ! 一起使用。|| 运算符返回第一个不为假值的表达式:
iex(1)> nil || false || 5 || true
5
因为 nil 和 false 都是假值表达式,所以返回数字 5。请注意,后续表达式根本不会被执行。如果所有表达式的结果都为假值,则返回最后一个表达式的结果。
&& 运算符返回第二个表达式,但仅在第一个表达式为真值时。否则,它返回第一个表达式,并且不会计算第二个表达式:
iex(1)> true && 5
5
iex(2)> false && 5
false
iex(3)> nil && 5
nil
短路操作可以用于优雅地链式操作。例如,如果您需要从缓存、本地磁盘或远程数据库中获取一个值,可以这样做:
read_cached() || read_from_disk() || read_from_database()
类似地,您可以使用 && 运算符来确保满足某些条件:
database_value = connection && read_data(connection)
在这两个例子中,短路运算符使得编写简洁的代码成为可能,而无需使用复杂的嵌套条件表达式。