查看源代码 Inspect 协议 (Elixir v1.16.2)

The Inspect 协议将 Elixir 数据结构转换为代数文档。

通常,当你想要自定义你自己的结构体在日志和终端中的显示方式时,就会用到它。

本文档介绍了如何为你的数据结构实现 Inspect 协议。要了解更多关于 inspect 的使用,请参考 Kernel.inspect/2IO.inspect/2

Inspect 表示

通常有三种 inspect 表示的选择。为了理解它们,让我们假设我们有以下的 User 结构体

defmodule User do
  defstruct [:id, :name, :address]
end

我们的选择是

  1. 使用 Elixir 的结构体语法打印结构体,例如:%User{address: "Earth", id: 13, name: "Jane"}。这是默认表示,如果所有结构体字段都是公共的,那么它就是最佳选择。

  2. 使用 #User<...> 符号打印,例如:#User<id: 13, name: "Jane", ...>。这种符号不会输出有效的 Elixir 代码,通常用于结构体具有私有字段的情况(例如,你可能想要隐藏 :address 字段以屏蔽个人身份信息)。

  3. 使用表达式语法打印结构体,例如:User.new(13, "Jane", "Earth")。这假设存在一个 User.new/3 函数。此选项主要用作表示自定义数据结构(例如 MapSetDate.Range 等)的选项 2 的替代方案。

你可以在遵循上述约定 的情况下为自己的结构体实现 Inspect 协议。选项 1 是默认表示,你可以通过继承 Inspect 协议快速实现选项 2。对于选项 3,你需要自定义实现。

继承

The Inspect 协议可以被继承以自定义字段的顺序(默认情况下按字母顺序排列)并从结构体中隐藏某些字段,这样它们就不会出现在日志、inspect 和类似的地方。后者对于包含私有信息的字段特别有用。

支持的选项有

  • :only - 在 inspect 时只包含给定的字段。

  • :except - 在 inspect 时移除给定的字段。

  • :optional - (从 v1.14.0 开始) 如果字段与默认值匹配,则不包含该字段。这可以用来简化结构体表示,但会隐藏信息。

无论何时使用 :only:except 来限制字段,结构体都将使用 #User<...> 符号打印,因为结构体不再能被复制粘贴为有效的 Elixir 代码。让我们看一个例子

defmodule User do
  @derive {Inspect, only: [:id, :name]}
  defstruct [:id, :name, :address]
end

inspect(%User{id: 1, name: "Jane", address: "Earth"})
#=> #User<id: 1, name: "Jane", ...>

如果你只使用 :optional 选项,结构体仍然会以 %User{...} 的形式打印。

自定义实现

你也可以通过定义 inspect/2 函数来定义你自己的协议实现。该函数接收要检查的实体,以及检查选项,这些选项由结构体 Inspect.Opts 表示。代数文档的构建是通过 Inspect.Algebra 完成的。

很多时候,检查一个结构体可以根据现有的实体来实现。例如,以下是 MapSetinspect/2 实现

defimpl Inspect, for: MapSet do
  import Inspect.Algebra

  def inspect(map_set, opts) do
    concat(["MapSet.new(", Inspect.List.inspect(MapSet.to_list(map_set), opts), ")"])
  end
end

The concat/1 函数来自 Inspect.Algebra,它将代数文档连接在一起。在上面的例子中,它将字符串 "MapSet.new("、由 Inspect.Algebra.to_doc/2 返回的文档以及最后的字符串 ")" 连接在一起。因此,包含数字 1、2 和 3 的 MapSet 将被打印为

iex> MapSet.new([1, 2, 3], fn x -> x * 2 end)
MapSet.new([2, 4, 6])

换句话说,MapSet 的 inspect 表示返回一个表达式,该表达式在被计算后会构建 MapSet 本身。

错误处理

如果在检查你的结构体时出现错误,Elixir 将会抛出 ArgumentError 错误,并将自动回退到原始表示来打印结构体。此外,在调试你自己的 Inspect 实现时,你必须小心,因为调用 IO.inspect/2dbg/1 可能会导致无限循环(因为为了检查/调试数据结构,你必须调用 inspect 本身)。

以下是一些提示

  • 为了调试,请使用 IO.inspect/2 以及 structs: false 选项,该选项会禁用自定义打印并避免递归调用 Inspect 实现

  • 要访问你自定义的 Inspect 实现中的底层错误,你可以直接调用该协议。例如,我们可以调用上面的 Inspect.MapSet 实现,如下所示

    Inspect.MapSet.inspect(MapSet.new(), %Inspect.Opts{})

总结

类型

t()

实现此协议的所有类型。

函数

term 转换为代数文档。

类型

@type t() :: term()

实现此协议的所有类型。

函数

@spec inspect(t(), Inspect.Opts.t()) :: Inspect.Algebra.t()

term 转换为代数文档。

除非在实现要传递给 Inspect.Opts 的自定义 inspect_fun 时,否则不应直接调用此函数。在其他所有地方,应优先使用 Inspect.Algebra.to_doc/2,因为它处理结构体和异常。