Elixir 中的模块属性有三个用途:
1.作为模块和函数注释
2.作为编译期间使用的临时模块存储
3.作为编译时常量
让我们检查一下这些示例。
作为注释
Elixir 引入了 Erlang 中的模块属性概念。例如:
在上面的例子中,我们使用模块属性语法定义模块文档。Elixir 有一些保留属性。以下是其中一些最常用的属性:
@moduledoc — 为当前模块提供文档。
@doc — 为属性后面的函数或宏提供文档。
@spec — 为属性后面的函数提供类型规范。
@behaviour —(注意英国拼写)用于指定 OTP 或用户定义的行为。
@moduledoc 和 @doc 是迄今为止最常用的属性,我们希望您经常使用它们。Elixir 将文档视为first-class,并提供了许多访问文档的函数。我们将在它们自己的章节中介绍它们。
让我们回到前面章节中定义的 Math 模块,添加一些文档并将其保存到 math.ex 文件中:
Elixir 提倡使用带有 heredocs 的 Markdown 来编写可读的文档。Heredocs 是多行字符串,它们以三重双引号开头和结尾,保持内部文本的格式。我们可以直接从 IEx 访问任何已编译模块的文档:
我们还提供了一个名为 ExDoc 的工具,用于从文档生成 HTML 页面。
您可以查看 Module 的文档,了解受支持属性的完整列表。Elixir 还使用属性通过 typespecs 注释我们的代码。
作为临时存储
到目前为止,我们已经了解了如何定义属性,但如何读取它们呢?让我们看一个例子:
注意:
不要在属性和其值之间添加换行符,否则 Elixir 会认为您正在读取值,而不是设置值。
尝试访问未定义的属性将打印警告:
defmodule MyServer do
@unknown
end
warning: undefined module attribute @unknown, please remove access to @unknown or explicitly set it before access
也可以在函数内部读取属性:
模块属性是在编译时定义的,其返回值(而不是函数调用本身)将替代属性。因此,上述代码将有效地编译为:
这对于预先计算值,然后将其结果注入模块非常有用。这就是我们所说的临时存储:模块编译后,模块属性将被丢弃,但已读取该属性的函数除外。请注意,您不能将在同一模块中定义的函数作为属性本身的一部分来调用,因为这些函数尚未定义。
每次我们在函数内读取属性时,Elixir 都会对其当前值进行快照。因此,如果您在多个函数内多次读取相同的属性,最终会增加编译时间,因为 Elixir 现在必须编译每个快照。一般来说,您希望避免多次读取相同的属性,而是将其移动到函数中。例如,不要这样:
最好这样:
作为编译时常量
模块属性也可用作编译时常量。一般来说,函数本身足以充当代码库中的常量。例如,无需定义:
您应该更喜欢:
如果需要在模块之间共享,您甚至可以定义一个公共函数。在许多项目中,通常有一个名为 MyApp.Constants 的模块,它定义整个代码库中使用的所有常量。
您甚至可以将复合数据结构作为常量,只要它们完全由其他数据类型组成(没有函数调用、没有运算符,也没有其他表达式)。例如,您可以按如下方式指定系统配置常量:
鉴于 Elixir 中的数据结构是不可变的,因此,只要它没有任何可执行表达式,则只会分配上述数据结构的单个实例并在所有函数调用之间共享。
当您需要在编译时执行某些工作,然后将其结果注入函数内部时,就会出现模块属性的用例。一种常见的情况是模式和保护内部的模块属性(作为 defguard/1 的替代方案),因为它们仅支持一组有限的表达式:
模块属性作为常量和临时存储最常一起使用:模块属性用于计算和存储很重要的值,然后从该模块中公开为常量。
更进一步
库和框架可以利用模块属性来提供自定义注释。要查看示例,只需查看 Elixir 的单元测试框架 ExUnit 即可。ExUnit 将模块属性用于多种不同的目的:
在上面的示例中,ExUnit 将 async: true 的值存储在模块属性中以更改模块的编译方式。标签也可以用作注释,并且可以多次提供,这要归功于 Elixir 累积属性的能力。然后,您可以使用标签来设置和过滤测试,例如在 Windows 上运行测试套件时避免执行特定于 Unix 的测试。
要完全理解 ExUnit 的工作原理,我们需要宏,因此我们将在元编程指南中重新讨论此模式,并学习如何使用模块属性作为自定义注释的存储。
在接下来的章节中,我们将探索结构和协议,然后转到异常处理和其他结构,如符号和理解。