user> (= 2 (+ 1 1))
true
user> (= (str "fo" "od") "food")
true
本文档讨论了 Clojure 中等值的概念,包括函数 =
、==
和 identical?
,以及它们与 Java 的 equals
方法的区别。它还对 Clojure 的 hash
进行了描述,以及它与 Java 的 hashCode
的区别。本指南的开头提供了最重要的信息摘要,以便快速参考,然后对细节进行了更广泛的回顾。
除非另有说明,本指南中的信息描述了 Clojure 1.10.0 的行为。
当比较表示相同值的不可变值,或比较相同对象的可变对象时,Clojure 的 =
为真。为方便起见,=
在用于比较 Java 集合彼此之间,或与 Clojure 的不可变集合之间时,如果它们的内容相等,也返回 true。但是,如果您使用非 Clojure 集合,则存在一些重要的注意事项。
当使用两个不可变标量值调用 Clojure 的 =
时,如果满足以下条件,则返回 true:
两个参数都是 nil
、true
、false
、相同的字符或相同的字符串(即相同的字符序列)。
两个参数都是符号,或者都是关键字,且具有相同的命名空间和名称。
两个参数都是同一“类别”中的数字,且数值相同,其中类别是以下之一:
整数或有理数
浮点数(浮点数或双精度数)
当使用两个集合调用 Clojure 的 =
时,如果满足以下条件,则返回 true:
两个参数都是顺序的(序列、列表、向量、队列或实现 java.util.List
的 Java 集合),且元素按相同顺序使用 =
进行比较。
两个参数都是集合(包括实现 java.util.Set
的 Java 集合),且元素使用 =
进行比较,忽略顺序。
两个参数都是映射(包括实现 java.util.Map
的 Java 映射),且具有 =
的键和值,忽略条目顺序。
两个参数都是使用 defrecord
创建的记录,且具有 =
的键和值,忽略顺序,并且它们具有相同的类型。当比较记录和映射时,=
返回 false
,而不管它们的键和值如何,因为它们没有相同的类型。
当使用两个可变 Clojure 对象(即 var、ref、atom 或 agent)或两个“挂起”的 Clojure 对象(即 future、promise 或 delay)调用 Clojure 的 =
时,如果满足以下条件,则返回 true:
两个参数是同一个对象,即 (identical? x y)
为真。
对于所有其他类型
两个参数都是使用 deftype
定义的相同类型。调用类型的 equiv
方法,其返回值成为 (= x y)
的值。
对于其他类型,Java 的 x.equals(y)
为真。
Clojure 的 ==
特别用于数值。
==
可用于不同数字类别之间的数字(例如整数 0
和浮点数 0.0
)。
如果任何要比较的值不是数字,则会抛出异常。
如果您使用超过两个参数调用 =
或 ==
,则当所有连续对都为 =
或 ==
时,结果为 true。hash
与 =
一致,但以下例外情况除外。
例外情况或可能令人意外的情况
在基于 Clojure 哈希的集合(作为映射键或集合元素)中使用非 Clojure 集合时,由于哈希行为的差异,它将不会与具有 Clojure 对应项的类似集合相等。(参见 等值和哈希 和 CLJ-1372)
当使用 =
比较集合时,集合中的数字也使用 =
进行比较,因此以上三个数字类别都很重要。
“非数字”值 ##NaN
、Float/NaN
和 Double/NaN
不等于任何东西,甚至不等于自身。建议:避免在 Clojure 数据结构中包含 ##NaN
,在这些数据结构中您希望使用 =
将它们彼此进行比较,并有时获得 true
作为结果。
0.0 等于 -0.0
Clojure 正则表达式,例如 #"a.*bc"
,是使用 Java java.util.regex.Pattern
对象实现的,Java 在两个 Pattern
对象上使用 equals
返回 (identical? re1 re2)
,即使它们被记录为不可变对象。因此 (= #"abc" #"abc")
返回 false,并且 =
仅在两个正则表达式碰巧是内存中相同的同一对象时才返回 true。建议:避免在 Clojure 数据结构中使用正则表达式实例,在这些数据结构中您希望使用 =
将它们彼此进行比较,并获得 true
作为结果,即使正则表达式实例不是同一对象。如果您觉得有必要,可以考虑先将它们转换为字符串,例如 (str #"abc")
→ "abc"
(参见 CLJ-1182)
Clojure 持久队列永远不会等于实现 java.util.List
的 Java 集合,即使它们具有按相同顺序使用 =
比较的元素也是如此(参见 CLJ-1059)
使用 =
比较排序映射与另一个映射,其中当比较它们的键彼此之间时,compare
抛出异常,因为它们具有不同的类型(例如关键字与数字),在某些情况下会抛出异常(参见 CLJ-2325)
在大多数情况下,hash
与 =
一致,这意味着:如果 (= x y)
,则 (= (hash x) (hash y))
。对于任何不满足此条件的值或对象,基于 Clojure 哈希的集合将无法正确查找或删除这些项目,即对于以这些项目作为元素的基于哈希的集合,或以这些项目作为键的基于哈希的映射。
Clojure 中的等值最常使用 =
进行测试。
user> (= 2 (+ 1 1))
true
user> (= (str "fo" "od") "food")
true
与 Java 的 equals
方法不同,Clojure 的 =
对许多类型不同的值返回 true。
user> (= (float 314.0) (double 314.0))
true
user> (= 3 3N)
true
=
不总是在两个数字具有相同数值时返回 true。
user> (= 2 2.0)
false
如果您想跨不同数字类别测试数值等值,请使用 ==
。有关详细信息,请参阅下面的 数字 部分。
具有相同顺序的相同元素的顺序集合(序列、向量、列表和队列)是相等的
user> (range 3)
(0 1 2)
user> (= [0 1 2] (range 3))
true
user> (= [0 1 2] '(0 1 2))
true
;; not = because different order
user> (= [0 1 2] [0 2 1])
false
;; not = because different number of elements
user> (= [0 1] [0 1 2])
false
;; not = because 2 and 2.0 are not =
user> (= '(0 1 2) '(0 1 2.0))
false
如果两个集合具有相同的元素,则它们是相等的。集合通常是无序的,但即使对于排序集合,在比较等值时也不会考虑排序顺序。
user> (def s1 #{1999 2001 3001})
#'user/s1
user> s1
#{2001 1999 3001}
user> (def s2 (sorted-set 1999 2001 3001))
#'user/s2
user> s2
#{1999 2001 3001}
user> (= s1 s2)
true
如果两个映射具有相同的键集,并且每个键在每个映射中都映射到相同的值,则它们是相等的。与集合一样,映射是无序的,并且对于排序映射,不会考虑排序顺序。
user> (def m1 (sorted-map-by > 3 -7 5 10 15 20))
#'user/m1
user> (def m2 {3 -7, 5 10, 15 20})
#'user/m2
user> m1
{15 20, 5 10, 3 -7}
user> m2
{3 -7, 5 10, 15 20}
user> (= m1 m2)
true
请注意,虽然向量是有索引的,并且具有一些类似映射的特性,但映射和向量在 Clojure 中永远不会比较为 =
user> (def v1 ["a" "b" "c"])
#'user/v1
user> (def m1 {0 "a" 1 "b" 2 "c"})
#'user/m1
user> (v1 0)
"a"
user> (m1 0)
"a"
user> (= v1 m1)
false
与 Clojure 集合关联的任何元数据在比较它们时都会被忽略。
user> (def s1 (with-meta #{1 2 3} {:key1 "set 1"}))
#'user/s1
user> (def s2 (with-meta #{1 2 3} {:key1 "set 2 here"}))
#'user/s2
user> (binding [*print-meta* true] (pr-str s1))
"^{:key1 \"set 1\"} #{1 2 3}"
user> (binding [*print-meta* true] (pr-str s2))
"^{:key1 \"set 2 here\"} #{1 2 3}"
user> (= s1 s2)
true
user> (= (meta s1) (meta s2))
false
使用 defrecord
创建的记录在许多方面与 Clojure 映射的行为类似。但是,它们仅等于相同类型的其他记录,并且仅在它们具有相同的键和相同的值时才等于。即使它们具有相同的键和值,它们也永远不会等于映射。
当您定义 Clojure 记录时,您这样做是为了创建可以与其他类型区分开来的不同类型——您希望每种类型在 Clojure 协议和多方法中具有自己的行为。
user=> (defrecord MyRec1 [a b])
user.MyRec1
user=> (def r1 (->MyRec1 1 2))
#'user/r1
user=> r1
#user.MyRec1{:a 1, :b 2}
user=> (defrecord MyRec2 [a b])
user.MyRec2
user=> (def r2 (->MyRec2 1 2))
#'user/r2
user=> r2
#user.MyRec2{:a 1, :b 2}
user=> (def m1 {:a 1 :b 2})
#'user/m1
user=> (= r1 r2)
false ; r1 and r2 have different types
user=> (= r1 m1)
false ; r1 and m1 have different types
user=> (into {} r1)
{:a 1, :b 2} ; this is one way to "convert" a record to a map
user=> (= (into {} r1) m1)
true ; the resulting map is = to m1
除了数字和 Clojure 集合之外,Clojure =
的行为与 Java 的 equals
相同。
布尔值和字符在等值方面很简单。
字符串也很简单,但在某些涉及 Unicode 的情况下除外,在这些情况下,由不同 Unicode 字符序列组成的字符串在显示时可能看起来相同,并且在某些应用程序中应被视为相等,即使 =
返回 false。如果您感兴趣,请参阅 Wikipedia 上关于 Unicode 等值 的“规范化”页面。如果您需要执行此操作,则可以使用像 ICU(Java 的 Unicode 国际组件)这样的库来提供帮助。
如果两个符号具有相同的命名空间和符号名称,则它们是相等的。在相同条件下,两个关键字是相等的。Clojure 使关键字的等值测试特别快(简单的指针比较)。它通过关键字类的 intern
方法实现了这一点,该方法保证所有具有相同命名空间和名称的关键字都将返回相同的关键字对象。
仅当类型和数值相同的情况下,Java equals
对两个数字才返回 true。因此,即使对于 Integer 1 和 Long 1,equals
也是 false,因为它们的类型不同。例外:如果两个 BigDecimal 值在数值上相等,但具有不同的刻度,则 Java equals
也是 false,例如 1.50M 和 1.500M 不相等。此行为记录在 BigDecimal 方法 equals
中。
如果“类别”和数值相同,则 Clojure =
为 true。类别是以下之一:
整数或比率,其中整数包括所有 Java 整数类型,例如Byte
、Short
、Integer
、Long
、BigInteger
和clojure.lang.BigInt
,比率用名为clojure.lang.Ratio
的 Java 类型表示。
浮点数:Float
和Double
十进制数:BigDecimal
因此(= (int 1) (long 1))
为真,因为它们属于同一整数类别,但(= 1 1.0)
为假,因为它们属于不同的类别(整数与浮点数)。虽然整数和比率在 Clojure 实现中是独立的类型,但出于=
的目的,它们实际上属于同一类别。如果比率的算术运算结果为整数,则会自动转换为整数。因此,任何类型为 Ratio 的 Clojure 数字都不能等于任何整数,因此当比较比率与整数时,=
始终给出正确的数值答案(false
)。
Clojure 还有==
,它仅用于比较数字。只要=
为真,它就返回真。对于数值相等的数字,即使它们属于不同的类别,它也返回真。因此(= 1 1.0)
为假,但(== 1 1.0)
为真。
您可能想知道,为什么=
对数字有不同的类别?如果=
的行为像==
一样,那么使hash
与=
保持一致将非常困难(甚至不可能)(参见部分相等性和哈希)。想象一下,尝试编写hash
,使其保证对(float 1.5)
、(double 1.5)
、BigDecimal 值 1.50M、1.500M 等以及比率(/ 3 2)
都返回相同的哈希值。
Clojure 使用=
比较用作集合元素或映射键的值是否相等。因此,如果您使用具有数值元素的集合或具有数值键的映射,Clojure 的数值类别就会发挥作用。
请注意,如果您以前没有了解过浮点数的近似特性,那么浮点值可能会以令您惊讶的方式表现。它们通常是近似值,仅仅因为它们是用固定数量的位表示的,因此许多值无法精确表示,必须进行近似(或超出范围)。这对于任何编程语言中的浮点数都是正确的。
user> (def d1 (apply + (repeat 100 0.1)))
#'user/d1
user> d1
9.99999999999998
user> (== d1 10.0)
false
有一个名为数值分析的完整领域专门研究使用数值近似的算法。有一些 Fortran 代码库被使用,因为它们的浮点运算顺序经过精心设计,可以保证其近似答案与精确答案之间的差异。"每个计算机科学家都应该了解的浮点运算"是您想要了解更多详细信息的好读物。
如果您想要至少某些类型问题的精确答案,比率或 BigDecimal 可能适合您的需求。请意识到,如果所需的位数增加(例如,经过多次算术运算后),这些需要可变数量的内存,并且需要更多计算时间。如果您想要 pi 或 2 的平方根的精确值,它们也无济于事。
Clojure 使用底层的 Java 双精度浮点数(64 位),其表示和行为由标准 IEEE 754 定义。有一个特殊的值NaN
(“非数字”),它甚至不等于自身。Clojure 将此值表示为符号值##NaN
。
user> (Math/sqrt -1)
##NaN
user> (= ##NaN ##NaN)
false
user> (== ##NaN ##NaN)
false
如果此“值”出现在您的数据中,则会导致一些奇怪的行为。虽然在将##NaN
作为集合元素或映射中的键添加时不会发生错误,但您随后无法搜索并找到它。您也无法使用disj
或dissoc
等函数将其删除。它将正常显示在从包含它的集合创建的序列中。
user> (def s1 #{1.0 2.0 ##NaN})
#'user/s1
user> s1
#{2.0 1.0 ##NaN}
user> (s1 1.0)
1.0
user> (s1 1.5)
nil
user> (s1 ##NaN)
nil ; cannot find ##NaN in a set, because it is not = to itself
user> (disj s1 2.0)
#{1.0 ##NaN}
user> (disj s1 ##NaN)
#{2.0 1.0 ##NaN} ; ##NaN is still in the result!
在许多情况下,包含##NaN
的集合将不等于另一个集合,即使它们看起来应该相等,因为(= ##NaN ##NaN)
为false
user> (= [1 ##NaN] [1 ##NaN])
false
奇怪的是,在某些情况下,包含##NaN
的集合看起来应该相等,而且它们确实相等,因为(identical? ##NaN ##NaN)
为true
user> (def s2 #{##NaN 2.0 1.0})
#'user/s2
user> s2
#{2.0 1.0 ##NaN}
user> (= s1 s2)
true
Java 在其equals
方法中有一个针对浮点值的特殊情况,使##NaN
等于自身。Clojure 的=
和==
则没有。
user> (.equals ##NaN ##NaN)
true
Java 使用equals
比较两个对象是否相等。
Java 有一个方法hashCode
,它与这种相等性概念一致(或者至少在文档中说明它应该如此)。这意味着对于任何两个对象x
和y
,如果equals
为真,则x.hashCode()
和y.hashCode()
也相等。
此哈希一致性属性使得可以使用hashCode
来实现基于哈希的数据结构,例如内部使用哈希技术的映射和集合。例如,可以使用哈希表来实现集合,并且可以保证具有不同hashCode
值的可以放入不同的哈希桶中,并且不同哈希桶中的对象永远不会彼此相等。
Clojure 使用=
和hash
的原因与此类似。由于 Clojure =
认为比 Java equals
更多的对象对彼此相等,因此 Clojure hash
必须对更多的对象对返回相同的哈希值。例如,无论=
元素的序列是在序列、向量、列表还是队列中,hash
始终返回相同的值
user> (hash ["a" 5 :c])
1698166287
user> (hash (seq ["a" 5 :c]))
1698166287
user> (hash '("a" 5 :c))
1698166287
user> (hash (conj clojure.lang.PersistentQueue/EMPTY "a" 5 :c))
1698166287
但是,由于当比较 Clojure 不可变集合与其非 Clojure 对应物时,hash
与=
不一致,因此混合使用两者会导致不良行为,如下面的示例所示。
user=> (def java-list (java.util.ArrayList. [1 2 3]))
#'user/java-list
user=> (def clj-vec [1 2 3])
#'user/clj-vec
;; They are =, even though they are different classes
user=> (= java-list clj-vec)
true
user=> (class java-list)
java.util.ArrayList
user=> (class clj-vec)
clojure.lang.PersistentVector
;; Their hash values are different, though.
user=> (hash java-list)
30817
user=> (hash clj-vec)
736442005
;; If java-list and clj-vec are put into collections that do not use
;; their hash values, like a vector or array-map, then those
;; collections will be equal, too.
user=> (= [java-list] [clj-vec])
true
user=> (class {java-list 5})
clojure.lang.PersistentArrayMap
user=> (= {java-list 5} {clj-vec 5})
true
user=> (assoc {} java-list 5 clj-vec 3)
{[1 2 3] 3}
;; However, if java-list and clj-vec are put into collections that do
;; use their hash values, like a hash-set, or a key in a hash-map,
;; then those collections will not be equal because of the different
;; hash values.
user=> (class (hash-map java-list 5))
clojure.lang.PersistentHashMap
user=> (= (hash-map java-list 5) (hash-map clj-vec 5))
false ; sorry, not true
user=> (= (hash-set java-list) (hash-set clj-vec))
false ; also not true
user=> (get (hash-map java-list 5) java-list)
5
user=> (get (hash-map java-list 5) clj-vec)
nil ; you were probably hoping for 5
user=> (conj #{} java-list clj-vec)
#{[1 2 3] [1 2 3]} ; you may have been expecting #{[1 2 3]}
user=> (hash-map java-list 5 clj-vec 3)
{[1 2 3] 5, [1 2 3] 3} ; I bet you wanted {[1 2 3] 3} instead
大多数时候,您在 Clojure 中使用映射时,不会指定是否想要数组映射或哈希映射。默认情况下,如果键最多 8 个,则使用数组映射,如果键超过 8 个,则使用哈希映射。Clojure 函数会在您对映射进行操作时为您选择实现。因此,即使您尝试始终如一地使用数组映射,在创建更大的映射时也可能会经常获得哈希映射。
我们不建议尝试避免在 Clojure 中使用基于哈希的集合和映射。它们使用哈希来帮助提高其操作的性能。相反,我们建议避免在 Clojure 集合中使用非 Clojure 集合作为部分。主要是因为大多数此类非 Clojure 集合是可变的,而可变性通常会导致细微的错误。另一个原因是hash
与=
不一致。
类似的行为发生在实现java.util.List
、java.util.Set
和java.util.Map
的 Java 集合以及 Clojure 的hash
与=
不一致的少数几种值类型上。
如果您在任何Clojure 集合中使用不一致的哈希值作为部分,即使是在列表或向量等顺序集合中的元素,这些集合也会彼此之间不一致。这是因为集合的哈希值是通过组合其部分的哈希值来计算的。
您可能想知道为什么hash
对于非 Clojure 集合与=
不一致。非 Clojure 集合在 Clojure 存在之前很久就使用了 Java 的hashCode
方法。当 Clojure 最初开发时,它使用与hashCode
相同的公式从集合元素计算哈希函数。
在 Clojure 1.6.0 发布之前,人们发现当使用小型集合作为集合元素或映射键时,这种对 Clojure 的hash
函数使用hashCode
的方法会导致许多哈希冲突。
例如,想象一个 Clojure 程序,它使用一个映射来表示一个具有 100 行和 100 列的二维网格的内容,该映射的键是范围在 [0, 99] 内的两个数字的向量。此网格中有 10,000 个这样的点,因此映射中有 10,000 个键,但hashCode
仅给出 3,169 个不同的结果。
user=> (def grid-keys (for [x (range 100), y (range 100)]
[x y]))
#'user/grid-keys
user=> (count grid-keys)
10000
user=> (take 5 grid-keys)
([0 0] [0 1] [0 2] [0 3] [0 4])
user=> (take-last 5 grid-keys)
([99 95] [99 96] [99 97] [99 98] [99 99])
user=> (count (group-by #(.hashCode %) grid-keys))
3169
因此,如果映射使用哈希映射的默认 Clojure 实现,则每个哈希桶平均有 10,000 / 3,169 = 3.16 次冲突。
Clojure 开发人员分析了几个备选哈希函数,并选择了一个基于 Murmur3 哈希函数的函数,该函数自 Clojure 1.6.0 以来一直在使用。它还使用与 Java 的hashCode
不同的方式来组合集合中多个元素的哈希值。
当时,Clojure 也可能已经更改了hash
以对非 Clojure 集合使用新技术,但人们认为这样做会显着降低 Java 方法hasheq
的速度,该方法用于实现hash
。请参阅CLJ-1372,了解迄今为止已考虑过的方法,但截至目前,还没有人发现一种具有竞争力的快速方法来实现它。
hash
与=
不一致的其他情况对于一些彼此=
的 Float 和 Double 值,它们的hash
值不一致
user> (= (float 1.0e9) (double 1.0e9))
true
user> (map hash [(float 1.0e9) (double 1.0e9)])
(1315859240 1104006501)
user> (hash-map (float 1.0e9) :float-one (double 1.0e9) :oops)
{1.0E9 :oops, 1.0E9 :float-one}
您可以通过在浮点代码中始终如一地使用其中一种类型来避免 Float 与 Double 哈希不一致。Clojure 将双精度数默认为浮点值,因此这可能是最方便的选择。
请参阅以下项目的代码,了解如何执行此操作以及更多内容的示例。特别是,来自标准 Java 对象的 Java 方法equals
和hashCode
以及 Clojure Java 方法equiv
和hasheq
对于=
和hash
的行为最相关。
org.flatland/ordered,但请注意,它需要更改,以便其自定义有序映射数据结构不等于任何 Clojure 记录:PR #42
Henry Baker 的论文"函数对象的平等权利,或者,事物变化越多,它们就越相同"包含用 Common Lisp 编写的函数EGAL
的代码,该函数是 Clojure 的=
的灵感来源。“深度相等”对不可变值有意义,但对可变对象(除非可变对象是内存中的同一个对象)意义不大,这与编程语言无关。
下面描述了EGAL
和 Clojure 的=
之间的一些差异。这些是关于EGAL
行为的相当深奥的细节,对于理解 Clojure 的=
来说,了解这些细节不是必要的。
当将可变对象与任何其他事物进行比较时,EGAL
被定义为false
,除非该其他事物是内存中的相同可变对象。
为方便起见,Clojure 的=
旨在在某些情况下返回true
,即在将 Clojure 不可变集合与非 Clojure 集合进行比较时。
Java 中没有方法可以确定任意集合是可变的还是不可变的,因此在 Clojure 中无法实现 EGAL
预期的行为,尽管如果 =
在其中一个参数是非 Clojure 集合时始终返回 false
,则可以认为它更接近 EGAL
。
Baker 建议在比较延迟值时,EGAL
应该强制求值(参见“函数对象平等权利”论文中第 3. J 节“延迟值”)。在将延迟序列与另一个顺序事物进行比较时,Clojure 的 =
会强制求值延迟序列,如果遇到非 =
的序列元素则停止。分块序列(例如,由 range
生成)可能会导致求值比该点稍稍进一步,就像 Clojure 中任何导致延迟序列部分求值的事件一样。
Clojure 的 =
在比较延迟、Promise 或 Future 对象时不会对其进行 deref
操作。相反,它通过 identical?
进行比较,因此只有当它们在内存中是同一个对象时才会返回 true
,即使对它们调用 deref
会导致 =
的值。
Baker 详细描述了在比较闭包时,EGAL
在某些情况下如何返回 true
(参见“函数对象平等权利”论文中第 3. D 节“函数和函数闭包的相等性”)。
当给定一个函数或闭包作为参数时,Clojure 的 =
只有在它们彼此 identical?
时才会返回 true
。
Baker 似乎之所以这样定义 EGAL
,是因为在一些 Lisp 家族语言中普遍使用闭包来表示对象,而这些对象可能包含可变状态或不可变值(参见下面的示例)。鉴于 Clojure 有多种其他方法来创建不可变值和可变对象(例如记录、reify、代理、deftype),因此使用闭包来做到这一点并不常见。
(defn make-point [init-x init-y]
(let [x init-x
y init-y]
(fn [msg]
(cond (= msg :get-x) x
(= msg :get-y) y
(= msg :get-both) [x y]
:else nil))))
user=> (def p1 (make-point 5 7))
#'user/p1
user=> (def p2 (make-point -3 4))
#'user/p2
user=> (p1 :get-x)
5
user=> (p2 :get-both)
[-3 4]
user=> (= p1 p2)
false ; We expect this to be false,
; because p1 and p2 have different x, y values
user=> (def p3 (make-point 5 7))
#'user/p3
user=> (= p1 p3)
false ; Baker's EGAL would return true here. Clojure
; = returns false because p1 and p3 are not identical?
原始作者:Andy Fingerhut