文章目录

Elixir 协议:defprotocol 与 defimpl

发布于 2026-04-19 05:18:50 · 浏览 8 次 · 评论 0 条

Elixir 协议:defprotocol 与 defimpl

Elixir 中的协议是一种实现多态的机制。它允许你根据传入的数据类型不同,为同一个函数定义不同的实现方式。这与面向对象语言中的“接口”概念类似,但更灵活。下面通过定义一个通用的数据转换协议,演示 defprotocoldefimpl 的完整使用流程。


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 的类型自动查找对应的实现。以下是其执行逻辑的流程图:

graph LR A["调用者: Formatter.format(data)"] --> B["协议分发机制"] B --> C{检查 data 类型} C -->|Map| D["执行: defimpl Formatter, for: Map"] C -->|User Struct| E["执行: defimpl Formatter, for: User"] C -->|Atom| F["执行: defimpl Formatter, for: Atom"] C -->|Other Type| G["执行: defimpl Formatter, for: Any"] D --> H["返回结果"] E --> H F --> H G --> H

通过这种机制,你可以轻松地扩展系统的功能,而不需要修改现有的核心代码。


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

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文