Sets in Dart — Hash Tables, Performance, and When to Use Them
A deep dive into Dart Sets: how hash tables achieve O(1) lookup, when to choose Set over List, the different Set implementations, and AI prompts to help you pick the right collection.
Why Sets exist — the uniqueness guarantee
Lists are great for ordered collections, but what if you need to ensure no duplicates? You could loop through a List checking for existing items before adding — but that's O(n) for every insertion. Sets solve this elegantly.
A Set is a collection of unique items with O(1) membership testing. The magic comes from hash tables — instead of scanning every element, Sets compute a hash code and jump directly to where an item would be stored.
// List approach — O(n) contains check
var items = <String>[];
if (!items.contains('apple')) { // O(n)
items.add('apple');
}
// Set approach — O(1) automatic deduplication
var items = <String>{};
items.add('apple'); // O(1), duplicates ignored
items.add('apple'); // No effect, still just one apple
When to use Set over List:
- You need uniqueness (tags, visited nodes, seen IDs)
- You frequently check membership (
contains)- You perform set operations (union, intersection, difference)
- Order doesn't matter (or insertion order is fine)
How hash tables achieve O(1) lookup
The key to Set's performance is the hash table. Instead of searching through items sequentially, a hash table computes where an item should live and checks that location directly.
The process:
1. Compute hashCode for the item
2. Apply modulo to get a bucket index: hashCode % bucketCount
3. Check/store at that bucket (handle collisions if needed)
Collisions happen when two different items hash to the same bucket. Dart uses open addressing — it probes the next slot until it finds an empty one. This keeps operations O(1) on average.
Set operations — union, intersection, difference
Sets aren't just about uniqueness — they support powerful mathematical operations that would be tedious to implement with Lists.
Real-world uses:
- union: Combine permissions from multiple roles
- intersection: Find common friends, shared tags
- difference: Find items to add/remove when syncing
Set implementations — LinkedHashSet, HashSet, SplayTreeSet
Dart offers three Set implementations, each with different tradeoffs. The default might surprise you.
import 'dart:collection';
// Default — preserves insertion order
var linked = {'c', 'a', 'b'}; // iterates: c, a, b
// Pure hash — no order guarantee
var hash = HashSet<String>()..addAll(['c', 'a', 'b']);
// Sorted — always in sorted order
var sorted = SplayTreeSet<String>()..addAll(['c', 'a', 'b']);
print(sorted); // {a, b, c}
Custom objects in Sets — hashCode and equality
For Sets to work correctly with custom objects, you must implement hashCode and == properly. The rule: equal objects must have equal hash codes.
class User {
final String id;
final String name;
User(this.id, this.name);
// Two users are equal if they have the same ID
@override
bool operator ==(Object other) =>
other is User && other.id == id;
// Hash code must be consistent with equality
@override
int get hashCode => id.hashCode;
}
void main() {
var users = <User>{};
users.add(User('123', 'Alice'));
users.add(User('123', 'Alice Updated')); // Same ID!
print(users.length); // 1 — deduplication works
}
Common mistakes:
- Using mutable fields in
hashCode (breaks Set when object changes)- Forgetting to override both
== and hashCode- Making
hashCode return a constant (kills performance)
Best practice: Use
@immutable classes or package:equatable to generate correct implementations automatically.
Performance patterns — when Set beats List
Let's look at concrete scenarios where switching from List to Set makes a dramatic difference.
Pattern 1: Membership testing in loops
// BAD: O(n) per check = O(n²) total
var visited = <String>[];
for (var node in nodes) {
if (!visited.contains(node.id)) { // O(n)
process(node);
visited.add(node.id);
}
}
// GOOD: O(1) per check = O(n) total
var visited = <String>{};
for (var node in nodes) {
if (visited.add(node.id)) { // O(1), returns false if exists
process(node);
}
}
Pattern 2: Removing duplicates
// Convert List to Set and back
var withDupes = ['a', 'b', 'a', 'c', 'b'];
var unique = withDupes.toSet().toList();
// ['a', 'b', 'c'] — order preserved (LinkedHashSet)
Pattern 3: Fast lookup tables
// Checking valid options
const validCodes = {'USD', 'EUR', 'GBP', 'JPY'};
if (validCodes.contains(userInput)) {
// O(1) check
}
Sets and parallel computation
Like Lists, Sets can be processed in Isolates for CPU-intensive operations. The same rules apply: data is copied, not shared.
import 'dart:isolate';
// Compute intersection of two large sets in parallel
Future<Set<int>> parallelIntersection(
Set<int> a,
Set<int> b,
) async {
return await Isolate.run(() => a.intersection(b));
}
void main() async {
var setA = Set.generate(100000, (i) => i);
var setB = Set.generate(100000, (i) => i * 2);
var common = await parallelIntersection(setA, setB);
print('Found ${common.length} common items');
}
When to parallelise Set operations:
- Large set operations (union/intersection/difference on 10k+ items)
- Complex filtering with expensive predicates
- Building Sets from large data sources
When NOT to parallelise:
- Small Sets (overhead exceeds benefit)
- Simple add/remove/contains (already O(1))
AI prompts — choosing the right Set strategy
Use these prompts with AI assistants to help choose optimal Set strategies for your use case.
Prompt 1 — Set vs List decision:
I have a Dart collection that stores [describe your data].
My operations:
- [list operations: add, remove, contains, iterate]
- Need duplicates? [yes/no]
- Need order? [insertion/sorted/none]
- Approximate size: [number of items]
Should I use Set or List? Which Set implementation?
Show me the performance implications of each choice.
Prompt 2 — hashCode and equality review:
Here is my Dart class that I want to use in a Set:
[paste your class]
Review my hashCode and == implementations for:
- Correctness (equal objects have equal hash codes)
- Performance (good hash distribution)
- Safety (no mutable fields in hash)
Suggest improvements or rewrite using best practices.
Prompt 3 — Set operation optimisation:
I need to perform these Set operations in Dart:
[describe operations: union, intersection, difference]
Set sizes: A = [size], B = [size]
Frequency: [how often this runs]
Analyze the complexity and suggest optimisations.
Should I use SplayTreeSet? Parallel processing?
Prompt 4 — Migration from List to Set:
Here is my Dart code that uses List.contains() frequently:
[paste your code]
Refactor this to use Set for O(1) membership testing.
Preserve the existing behaviour and handle any edge
cases (ordering, duplicates that might be intentional).
Sets in Dart
Sets are the unsung heroes of performance optimisation. This episode explores how hash tables achieve O(1) lookup, when to choose Set over List, and how to implement custom equality correctly.
Sets Deep Dive Quiz
7 questions
Test your understanding of Dart Sets, hash tables, and performance