总的来说,您不会经常使用协议、记录和自定义类型。对于大多数应用领域的特定逻辑,使用正常的映射和常规函数就足够了。不过,我还是会尽量解释您何时需要使用它们。
TL;DR(三行或更少阅读)
当您需要实现需要内部可变性的自定义数据容器,或某种形式的数据封装时,请使用 deftype。这主要用于非常基础的结构,如数据结构或引用类型。
如果您的函数在运行时使用不同的事情调用,并且它所执行的操作必须专门针对调用它时所使用的事物。那么,您想要使用协议或多方法。如果可以通过根据第一个参数的类型来选择要做什么,则使用协议,否则,使用多方法。
如果您的函数应该根据 Map 的类型执行不同的操作。比如说,您需要将一个 Map 标记为表示某物,如 Person、Car、User 等。并且您有使用超过一种类型的 Maps 调用的函数,您希望它们根据这种类型来选择要做什么,那么请使用 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。一个记录实际上就是一个 Map,但是它的类型被记录类型名称所替换。所以,而不是有类型 Map,它将具有记录名称作为类型。
比如说,你想要一个类型为 Person 的 Map,你将使用 defrecord 完成。
这只有在需要一份代码在运行时根据类型做不同的事情时才真正有用。例如,如果有一段代码将要接收不同类型的 Maps,并且你希望它对于每个类型都做不同的事情,你还想要更好的性能。
我这么说,因为实际上有很多方法可以提供这种动态功能。一种方法是使用原生类型,由记录和协议提供。另一种方法是手动构建类型和用 defmulti。后者更强大,因为你可以把类型表示成任何你想要的东西,类型甚至可以从数据的结构或值本身推断出来。另一方面,它可能会更慢。
defrecord 仅仅允许函数根据它们第一个参数的类型执行不同的操作,记录的结构和值不能决定其类型。这通常被称为名义类型。你的记录之所以属于某个类型,是因为你明确地给它起了那个名字。另一方面,defmulti 允许鸭子类型,即一个 Map 可以因为其内在的结构以及包含的值而具有某种类型。
defprotocol
当你想要创建基于其第一个参数原生类型的不同的函数时,你会使用 defprotocol。比如说,一个将要被不同的记录调用,并为每种不同的记录类型执行不同操作的函数。
defmulti
像我简要谈到的记录一样。如果你想要一个函数根据第一个参数的类型执行不同的操作,同时还因为其他参数的类型、数据的结构、任何参数的值,或者任何参数的值而执行不同的操作,你应该使用 defmulti 而不是 defprotocol。
多重参数
此外,如果你只是想让一个函数根据它被调用的参数数量做不同的事情,你可以使用多重参数函数。