14-模組屬性

StraightDave發表於2014-09-17

14-模組屬性

作為註釋
作為常量
作為臨時儲存

在Elixir中,模組屬性(attributes)主要服務於三個目的:
1. 作為一個模組的註釋,通常附加上使用者或虛擬機器用到的資訊
2. 作為常量
3. 在編譯時作為一個臨時的模組儲存

讓我們一個一個講解。

14.1-作為註釋

Elixir從Erlang帶來了模組屬性的概念。例子:

defmodule MyServer do
  @vsn 2
end

這個例子中,我們顯式地為該模組設定了版本這個屬性。 屬性標識@vsn會被Erlang虛擬機器的程式碼裝載機制用到,讀取並檢查該模組是否在某處被更新了。 如果不顯式註明版本號,它會設定為這個模組函式的md5 checksum。

Elixir有好多系統保留屬性。這裡只列舉一些最常用的:

  • @moduledoc
    為當前模組提供文件
  • @doc
    為該屬性後面的函式或巨集提供文件
  • @behaviour
    (注意這個單詞是英式拼法)用來註明一個OTP或使用者自定義行為
  • @before_compile
    提供一個每當模組被編譯之前執行的鉤子。這使得我們可以在模組被編譯之前往裡面注入函式。

@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

上面例子使用了heredocs註釋。heredocs是多行的文字,用三個引號包裹,保持裡面內容的格式。 下面例子演示在iex中,用h命令讀取模組的註釋:

$ 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
...

Elixir還提供了ExDoc工具,利用註釋文件生成HTML頁。

你可以看看模組裡面列出的模組屬性列表, 看看Elixir還支援那些模組屬性。

Elixir還用屬性來定義typespecs

  • @spec
    為一個函式提供specification
  • @callback
    為行為回撥函式提供spec
  • @type
    定義一個@spec中用到的型別
  • @typep
    定義一個私有型別,用於@spec
  • @opaque
    定義一個opaque型別用於@spec

本節講了一些內建的屬性。當然,屬性可以被開發者、或者被類庫擴充套件來支援自定義的行為。

14.2-作為常量

Elixir開發者經常會模組屬性當作常量使用:

defmodule MyServer do
  @initial_state %{host: "147.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 to nil 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

注意在函式內讀取某屬性讀取的是該屬性當前值的快照。換句話說,讀取的是編譯時的值,而非執行時。 我們即將看到,這點使得屬性可以作為模組在編譯時的臨時儲存。

14.3-作為臨時儲存

Elixir組織中有一個專案,叫做Plug。這個專案的目標是建立一個Web的庫和框架。

類似於rack?

Plug庫允許開發者定義它們自己的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巨集來連線各個在處理請求時會被呼叫的函式。 在內部,每當你呼叫plug/1時,Plug庫把引數儲存在@plug屬性裡。 在模組被編譯之前,Plug執行一個回撥函式,這個函式定義了處理http請求的方法。 這個方法將順序執行所有儲存在@plug屬性裡的plugs。

為了理解底層的程式碼,我們需要巨集。因此我們將回顧一下超程式設計手冊裡這種模式。 但是這裡關注的重點是怎樣使用屬性來儲存資料,讓開發者得以建立DSL。

另一個例子來自ExUnit框架,它使用模組屬性作為註釋和儲存:

defmodule MyTest do
  use ExUnit.Case

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

ExUnit中,@tag標籤被用來註釋該測試用例。之後,這些標籤可以作為過濾測試用例之用。 例如,你可以避免執行那些被標記成:external的測試,因為它們執行起來很慢。

本章帶你一窺Elixir超程式設計的冰山一角,講解了模組屬性在開發中是如何扮演關鍵角色的。

下一章將講解結構體和協議。

相關文章