你的函数有副作用。根据 的当前值,使用相同的输入调用它们两次可能会给出不同的返回值*some-global-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 或全局变量。