Clojure多方法:定义、应用与使用场景
立即解锁
发布时间: 2025-08-19 02:29:24 阅读量: 11 订阅数: 3 

### Clojure 多方法:定义、应用与使用场景
#### 1. 定义多方法
在 Clojure 中,定义多方法可以使用 `defmulti` 函数,其基本语法如下:
```clojure
(defmulti name dispatch-fn)
```
其中,`name` 是新多方法的名称,Clojure 会将 `dispatch-fn` 应用于方法参数,以选择多方法的特定实现。
以 `my-print` 为例,它接受一个参数,即要打印的内容,我们希望根据该参数的类型选择特定的实现。因此,`dispatch-fn` 需要是一个接受一个参数并返回该参数类型的函数。Clojure 内置的 `class` 函数正好符合这个要求,使用 `class` 函数创建 `my-print` 多方法的代码如下:
```clojure
; src/examples/multimethods.clj
(defmulti my-print class)
```
此时,我们只是描述了多方法如何选择特定的方法,但还没有实际的具体方法。因此,尝试调用 `my-print` 会失败:
```clojure
(my-println "foo")
; -> java.lang.IllegalArgumentException:
; No method for dispatch value
```
要为 `my-println` 添加特定的方法实现,可以使用 `defmethod`:
```clojure
(defmethod name dispatch-val & fn-tail)
```
`name` 是多方法的名称,`dispatch-val` 是调度值,Clojure 会将 `defmulti` 的调度函数的结果与 `dispatch-val` 进行匹配,以选择方法,`fn-tail` 包含参数和主体形式,就像普通函数一样。
创建一个匹配字符串的 `my-print` 实现:
```clojure
; src/examples/multimethods.clj
(defmethod my-print String [s]
(.write *out* s))
```
现在,使用字符串参数调用 `my-println`:
```clojure
(my-println "stu")
; | stu
; -> nil
```
接着,创建一个匹配 `nil` 的 `my-print` 实现:
```clojure
; src/examples/multimethods.clj
(defmethod my-print nil [s]
(.write *out* "nil"))
```
这样,我们就解决了之前提到的问题。每个 `my-println` 的实现都是独立的,而不是合并在一个大的 `cond` 中。多方法的方法可以位于源代码的任何位置,并且可以随时添加新的方法,而无需修改原始代码。
#### 2. 调度支持继承
多方法调度了解 Java 继承。例如,创建一个处理 `Number` 类型的 `my-print` 实现,通过打印数字的 `toString` 表示:
```clojure
; src/examples/multimethods.clj
(defmethod my-print Number [n]
(.write *out* (.toString n)))
```
使用整数测试 `Number` 实现:
```clojure
(my-println 42)
; | 42
; -> nil
```
虽然 `42` 是 `Integer` 类型,而不是 `Number` 类型,但多方法调度足够智能,知道整数是数字,并能进行匹配。内部使用 `isa?` 函数:
```clojure
(isa? child parent)
```
`isa?` 函数了解 Java 继承,因此知道 `Integer` 是 `Number` 的子类:
```clojure
(isa? Integer Number)
; -> true
```
`isa?` 函数不仅限于继承,其行为可以在运行时动态扩展。
#### 3. 多方法默认值
如果 `my-print` 能有一个回退表示,用于处理未明确定义的任何类型,那就更好了。可以使用 `:default` 作为调度值来处理任何更具体的匹配都不匹配的方法。使用 `:default` 创建一个 `my-println`,打印对象的 Java `toString` 值,并用 `#<>` 包裹:
```clojure
; src/examples/multimethods.clj
(defmethod my-print :default [s]
(.write *out* "#<")
(.write *out* (.toString s))
(.write *out* ">"))
```
现在测试 `my-println` 使用默认方法打印随机对象:
```clojure
(my-println (java.sql.Date. 0))
; -> #<1969-12-31>
(my-println (java.util.Random.))
; -> #<java.util.Random@1c398896>
```
如果 `:default` 在你的领域中已经有特定的含义,可以使用以下替代签名创建多方法:
```clojure
(defmulti name dispatch-fn :default default-value)
```
`default-value` 允许你指定自己的默认值。例如,将其命名为 `:everything-else`:
```clojure
; src/examples/multimethods/default.clj
(defmulti my-print class :default :everything-else)
(defmethod my-print String [s]
(.write *out* s))
(defmethod my-print :everything-else [_]
(.write *out* "Not implemented yet..."))
```
任何不匹配其他更具体调度值的调度值现在都将与 `:everything-else` 匹配。
#### 4. 超越简单调度
Clojure 的 `print` 函数将各种“类序列”的东西打印为列表。如果希望 `my-print` 也能实现类似的功能,可以添加一个基于 Java 继承层次结构中较高的集合接口(如 `Collection`)进行调度的方法:
```clojure
; src/examples/multimethods.clj
(require '[clojure.string :as str])
(defmethod my-print java.util.Collection [c]
(.write *out* "(")
(.write *out* (str/join " " c))
(.write *out* ")"))
```
现在,尝试各种序列,看看它们是否能得到良好的打印表示:
```clojure
(my-println (take 6 (cycle [1 2 3])))
; | (1 2 3 1 2 3)
; -> nil
(my-println [1 2 3])
; | (1 2 3)
; -> nil
```
由于向量的打印使用圆括号,而不是其字面方括号语法,因此添加另一个处理向量的 `my-print` 方法:
```clojure
; src/examples/multimethods.clj
(defmethod my-print clojure.lang.IPersistentVector [c]
(.write *out* "[")
(.write *out* (str/join " " c))
(.write *out* "]"))
```
然而,这会导致问题。打印向量现在会抛出异常:
```clojure
(my-println [1 2 3])
; -> java.lang.IllegalArgumentException: Multiple methods match
; dispatch value: class clojure.lang.LazilyPersistentVector ->
; interface clojure.lang.IPersistentVector and
; interface java.util.Collection,
; and neither is preferred
```
问题在于,对于向量,现在有两个调度值匹配:`Coll
0
0
复制全文
相关推荐










