模块属性

Elixir 中的模块属性有三个目的:

  1. 它们用于注释模块,通常用于由用户或虚拟机使用的信息。

2. 它们作为常数工作。

3. 它们在编译期间用作临时模块存储。

让我们逐个检查每个案例。

作为注释

Elixir 带来了来自 Erlang 的模块属性的概念。例如:

defmodule MyServer do
  @vsn 2
end

在上面的例子中,我们明确地设置了该模块的版本属性。@vsn被 Erlang 虚拟机中的代码重载机制用来检查模块是否已被更新。如果未指定版本,则版本将设置为模块功能的 MD5 校验和。

Elixir 有一些保留的属性。以下是其中的一些,最常用的:

  • @moduledoc - 为当前模块提供文档。
  • @doc - 为属性后面的函数或宏提供文档。
  • @behaviour - (注意英式拼写)用于指定 OTP 或用户定义的行为。
  • @before_compile - 提供了一个将在模块编译之前被调用的钩子。这使得在编译之前可以在模块内部注入函数。

@moduledoc并且@doc是迄今为止最常用的属性,我们希望您能够使用它们很多。Elixir 将文档视为一流,并提供许多功能来访问文档。您可以在我们的官方文档中阅读有关在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 与 heredocs 编写可读文档。Heredocs 是多行字符串,它们以三重双引号开头和结尾,保留内部文本的格式。我们可以直接从 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 还使用属性来定义 typespecs。

本节介绍内置属性。但是,属性也可以由开发人员使用或由库扩展以支持自定义行为。

作为“常量”

Elixir 开发人员经常使用模块属性作为常量:

defmodule MyServer do
  @initial_state %{host: "127.0.0.1", port: 3456}
  IO.inspect @initial_state
end

注意:与Erlang不同,默认情况下用户定义的属性不存储在模块中。该值仅在编译期间存在。开发人员可以通过调用Module.register_attribute/3将属性配置为更接近 Erlang 。

尝试访问未定义的属性将会显示警告:

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 组织中的Plug其中一个项目就是项目项目旨在成为在Elixir中构建 Web 库和框架的通用基础。

Plug 库还允许开发人员定义可以在 Web 服务器中运行的自己的插件:

defmodule MyPlug do
  use Plug.Builder

  plug :set_header
  plug :send_ok

  def set_header(conn, _opts) do
    put_resp_header(conn, "x-header", "set")
  end

  def send_ok(conn, _opts) do
    send(conn, 200, "ok")
  end
end

IO.puts "Running MyPlug with Cowboy on http://localhost:4000"
Plug.Adapters.Cowboy.http MyPlug, []

在上面的例子中,我们使用plug/1宏来连接将在有 Web 请求时调用的函数。在内部,每次调用时plug/1,Plug 库都会将给定的参数存储在@plugs属性中。就在编译模块之前,Plug 运行一个回调,它定义了一个call/2处理 HTTP 请求的函数()。该功能将按@plugs顺序运行所有插头。

为了理解底层代码,我们需要宏,所以我们将在元编程指南中重新讨论这种模式。然而,这里的重点是如何使用模块属性作为存储允许开发人员创建 DSL。

另一个例子来自使用模块属性作为注释和存储的 ExUnit 框架

defmodule MyTest do
  use ExUnit.Case

  @tag :external
  test "contacts external service" do
    # ...
  end
end

ExUnit 中的标签用于注释测试。标签稍后可用于过滤测试。例如,您可以避免在计算机上运行外部测试,因为它们速度较慢并且依赖于其他服务,但仍可以在构建系统中启用它们。

我们希望本节能够展示 Elixir 如何支持元编程,以及模块属性如何在此过程中发挥重要作用。

在接下来的章节中,我们将探讨结构和协议,然后再讨论异常处理和其他结构(如签名和理解)。