Elixir 协议与行为

发布于:2025-08-02 ⋅ 阅读:(14) ⋅ 点赞:(0)

协议(protocol)和行为(behaviour)是 Elixir 实现多态的两种方式。类似于其他语言中的接口,它们都是一种接口约束,本质上是一个函数集合。那么为什么 Elixir 中会出现协议和行为两个概念呢?

这是因为协议和行为针对的对象不同,协议的对象是类型,而行为的对象是模块。我们先说协议,再说行为。

协议(protocol)

协议是指对于一个函数,可以根据参数类型的不同表现出不同的行为。比如 Elixir 的 IO 模块中的 inspect 就是一个协议,针对数字和字符串类型肯定有不同的实现方式。因为要根据参数类型决定调用的具体函数,因此协议的函数必须至少有一个参数,当然也可以有不止一个参数,这时根据第一个参数的类型决定调用的函数。

定义协议使用 defprotocol 关键字,和定义模块挺像的,只不过 protocol 中的函数只有声明,没有具体实现。

实现协议使用的是 defimpl 关键字,后面跟协议名,然后是一个关键字列表, :for 是要实现该协议的类型, :do 是协议函数的具体实现,与定义模块一样。

下面是一个摘自《Programming Elixir》的完整的例子,定义一个 Collection 协议,用来告诉用户某个值是否是集合类型。创建一个 is_collection.exs 文件输入以下代码:

defprotocol Collection do
	@fallback_to_any true 
	def is_collection?(value)
end

defimpl Collection, for: [List, Tuple, BitString, Map] do 
	def is_collection?(_), do: true
end

defimpl Collection, for: Any do 
	def is_collection?(_), do: false
end

Enum.each [ 1, 1.0, [1,2], {1,2}, %{}, "cat" ], fn value -> 
	IO.puts "#{inspect value}: #{Collection.is_collection?(value)}"
end

使用命令 elixir is_collection.exs 运行脚本,不出意外的话能得到下面的输出:

1: false 
1.0: false 
[1,2]: true 
{1,2}: true 
%{}: true 
"cat": true

协议在使用时直接调用协议的函数即可,和调用模块中的函数没什么区别,我们可以将 Collection 就当作一个模块。运行时,Elixir 会帮我们去执行对应的实现,这一步应该是在编译阶段就完成了。

Any 是一个特殊类型,会匹配任意类型。如果协议的函数前面加了 @fallback_to_any true 声明,那么对于未实现该协议的类型就匹配上 Any 的实现。这是一种保护机制,否则就会抛出协议未实现异常。

相比于接口,协议可以自由扩展内置类型,让它们实现任何自定义协议。而接口是无法做到这一点的,即便是 Go 语言,也只能通过 type MyInt int 定义一个新类型,然后在 MyInt 上实现自定义接口,然而在使用时就会多一步类型转换。

行为(behaviour)

行为是针对模块的接口约束,它的存在是因为 Elixir 模块的特殊性。

现代编程语言几乎都有着 module 或 package 这样的概念,用来组织代码,而不仅仅是通过文件。在绝大多数语言中,对于编程来讲,包只是提供一个命名空间,包名只是字面量。也就是说,包名只能用在 import xxxxxx.yyy 来引用模块的变量或函数。

但是 Elixir 不同,在 Elixir 中,模块名是一个原子类型的变量,用法上也和其他原子类型变量没有任何区别,甚至可以用作函数的参数,而且这种用法在 Elixir 中极为常见,而这在其他语言中是几乎不可能的。

从这一点来看,Elixir 的模块更像是面对对象语言中的静态类,但是模块没有属性,也不需要实例化。而且 Elixir 在编译模块时,会把每一个模块编译成一个与模块同名的 .beam 文件,即便是嵌套的模块也会编译成单独的 .beam 文件,我猜这样有利于虚拟机按需加载模块。

模块做为函数的集合,又类似于静态类,让模块可以实现接口似乎也是一件很合理的事情。定义行为和定义模块一样,也是使用 defmodule 关键字,但是其中的函数只有声明,再加上 @callback 属性就可以了。

defmodule Bucket do
	@callback upload(path :: String.t, opts)
	@callback download(filename :: String.t, path :: String.t)
end

我们可以定义一个云存储的行为,然后让不同供应商的云存储模块都实现这一行为。

实现行为只需要在模块中使用 @behaviour 行为模块名 属性声明即可,编译器会帮我们检测行为的实现是否完整。在实现的函数上,我们还可以使用 @impl 行为模块名 或更简单的 @impl true 属性标记该函数为行为的实现,但这只是给人看的,并不是编译必须的。

defmodule TxOSS do
	@behaviour Bucket
	
	@impl Bucket
	def upload(path, opts) do
		...
	end
	
	@impl true
	def download(filename, path) do
		...
	end
end

defmodule AliOSS do
	@behaviour Bucket
	
	@impl Bucket
	def upload(path, opts) do
		...
	end
	
	@impl Bucket
	def download(filename, path) do
		...
	end
end

因为 Elixir 是动态类型语言,在编程时,除了在 @behaviour 中我们几乎也用不到行为,顶多再在使用到行为的函数的 @spec 属性中用于声明,但是这也是写给人和分析器看的,也不是编译器必须的。


网站公告

今日签到

点亮在社区的每一天
去签到