FrozenDictionary and FrozenSet in C#

With the introduction of FrozenDictionary and FrozenSet in .NET 8, developers now have powerful tools to enhance the efficiency of their applications, particularly in scenarios involving immutable collections. This article is an introduction to these data structures, their theoretical background, and practical applications through real-life code examples.

FrozenDictionary and FrozeSet theory

FrozenDictionary and FrozenSet are immutable, highly optimized versions of Dictionary and HashSet, respectively. These data structures are designed for scenarios where a collection is created once and then repeatedly read, but never modified. By sacrificing mutability, they offer significant performance improvements in terms of memory usage and lookup speed. The immutability guarantee allows for internal optimizations, such as perfect hashing, which eliminates collisions and enables faster key lookups. Additionally, these frozen collections are thread-safe by design, eliminating the need for synchronization in multi-threaded environments.

FrozenDictionary: A Real-Life Code Example

Let's consider a scenario where we're building a caching system for a web application that serves information about countries. We'll use FrozenDictionary to store country data, as this information rarely changes and is frequently accessed.

using System.Collections.Frozen;

public class CountryInfoCache{
    // FrozenDictionary to store country information
    private readonly FrozenDictionary<string, CountryInfo> _countryCache;

    public CountryInfoCache(IEnumerable<CountryInfo> countries){
        // Create a FrozenDictionary from the initial country data
        _countryCache = countries.ToFrozenDictionary(c => c.IsoCode, c => c);
    }

    public CountryInfo GetCountryInfo(string isoCode){
        // Efficient lookup using FrozenDictionary
        if (_countryCache.TryGetValue(isoCode, out var countryInfo)){
            return countryInfo;
        }
        throw new KeyNotFoundException($"Country with ISO code {isoCode} not found.");
    }
}

public record CountryInfo(string IsoCode, string Name, long Population);

// ---------------
// USAGE EXAMPLE

var countries = new List<CountryInfo>{
    new("US", "United States", 331002651),
    new("CN", "China", 1439323776),
    new("IN", "India", 1380004385)
};

var cache = new CountryInfoCache(countries);
var usInfo = cache.GetCountryInfo("US");
Console.WriteLine($"Country: {usInfo.Name}, Population: {usInfo.Population}");

In this example, we use FrozenDictionary to create an efficient, immutable cache of country information. The ToFrozenDictionary method is used to create the cache from the initial data. The resulting _countryCache offers fast, thread-safe lookups, making it ideal for a frequently accessed, rarely changing dataset.

FrozenSet: A Real-Life Code Example

Now, let's look at a scenario where we're implementing a spell checker that needs to quickly verify if a word exists in a predefined dictionary.

using System.Collections.Frozen;

public class SpellChecker {
    // FrozenSet to store valid words
    private readonly FrozenSet<string> _dictionary;

    public SpellChecker(IEnumerable<string> words) {
        // Create a FrozenSet from the initial word list
        _dictionary = words.ToFrozenSet(StringComparer.OrdinalIgnoreCase);
    }

    public bool IsWordValid(string word) {
        // Efficient lookup using FrozenSet
        return _dictionary.Contains(word);
    }

    public IEnumerable<string> GetSuggestions(string word) {
        // Simple suggestion algorithm: words that start with the same letter
        return _dictionary
            .Where(w => w.Length > 1 && w[0] == char.ToLowerInvariant(word[0]))
            .Take(5);
    }
}

// ---------------
// USAGE EXAMPLE

var words = new List<string> { "apple", "banana", "cherry", "date", "elderberry" };
var spellChecker = new SpellChecker(words);

Console.WriteLine($"Is 'apple' valid? {spellChecker.IsWordValid("apple")}");
Console.WriteLine($"Is 'apricot' valid? {spellChecker.IsWordValid("apricot")}");

Console.WriteLine("Suggestions for 'app':");
foreach (var suggestion in spellChecker.GetSuggestions("app")){
    Console.WriteLine(suggestion);
}

In this example, we use FrozenSet to create an efficient, immutable set of valid words. The ToFrozenSet method is used to create the dictionary, with StringComparer.OrdinalIgnoreCase ensuring case-insensitive lookups. The resulting _dictionary offers fast, thread-safe containment checks and enumeration, making it ideal for a spell checker application.

Best Practices

When using FrozenDictionary and FrozenSet, consider the following best practices:

  1. Use these collections for read-only scenarios where the data doesn't change after initialization.
  2. Prefer them over their mutable counterparts when dealing with large datasets that are frequently accessed.
  3. Utilize them in multi-threaded environments to eliminate the need for explicit synchronization.
  4. Consider the trade-off between creation time and lookup performance. While these collections offer faster lookups, they may take longer to initialize compared to regular dictionaries and sets.
  5. Use the ToFrozenDictionary and ToFrozenSet extension methods for easy creation from existing collections.
  6. When possible, provide a capacity hint during creation to optimize memory usage and initialization time.
  7. Remember that these collections are immutable; if you need to modify the data, you'll have to create a new instance.
  8. Leverage these collections in performance-critical parts of your application, such as caching systems or frequently accessed lookup tables.

You'll only receive email when they publish something new.

More from GSLF
All posts