Immutable vs Frozen vs Readonly Collections in .Net

Furkan Güngör
4 min readJan 6, 2025

--

Immutable Frozen ve Readonly bu üç kavram benzer ve kafa karıştırıcı görünebilir. Modern uygulamalarda değişmezlik (immutability) önemli bir araçtır. Her terim, değişmezliğin merkezi bir rol oynadığı farklı bir senaryoyu ele alıyor.

Readonly Collections

Mutable bir collection üzerinden AsReadOnly() metodu ile sadece okunabilen bir görüntü oluşturur. Herhangi bir ReadOnly collection, oluşturulduğu orjinal koleksiyonun sadece okunabilen bir görünümüdür. Bu nedenle, içerisinde Add() gibi metodlar barındırmaz. Ancak oluşturulduğu orjinal koleksiyonun güncellenmesi halinde, güncel görünüm ReadOnly koleksiyona aktarılır.

Immutable Collections

Immutable koleksiyonlar, sadece bir görünüm değil yeni bir instance yaratır. Immutable bir koleksiyona sahip olduğunuzda Add() , Remove() , Clear() gibi yöntemleri de kullanabilirsiniz. Ancak immutable olduğu için her işlemden sonra yeni bir koleksiyon oluşturulur.

Aşağıdaki resimde de görüldüğü gibi her işlem yeni bir instance üzerinden işlem görecektir. (Not : string veri tipi de immutable yapıdadır.)

Frozen Collections

Aslında System.Collections.Frozen içerisinde sadece FrozenDictionary<key,value> ve FrozenSet<T> bulunur. Frozen Collections nispeten yeni bir eklentidir ve odak noktası immutable collections göre daha iyi performans ve bellek açısından daha verimli bir alternatif sağlamaktır. Temel özelliği, mutable olarak başlamaları ve eklenme veya değiştirme işlemleri bittikten sonra koleksiyonu dondurarak bu noktadan sonra immutable hale getirilmesidir.

Create Benchmark

[Orderer(SummaryOrderPolicy.FastestToSlowest)]
[MemoryDiagnoser(false)]
[MarkdownExporter]
public class CreateBenchmark
{
private const int itemsCount = 10_000;

[Benchmark]
public void CreateDictionary()
{
Dictionary<int, int> dictionary = Enumerable.Range(0, itemsCount).ToDictionary(key => key);
}

[Benchmark]
public void CreateImmutableDictionary()
{
ImmutableDictionary<int, int> dictionary = Enumerable.Range(0, itemsCount).ToImmutableDictionary(key => key);
}

[Benchmark]
public void CreateFrozenDictionary()
{
FrozenDictionary<int, int> frozenDictionary = Enumerable.Range(0, itemsCount).ToFrozenDictionary(key => key);
}

[Benchmark]
public void CreateList()
{
List<int> list = Enumerable.Range(0, itemsCount).ToList();
}

[Benchmark]
public void CreateImmutableList()
{
ImmutableList<int> list = Enumerable.Range(0, itemsCount).ToImmutableList();
}

[Benchmark]
public void CreateHashSet()
{
HashSet<int> hashSet = Enumerable.Range(0, itemsCount).ToHashSet();
}

[Benchmark]
public void CreateImmutableHashSet()
{
ImmutableHashSet<int> hashSet = Enumerable.Range(0, itemsCount).ToImmutableHashSet();
}

[Benchmark]
public void CreateFrozenSet()
{
FrozenSet<int> frozenSet = Enumerable.Range(0, itemsCount).ToFrozenSet();
}
}

TryGetValue Benchmark

