总的来说,你不太可能经常使用协议、记录和自定义类型。对于大多数应用域特定逻辑,使用普通映射和常规函数就足够了。不过,我会尽力解释何时需要使用它们。
TL;DR
使用 deftype 在需要实现需要内部可变性的自定义数据容器,或某种形式的数据封装时。这主要用于非常原始的结构,如数据结构或引用类型。
如果你有一个在运行时调用不同参数的函数,其行为必须特定于所调用的参数。那么你想使用协议或多方法。如果你可以仅基于第一个参数的类型来选择执行的操作,则使用协议;如果不可以,则使用多方法。
如果你有一个函数应该根据 Map 的类型执行不同的操作。比如说,你需要将 Map 标记为表示某个东西,比如人、车、用户等。你有一些将要使用不同类型 Map 的函数,你想要它们根据这种类型选择要做什么,那么可以使用 defrecord。
如果你有一个根据所调用参数的数量需要执行不同操作的函数,使用多参数就足够了。
最后,如果你发现自己写了很多以相同名称开始的函数,但以某种与类型相关的判别器结束,比如:`add-user`,`add-role`,`add-item`,这可能表明你可以用协议或多方法及其单个`add`函数来建模这一点。
deftype
让我从 deftype 开始。你使用 deftype 的频率最低。deftype 允许您实现新的抽象数据类型(ADT)。它用于创建新的数据容器类型。基本上,任何需要封装数据(通常是可变数据)并提供通过一个安全界面访问数据的方式,这个界面以对该类型有意义的方式强制执行底层数据的所有不变性。
例如,如果你需要添加一个新的数据结构,比如你需要实现一个双向链表,那么你将使用 deftype。
现在,您几乎永远不需要使用它,因为大部分有用的数据容器已经为您在核心、互通或作为库中实现了。例如,Java提供了一个双链表:[https://docs.oracle.com/javase/8/docs/api/java/util/LinkedList.html](https://docs.oracle.com/javase/8/docs/api/java/util/LinkedList.html)
为了更好地理解,deftype 允许数据封装。在一般情况下,在 Clojure 中,您不需要数据封装,因为默认的数据结构和绑定是不可变的,因此向外公开数据作为只读是没有危险的。话虽如此,对于实现数据容器本身,例如不可变的数据结构或者像 Atom 这样各种引用类型,您将需要变异来提供空间和时间效率高的实现。在这种情况下,您不希望外界随意触及数据,因为他们很容易破坏所需的不变性。因此,您想要提供一个抽象接口,例如,对于一个双链表,您可能会有 add-first(在前面添加)、add-last(在后面添加)、remove(移除)、next(下一个)、prev(上一个)等。因此,再次强调,deftype 对于将任何形式的数据容器添加到语言中非常有用,您几乎不必自己这样做。
defrecord
当您需要创建一个自定义类型以向某些 Map 添加语义意义时,您会使用 defrecord。实际上,Record 只是一个 Map,但其类型被替换为记录类型名。因此,它不会有 Map 类型,而是有记录名称作为类型。
例如,如果您想要一个类型为 Person 的 Map,您将使用 defrecord。
这仅在实际需要根据类型在运行时执行不同操作的情况下才有用。因此,如果有代码将接收不同类型的 Maps,并且您希望它对每种类型都执行不同的操作,同时您还想获得更好的性能。
我还想说,实际上有许多方法可以提供这种动态功能。一种是用原生类型,由记录和协议提供。另一种是手动建模的类型和 defmulti 的使用。后者更强大,因为您可以用您想要的任何东西来表示类型,类型甚至可以从数据本身的结构或值中推断出来。另一方面,它会更慢。
defrecord 只允许函数基于它们的第一个参数的类型执行不同的事情,并且记录的结构和值不能决定它的类型。这通常被称为名义类型。您的记录是某种类型,因为您明确这样命名了。另一方面,defmulti 允许鸭子类型,即 Map 可以根据其固有的结构及其包含的值具有某种类型。
defprotocol
当您需要函数基于它们第一个参数的本地类型执行不同的事情时,您将使用 defprotocol。例如,一个将使用不同记录并应根据记录类型执行不同操作的区域函数。
defmulti
正如我之前简要介绍记录时所说。如果您想使函数根据第一个参数的类型以及其他参数的类型、数据结构或任何参数的值执行不同的操作,您将使用 defmulti 而不是 defprotocol。
多态性
此外,如果您只想根据函数调用时传入的参数数量执行不同的操作,您可以使用多态函数。