总体来说,您不太经常使用协议、记录和自定义类型。对于大多数应用领域特定逻辑,使用常规的映射和常规函数就足够了。不过,我会尽量解释何时需要使用它们。
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
为了更好地理解,deftype 允许数据封装。一般来说,你不需要在Clojure中使用数据封装,因为默认的数据结构和绑定是不可变的,所以将数据以只读形式暴露给外部没有危险。然而,对于实现数据容器本身,如不可变的数据结构或各种引用类型(如Atom),你需要进行修改来提供空间和时间效率高的实现。在这种情况下,你不应该让外部自由访问数据,因为它们很容易破坏必要的不变性。因此,你希望提供一个抽象接口,例如对于双链表,你可能有add-first、add-last、remove、next、prev等操作。因此,deftype对于将任何形式的数据容器添加到语言中都是有用的,你几乎永远不会自己这样做。
defrecord
当需要创建自定义类型,为了给某些Map添加语义意义时,你会使用defrecord。
例如,如果你想有一个类型为Person的Map,你将使用defrecord。
这只有在你需要代码在运行时根据类型执行不同操作时才有用。例如,有一条代码将接收不同类型的Map,并希望它为每种类型执行不同的操作,同时希望获得更好的性能。
我这样说是有原因的,因为实际上有许多方法可以提供这种动态功能。一种方法是使用原生类型,这是由记录和协议提供的。另一种方法是手动建模类型并使用defmulti。后者更强大,因为你可以将类型表示为你想要的任何东西,类型甚至可以来自数据的结构或值。另一方面,它将更慢。
defrecord只能允许根据函数第一个参数的类型执行不同的操作,记录的结构和值不能决定其类型。这通常被称为命名类型。你的记录之所以是某个类型,是因为你明确地那样命名。另一方面,defmulti允许鸭式类型,即一个Map之所以是某个类型,是因为它的内在结构及其/所包含的值。
defprotocol
当你想创建基于其第一个参数的本地类型的函数时,你会使用defprotocol。例如,一个将调用不同记录并应根据每个不同的记录类型执行不同操作的函数。
defmulti
就像我在简要介绍记录时 解释的那样。如果你想让函数根据第一个参数的类型以及其他参数的类型(或数据的结构/值)执行不同的操作,你应该使用defmulti而不是defprotocol。
多参函数
此外,如果你想根据函数被调用的参数数量执行不同的操作,你可以使用多参函数。