总的来说,你不太会经常使用协议、记录和自定义类型。使用普通的映射和常规函数就已经足以处理大多数应用程序领域的特定逻辑。尽管如此,我仍会尝试解释在何时需要使用它们。
TL;DR
在使用需要底层可变性的自定义数据容器时,或需要某种形式的数据封装时,请使用 deftype。这主要用于像数据结构或引用类型这样非常原始的结构。
如果你的函数在运行时要用不同的内容调用,且其行为必须针对调用它的内容具体化。那么你想使用协议或多方法。如果你能够根据第一个参数的类型选择要执行的操作,请使用协议;如果不能,请使用多方法。
如果你的函数需要根据映射的类型执行不同的操作。比如说你需要给映射标记为表示某物,如Person,Car,User等。并且你有需要用多种类型的映射调用的函数,你想让它们根据类型选择操作,那么请使用 defrecord。
如果你的函数需要根据被调用的参数个数执行不同的操作,请使用多arity。
最后,如果你发现自己写了大量以相同名称开头但以某种与调用它的类型相关的区分符结尾的函数。比如: 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,只是将其类型替换为record类型名。所以,它不会有Map类型的类型,而是会有record名称作为类型。
假设你想要一个Person类型的Map,你会使用defrecord。
这种情况下的优点在于你需要一段代码在不同的运行时根据类型做不同的事情。例如,如果有代码将接收不同类型的Map,并且你想让它在每种特定类型上有所不同,同时希望获得更好的性能。
我在这里也提到这一点,因为实际上有很多方法可以提供这种动态功能。一种方法是用原生类型,由record和protocol提供。另一种是通过手动建模的类型和defmulti的使用。后者更强大,因为你可以将类型表示为你想要的任何东西,类型甚至可以从中自分数据结构或值的本身推断出来。另一方面,它会更慢。
defrecord只能允许函数根据其第一个参数的类型以及record的结构和值执行不同的操作。这通常被称作名义类型。你的record是指定类型的,因为你是显式命名它的。另一方面,defmulti支持鸭子类型,即Map可以根据其内在的结构和/或包含的值具有特定的类型。
defprotocol
当你想要函数根据其第一个参数的本地类型做不同的事情时,你会使用defprotocol。例如,一个将用不同的records调用的函数,并且应该对每种不同的record类型执行不同的操作。
defmulti
正如我简要讨论record时所解释的,如果你想让一个函数根据第一个参数的类型、其他参数的类型或数据的结构以及任何/所有参数的值做不同的事情,你就应该使用defmulti而不是defprotocol。
多参数
此外,如果你想根据函数被调用的参数数量执行不同的操作,你可以简单地使用多参数函数。