Skip to content

向哈希表添加值

哈希表是 Common Lisp 中用于存储键值对的基本且高效的数据结构。它们为查找、插入和删除操作提供了接近常数时间(constant-time)的性能。本指南涵盖了在现代 Lisp 项目中创建、操作和有效使用哈希表。

你可以使用 make-hash-table 创建哈希表。一个关键参数是 :test,它指定了用于比较键的函数。:test 的选择对于正确性至关重要。

  • 'eq:比较两个对象在内存中是否相同。用于符号或你确认为相同(identical)的对象。
  • 'eql(默认):使用 eq 比较对象,但也适用于相同的数字和字符。
  • 'equal:比较两个对象是否具有相似的结构和内容。用于字符串键。
  • 'equalp:equal 的更宽松版本,忽略字符串中的大小写并比较数字类型。
;; 用于符号键的哈希表(默认 :test 为 'eql)
(defvar *user-ids* (make-hash-table))
;; 用于字符串键的哈希表(最佳实践是 :test 'equal)
(defvar *user-prefs* (make-hash-table :test 'equal))

在哈希表中添加或更新值的标准方法是使用 setf 配合 gethash。如果键已存在,则其值会被更新;否则,将创建新的键值对。

;; gethash 检索键的值。如果键不存在,则返回 nil。
;; setf 修改位置,将新值与键关联。
(setf (gethash 'user-001 *user-ids*) 1024)
(setf (gethash "username" *user-prefs*) "alex")
(setf (gethash "theme" *user-prefs*) "dark")

gethash 用于检索值。它返回两个值:找到的值(如果未找到则为 nil),以及一个布尔值 t 或 nil,指示键是否存在。检查第二个返回值是区分存储值为 nil 和键不存在的唯一可靠方法。

;; 简单检索
(gethash 'user-001 *user-ids*) ;=> 1024, t
;; 检索不存在的键
(gethash 'user-999 *user-ids*) ;=> NIL, NIL
;; 正确处理两个返回值
(multiple-value-bind (value foundp) (gethash "theme" *user-prefs*)
(if foundp
(format t "Theme is: ~a~%" value)
(format t "No theme set.~%")))

使用 remhash 从哈希表中移除键值对。如果找到了键并成功移除,则返回 t,否则返回 nil。

(remhash 'user-001 *user-ids*) ;=> t

让我们构建一个函数来计算文本中的单词频率,这是哈希表的一个经典用例。

(defun word-frequency (text)
"统计文本字符串中每个单词的频率。"
(let ((counts (make-hash-table :test 'equalp)) ; 'equalp 用于不区分大小写
(words (str:words (str:downcase text)))) ; 使用 'str' 库
(dolist (word words)
;; (incf (gethash word counts 0)) 是一种简洁的方式来增加计数。
;; 如果键不存在,(gethash ...) 返回默认值 0,然后将其递增到 1 并存储。
(incf (gethash word counts 0)))
counts))
;; 示例用法(需要 ql:quickload "str")
(let* ((text "To be or not to be, that is the question.")
(freqs (word-frequency text)))
(maphash (lambda (key value)
(format t "~a: ~a~%" key value))
freqs))
QUESTION: 1
THE: 1
IS: 1
THAT: 1
BE: 2
NOT: 1
OR: 1
TO: 2
  • 错误的 :test 函数:最常见的错误。对字符串键使用默认的 'eql 将会失败,因为两个看起来相同的字符串不一定是 eql 的。始终对字符串键使用 'equal 或 'equalp。
  • 假设 gethash 只返回一个值:忘记检查第二个返回值 (foundp) 可能会导致当键的值合法地为 nil 时出现错误。