查看源代码 模块属性

Elixir 中的模块属性有三个作用

  1. 它们用于注释模块,通常包含用户或 VM 使用的信息。
  2. 它们充当常量。
  3. 它们充当编译期间使用的临时模块存储。

让我们逐一检查每个情况。

作为注释

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

在上面的示例中,ExUnitasync: true 的值存储在模块属性中,以更改模块的编译方式。标签也定义为 accumulate: true 属性,它们存储可用于设置和过滤测试的标签。例如,您可以避免在机器上运行外部测试,因为它们速度慢且依赖于其他服务,而它们仍然可以在您的构建系统中启用。

为了理解底层代码,我们需要宏,因此我们将在元编程指南中重新讨论这种模式,并学习如何使用模块属性作为存储,以允许开发人员创建领域特定语言 (DSL)。

在接下来的章节中,我们将探索结构和协议,然后再转到异常处理和其他结构,例如符号和推导。