Skip to content

Day 29: std.hash模块:哈希函数

欢迎来到第二十九天!哈希(Hashing)是将任意大小的数据映射到一个固定大小的值(哈希值)的过程。这个过程是确定性的:对于相同的输入,总是产生相同的输出。哈希是现代计算中一个极其重要的概念,它是一切 HashMap(哈希表)或 HashSet 数据结构的核心,也广泛用于数据校验、缓存和密码学等领域。今天,我们将探索Zig的 std.hash 模块,学习如何使用它提供的哈希算法来为数据生成哈希值。

std.hash 模块提供了多种哈希算法的实现,它们有不同的特性和适用场景:

  • Adler32, Crc32: 这些是速度非常快的非密码学哈希函数,常用于数据完整性校验(例如在 zlib 和 gzip 压缩格式中)。它们的碰撞率(不同输入产生相同哈希值的概率)相对较高,不适合用作哈希表的键。

  • Fnv1a_32, Fnv1a_64: FNV-1a 算法的32位和64位版本。这是一种简单而快速的哈希算法,在许多应用中表现良好。

  • SipHash: std.HashMap 默认使用的哈希算法。它是一种密码学哈希函数,被设计用来抵抗哈希冲突攻击(Hash-flooding attacks)。这意味着恶意用户很难构造出大量会导致哈希碰撞的输入。它比 FNV-1a 稍慢,但更安全。

std.hash 中的每种算法都遵循一个通用的 Hasher 接口。

  1. 创建一个 Hasher 实例,例如 std.hash.Crc32.init(seed)。
  2. 使用 update(bytes: []const u8) 方法来送入数据。你可以多次调用 update 来增量地计算哈希。
  3. 使用 final() 方法来获取最终的哈希值。
const std = @import("std");
// 创建一个Crc32 hasher
var hasher = std.hash.Crc32.init(0);
hasher.update("hello");
hasher.update(" ");
hasher.update("world");
const hash_value = hasher.final(); // 32位哈希值

对于最常见的场景——哈希一个字符串——std.hash 提供了一个便捷函数 hashString。

const hash = std.hash.SipHash.hashString(0, "my_key");

这个函数在内部为你完成了 init, update, final 的所有步骤。

要将一个自定义结构体用作 HashMap 的键,你需要告诉 HashMap 如何哈希它。这通常通过实现一个 hash 方法来完成,该方法接收一个 *Hasher 并将结构体的所有字段送入其中。

const User = struct {
id: u64,
name: []const u8,
// 为User实现hash逻辑
pub fn hash(self: User, hasher: *std.hash.SipHash) void {
// 将id的字节表示送入hasher
hasher.update(@bytesFromValue(self.id));
// 将name的内容送入hasher
hasher.update(self.name);
}
};

@bytesFromValue 是一个内置函数,它可以获取任何“普通旧数据”(Plain Old Data)类型的字节表示。

让我们看看 std.HashMap 在 put 操作中是如何利用哈希的(这是一个概念性的简化代码)。

// 概念代码
fn put(map: *HashMap, key: Key, value: Value) !void {
// 1. 计算键的哈希值
var hasher = std.hash.SipHash.init(map.seed);
key.hash(&hasher); // 调用键的hash方法
const hash_value = hasher.final();
// 2. 使用哈希值找到存储位置(桶)
const index = hash_value % map.capacity;
// 3. 在该位置存储键和值
// ... (处理哈希碰撞等逻辑)
}

7. 实践练习:为你的 Vec3 实现哈希

Section titled “7. 实践练习:为你的 Vec3 实现哈希”

回到第27天的 Vec3 练习。为你的 Vec3 结构体实现一个 hash 方法。它应该将 x, y, z 三个浮点数字段的字节表示依次送入 hasher。

问:什么是哈希碰撞(Hash Collision)?

答:当两个不同的输入产生了完全相同的哈希值时,就发生了哈希碰撞。碰撞是不可避免的(因为输入空间是无限的,而输出空间是有限的)。哈希表的设计必须能够处理碰撞,通常是通过“链地址法”(在同一个桶中存储一个链表)或“开放寻址法”(如果桶被占用,就去寻找下一个可用的桶)来实现。

问:seed(种子)有什么用?

答:seed 是哈希算法的初始值。使用不同的种子,即使输入相同,产生的哈希值也不同。std.HashMap 在创建时会使用一个随机的种子。这是一种安全措施,可以防止攻击者预先计算好能导致大量哈希碰撞的键来攻击你的哈希表(哈希冲突攻击)。

今天,我们学习了 std.hash 模块以及它在数据处理中的重要作用。我们了解了不同的哈希算法,掌握了使用 Hasher 接口和 hashString 函数来计算哈希值,并学会了如何为我们自己的数据结构实现哈希逻辑。理解哈希是理解现代数据结构(尤其是哈希表)工作原理的关键一步,也是编写高性能、安全应用程序的重要基础。

明天,我们将学习Zig的内置测试框架:std.testing模块,看看如何为我们的代码编写单元测试和基准测试。