协议
了解 Elixir 中的协议及其使用方法。
简介
在本课程中,我们多次使用了 inspect 函数。它会将任何值转换为可打印的二进制形式,也就是我们所说的字符串。
但让我们停下来思考一下:Elixir 本身没有对象,它是如何知道调用哪个函数来完成这种转换的呢?我们可以向 inspect 传递任意类型的值,而 Elixir 总能正确处理。这可以通过守卫语句实现:
1def inspect(value) when is_atom(value), do: ... 2def inspect(value) when is_binary(value), do: ... 3: :
但还有更好的方法。
Elixir 引入了“协议”(protocols)的概念。协议有点类似于前一章中看到的“行为”(behaviours),因为它定义了实现某项功能所必须提供的函数。但行为是模块内部的,由模块自身实现;而协议则不同,它的实现可以完全放在模块外部。这意味着我们可以在不修改原模块代码的情况下扩展其功能。事实上,即使我们无法获取模块的源码,也可以对其进行功能扩展。
定义协议
协议的定义与普通模块定义非常相似。它可以包含模块级和函数级的文档注释(@moduledoc 和 @doc),并包含一个或多个函数定义。不过,这些函数没有函数体,它们的作用仅仅是声明协议所需接口。
例如,以下是 Inspect 协议的定义:
1defprotocol Inspect do 2 @fallback_to_any true 3 def inspect(thing, opts) 4end
就像模块一样,协议定义了一个或多个函数。但具体的实现代码是单独提供的。
实现协议
defimpl 宏允许我们为一个或多个类型提供协议的具体实现。下面的代码展示了 Inspect 协议对 PID 和引用(Reference)类型的实现:
1defimpl Inspect, for: PID do 2 def inspect(pid, _opts) do 3 "#PID" <> IO.iodata_to_binary(pid_to_list(pid)) 4 end 5end 6 7defimpl Inspect, for: Reference do 8 def inspect(ref, _opts) do 9 '#Ref' ++ rest = :erlang.ref_to_list(ref) 10 "#Reference" <> IO.iodata_to_binary(rest) 11 end 12end
最后,Kernel 模块实现了 inspect 函数,它调用 Inspect.inspect 并传入参数。这意味着当我们调用 inspect(self) 时,实际上等价于调用 Inspect.inspect(self)。由于 self 是一个 PID,因此会解析为类似 "#PID<0.25.0>" 的结果。
在底层,defimpl 会将每个协议与类型组合的实现放入一个独立的模块中。例如,Inspect 协议对 PID 类型的实现位于模块 Inspect.PID 中。由于模块可以重新编译,我们可以通过协议访问的函数实现也可以被动态更改。
1iex> inspect self 2"#PID<0.25.0>" 3iex> defimpl Inspect, for: PID do 4...> def inspect(pid, _) do 5...> "#Process: " <> IO.iodata_to_binary(:erlang.pid_to_list(pid)) <> "!!" 6...> end 7...> end 8iex:3: redefining module Inspect.PID 9{:module, Inspect.PID, <<70,79,... 10 11iex> inspect self 12"#Process: <0.25.0>!!"
请尝试运行上面的命令: