Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C# & .NET (C# 14, .NET 10 LTS)  Module 5 – Arrays and collections

Read-only, immutable and frozen collections

What a read-only view still lets change, how ImmutableArray and ImmutableList make new versions, and when FrozenDictionary and FrozenSet speed up lookups.

  • Intermediate
  • 20 minutes
  • Examples run with .NET 10.0.401
  • By MySmartCoPilot
Free core, Pro depth: 7 of 20 minutes free
Free: A read-only view still shows every change to the list behind it · Immutable collections give you a new collection for every change · Frozen collections are built once for fast reading. With a pass: 3 in-depth parts and 3 interview answers, about 13 minutes.

What you will learn

  • Distinguish a read-only view, an immutable collection and a frozen collection by what can still change
  • Build ImmutableArray<T> and ImmutableList<T> values one change at a time and with a builder
  • Create a FrozenDictionary or FrozenSet for data that never changes after start-up
  • Choose a collection that many threads can read without locks

Before you start

What you’ll learn · Before you start

What you will learn

  • Distinguish a read-only view, an immutable collection and a frozen collection by what can still change
  • Build ImmutableArray<T> and ImmutableList<T> values one change at a time and with a builder
  • Create a FrozenDictionary or FrozenSet for data that never changes after start-up
  • Choose a collection that many threads can read without locks

Before you start

On this page

A timetable class handed out its station list as a ReadOnlyCollection<string>, and a moment later the same object showed 4 stations instead of 3. Read-only means the reader cannot change it; it does not mean nobody can.

You will tell apart the three promises .NET makes, a read-only view, an immutable collection and a frozen one, build immutable values one change at a time and with a builder, and choose the kind that fits data that is built once and read by many threads.

A read-only view still shows every change to the list behind it

list.AsReadOnly() returns a read-only view: A wrapper such as the ReadOnlyCollection<T> that list.AsReadOnly() returns: changes made through it throw NotSupportedException, but it reads the collection behind it, so it shows every change the owner makes., a ReadOnlyCollection<T> that wraps the list. The documentation says such a wrapper prevents changes made through it and reflects changes made to the collection behind it, and the run shows both:

views.cs: a read-only view of a list that keeps changing C# · views/views.cs
using System.Collections.ObjectModel;

// A timetable class keeps its stations in a list and hands out a read-only view of it.
List<string> stations = ["NDLS", "AGC", "GWL"];
ReadOnlyCollection<string> view = stations.AsReadOnly();
Console.WriteLine($"view                  Count {view.Count}: {string.Join(", ", view)}");

// The view refuses changes made through it...
try
{
    ((IList<string>)view).Add("JHS");
}
catch (NotSupportedException e)
{
    Console.WriteLine($"Add through the view  {e.GetType().Name}");
}

// ...but it is a window onto the list, so it shows every change its owner makes.
stations.Add("JHS");
stations[0] = "DLI";
Console.WriteLine($"after the owner's two changes  Count {view.Count}: {string.Join(", ", view)}");

// A reader in the middle of a foreach over the view feels the change too.
try
{
    foreach (string code in view)
    {
        if (code == "AGC")
        {
            stations.Add("BPL");   // the owner adds while someone reads
        }
    }
}
catch (InvalidOperationException e)
{
    Console.WriteLine($"owner adds during a reader's foreach  {e.GetType().Name}");
}

// The same holds for dictionaries: AsReadOnly gives a ReadOnlyDictionary over the same entries.
var platforms = new Dictionary<string, int> { ["12951"] = 1 };
IReadOnlyDictionary<string, int> platformView = platforms.AsReadOnly();
platforms["12002"] = 3;
Console.WriteLine($"ReadOnlyDictionary    Count {platformView.Count}, after the owner added a train");

// A copy is the only way to hand out stations that will not change afterwards.
string[] snapshot = [.. stations];
stations.Clear();
Console.WriteLine($"after Clear()         view Count {view.Count}, snapshot Length {snapshot.Length}");

Output

view                  Count 3: NDLS, AGC, GWL
Add through the view  NotSupportedException
after the owner's two changes  Count 4: DLI, AGC, GWL, JHS
owner adds during a reader's foreach  InvalidOperationException
ReadOnlyDictionary    Count 2, after the owner added a train
after Clear()         view Count 0, snapshot Length 5

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run views.cs

