Elixir語言中 Protocol
相關用法介紹如下。
使用協議的參考和函數。
協議指定應由其實現定義的 API。協議用
定義,其實現用 Kernel.defprotocol/2
定義。Kernel.defimpl/3
真實案例
在 Elixir 中,我們有兩個名詞用於檢查數據結構中有多少項:length
和 size
。 length
表示必須計算信息。例如length(list)
需要遍曆整個列表來計算它的長度。另一方麵,tuple_size(tuple)
和byte_size(binary)
不依賴於元組和二進製大小,因為大小信息是在數據結構中預先計算的。
盡管 Elixir 包含特定的函數,例如 tuple_size
、 binary_size
和 map_size
,但有時我們希望能夠檢索數據結構的大小而不管其類型如何。在 Elixir 中,我們可以使用協議編寫多態代碼,即適用於不同形狀/類型的代碼。大小協議可以實現如下:
defprotocol Size do
@doc "Calculates the size (and not the length!) of a data structure"
def size(data)
end
既然可以為每個數據結構實現該協議,該協議可能有一個兼容的實現:
defimpl Size, for: BitString do
def size(binary), do: byte_size(binary)
end
defimpl Size, for: Map do
def size(map), do: map_size(map)
end
defimpl Size, for: Tuple do
def size(tuple), do: tuple_size(tuple)
end
請注意,我們沒有為列表實現它,因為我們沒有關於列表的 size
信息,而是需要使用 length
計算它的值。
您實現協議的數據結構必須是協議中定義的所有函數的第一個參數。
可以為所有 Elixir 類型實現協議:
- 結構(參見下麵的“協議和結構”部分)
- Tuple
- Atom
- List
BitString
Integer
Float
- Function
PID
- Map
- Port
Reference
Any
(請參閱下麵的“回退到Any
”部分)
協議和結構
當與結構混合時,協議的真正好處就來了。例如,Elixir 附帶了許多實現為結構的數據類型,例如
。我們也可以為這些類型實現MapSet
Size
協議:
defimpl Size, for: MapSet do
def size(map_set), do: MapSet.size(map_set)
end
在為結構實現協議時,如果
調用位於定義結構的模塊內,則可以省略 defimpl/3
:for
選項:
defmodule User do
defstruct [:email, :name]
defimpl Size do
# two fields
def size(%User{}), do: 2
end
end
如果未找到給定類型的協議實現,則調用該協議將引發,除非將其配置為回退到 Any
。還提供了在現有實現之上構建實現的便利,請查看
以獲取有關派生協議的更多信息。defstruct/1
回退到Any
在某些情況下,為所有類型提供默認實現可能會很方便。這可以通過在協議定義中將@fallback_to_any
屬性設置為true
來實現:
defprotocol Size do
@fallback_to_any true
def size(data)
end
現在可以為 Any
實現 Size
協議:
defimpl Size, for: Any do
def size(_), do: 0
end
盡管上麵的實現可以說是不合理的。例如,說 PID 或整數的大小為 0
是沒有意義的。這就是為什麽@fallback_to_any
是一種選擇加入行為的原因之一。對於大多數協議,在協議未實現時引發錯誤是正確的行為。
多種實現
協議也可以同時為多種類型實現:
defprotocol Reversible do
def reverse(term)
end
defimpl Reversible, for: [Map, List] do
def reverse(term), do: Enum.reverse(term)
end
在
中,您可以使用 defimpl/3
@protocol
訪問正在實現的協議,並使用 @for
訪問正在為其定義的模塊。
類型
定義協議會自動定義一個名為 t
的 zero-arity 類型,可以按如下方式使用:
@spec print_size(Size.t()) :: :ok
def print_size(data) do
result =
case Size.size(data) do
0 -> "data has no items"
1 -> "data has one item"
n -> "data has #{n} items"
end
IO.puts(result)
end
上麵的@spec
表示允許實現給定協議的所有類型都是給定函數的有效參數類型。
反射
任何協議模塊都包含三個額外的函數:
-
__protocol__/1
- 返回協議信息。該函數采用以下原子之一::consolidated?
- 返回協議是否合並:functions
- 返回協議函數及其參數的關鍵字列表:impls
- 如果合並,則返回{:consolidated, modules}
以及實現協議的模塊列表,否則返回:not_consolidated
:module
- 協議模塊原子名稱
-
impl_for/1
- 返回為給定參數實現協議的模塊,否則返回nil
-
impl_for!/1
- 與上麵相同,但如果未找到實現,則會引發Protocol.UndefinedError
例如,對於
協議,我們有:Enumerable
iex> Enumerable.__protocol__(:functions)
[count: 1, member?: 2, reduce: 3, slice: 1]
iex> Enumerable.impl_for([])
Enumerable.List
iex> Enumerable.impl_for(42)
nil
此外,每個協議實現模塊都包含__impl__/1
函數。該函數采用以下原子之一:
-
:for
- 返回負責協議實現的數據結構的模塊 -
:protocol
- 返回為其提供此實現的協議模塊
例如,為列表實現
協議的模塊是 Enumerable
。因此,我們可以在這個模塊上調用Enumerable.List
__impl__/1
:
iex(1)> Enumerable.List.__impl__(:for)
List
iex(2)> Enumerable.List.__impl__(:protocol)
Enumerable
合並
為了加快協議調度,隻要知道所有協議實現up-front,通常在編譯項目中的所有 Elixir 代碼後,Elixir 都會提供一個名為 protocol consolidation
的函數。整合直接將協議鏈接到它們的實現,從整合協議中調用一個函數相當於調用兩個遠程函數。
默認情況下,協議合並在編譯期間應用於所有 Mix 項目。這可能是測試期間的問題。例如,如果您想在測試期間實現一個協議,那麽該實現將不起作用,因為該協議已經被合並了。一種可能的解決方案是在 mix.exs 中包含特定於您的測試環境的編譯目錄:
def project do
...
elixirc_paths: elixirc_paths(Mix.env())
...
end
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
然後您可以在 test/support/some_file.ex
中定義特定於測試環境的實現。
另一種方法是在 mix.exs 中的測試期間禁用協議整合:
def project do
...
consolidate_protocols: Mix.env() != :test
...
end
盡管不建議這樣做,因為它可能會影響您的測試套件性能。
最後,請注意,所有協議都是在 debug_info
設置為 true
的情況下編譯的,而與 elixirc
編譯器設置的選項無關。調試信息用於合並,除非全局設置,否則在合並後將其刪除。
相關用法
- Elixir Protocol.extract_protocols用法及代碼示例
- Elixir Protocol.consolidate用法及代碼示例
- Elixir Protocol.derive用法及代碼示例
- Elixir Protocol.extract_impls用法及代碼示例
- Elixir Process.monitor用法及代碼示例
- Elixir Process.demonitor用法及代碼示例
- Elixir Process.delete用法及代碼示例
- Elixir Process.whereis用法及代碼示例
- Elixir Process.group_leader用法及代碼示例
- Elixir Process.put用法及代碼示例
- Elixir Process.registered用法及代碼示例
- Elixir Process.register用法及代碼示例
- Elixir Process.get用法及代碼示例
- Elixir Process.exit用法及代碼示例
- Elixir Process.unregister用法及代碼示例
- Elixir Process.get_keys用法及代碼示例
- Elixir Process.sleep用法及代碼示例
- Elixir Process.send用法及代碼示例
- Elixir Process.spawn用法及代碼示例
- Elixir Process.send_after用法及代碼示例
- Elixir Process.list用法及代碼示例
- Elixir Path.basename用法及代碼示例
- Elixir Path.rootname用法及代碼示例
- Elixir Path.type用法及代碼示例
- Elixir Path.split用法及代碼示例
注:本文由純淨天空篩選整理自elixir-lang.org大神的英文原創作品 Protocol。非經特殊聲明,原始代碼版權歸原作者所有,本譯文未經允許或授權,請勿轉載或複製。