你的函数有副作用。根据*some-global-var* 的当前值,使用相同的输入调用它们两次可能会给出不同的返回值。这使得事情难以测试和推理,尤其是当您有多个这些全局变量在浮动时。
调用您的函数的人可能甚至不知道您的函数取决于全局 var 的值,而无需检查源代码。如果他们忘记初始化全局变量怎么办?很容易忘记。如果您有两组代码都试图使用依赖于这些全局变量的库怎么办?除非您使用binding,否则它们可能会互相踩踏。每次从 ref 访问数据时,您还会增加开销。
如果您编写代码没有副作用,这些问题就会消失。一个函数独立存在。它很容易测试:传递一些输入,检查输出,它们总是相同的。很容易看出函数依赖于哪些输入:它们都在参数列表中。现在你的代码是线程安全的。而且可能跑得更快。
如果您习惯于“改变一堆对象/内存”的编程风格,那么以这种方式思考代码会很棘手,但是一旦您掌握了它的窍门,以这种方式组织您的程序就变得相对简单了.您的代码通常与相同代码的全局变异版本一样简单或更简单。
这是一个非常人为的例子:
(def *address-book* (ref {}))
(defn add [name addr]
(dosync (alter *address-book* assoc name addr)))
(defn report []
(doseq [[name addr] @*address-book*]
(println name ":" addr)))
(defn do-some-stuff []
(add "Brian" "123 Bovine University Blvd.")
(add "Roger" "456 Main St.")
(report))
孤立地查看do-some-stuff,它到底在做什么?有很多事情是隐含地发生的。沿着这条路走下去就是意大利面。可以说是更好的版本:
(defn make-address-book [] {})
(defn add [addr-book name addr]
(assoc addr-book name addr))
(defn report [addr-book]
(doseq [[name addr] addr-book]
(println name ":" addr)))
(defn do-some-stuff []
(let [addr-book (make-address-book)]
(-> addr-book
(add "Brian" "123 Bovine University Blvd.")
(add "Roger" "456 Main St.")
(report))))
现在很清楚do-some-stuff 在做什么,即使是孤立的。您可以随心所欲地使用任意数量的通讯簿。多个线程可以有自己的。您可以安全地从多个命名空间使用此代码。您不能忘记初始化通讯簿,因为您将它作为参数传递。您可以轻松地测试report:只需将所需的“模拟”地址簿传入并查看它打印的内容。您不必关心任何全局状态或任何事情,只需要您目前正在测试的函数。
如果您不需要从多个线程协调对数据结构的更新,通常不需要使用 refs 或全局变量。