Add through the view threw NotSupportedException, yet after the owner added JHS and renamed the first station, the view showed 4 stations, DLI first. A reader in the middle of a foreach felt the owner’s Add as an InvalidOperationException, and a ReadOnlyDictionary made with AsReadOnly() behaved the same way. Only the copy, [.. stations], kept its 5 stations when the list was cleared. Read-only views are the right tool when callers should see live data but not change it; they promise nothing about the data staying put.

Checkpoint

Check: Read-only views

  1. Question 1 of 2 var list = new List<int> { 1, 2 }; var view = list.AsReadOnly(); list.Add(3); What is view.Count?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 3 items

    A read-only view reads the list behind it, so it shows the owner's Add at once, as the timetable view went from 3 stations to 4.

  2. Question 2 of 2 A reader loops over a ReadOnlyCollection<string> while the owner adds a station to the list behind it. What can happen?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The reader's foreach throws InvalidOperationException

    A view protects the data from the reader, not the reader from changes. Hand out a copy or an immutable collection when readers must not be disturbed.

Immutable collections give you a new collection for every change

An immutable collection: A collection that never changes after it is made, such as ImmutableArray<T> or ImmutableList<T>. Its Add and Remove return a new collection and leave the old one as it was, so any number of threads can read it without locks. never changes after it is made. Its Add, Remove and SetItem return a new collection and leave the old one as it was, so the documentation of System.Collections.Immutable lists sharing a collection that never changes, and reading it from many threads without locks, among its uses. Step through three versions of one list:

immutable.cs: versions of an immutable list, an array and a builder C# · immutable/immutable.cs
using System.Collections.Immutable;

// An immutable list never changes: every "change" returns a new list, and the old one stays as it was.
ImmutableList<string> v1 = ["NDLS", "AGC"];
ImmutableList<string> v2 = v1.Add("GWL");
ImmutableList<string> v3 = v2.SetItem(0, "DLI").Remove("AGC");
Console.WriteLine($"v1  {string.Join(", ", v1)}");
Console.WriteLine($"v2  {string.Join(", ", v2)}");
Console.WriteLine($"v3  {string.Join(", ", v3)}");

// Calling a method and dropping its result changes nothing, as with strings.
v1.Add("JHS");
Console.WriteLine($"after v1.Add(\"JHS\") on a line of its own, v1 still has {v1.Count} stations");

// ImmutableArray<T> is a struct around an array: fast to read, and every change copies the whole array.
ImmutableArray<int> fares = [450, 520, 610];
ImmutableArray<int> raised = fares.SetItem(0, 480);
Console.WriteLine($"fares [{string.Join(", ", fares)}], raised [{string.Join(", ", raised)}], fares[2] = {fares[2]}");

// Many changes at once: a builder collects them and makes one immutable result.
ImmutableArray<int>.Builder builder = ImmutableArray.CreateBuilder<int>(5);
for (int seat = 1; seat <= 5; seat++)
{
    builder.Add(seat * 10);
}
ImmutableArray<int> seats = builder.MoveToImmutable();
Console.WriteLine($"from a builder        [{string.Join(", ", seats)}], builder Count now {builder.Count}");

// Two lists that look equal are still compared by reference, unless you compare their items.
ImmutableList<string> again = ["NDLS", "AGC"];
Console.WriteLine($"v1 == again: {v1 == again}; same items: {v1.SequenceEqual(again)}");

Output

v1  NDLS, AGC
v2  NDLS, AGC, GWL
v3  DLI, GWL
after v1.Add("JHS") on a line of its own, v1 still has 2 stations
fares [450, 520, 610], raised [480, 520, 610], fares[2] = 610
from a builder        [10, 20, 30, 40, 50], builder Count now 0
v1 == again: False; same items: True

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run immutable.cs

