查看源代码 模块属性
Elixir 中的模块属性有三个作用
- 它们用于注释模块,通常包含用户或 VM 使用的信息。
- 它们充当常量。
- 它们充当编译期间使用的临时模块存储。
让我们逐一检查每个情况。
作为注释
Elixir 从 Erlang 中引入了模块属性的概念。例如
defmodule MyServer do
@moduledoc "My server code."
end
在上面的示例中,我们使用模块属性语法定义了模块文档。Elixir 有几个保留属性。以下是一些最常用的属性:
@moduledoc
— 提供当前模块的文档。@doc
— 提供跟随属性的函数或宏的文档。@spec
— 提供跟随属性的函数的类型说明。@behaviour
— (注意英国拼写)用于指定 OTP 或用户定义的行为。
@moduledoc
和 @doc
是迄今为止最常用的属性,我们希望您经常使用它们。Elixir 将文档视为一等公民,并提供许多访问文档的函数。我们将在 专门的章节 中介绍它们。
让我们回到前面几章中定义的 Math
模块,添加一些文档并将其保存到 math.ex
文件中
defmodule Math do
@moduledoc """
Provides math-related functions.
## Examples
iex> Math.sum(1, 2)
3
"""
@doc """
Calculates the sum of two numbers.
"""
def sum(a, b), do: a + b
end
Elixir 提倡使用 Markdown 和 heredoc 来编写可读的文档。Heredoc 是多行字符串,它们以三个双引号开头和结尾,保留内部文本的格式。我们可以直接从 IEx 访问任何已编译模块的文档
$ elixirc math.ex
$ iex
iex> h Math # Access the docs for the module Math
...
iex> h Math.sum # Access the docs for the sum function
...
我们还提供了一个名为 ExDoc 的工具,用于从文档生成 HTML 页面。
您可以查看 Module
的文档,以获取支持属性的完整列表。Elixir 还使用属性来定义 类型说明,这些类型说明可用于稍后声明模块之间的契约。
作为“常量”
Elixir 开发人员经常在希望使值更可见或更可重用时使用模块属性
defmodule MyServer do
@initial_state %{host: "127.0.0.1", port: 3456}
IO.inspect @initial_state
end
尝试访问未定义的属性将打印警告
defmodule MyServer do
@unknown
end
warning: undefined module attribute @unknown, please remove access to @unknown or explicitly set it before access
属性也可以在函数内部读取
defmodule MyServer do
@my_data 14
def first_data, do: @my_data
@my_data 13
def second_data, do: @my_data
end
MyServer.first_data #=> 14
MyServer.second_data #=> 13
不要在属性与其值之间添加换行符,否则 Elixir 将假设您正在读取值,而不是设置值。
在定义模块属性时可以调用函数
defmodule MyApp.Status do
@service URI.parse("https://example.com")
def status(email) do
SomeHttpClient.get(@service)
end
end
上面的函数将在编译时调用,它的返回值(而不是函数调用本身)将被替换为属性。因此,上面的代码实际上会编译成以下代码
defmodule MyApp.Status do
def status(email) do
SomeHttpClient.get(%URI{
authority: "example.com",
host: "example.com",
port: 443,
scheme: "https"
})
end
end
这对于预先计算常量值很有用,但如果您期望函数在运行时调用,这也会导致问题。例如,如果您从数据库或环境变量中读取属性内的值,请注意它只会在编译时读取该值。但是请注意,您不能在模块属性本身中调用定义在同一模块中的函数,因为这些函数尚未定义。
每次在函数内部读取属性时,Elixir 都会对其当前值进行快照。因此,如果您在多个函数内部多次读取同一个属性,您最终可能会创建多个副本。这通常不是问题,但如果您使用函数来计算大型模块属性,这可能会减慢编译速度。解决方案是将属性移动到共享函数。例如,不要使用以下代码
def some_function, do: do_something_with(@example)
def another_function, do: do_something_else_with(@example)
优先使用以下代码
def some_function, do: do_something_with(example())
def another_function, do: do_something_else_with(example())
defp example, do: @example
如果 @example
的计算成本很低,最好完全跳过模块属性,并在函数内部计算其值。
累积属性
通常,重复模块属性会导致其值被重新赋值,但有些情况下您可能希望 配置模块属性 以便累积其值
defmodule Foo do
Module.register_attribute(__MODULE__, :param, accumulate: true)
@param :foo
@param :bar
# here @param == [:bar, :foo]
end
作为临时存储
要查看使用模块属性作为存储的示例,您可以查看 Elixir 的单元测试框架,即 ExUnit
。ExUnit 将模块属性用于多种不同的目的
defmodule MyTest do
use ExUnit.Case, async: true
@tag :external
@tag os: :unix
test "contacts external service" do
# ...
end
end
在上面的示例中,ExUnit
将 async: true
的值存储在模块属性中,以更改模块的编译方式。标签也定义为 accumulate: true
属性,它们存储可用于设置和过滤测试的标签。例如,您可以避免在机器上运行外部测试,因为它们速度慢且依赖于其他服务,而它们仍然可以在您的构建系统中启用。
为了理解底层代码,我们需要宏,因此我们将在元编程指南中重新讨论这种模式,并学习如何使用模块属性作为存储,以允许开发人员创建领域特定语言 (DSL)。
在接下来的章节中,我们将探索结构和协议,然后再转到异常处理和其他结构,例如符号和推导。