Immutable vs Frozen vs Readonly Collections in .Net
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 Frozen
koleksiyonlardan 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. 👋