Interactive · step by step Three versions of one immutable list Step through the first lines of immutable.cs. Variables are on the left, the lists they refer to on the right.
Three versions of one immutable list, step 5 of 5. Showing v1, ImmutableList, 2 items, v2, ImmutableList, 3 items, v3, ImmutableList, 2 items. In focus: ImmutableList, 2 items, ImmutableList, 3 items, ImmutableList, 2 items.v1ImmutableList, 2 itemsNDLS, AGCv2ImmutableList, 3 itemsNDLS, AGC, GWLv3ImmutableList, 2 itemsDLI, GWL

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The first version. A collection expression makes v1, an immutable list of two stations. Nothing will ever change it.
  2. v2 = v1.Add("GWL"). Add does not change v1. It returns a new list with the extra station, and the program stores it in v2.
  3. Two changes, one more version. SetItem(0, "DLI") and then Remove("AGC") each return a new list; v3 keeps the last one, DLI and GWL. v2 still holds three stations.
  4. v1.Add("JHS") on its own. This call makes a fourth list and throws it away, because nothing stores the result. v1 still has 2 stations, as the run printed.
  5. Every version stays valid. All three lists exist side by side. Code that was handed v1 keeps seeing NDLS and AGC, whatever any other code does, which is what makes the list safe to share.

v1.Add("JHS") on a line of its own changed nothing, the same slip as calling a string method and dropping its result. The two main types differ inside, and the documentation’s table gives their costs:

What differs ImmutableArray<T> ImmutableList<T>
Inside an array, in a struct a balanced tree
Read by index O(1) O(log n)
Add O(n): copies the array O(log n): shares most of the tree
Choose it when updates are rare or there are few items updates are common

For many changes at once, a builder such as ImmutableArray.CreateBuilder<int>(5) collects them like a list and then makes one immutable result; MoveToImmutable() handed over its array and left the builder empty.

Checkpoint

Check: Immutable versions

  1. Question 1 of 2 What does this print?

    Read the code, then choose one answer.

    ImmutableList<string> stops = ["NDLS", "AGC"];
    stops.Add("GWL");
    Console.WriteLine(stops.Count);
    Show the answer: Question 1 of 2

    Answer: 2

    Write stops = stops.Add("GWL") to keep the new version.

  2. Question 2 of 2 A rule set of 12 entries is read on every request and changed about once a day. Which immutable type fits?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: ImmutableArray<T>: reads are O(1), and a daily O(n) copy of 12 entries costs nothing

    Choose by the mix: many reads and few or small changes suit ImmutableArray<T>, frequent changes to a large collection suit ImmutableList<T>.

Frozen collections are built once for fast reading

frozen collection: A FrozenDictionary<TKey,TValue> or FrozenSet<T> (.NET 8): an immutable collection that studies its keys when it is built, which takes longer than building a Dictionary, so that every later lookup is fast., FrozenDictionary<TKey,TValue> and FrozenSet<T> (.NET 8), are immutable too, but built for one job. The documentation says a frozen dictionary has a relatively high cost to create and excellent lookup performance, which makes it ideal for data created once, at start-up, and read for the life of the application:

frozen.cs: station names frozen at start-up C# · frozen/frozen.cs
using System.Collections.Frozen;

// Station codes and names, loaded once when a service starts and read on every request after that.
var source = new Dictionary<string, string>
{
    ["NDLS"] = "New Delhi",
    ["MAS"] = "Chennai Central",
    ["HWH"] = "Howrah",
    ["SBC"] = "KSR Bengaluru",
    ["CSMT"] = "Mumbai CSMT",
};

FrozenDictionary<string, string> stations = source.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
Console.WriteLine($"Count {stations.Count}; stations[\"ndls\"] = {stations["ndls"]}; TryGetValue(\"BPL\") {stations.TryGetValue("BPL", out _)}");

// A frozen dictionary has no Add or Remove, and its interface methods that would change it throw.
try
{
    ((IDictionary<string, string>)stations).Add("BPL", "Bhopal");
}
catch (NotSupportedException e)
{
    Console.WriteLine($"Add through IDictionary  {e.GetType().Name}");
}

// Changing the source afterwards does not reach the frozen copy.
source["BPL"] = "Bhopal";
Console.WriteLine($"source has {source.Count} keys, the frozen dictionary still {stations.Count}");

// When it is created, it looks at the keys it was given and picks a lookup strategy for them.
Console.WriteLine($"picked for these keys    {stations.GetType().Name}");

// FrozenSet<T> is the same idea for a set.
FrozenSet<string> junctions = new[] { "NDLS", "ET", "BZA", "NGP" }.ToFrozenSet();
Console.WriteLine($"FrozenSet Contains(\"ET\") {junctions.Contains("ET")}, Contains(\"et\") {junctions.Contains("et")}: no comparer, so case counts");

Output

