16 协议

协议是Elixir中实现多态的一套机制。任何数据类型,只要实现了相关的协议,都能使用这个协议。让我们看一个例子:

在Elixr中,只有falsenil被当成非真值。其他的都被认为是真值。对某些应用来说,也许实现一个blank?协议是非常重要的,用它来在其他数据类型上返回一个布尔值。例如,一个空列表或空二进制可以被认为是空的。

我们用下面的访法定义这个协议:

  1. defprotocol Blank do
  2. @doc "Returns true if data is considered blank/empty"
  3. def blank?(data)
  4. end

这个协议期待实现一个单个参数的函数的blank?。我们能为不同的Elixir数据类型实现这个协议:

  1. # Integers are never blank
  2. defimpl Blank, for: Integer do
  3. def blank?(_), do: false
  4. end
  5. # Just empty list is blank
  6. defimpl Blank, for: List do
  7. def blank?([]), do: true
  8. def blank?(_), do: false
  9. end
  10. # Just empty map is blank
  11. defimpl Blank, for: Map do
  12. # Keep in mind we could not pattern match on %{} because
  13. # it matches on all maps. We can however check if the size
  14. # is zero (and size is a fast operation).
  15. def blank?(map), do: map_size(map) == 0
  16. end
  17. # Just the atoms false and nil are blank
  18. defimpl Blank, for: Atom do
  19. def blank?(false), do: true
  20. def blank?(nil), do: true
  21. def blank?(_), do: false
  22. end

同时我们也能效法于其他的内置数据类型上。它们是:

  • Atom
  • BitString
  • Float
  • Function
  • Integer
  • List
  • Map
  • PID
  • Por
  • Reference
  • Tuple

现在用了协议和它的实现在手,我们能调用了:

  1. iex> Blank.blank?(0)
  2. false
  3. iex> Blank.blank?([])
  4. true
  5. iex> Blank.blank?([1, 2, 3])
  6. false

传递一个还没有实现协议的数据类型,会导致一个错误:

  1. iex> Blank.blank?("hello")
  2. ** (Protocol.UndefinedError) protocol Blank not implemented for "hello"

16.1 现已和structs

Elixir的可扩展性的力量只有当协议和struct结合在一起的时候才显露出来。

在之前的一章,我们已经学习到了虽然struct就是表单,但它们并没有和表单共享协议的实现。然我们同在前面一章一样,定义一个Userstruct:

  1. iex> defmodule User do
  2. ...> defstruct name: "jose", age: 27
  3. ...> end
  4. {:module, User,
  5. <<70, 79, 82, ...>>, {:__struct__, 0}}

然后

  1. iex> Blank.blank?(%{})
  2. true
  3. iex> Blank.blank?(%User{})
  4. ** (Protocol.UndefinedError) protocol Blank not implemented for %User{age: 27, name: "jose"}

由于没有能和表单分享协议实现,struct需要它们自己的实现:

  1. defimpl Blank, for: User do
  2. def blank?(_), do: false
  3. end

如果需要,你可以使用你自己的对用户是否为空的定义。不仅如此,你能用struct来编写更健壮的数据类型,比如请求,和为这个数据实现所有的相关协议,例如Enumerable和甚至Blank

在许多的实际应用中,开发者也许希望为struct提供一个默认的实现,因为为每一个struct都实现这个协议会很无聊。这时就是falling back to any显示威力的时候:

16.2 Falling back to Any

如果我们能为所有的类型提供一个默认的实现,将是非常方便的。这可以通过在协议定义中讲设置@fallback_to_anytrue来实现:

  1. defprotocol Blank do
  2. @fallback_to_any true
  3. def blank?(data)
  4. end

现在它可以这样被实现:

  1. defimpl Blank, for: Any do
  2. def blank?(_), do: false
  3. end

现在所有的那些我们还没有实现Blank协议的的数据类型(包括struct),都会被视为不空。

16.3 内建协议

Elixir包含了一个内建的协议。在之前的几章中,我们已经讨论了Enum模块就提供了许多的能通用于任何实现了Enumerable协议的数据类型:

  1. iex> Enum.map [1, 2, 3], fn(x) -> x * 2 end
  2. [2,4,6]
  3. iex> Enum.reduce 1..3, 0, fn(x, acc) -> x + acc end
  4. 6

另一个有用的例子是String.Chars协议,它指定了如何将一个包含字符串的数据结构转换成字符串。它是通过函数to_string来曝光的:

  1. iex> to_string :hello
  2. "hello"

注意Elixir中的字符串解析就调用了to_string函数:

  1. iex> "age: #{25}"
  2. "age: 25"

上的片段能运行因为数字实现了协议String.Chars。如果传递一个元组,会导致一个错误:

  1. iex> tuple = {1, 2, 3}
  2. {1, 2, 3}
  3. iex> "tuple: #{tuple}"
  4. ** (Protocol.UndefinedError) protocol String.Chars not implemented for {1, 2, 3}

当有需要去“打印”更复杂的数据结构的时候,简单调用inspect函数就行,它是基于协议Inspect

  1. iex> "tuple: #{inspect tuple}"
  2. "tuple: {1, 2, 3}"

Inspect协议用于把任何数据结构转换成可都的文字呈现。这也是类似IEx的工具打印的结果:

  1. iex> {1, 2, 3}
  2. {1,2,3}
  3. iex> %User{}
  4. %User{name: "jose", age: 27}

谨记,作为一个约定,当打印出的值以#开头,它是在用Elixir中非法的语法来呈现一个数据结构。这说明,inspect协议是不可逆的,因为在这个过程中有些信息丢失了。

  1. iex> {1, 2, 3}
  2. {1,2,3}
  3. iex> %User{}
  4. %User{name: "jose", age: 27}

除此之外,Elixir中还有一些其他的协议, 但这一章涵盖了最常见的几个。在下一章我们讲学习一点Elixir的异常和错误处理。