檢視原始碼 模組屬性

Elixir 中的模組屬性有三個用途

  1. 它們用於註解模組,通常包含使用者或 VM 要使用的資訊。
  2. 它們用作常數。
  3. 它們用作編譯期間使用的暫時模組儲存。

讓我們逐一查看每個案例。

作為註解

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 的單元測試架構,稱為 ExUnitExUnit 將模組屬性用於多種不同的目的

defmodule MyTest do
  use ExUnit.Case, async: true

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

在上述範例中,ExUnitasync: true 的值儲存在模組屬性中,以變更模組的編譯方式。標籤也定義為 accumulate: true 屬性,它們會儲存可用于設定和篩選測試的標籤。例如,您可以避免在機器上執行外部測試,因為它們很慢且依賴於其他服務,而它們仍然可以在您的建置系統中啟用。

為了瞭解底層程式碼,我們需要巨集,所以我們將在元程式設計指南中重新檢視此模式,並學習如何使用模組屬性作為儲存空間,以允許開發人員建立特定領域語言 (DSL)。

在接下來的章節中,我們將在進入例外處理和其他建構(例如符號和理解)之前,探討結構和協定。