模块属性 | Module attributes
模块属性
Elixir 中的模块属性有三个目的:
- 它们用于注释模块,通常用于由用户或虚拟机使用的信息。
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 如何支持元编程,以及模块属性如何在此过程中发挥重要作用。
在接下来的章节中,我们将探讨结构和协议,然后再讨论异常处理和其他结构(如签名和理解)。