Elixir实战: 4 数据抽象 (3)使用协议的多态性
myzbx 2025-01-21 20:00 16 浏览
4.3 使用协议的多态性
多态性是在运行时根据输入数据的性质决定执行哪个代码。在 Elixir 中,基本的(但不是唯一的)实现方式是使用称为协议的语言特性。
在讨论协议之前,让我们看看它们的实际应用。您已经见过多态代码。例如,整个 Enum 模块是通用代码,可以在任何可枚举的对象上工作,以下片段说明了这一点:
Enum.each([1, 2, 3], &IO.inspect/1)
Enum.each(1..3, &IO.inspect/1)
Enum.each(%{a: 1, b: 2}, &IO.inspect/1)
注意你如何使用相同的 Enum.each/2 函数,发送不同的数据结构:列表、范围和映射。 Enum.each/2 如何知道如何遍历每个结构?它不知道。 Enum.each/2 中的代码是通用的,并依赖于一个契约。这个契约称为协议,必须为每个你希望与 Enum 函数一起使用的数据类型实现。接下来,让我们学习如何定义和使用协议。
4.3.1 协议基础
协议是一个模块,在其中你声明函数而不实现它们。可以将其视为面向对象接口的粗略等价物。通用逻辑依赖于协议并调用其函数。然后,你可以为不同的数据类型提供协议的具体实现。
让我们看一个例子。协议 String.Chars 是由 Elixir 标准库提供的,用于将数据转换为二进制字符串。以下是该协议在 Elixir 源代码中的定义:
defprotocol String.Chars do
def to_string(term)
end
协议的定义
协议功能声明
这类似于模块定义,显著的区别在于函数被声明但未实现。
注意函数的第一个参数( term )。在运行时,这个参数的类型决定了调用的实现。让我们看看这个实际应用。Elixir 已经为原子、数字和一些其他数据类型实现了该协议,因此您可以发出以下调用:
iex(1)> String.Chars.to_string(1)
"1"
iex(2)> String.Chars.to_string(:an_atom)
"an_atom"
如果未针对给定数据类型实现协议,则会引发错误:
iex(3)> String.Chars.to_string(TodoList.new())
** (Protocol.UndefinedError) protocol String.Chars not implemented
通常,您不需要直接调用协议函数。更常见的是,有通用代码依赖于该协议。在 String.Chars 的情况下,这是自动导入的函数 Kernel.to_string/1 :
iex(4)> to_string(1)
"1"
iex(5)> to_string(:an_atom)
"an_atom"
iex(6)> to_string(TodoList.new())
** (Protocol.UndefinedError) protocol String.Chars not implemented
如您所见, to_string/1 的行为与 String.Chars.to_string/1 完全相同。这是因为 Kernel.to_string/1 委托给 String.Chars 实现。
此外,您可以将任何实现 String.Chars 的内容发送到 IO.puts/1 :
iex(7)> IO.puts(1)
1
iex(8)> IO.puts(:an_atom)
an_atom
iex(9)> IO.puts(TodoList.new())
** (Protocol.UndefinedError) protocol String.Chars not implemented
如您所见, TodoList 的一个实例无法打印,因为对应类型未实现 String.Chars 。
4.3.2 实施协议
如何为特定类型实现协议?让我们再次参考 Elixir 源代码。以下代码片段为整数实现了 String.Chars :
defimpl String.Chars, for: Integer do
def to_string(term) do
Integer.to_string(term)
end
end
您通过调用 defimpl 宏开始实现。然后,您指定要实现的协议和相应的数据类型。最后, do/end 块包含每个协议函数的实现。在示例中,实施委托给现有的标准库函数 Integer.to_string/1 。
for: Type 部分值得一些解释。类型是一个原子,可以是以下别名中的任何一个: Tuple 、 Atom 、 List 、 Map 、 BitString 、 Integer 、 Float 、 Function 、 PID 、 Port 或 Reference 。这些值对应于内置的 Elixir 类型。
此外,允许使用别名 Any ,这使得可以指定一个后备实现。如果某个类型没有定义协议,将会引发错误,除非在协议定义中指定了对 Any 的后备,并且存在 Any 实现。有关详细信息,请参阅协议文档 (https://hexdocs.pm/elixir/Protocol.xhtml)。
最后,最重要的是,类型可以是任何其他任意别名(但不能是常规的简单原子):
defimpl String.Chars, for: SomeAlias do
...
end
此实现将在协议函数的第一个参数是相应模块中定义的结构时被调用。例如,您可以如下实现 String.Chars 对 TodoList 的支持:
iex(1)> defimpl String.Chars, for: TodoList do
def to_string(_) do
"#TodoList"
end
end
现在,您可以将待办事项列表实例传递给 IO.puts/1 :
iex(2)> IO.puts(TodoList.new())
#TodoList
重要的是要注意,协议实现不需要成为任何模块的一部分。这具有强大的影响:您可以为一个类型实现协议,即使您无法修改该类型的源代码。您可以将协议实现放置在您自己代码的任何位置,运行时将能够利用它。
4.3.3 内置协议
Elixir 提供了一些预定义的协议。最好查阅在线文档以获取完整参考(https://hexdocs.pm/elixir),但我们还是提到一些最重要的协议。
您已经看过 String.Chars ,它指定了将数据转换为二进制字符串的合同。还有 List.Chars 协议,它将输入数据转换为字符字符串(字符列表)。
如果您想控制结构在调试输出中的打印方式(通过 inspect 函数),您可以实现 Inspect 协议。
可以说,最重要的协议是 Enumerable 。通过实现它,您可以使您的数据结构可枚举。这意味着您可以免费使用 Enum 和 Stream 模块中的所有功能!这可能是协议有用性的最佳示范。 Enum 和 Stream 都是通用模块,提供许多有用的功能,只要您实现了 Enumerable 协议,它们就可以在您的自定义数据结构上工作。
与枚举密切相关的是 Collectable 协议。回想一下第 3 章,可收集结构是指可以反复添加元素的结构。可收集结构可以与推导式一起使用以收集结果,或与 Enum.into/2 一起使用以将一个结构(可枚举)中的元素转移到另一个结构(可收集)。
当然,您可以定义自己的协议并将其实现为任何可用的数据结构(您自己的或他人的)。有关更多信息,请参见 Kernel.defprotocol/2 文档。
可收集的待办事项列表
让我们看一个更复杂的例子。你将使你的待办事项列表可收集,以便将其用作理解目标。这是一个稍微高级的例子,所以如果你第一次没有理解每个细节也不用担心。
要使抽象可收集,您必须实现相应的协议:
defimpl Collectable, for: TodoList do
def into(original) do
{original, &into_callback/2}
end
defp into_callback(todo_list, {:cont, entry}) do
TodoList.add_entry(todo_list, entry)
end
defp into_callback(todo_list, :done), do: todo_list
defp into_callback(_todo_list, :halt), do: :ok
end
返回 appender lambda
附加器实现
导出的函数 into/1 被通用代码(例如,推导式)调用。在这里,您提供返回附加器 lambda 的实现。然后,通用代码会重复调用此附加器 lambda,将每个元素附加到您的数据结构中。
附加函数接收一个待办事项列表和一个指令提示。如果收到 {:cont, entry} ,则必须添加一个新条目。如果收到 :done ,则返回列表,此时包含所有附加元素。最后, :halt 表示操作已被取消,返回值被忽略。
让我们看看这个如何运作。将之前的代码复制并粘贴到终端中,然后尝试以下操作:
iex(1)> entries = [
%{date: ~D[2023-12-19], title: "Dentist"},
%{date: ~D[2023-12-20], title: "Shopping"},
%{date: ~D[2023-12-19], title: "Movies"}
]
iex(2)> Enum.into(entries, TodoList.new())
%TodoList{...}
收集到待办事项列表
通过实现 Collectable 协议,您实际上将 TodoList 抽象适配到任何依赖该协议的通用代码,例如 Enum.into/2 或 for 理解。
摘要
- 模块用于创建抽象。模块的函数创建、操作和查询数据。客户端可以检查整个结构,但不应依赖其形状。
- 地图可以用来将不同领域组合在一个结构中。
- 结构体是一种特殊的映射,允许您定义与模块相关的数据抽象。
- 多态可以通过协议实现。协议定义了一个由泛型逻辑使用的接口。然后,您可以为数据类型提供特定的协议实现。
相关推荐
- 荒野大镖客2游戏壁纸图片
-
...
- 巫师3:狂猎游戏壁纸图片
-
...
- 腾讯混元发布游戏视觉生成平台
-
5月20日,腾讯正式发布混元游戏视觉生成平台(简称“混元游戏”),这是依托混元大模型打造的首个工业级AIGC游戏内容生产引擎,大幅优化游戏资产生成与游戏制作流程。其面向游戏工业级内容生产,为游戏美术设...
- 腾讯混元游戏视觉生成平台发布,首个工业级AIGC游戏内容生产引擎
-
IT之家5月20日消息,腾讯混元团队刚刚宣布:腾讯混元游戏视觉生成平台(以下简称“混元游戏”)正式发布。这是首个工业级AIGC游戏内容生产引擎,大幅优化了游戏资产生成与游戏制作流程。据官方...
- 巫师3:狂猎 游戏壁纸图片
-
点个“关注”,我们一起看世界。...
- 【防诈宣传】检察官提醒您:警惕高考“分数游戏”,守护学子未来梦想!
-
亲爱的考生和家长们:随着高考的结束,相信每一位考生都期待着那激动人心的成绩公布时刻。然而,在这个特殊的时期,一些不法分子也趁机而动,利用考生和家长们的急切心理,炮制出各种“提前查分”“花钱改分”“低分...
- 让轻薄本也能玩转3A游戏和AI 当前主流核显到底什么水平
-
集成显卡和入门级独立显卡在过去的一段时间内可以说是拼的你死我活,但随着英特尔锐炬、AMDRadeonM的出现,以N卡为主的入门级独显被逐渐淘汰掉,其中最知名的莫过于MX350、MX450等,而现在...
- 通俗解读段位六合一 -游戏资讯-和平精英
-
我来个逗啊~新赛季冲战神的策略和玩法变了,刚刚和平精英官方发布了最新的段位和加分模式,很多同学们看的是一头雾水,今天学友哥一分钟帮大伙们通俗解读段位新赛季段位六合一,新加分机制和匹配模式对玩家的影响;...
- 永久死亡游戏哪些人气高 最新永久死亡游戏排行榜
-
探索游戏世界最炙手可热的秘境!最新永久死亡游戏排行榜揭示玩家心水之选。从生存挑战到硬核策略,这些人气之作带你领略不死不休的冒险体验。一窥究竟,哪款游戏能在生死边缘激起你的无尽热情?继续阅读,揭晓年度最...
- 第三十一篇 七巧板游戏
-
昨晚和爱人聊天,她说到他们学校培训部门开发了适合他们集团特色的七巧板拓展训练,让我回想起我们曾经也在一次拓展训练中进行过一次七巧板游戏,给我留下了极深印象,特别是随着这几年的复盘,我认为它超越我曾经参...
- 玩家愤怒源于期待落差!诛仙世界是创新还是换皮?
-
嘿,亲爱的小伙伴们,诛仙世界,你玩了吗?就算没玩过,这几天,我想你应该也听过这游戏的名字,无论是夸的还是喷的......好吧,大多数都是喷的。但不管怎么看,作为2024年问世的唯一一款纯PC端游,在这...
- 从3岁到99岁都能玩!扑克牌「天花板级」玩法攻略,速藏!
-
扑克牌的玩法种类繁多,不同地区和文化衍生出各具特色的规则。以下是一些常见且有趣的扑克玩法分类整理,供你参考:一、经典传统玩法1.斗地主-3人游戏,1位地主对抗2位农民,通过出牌速度和策略取胜。2....
- 小学成绩到底有没有实际意义?别让分数蒙住你的眼
-
家人们!刷到这条先别划走!今天咱必须唠唠这个让无数家长抓头发、学生心里打鼓的世纪难题——小学成绩到底有没有实际意义?有人拍着胸脯说“小学那点知识,到中学努努力就补回来了”,也有人愁得直叹气“孩子小学成...
- 游戏玩家吵了10年的问题,终于被我们测出了答案!
-
友友们大家来啦!今天来和大家一起分享精彩话题老规矩先点赞再看文!1.4090这显卡,贵得我心疼,但专业领域它就是王!2.3060和A3000L,比比看,谁才是视频编辑的王者?3.游戏画质这么高,黑...
- 3分钟玩转国服新实装的职阶星图分值系统 建议无脑强化狂阶打手
-
fgo国服这次的系统大规模的更新,引入了全新的系统,也就是所谓的职阶星图分值系统。对于这个系统,估计不少玩家没整明白到底是怎么回事,这次就来手把手的说明和解释一下,方便玩转这个新系统,包括该系统带来的...
- 一周热门
- 最近发表
- 标签列表
-
- HTML 简介 (30)
- HTML 响应式设计 (31)
- HTML URL 编码 (32)
- HTML Web 服务器 (31)
- HTML 表单属性 (32)
- HTML 音频 (31)
- HTML5 支持 (33)
- HTML API (36)
- HTML 总结 (32)
- HTML 全局属性 (32)
- HTML 事件 (31)
- HTML 画布 (32)
- HTTP 方法 (30)
- 键盘快捷键 (30)
- CSS 语法 (35)
- CSS 选择器 (30)
- CSS 轮廓宽度 (31)
- CSS 谷歌字体 (33)
- CSS 链接 (31)
- CSS 定位 (31)
- CSS 图片库 (32)
- CSS 图像精灵 (31)
- SVG 文本 (32)
- 时钟启动 (33)
- HTML 游戏 (34)