Count 5; stations["ndls"] = New Delhi; TryGetValue("BPL") False
Add through IDictionary  NotSupportedException
source has 6 keys, the frozen dictionary still 5
picked for these keys    LengthBucketsFrozenDictionary`1
FrozenSet Contains("ET") True, Contains("et") False: no comparer, so case counts

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run frozen.cs

ToFrozenDictionary(StringComparer.OrdinalIgnoreCase) found ndls, refused an Add through IDictionary, and kept 5 stations after the source gained a sixth. It also looked at its keys and picked an implementation for them, LengthBucketsFrozenDictionary, which, as its name says, files the keys by their length. Frozen collections suit settings, code tables and lookups that are written at start-up and read on every request. Answer three questions about your data, and the table rules out the kinds that would get it wrong:

Interactive · answer and compare Which read-only collection fits your data? Answer three questions about the data you hand out. The table rules out the kinds that would get it wrong.
How often does the data change after it is first built?
Must a reader keep exactly what it was given, even if the owner changes later?
How is it read?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Five ways to hand out data that callers must not change, against five things that decide the choice
Kind Callers cannot change it Stays as handed out One change is cheap Fast reads Many threads may read
Read-only view Made with: list.AsReadOnly() Good changes through it throw NotSupportedException Poor shows every change the owner makes: Count went from 3 to 4 Good the owner changes its own list in place Good reads go straight to the list behind it Poor not while the owner changes the list: a foreach threw
A copy Made with: [.. list] or ToArray() Good callers get an array of their own Good a snapshot: it kept 5 stations after the list was cleared Poor every hand-out copies all the data again Good an array: reading by index is O(1) Mixed safe while nobody changes the copy, which nothing prevents
ImmutableArray<T> Made with: an array that never changes Good it has no method that changes it Good never changes once made Poor each change copies the whole array: O(n) Good an array inside: O(1) by index Good immutable, so any thread may read it without a lock
ImmutableList<T> Made with: a tree that never changes Good it has no method that changes it Good never changes once made Mixed O(log n): a new path through the tree, the rest shared Mixed O(log n) by index; foreach is fast Good immutable, so any thread may read it without a lock
Frozen collection Made with: ToFrozenDictionary() Good no Add or Remove; IDictionary.Add threw Good the source changed and the frozen copy did not Poor it never changes: a new version means building it again Good built for fast lookups by key, at a higher cost to create Good immutable, so any thread may read it without a lock
How the answers decide
  • “Must a reader keep exactly what it was given, even if the owner changes later?” Yes, a fixed snapshot: rules out Read-only view. A view reads the live list, so a reader sees every later change instead of keeping what it was given.
  • “How often does the data change after it is first built?” Often: rules out Frozen collection and adds a caveat to ImmutableArray<T> and A copy. A frozen collection cannot change at all, and copying a whole array on every change grows with its size.
  • “How often does the data change after it is first built?” Rarely, far less than it is read: adds a caveat to Frozen collection. Each new version means building the frozen collection again, which costs more than building a dictionary.
  • “How often does the data change after it is first built?” Never, it is built once and “How is it read?” By key or by membership: adds a caveat to ImmutableList<T>. A tree pays O(log n) on every lookup for changes that never come; a frozen collection is built for this.
  • “How is it read?” By position or in order: rules out Frozen collection. FrozenDictionary and FrozenSet find by key or membership; for positions use an array or ImmutableArray<T>.

In short: a view for live data that callers must not change, an immutable collection for values that change by making new versions, and a frozen collection for lookups that never change once built.

Key idea

A read-only view stops the reader, an immutable collection stops everyone, and a frozen collection stops everyone and spends its build time on making lookups fast.

Checkpoint

Check: Frozen collections

  1. Question 1 of 2 A FrozenDictionary was made with source.ToFrozenDictionary(), and then source gains a key. What does the frozen dictionary hold?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The keys it was made from, without the new one

    Frozen collections are built once from a source and are immutable from then on.

  2. Question 2 of 2 Which data suits a FrozenDictionary best?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Station codes and names loaded at start-up and looked up on every request

    A frozen collection pays at build time to answer lookups fast, so it pays off for data that is read far more often than it is built.

Lookups measured: Dictionary, FrozenDictionary and ImmutableDictionary Pro: opens with a Pro, Premium or Ultimate pass

Read-heavy data is where the three dictionaries differ most, and the run below puts numbers on it. It builds each one from 1,000 branch codes and then asks it for a million codes.

In this part:

  • Building and reading 1,000 branch codes, timed
  • What the frozen dictionary chose for these keys

1 runnable example (C#) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Changing immutable collections in bulk, measured Pro: opens with a Pro, Premium or Ultimate pass

An immutable collection makes a new version for every change, and code that adds items one at a time pays for every version. The run below counts the bytes of 1,000 Adds against a builder, then shows a trap of ImmutableArray<T>.

In this part:

  • One change at a time against a builder, in bytes
  • The default ImmutableArray and other traps

2 runnable examples (C#) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Sharing data across threads without locks Pro: opens with a Pro, Premium or Ultimate pass

Immutable and frozen collections earn their place when several threads read the same data while it is being replaced. The run below swaps a fare table 1,000 times under four readers, then lets four writers change one shared dictionary at once.

In this part:

  • Swap a whole snapshot instead of changing one in place
  • Small changes from many threads with ImmutableInterlocked

1 runnable example (C#) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Practise: a station lookup built at start-up

lookup.cs checks a method that you write. BuildLookup turns the rows of a station file into a lookup from code to name, built once and then read by many threads, which finds a code whatever its case. As given, the method throws NotImplementedException, and the 6 checks fail:

lookup.cs: six checks of BuildLookup C# · lookup/lookup.cs
using System.Collections.Frozen;

// Practice: write BuildLookup so that every check below passes, then run this file again.
// BuildLookup turns the rows of a station file into a lookup from code to name that is built once at start-up and
// then only read, by many threads: a FrozenDictionary that finds a code whatever its case.
(string Code, string Name)[] rows = [("NDLS", "New Delhi"), ("MAS", "Chennai Central"), ("HWH", "Howrah")];

var checks = new List<(string Name, Func<string> Run, string Expected)>
{
    ("finds NDLS", () => BuildLookup(rows)["NDLS"], "New Delhi"),
    ("finds mas in lower case", () => BuildLookup(rows)["mas"], "Chennai Central"),
    ("misses BPL without throwing", () => BuildLookup(rows).TryGetValue("BPL", out _).ToString(), "False"),
    ("holds three stations", () => BuildLookup(rows).Count.ToString(), "3"),
    ("refuses Add", () =>
    {
        IDictionary<string, string> asDictionary = BuildLookup(rows);
        try
        {
            asDictionary.Add("BPL", "Bhopal");
            return "added";
        }
        catch (NotSupportedException)
        {
            return "refused";
        }
    }, "refused"),
    ("an empty file", () => BuildLookup([]).Count.ToString(), "0"),
};

int passed = 0;
foreach (var (name, run, expected) in checks)
{
    string verdict;
    try
    {
        string got = run();
        verdict = got == expected ? "ok" : $"FAIL, got \"{got}\"";
    }
    catch (Exception e)
    {
        verdict = $"FAIL, {e.GetType().Name}";
    }
    if (verdict == "ok")
    {
        passed++;
    }
    Console.WriteLine($"{name}: {verdict}");
}
Console.WriteLine($"{passed} of {checks.Count} checks pass");
return passed == checks.Count ? 0 : 1;

static FrozenDictionary<string, string> BuildLookup(IEnumerable<(string Code, string Name)> rows)
{
    throw new NotImplementedException("Make key-value pairs from the rows, then call ToFrozenDictionary with a comparer.");
}

Output (exit status 1)

finds NDLS: FAIL, NotImplementedException
finds mas in lower case: FAIL, NotImplementedException
misses BPL without throwing: FAIL, NotImplementedException
holds three stations: FAIL, NotImplementedException
refuses Add: FAIL, NotImplementedException
an empty file: FAIL, NotImplementedException
0 of 6 checks pass

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run lookup.cs

Write BuildLookup

Copy lookup.cs and replace the body of BuildLookup: turn each row into a key and a value and call ToFrozenDictionary with StringComparer.OrdinalIgnoreCase. Run dotnet run lookup.cs until it prints 6 of 6 checks pass.

These questions return to views, versions and frozen lookups, the idea you know least first:

Practice that adapts to you

Practise this lesson

6 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 6 What does a ReadOnlyCollection<T> made with list.AsReadOnly() promise?

    Choose one answer.

    Show the answer: Question 1 of 6

    Answer: That nobody can change the list through the view

    Read-only is a promise about the reader's access, not about the data.

  2. Question 2 of 6 A class gives a report generator its list of stations, and the generator runs for a minute while the class keeps adding stations. The report must show the stations of the moment it started. What do you hand over?

    Choose one answer.

    Show the answer: Question 2 of 6

    Answer: A copy, or an immutable collection, taken when the report starts

    Choose a view for live data that callers must not change, and a snapshot when the reader needs data that holds still.

  3. Question 3 of 6 v1 is an ImmutableList of 2 stations. v2 = v1.Add("GWL"); v3 = v2.Remove("AGC"); How many stations does v2 hold?

    Type a number.

    Show the answer: Question 3 of 6

    Answer: 3 stations

    Remove made v3 and left v2 as it was: NDLS, AGC and GWL. Every version of an immutable collection stays valid.

  4. Question 4 of 6 A history of 200,000 events gets a new event many times a second, and readers walk it with foreach. Which immutable type fits better?

    Choose one answer.

    Show the answer: Question 4 of 6

    Answer: ImmutableList<T>: each Add is O(log n), and foreach over it is fast

    The array copies on every change, so it suits rare changes; the tree shares most of its nodes, so it suits frequent ones.

  5. Question 5 of 6 What did frozen.cs show about the implementation a FrozenDictionary uses?

    Choose one answer.

    Show the answer: Question 5 of 6

    Answer: It picked one for the keys it was given: LengthBucketsFrozenDictionary for these codes

    The analysis of the keys is the build time a frozen collection spends, and the reason its lookups are fast.

  6. Question 6 of 6 A price table changes every few seconds and is read a few times between changes. A colleague freezes it after every change. What do you say?

    Choose one answer.

    Show the answer: Question 6 of 6

    Answer: A frozen table only pays off when it is read far more often than built; use a Dictionary under a lock, or an immutable dictionary

    Match the type to the ratio of reads to builds: frozen for build once, read always.

Interview questions

Fresher · What is the difference between a read-only collection and an immutable collection?

A read-only collection, such as the ReadOnlyCollection<T> that list.AsReadOnly() returns, is a view: it refuses changes made through it, but it reads the collection behind it, so it shows every change the owner makes, and a reader in a foreach can be broken by one. An immutable collection, such as ImmutableList<T>, can never change at all: Add and Remove return a new collection and leave the old one as it was. That makes an immutable collection safe to keep, to share and to read from many threads without locks, at the price of a new version for every change. A frozen collection is immutable as well, and spends extra time when it is built to make lookups fast.

  1. Mid to senior

    A settings class exposes its values as a ReadOnlyDictionary over an internal Dictionary, and a background task refreshes the internal dictionary every minute. Request handlers sometimes throw InvalidOperationException in a foreach, or see some old and some new settings. Why, and what would you use instead?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A service loads 50,000 product codes at start-up and looks them up about 20,000 times a second. A colleague proposes FrozenDictionary instead of Dictionary. When does that pay off, and how would you decide with measurements?

    Model answer, Pro. First step and follow-ups

  3. Fresher to mid

    A batch job builds an ImmutableArray<Booking> of 200,000 bookings by calling bookings = bookings.Add(b) in a loop. It is slow and allocates far more memory than the bookings need. Why, and how do you fix it?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A read-only view such as AsReadOnly() blocks changes through it but shows every change the owner makes.
  • An immutable collection never changes: its methods return new versions, ImmutableArray<T> reads in O(1) and copies on change, ImmutableList<T> changes in O(log n), and builders make many changes at once.
  • A frozen collection is immutable and built once for fast lookups, which suits data written at start-up and read by every request.

Terms in this lesson

frozen collection
A FrozenDictionary<TKey,TValue> or FrozenSet<T> (.NET 8): an immutable collection that studies its keys when it is built, which takes longer than building a Dictionary, so that every later lookup is fast.
immutable collection
A collection that never changes after it is made, such as ImmutableArray<T> or ImmutableList<T>. Its Add and Remove return a new collection and leave the old one as it was, so any number of threads can read it without locks.
read-only view
A wrapper such as the ReadOnlyCollection<T> that list.AsReadOnly() returns: changes made through it throw NotSupportedException, but it reads the collection behind it, so it shows every change the owner makes.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.