28

我发现自己最近在 clojure 代码中使用了以下成语。

(def *some-global-var* (ref {}))

(defn get-global-var []
  @*global-var*)

(defn update-global-var [val]
  (dosync (ref-set *global-var* val)))

大多数时候,这甚至不是多线程代码,可能需要 refs 为您提供的事务语义。感觉就像 refs 不仅仅适用于线程代码,而且基本上适用于任何需要不变性的全局。有更好的做法吗?我可以尝试重构代码以仅使用 binding 或 let 但这对于某些应用程序可能会变得特别棘手。

4

2 回答 2

31

当我看到这种模式时,我总是使用原子而不是引用——如果你不需要事务,只需要一个共享的可变存储位置,那么原子似乎是要走的路。

例如,对于键/值对的可变映射,我将使用:

(def state (atom {}))

(defn get-state [key]
  (@state key))

(defn update-state [key val]
  (swap! state assoc key val))
于 2010-07-16T19:39:58.613 回答
25

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

于 2010-07-16T19:42:43.440 回答