14-模块属性

在Elixir中,模块属性(module attributes)主要服务于三个目的:

  1. 作为一个模块的注解(annotations),通常附加上用户或虚拟机会用到的信息
  2. 作为常量
  3. 在编译时作为一个临时的模块存储机制

下面让我们来一一讲解。

作为注解(annotations)

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

  1. defmodule MyServer do
  2. @vsn 2
  3. end

这个例子中,我们显式地为该模块设置了 版本(vsn即version) 属性。 @vsn是一个系统保留的属性名称,它被Erlang虚拟机的代码装载机制使用,以检查该模块是否被更新过。 如果不注明版本号,该属性的值会自动设置为模块函数的md5 checksum。

Elixir还有好多系统保留的预定义注解。下面是一些比较常用的:

  • @moduledoc - 为当前模块提供文档说明
  • @doc - 为该属性标注的函数或宏提供文档说明
  • @behaviour - (注意这个单词是英式拼法)用来注明一个OTP或用户自定义行为
  • @before_compile - 提供一个每当模块被编译之前执行的钩子。这使得我们可以在模块被编译之前往里面注入函数

@moduledoc@doc是目前最常用的注解属性,我们也希望你能够多使用它们。 Elixir视文档为一等公民,而且提供了很多方法来访问这些文档。 你可以拓展阅读文章《用我们官方的方式写Elixir程序文档》

让我们回到上几章定义的Math模块,为它添加文档,然后依然保存在math.ex文件中:

  1. defmodule Math do
  2. @moduledoc """
  3. Provides math-related functions.
  4. ## Examples
  5. iex> Math.sum(1, 2)
  6. 3
  7. """
  8. @doc """
  9. Calculates the sum of two numbers.
  10. """
  11. def sum(a, b), do: a + b
  12. end

Elixir推荐使用markdown语法和多行文本(heredocs)书写容易阅读的文档。 heredocs是多行的字符串,用三个双引号包裹,它会保持里面内容的格式不变。 我们可以在IEx中读取任何编译的模块的文档:

  1. $ elixirc math.ex
  2. $ iex
  1. iex> h Math # Access the docs for the module Math
  2. ...
  3. iex> h Math.sum # Access the docs for the sum function
  4. ...

Elixir还提供了ExDoc工具, 利用注释生成HTML页文档。

你可以看看模块 里面列出的完整的模块注解列表,Elixir还利用注解来定义typespecs

本节讲了一些内置的注解。当然,注解可以被开发者和类库扩展使用,来支持自定义的行为。

作为常量

Elixir开发者经常会将模块属性当作常量使用:

  1. defmodule MyServer do
  2. @initial_state %{host: "147.0.0.1", port: 3456}
  3. IO.inspect @initial_state
  4. end

不同于Erlang,默认情况下用户定义的属性不会被存储在模块里。属性值仅在编译时存在。 开发者可以通过调用Module.register_attribute/3来使这种属性的行为更接近Erlang。

访问一个未定义的属性会报警告:

  1. defmodule MyServer do
  2. @unknown
  3. end
  4. warning: undefined module attribute @unknown, please remove access to @unknown or explicitly set it to nil before access

最后,属性也可以在函数中被读取:

  1. defmodule MyServer do
  2. @my_data 14
  3. def first_data, do: @my_data
  4. @my_data 13
  5. def second_data, do: @my_data
  6. end
  7. MyServer.first_data #=> 14
  8. MyServer.second_data #=> 13

注意,在函数内读取某属性,读取的是该属性值的一份快照。换句话说,读取的是编译时的值,而非运行时。 后面我们将看到,这个特点使得属性可以作为模块在编译时的临时存储,十分有用。

作为临时存储

Elixir组织中有一个项目,叫做Plug, 这个项目的目标是创建一个通用的Web库和框架。

注:我想功能应该类似于ruby的rack。你可以定义各种plug,这这些plug会像链条一样, 按顺序各自对http请求进行加工处理,最后返回。这类似给rails或sinatra定义各种rack中间件, 也有些类似Java filter的概念。最终,Plug框架会组织和执行它们。

Plug库允许开发者定义它们自己的plug,运行在web服务器上:

  1. defmodule MyPlug do
  2. use Plug.Builder
  3. plug :set_header
  4. plug :send_ok
  5. def set_header(conn, _opts) do
  6. put_resp_header(conn, "x-header", "set")
  7. end
  8. def send_ok(conn, _opts) do
  9. send(conn, 200, "ok")
  10. end
  11. end
  12. IO.puts "Running MyPlug with Cowboy on http://localhost:4000"
  13. Plug.Adapters.Cowboy.http MyPlug, []

上面例子中,我们用了plug/1宏来连接处理请求时会被调用的函数。 在代码背后,每次调用宏plug/1时,Plug库把提供的参数(即plug的名字)存储在@plugs属性里。 就在模块被编译之前,Plug会执行一个回调函数,该回调函数定义一个函数(call/2)来处理http请求。 这个函数将按顺序执行所有保存在@plugs属性里的plugs。

要理解底层的代码,我们还需要了解宏,因此我们将在后期《元编程》章节中回顾这个模式。 这里的重点是怎样使用属性来存储数据,让开发者可以创建DSL(领域特定语言)。

另一个例子来自ExUnit框架, 它使用模块属性作为注解和存储:

  1. defmodule MyTest do
  2. use ExUnit.Case
  3. @tag :external
  4. test "contacts external service" do
  5. # ...
  6. end
  7. end

ExUnit中,标签(Tag)被用来注解该测试用例。在标记之后,这些标签可以用来过滤测试用例。 例如,你可以避免执行那些被标记成:external的测试,因为它们执行起来很慢而且可以依赖外部的东西。 但是它们依然在你的工程之内。

本章带你一窥Elixir元编程的冰山一角,讲解了模块属性在开发中是如何扮演关键角色的。 下一章将讲解结构体(structs)和协议(protocols),在前进到其它更远的知识点(诸如异常处理等)之前。