[Orderer(SummaryOrderPolicy.FastestToSlowest)]
[MarkdownExporter]
public class TryGetValueBenchmark
{
private const int itemsCount = 100_000;
private const int keyToFind = 500;

private Dictionary<int, int> _dictionary = Enumerable.Range(0, itemsCount).ToDictionary(key => key);
private ImmutableDictionary<int, int> _immutableDictionary = Enumerable.Range(0, itemsCount).ToImmutableDictionary(key => key);

private HashSet<int> _hashSet = Enumerable.Range(0, itemsCount).ToHashSet();
private ImmutableHashSet<int> _immutableHashSet = Enumerable.Range(0, itemsCount).ToImmutableHashSet();

private FrozenDictionary<int, int> _frozenDictionary = Enumerable.Range(0, itemsCount).ToFrozenDictionary(key => key);
private FrozenSet<int> _frozenSet = Enumerable.Range(0, itemsCount).ToFrozenSet();

[Benchmark]
public void TryGetValueDictionary()
{
_dictionary.TryGetValue(keyToFind, out _);
}

[Benchmark]
public void TryGetValueImmutableDictionary()
{
_immutableDictionary.TryGetValue(keyToFind, out _);
}

[Benchmark]
public void TryGetValueFrozenDictionary()
{
_frozenDictionary.TryGetValue(keyToFind, out _);
}

[Benchmark]
public void TryGetValueHashSet()
{
_hashSet.TryGetValue(keyToFind, out _);
}

[Benchmark]
public void TryGetValueImmutableHashSet()
{
_immutableHashSet.TryGetValue(keyToFind, out _);
}

[Benchmark]
public void TryGetValueFrozenSet()
{
_frozenSet.TryGetValue(keyToFind, out _);
}
}

Lookup Benchmark

[Orderer(SummaryOrderPolicy.FastestToSlowest)]
[MarkdownExporter]
public class LookupBenchmark
{
private const int itemsCount = 100_000;
private const int iterations = 1_000;

private Dictionary<int, int> _dictionary = Enumerable.Range(0, itemsCount).ToDictionary(key => key);
private ImmutableDictionary<int, int> _immutableDictionary = Enumerable.Range(0, itemsCount).ToImmutableDictionary(key => key);

private List<int> _list = Enumerable.Range(0, itemsCount).ToList();
private ImmutableList<int> _immutableList = Enumerable.Range(0, itemsCount).ToImmutableList();

private HashSet<int> _hashSet = Enumerable.Range(0, itemsCount).ToHashSet();
private ImmutableHashSet<int> _immutableHashSet = Enumerable.Range(0, itemsCount).ToImmutableHashSet();

private FrozenDictionary<int, int> _frozenDictionary = Enumerable.Range(0, itemsCount).ToFrozenDictionary(key => key);
private FrozenSet<int> _frozenSet = Enumerable.Range(0, itemsCount).ToFrozenSet();

[Benchmark]
public void LookupDictionary()
{
for (int i = 0; i < iterations; i++)
_ = _dictionary.ContainsKey(i);
}

[Benchmark]
public void LookupImmutableDictionary()
{
for (int i = 0; i < iterations; i++)
_ = _immutableDictionary.ContainsKey(i);
}

[Benchmark]
public void LookupFrozenDictionary()
{
for (var i = 0; i < iterations; i++)
_ = _frozenDictionary.ContainsKey(i);
}

[Benchmark]
public void LookupList()
{
for (int i = 0; i < iterations; i++)
_ = _list.Contains(i);
}

[Benchmark]
public void LookupImmutableList()
{
for (int i = 0; i < iterations; i++)
_ = _immutableList.Contains(i);
}

[Benchmark]
public void LookupHashSet()
{
for (var i = 0; i < iterations; i++)
_ = _hashSet.Contains(i);
}

[Benchmark]
public void LookupImmutableHashSet()
{
for (var i = 0; i < iterations; i++)
_ = _immutableHashSet.Contains(i);
}

[Benchmark]
public void LookupFrozenSet()
{
for (var i = 0; i < iterations; i++)
_ = _frozenSet.Contains(i);
}
}

Hangi koleksiyon türünü seçeceğiniz, projenizin ihtiyaçlarına bağlıdır. Veri bütünlüğü ve thread güvenliğinin kritik olduğu senaryolarda Immutable koleksiyonları tercih etmek, aynı verinin yüksek erişim gerektirdiği durumlarda Frozenkoleksiyonlardan faydalanmak akıllıca olacaktır. C#’ın sunduğu bu zengin koleksiyon çeşitliliği, yazılımcılara esnek ve performans odaklı çözümler üretme imkânı tanır. Değişmez koleksiyonları doğru bir şekilde kullanmak, yazılımınızın daha güvenli, hızlı ve sürdürülebilir olmasına katkı sağlar. 👋

--

--

Furkan Güngör
Furkan Güngör

Written by Furkan Güngör

Solution Developer — I want to change the world, give me the source code.

No responses yet