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。非经特殊声明,原始代码版权归原作者所有,本译文未经允许或授权,请勿转载或复制。