檢視原始碼 模組屬性
Elixir 中的模組屬性有三個用途
- 它們用於註解模組,通常包含使用者或 VM 要使用的資訊。
- 它們用作常數。
- 它們用作編譯期間使用的暫時模組儲存。
讓我們逐一查看每個案例。
作為註解
Elixir 從 Erlang 引入了模組屬性的概念。例如
defmodule MyServer do
@moduledoc "My server code."
end
在上面的範例中,我們使用模組屬性語法定義模組文件。Elixir 有許多保留的屬性。以下是其中幾個最常用的屬性
@moduledoc
— 提供目前模組的文件。@doc
— 提供屬性之後函式或巨集的文件。@spec
— 提供屬性之後函式的型別規格。@behaviour
— (注意英國拼寫) 用於指定 OTP 或使用者定義的行為。
@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
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 也使用屬性來定義 型別規格,稍後可用於宣告模組之間的合約。
作為「常數」
Elixir 開發人員在希望讓值更明顯或可重複使用時,通常會使用模組屬性
defmodule MyServer do
@initial_state %{host: "127.0.0.1", port: 3456}
IO.inspect @initial_state
end
嘗試存取未定義的屬性會印出警告
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 會假設您正在讀取值,而不是設定值。
定義模組屬性時可以呼叫函式
defmodule MyApp.Status do
@service URI.parse("https://example.com")
def status(email) do
SomeHttpClient.get(@service)
end
end
上述函式會在編譯時呼叫,而其回傳值(而非函式呼叫本身)會取代屬性。因此,上述程式碼實際上會編譯成這樣
defmodule MyApp.Status do
def status(email) do
SomeHttpClient.get(%URI{
authority: "example.com",
host: "example.com",
port: 443,
scheme: "https"
})
end
end
這對於預先計算常數值很有用,但如果您預期函式在執行時會被呼叫,也可能會造成問題。例如,如果您在屬性內部從資料庫或環境變數讀取值,請注意它只會在編譯時讀取該值。但是,請注意您不能呼叫在同一個模組中定義的函式作為屬性的一部分,因為這些函式尚未定義。
每次在函式內部讀取屬性時,Elixir 會擷取其當前值的快照。因此,如果您在多個函式內部多次讀取同一個屬性,您可能會建立多個其副本。這通常不是問題,但如果您使用函式來計算大型模組屬性,這可能會減慢編譯速度。解決方案是將屬性移至共用函式。例如,不要這樣做
def some_function, do: do_something_with(@example)
def another_function, do: do_something_else_with(@example)
改用這種方式
def some_function, do: do_something_with(example())
def another_function, do: do_something_else_with(example())
defp example, do: @example
如果 @example
計算成本低,跳過模組屬性並在函式內部計算其值會更好。
累積屬性
通常,重複模組屬性會導致其值重新指派,但在某些情況下,您可能希望設定模組屬性,以便累積其值
defmodule Foo do
Module.register_attribute(__MODULE__, :param, accumulate: true)
@param :foo
@param :bar
# here @param == [:bar, :foo]
end
作為暫存
要查看使用模組屬性作為儲存的範例,請參閱 Elixir 的單元測試架構,稱為 ExUnit
。ExUnit
將模組屬性用於多種不同的目的
defmodule MyTest do
use ExUnit.Case, async: true
@tag :external
@tag os: :unix
test "contacts external service" do
# ...
end
end
在上述範例中,ExUnit
將 async: true
的值儲存在模組屬性中,以變更模組的編譯方式。標籤也定義為 accumulate: true
屬性,它們會儲存可用于設定和篩選測試的標籤。例如,您可以避免在機器上執行外部測試,因為它們很慢且依賴於其他服務,而它們仍然可以在您的建置系統中啟用。
為了瞭解底層程式碼,我們需要巨集,所以我們將在元程式設計指南中重新檢視此模式,並學習如何使用模組屬性作為儲存空間,以允許開發人員建立特定領域語言 (DSL)。
在接下來的章節中,我們將在進入例外處理和其他建構(例如符號和理解)之前,探討結構和協定。