Elixir 协议:defprotocol 与 defimpl
Elixir 中的协议是一种实现多态的机制。它允许你根据传入的数据类型不同,为同一个函数定义不同的实现方式。这与面向对象语言中的“接口”概念类似,但更灵活。下面通过定义一个通用的数据转换协议,演示 defprotocol 和 defimpl 的完整使用流程。
1. 定义协议接口
打开你的编辑器,创建一个新的文件 formatter.ex。协议主要包含一组函数签名,不包含具体实现逻辑。
输入以下代码来定义一个名为 Formatter 的协议,它要求实现一个 format/1 函数:
# formatter.ex
defprotocol Formatter do
@doc "将数据格式化为可读字符串"
def format(data)
end
保存文件。此时,任何想要被 Formatter 处理的数据类型,都必须实现 format/1 函数。
2. 为内置类型实现协议
Elixir 的许多内置类型(如 Atom, Map, List, Tuple)默认并未实现你的自定义协议。你需要使用 defimpl 为它们添加实现。
打开终端,进入 IEx 交互模式:
iex -S mix
或者直接在 formatter.ex 文件中追加以下代码。这里我们为 Atom 和 Map 两种内置类型分别定义格式化规则。
追加以下代码到文件中:
# 为 Atom 类型实现
defimpl Formatter, for: Atom do
def format(atom) do
"Atom: #{Atom.to_string(atom)}"
end
end
# 为 Map 类型实现
defimpl Formatter, for: Map do
def format(map) do
"Map with keys: #{Map.keys(map) |> Enum.join(", ")}"
end
end
编译并测试代码。在 IEx 中输入:
c "formatter.ex"
Formatter.format(:hello)
Formatter.format(%{name: "Alice", age: 20})
观察输出结果,针对 :hello(Atom)和 Map 返回了不同格式的字符串。
3. 为自定义结构体实现协议
在开发中,更常见的场景是为自定义的模块和结构体实现协议。
创建一个新文件 user.ex,定义一个用户结构体:
# user.ex
defmodule User do
defstruct [:name, :email]
end
回到 formatter.ex,追加针对 User 结构体的实现。注意 for: User 指向的是模块名。
# 为 User 结构体实现
defimpl Formatter, for: User do
def format(%User{name: name, email: email}) do
"User <#{email}> is named #{name}"
end
end
重新加载代码并测试:
c "user.ex"
c "formatter.ex"
alice = %User{name: "Alice", email: "alice@example.com"}
Formatter.format(alice)
确认输出结果使用了专门为 User 定制的格式化逻辑。
4. 设置后备实现
如果传入了一个未实现协议的类型,Elixir 会报错。为了避免这种情况,可以启用后备实现,让所有未明确实现的类型都使用同一段逻辑。
修改 formatter.ex 中的协议定义,添加 @fallback_to_any 属性:
defprotocol Formatter do
@fallback_to_any true
def format(data)
end
追加针对 Any 类型的实现,这将作为兜底逻辑:
defimpl Formatter, for: Any do
def format(data) do
"Unknown data: #{inspect(data)}"
end
end
测试未实现的类型,例如整数或列表:
Formatter.format(123)
Formatter.format([1, 2, 3])
5. 理解协议分发流程
协议的核心在于动态分发。当你调用 Formatter.format(data) 时,Elixir 会根据 data 的类型自动查找对应的实现。以下是其执行逻辑的流程图:
通过这种机制,你可以轻松地扩展系统的功能,而不需要修改现有的核心代码。
6. 结构体内置协议
Elixir 提供了一些非常有用的内置协议,如 String.Chars(转换为字符串)、Inspect(用于调试打印)和 Enumerable(用于遍历)。你可以为自己的结构体实现这些内置协议,从而获得像原生类型一样的体验。
打开 user.ex,实现 String.Chars 协议,以便可以直接使用 to_string/1:
defimpl String.Chars, for: User do
def to_string(%User{name: name}) do
"User: #{name}"
end
end
测试字符串插值功能,这会隐式调用 to_string:
c "user.ex"
alice = %User{name: "Bob"}
"Hello, #{alice}" # 此时会调用上面的实现
验证字符串输出为 Hello, User: Bob,说明协议已成功集成。
7. 协议总结表
下表总结了在使用协议时的关键点和区别:
| 特性 | 说明 | 示例 |
|---|---|---|
| 定义接口 | 使用 defprotocol 声明函数名,不写逻辑 |
defprotocol Calc, do: def area(x) |
| 具体实现 | 使用 defimpl 为特定类型编写逻辑 |
defimpl Calc, for: Circle, do: ... |
| 内置类型 | 可以为 Map, List, Tuple 等原生类型实现 | defimpl Calc, for: Map |
| 自定义类型 | 最常用于给 Struct 添加行为 | defimpl Calc, for: MyStruct |
| 后备机制 | 通过 @fallback_to_any 兜底未定义的类型 |
defimpl Calc, for: Any |

暂无评论,快来抢沙发吧!