总的来说,您不太经常使用协议、记录和自定义类型。对于大多数应用领域特定逻辑,使用常规图和常规函数就足够了。尽管如此,我仍会尝试解释何时需要使用它们。
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
为了更好地理解,deftype 允许数据封装。通常情况下,在 Clojure 中不需要数据封装,因为默认的数据结构和绑定都是不可变的,所以将数据暴露给外部作为只读不会存在危险。但就实现数据容器本身而言,如不可变数据结构或各种引用类型(如 Atom),你需要通过变异来提供空间和时间的有效实现。在这种情况下,你不应该让外部人员自由地访问数据,因为他们很容易破坏所需的性质。因此,你希望提供抽象接口,例如,对于双向链表,你可能会有 add-first(添加到头部)、add-last(添加到尾部)、remove(移除)、next(下一个)、previous(上一个)等。所以再次强调,deftype 对向语言添加任何形式的数据容器都很有用,而你几乎不需要自己这样做。
defrecord
当你需要一个自定义类型来给 Map 增加语义意义时,你会选择使用 defrecord。实际上,record 只是一个 Map,但它将类型替换为 record 类型名称。因此,它将不是 Map 类型类型,而是记录名称的类型。
如果你想有一个类型为 Person 的 Map,你会使用 defrecord。
这只有在需要根据类型在运行时执行不同的操作时才真正有用。也就是说,如果有一段代码将接收到不同类型的 Maps,并且你希望它针对每种类型执行不同的操作,并且你还希望有更好的性能。
我之所以这样说,是因为实际上有几种方式可以提供这种动态功能。一种是通过使用原生类型,并使用记录和协议提供。另一种是通过手动构建类型和使用 defmulti。后者更强大,因为你可以将类型表示为任何你想要的东西,并且类型甚至可以从中或数据的值本身推断出来。另一方面,它将更慢。
defrecord 只允许函数根据其第一个参数的类型 differently 处理,并且记录的结构和值不能决定其类型。这通常被称为名义类型。你的记录是某种类型,因为你明确地这样命名。另一方面,defmulti 允许使用 duck 类型,即一个 Map 可以根据其内在结构及其包含的值具有某种类型。
defprotocol
当你想要执行不同操作的功能时,这些操作的第一个参数基于其原生类型。例如,一个会以不同的记录调用,并且针对每种不同的记录类型执行不同操作的功能。
defmulti
就像我在简要介绍记录时解释的那样。如果你想让函数根据第一个参数的类型执行不同的操作,而不是因为其他参数的类型,或者因为任意/所有参数中的数据结构,或者因为任意/所有参数的值,你应该使用 defmulti 而不是 defprotocol。
多参数
另外,如果你只希望根据函数调用的参数数量以不同方式执行函数,你可以使用多参数函数。