总的来说,您不太经常使用协议、记录和自定义类型。使用普通映射和常规函数就能应对大多数应用域特定逻辑。不过,我将解释何时您需要使用它们。
TL;DR(太长不看,即总结)
当您需要实现自定义数据容器,并且在内部需要可变性或某种形式的数据封装时,请使用 deftype。这主要用于非常原始的结构,如数据结构或引用类型。
如果您有一个在运行时以不同方式调用的函数,且其行为必须针对所调用的事物特定化。那么您想要使用协议或多方法。如果您可以根据第一个参数的类型选择要执行的操作,则使用协议;如果不能,则使用多方法。
如果您有一个根据 Map 的类型执行不同操作的函数。例如,您需要将 Map 标记为表示某物,如 Person、Car、User 等。您还有将调用带有多种类型 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,为了提供一个空间和时间效率高的实现,你需要进行突变。在这种情况下,你不应该让外人能够随意接触数据,因为他们很可能会破坏所需的 Doe。因此,你希望提供一个抽象接口,比如对于双链表,你可能会有 add-first(添加第一个元素)、add-last(添加最后一个元素)、remove(移除)、next(下一项)、prev(上一项)等。所以,deftype 对向语言添加任何形式的数据容器都很有用,而你几乎不需要自己这样做。
defrecord
当你需要创建一个自定义类型,以便为某些 Map 添加语义意义时,你会用 defrecord。
比如说你想要一个类型为 Person 的 Map,你会用 defrecord 来实现。
这只是在需要运行时根据类型执行不同操作的代码时才有用。比如,如果有一段代码会接收到不同类型的 Maps,并希望它为每种类型都执行不同的操作,并且希望有更好的性能。
我之所以说“也”,因为实际上有几种方法可以提供这种动态功能。一种可以是通过使用本地类型,record 和 protocols 提供了这一点。另一种是手动模拟类型和使用 defmulti。后者更强大,因为你可以将类型表示为任何你想要的东西,类型甚至可以从数据的结构或值中推断出来。另一方面,它可能会更慢。
defrecord 只允许函数根据它们的第一个参数的类型执行不同的操作,记录的结构和值不能决定它的类型。这通常被称为名义类型。你的记录是某种类型,因为你明确地这样命名了。另一方面,defmulti 允许进行“鸭子类型”实现,即一个 Map 可以是某种类型,因为它的内在结构以及它包含的值。
defprotocol
当你想要根据其第一个参数的本地类型执行不同操作的功能时,你会选择 defprotocol。比如说,一个将用不同记录调用的函数,针对不同的记录类型应该执行不同的操作。
defmulti
正如我在简要介绍 records 时解释的那样。如果你希望一个函数不仅仅根据第一个参数的类型执行不同的操作,而且还根据其他参数的类型、数据中的结构,或者数据中的值执行不同的操作,那么你应该使用 defmulti 而不是 defprotocol。
多元函数
此外,如果你只是想让函数根据它被调用的参数数量执行不同的操作,你只需使用多元函数。