14-模块属性

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

    下面让我们来一一讲解。

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

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

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

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

    @doc是目前最常用的注解属性,我们也希望你能够多使用它们。
    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. @doc """
    8. Calculates the sum of two numbers.
    9. """
    10. end

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

    1. $ elixirc math.ex
    2. $ iex

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

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

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

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

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

    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

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

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

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

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

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

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

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

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

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

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

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