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
 BitStringIntegerFloat- Function
 PID- Map
 - Port
 ReferenceAny(请参阅下面的“回退到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。非经特殊声明,原始代码版权归原作者所有,本译文未经允许或授权,请勿转载或复制。
