zig-skills/references/std-array-hash-map.md

232 lines
5.9 KiB
Markdown

# Array Hash Map Migration (Zig 0.16.0)
Primary release-note source: https://ziglang.org/download/0.16.0/release-notes.html
Zig 0.16 removed the managed array hash map aliases:
- `std.ArrayHashMap` removed.
- `std.AutoArrayHashMap` removed.
- `std.StringArrayHashMap` removed.
- The unmanaged root aliases still exist but are deprecated; prefer `std.array_hash_map.Auto`, `.String`, and `.Custom`.
All examples below use the Zig 0.16 unmanaged API: initialize with `.empty`, pass an allocator to operations that may allocate, and pass it again to `deinit`/`clearAndFree`.
A hash map that preserves insertion order and stores keys/values in contiguous arrays. Combines hash table lookup with array-like iteration.
## When to Use
- Need deterministic iteration order (insertion order)
- Need array-style access to keys/values
- JSON object preservation
- When iteration performance matters more than removal performance
## Variants
| Type | Description |
|------|-------------|
| `std.array_hash_map.Auto(K, V)` | Auto-hashing for common key types |
| `std.array_hash_map.Custom(K, V, Ctx, store_hash)` | Custom hash/equal context |
| `std.array_hash_map.String(V)` | String keys |
## Basic Usage
```zig
const std = @import("std");
var map: std.array_hash_map.Auto(u32, []const u8) = .empty;
defer map.deinit(allocator);
// Insert
try map.put(allocator, 1, "one");
try map.put(allocator, 2, "two");
try map.put(allocator, 3, "three");
// Lookup
if (map.get(2)) |value| {
std.debug.print("2 = {s}\n", .{value});
}
// Check existence
if (map.contains(1)) {
// key exists
}
```
## Insertion Order Preserved
```zig
try map.put(allocator, 10, "ten");
try map.put(allocator, 5, "five");
try map.put(allocator, 15, "fifteen");
// Iteration is in insertion order: 10, 5, 15
var it = map.iterator();
while (it.next()) |entry| {
std.debug.print("{}: {s}\n", .{ entry.key_ptr.*, entry.value_ptr.* });
}
```
## Array Access
```zig
// Direct access to underlying arrays
const keys = map.keys(); // []K slice of all keys
const values = map.values(); // []V slice of all values
// Access by index
for (keys, values) |k, v| {
std.debug.print("{}: {s}\n", .{ k, v });
}
```
## Removal (Two Options)
```zig
// O(1) removal - swaps with last element, changes order
_ = map.swapRemove(key);
// O(n) removal - shifts elements, preserves order
_ = map.orderedRemove(key);
// Fetch and remove
if (map.fetchSwapRemove(key)) |kv| {
std.debug.print("removed {}: {s}\n", .{ kv.key, kv.value });
}
```
## Get or Put
```zig
// Get existing or insert new
const result = try map.getOrPut(allocator, key);
if (!result.found_existing) {
result.value_ptr.* = "new_value";
}
// Get or put with default value
const result2 = try map.getOrPutValue(allocator, key, "default");
```
## Index-Based Operations
```zig
// Get index of key
if (map.getIndex(key)) |idx| {
// Remove by index
map.swapRemoveAt(idx);
// or
map.orderedRemoveAt(idx);
}
```
## Capacity Management
```zig
try map.ensureTotalCapacity(allocator, 100);
try map.ensureUnusedCapacity(allocator, 10);
const cap = map.capacity();
const len = map.count();
map.clearRetainingCapacity();
map.clearAndFree(allocator);
```
## String Keys
```zig
var map: std.array_hash_map.String(i32) = .empty;
defer map.deinit(allocator);
try map.put(allocator, "apple", 1);
try map.put(allocator, "banana", 2);
// Keys are stored by reference, not copied
// Make sure string lifetime exceeds map usage
```
## Custom Context
```zig
const CaseInsensitiveContext = struct {
pub fn hash(_: @This(), key: []const u8) u32 {
var h: u32 = 0;
for (key) |c| {
h = h *% 31 +% std.ascii.toLower(c);
}
return h;
}
pub fn eql(_: @This(), a: []const u8, b: []const u8, _: usize) bool {
return std.ascii.eqlIgnoreCase(a, b);
}
};
var map: std.array_hash_map.Custom(
[]const u8,
i32,
CaseInsensitiveContext,
true, // store_hash for better performance
) = .empty;
defer map.deinit(allocator);
try map.putContext(allocator, "Hello", 1, .{});
_ = map.getContext("HELLO", .{}); // finds it!
```
## Complete Example: Word Counter
```zig
const std = @import("std");
pub fn main() !void {
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var counts: std.array_hash_map.String(u32) = .empty;
defer counts.deinit(allocator);
const words = [_][]const u8{ "apple", "banana", "apple", "cherry", "banana", "apple" };
for (words) |word| {
const result = try counts.getOrPut(allocator, word);
if (result.found_existing) {
result.value_ptr.* += 1;
} else {
result.value_ptr.* = 1;
}
}
// Print in insertion order
var it = counts.iterator();
while (it.next()) |entry| {
std.debug.print("{s}: {}\n", .{ entry.key_ptr.*, entry.value_ptr.* });
}
// Output (insertion order):
// apple: 3
// banana: 2
// cherry: 1
}
```
## Comparison with HashMap
| Feature | HashMap | ArrayHashMap |
|---------|---------|--------------|
| Lookup | O(1) | O(1) |
| Insert | O(1) amortized | O(1) amortized |
| swapRemove | O(1) | O(1) |
| orderedRemove | N/A | O(n) |
| Iteration order | Undefined | Insertion order |
| Key/value arrays | No | Yes |
| Sequential iteration storage | Not exposed as direct key/value arrays | Direct key/value arrays |
## Notes
- Iteration order equals insertion order
- `swapRemove` is O(1) but changes order
- `orderedRemove` preserves order but is O(n)
- `store_hash=true` trades memory for avoiding some repeated hash work; benchmark it with the actual key/context workload
- Keys/values are stored in `MultiArrayList` (cache-friendly)
- Any modification invalidates iterators. Growth/rehash can invalidate key/value pointers; consult the API's pointer-locking rules rather than assuming a capacity reservation guarantees stability across all mutations.