协议具有以下特点:
defprotocol 定义。defimpl 为特定的数据类型实现。协议的一大优势在于:为某个特定数据类型实现协议时,不必把它和数据类型的定义或协议的定义捆绑在一起。这样就能编写出易于扩展的库:用户无需修改库本身,就能让它支持自己定义的数据类型。
可以为 Elixir 的所有类型实现协议:Tuple、Atom、List、BitString、Integer、Float、Function、PID、Map、Port、Reference、Any,以及结构体。
可以同时为多个类型实现协议:
defimpl Reversible, for: [Map, List] do
def reverse(term) do
Enum.reverse(term)
end
end
在定义结构体的模块内部直接为该结构体实现协议时,可以省略 :for 参数:
defmodule ShoppingCart do
defstruct [:items]
defimpl Reversible do
def reverse(shopping_cart) do
Enum.reverse(shopping_cart)
end
end
end
Any
默认情况下,为某个数据类型调用一个尚未针对它实现的协议,会抛出 Protocol.UndefinedError 错误:
to_string({})
# => ** (Protocol.UndefinedError) protocol String.Chars not implemented for {} of type Tuple
# (elixir 1.10.3) lib/string/chars.ex:3: String.Chars.impl_for!/1
# (elixir 1.10.3) lib/string/chars.ex:22: String.Chars.to_string/1
可以提供一个回退实现来避免这种情况,遇到上述情形时就会使用它。为此,在定义协议时把 @fallback_to_any 模块属性 设为 true,并为 Any 类型写一个实现:
defprotocol Reversible do
@fallback_to_any true
def reverse(term)
end
defimpl Reversible, for: Any do
def reverse(term) do
term
end
end
注意,大多数情况下抛出错误是更可取的做法,因为很难为_所有_数据类型对某个协议做出合理的假设。
Enumerable:Enum 和 Stream 模块中的函数可以处理实现了该协议的数据类型。Collectable:函数 Enum.into/2 使用该协议把一个可枚举对象插入到集合中。Inspect:你可以实现这个协议,来改变 Kernel.inspect/2 把某个数据类型表示为字符串的方式。String.Chars:你可以实现这个协议,来改变 Kernel.to_string/1 把某个数据类型表示为字符串的方式。