[{"data":1,"prerenderedAt":1218},["ShallowReactive",2],{"blog-\u002Fblog\u002Fdotnet-collections-list-dictionary-span":3},{"id":4,"title":5,"body":6,"description":1203,"difficulty":1204,"extension":1205,"framework":1206,"frameworkSlug":1207,"meta":1208,"navigation":103,"order":88,"path":1209,"qaPath":1210,"seo":1211,"stem":1212,"subtopic":1213,"topic":1214,"topicSlug":1215,"updated":1216,"__hash__":1217},"blog\u002Fblog\u002Fdotnet-collections-list-dictionary-span.md","Choosing the Right C# Collection",{"type":7,"value":8,"toc":1189},"minimark",[9,14,39,43,53,59,117,124,220,250,254,263,281,284,404,409,438,442,562,580,584,690,705,709,792,810,814,823,928,941,945,1037,1041,1127,1131,1185],[10,11,13],"h2",{"id":12},"why-collection-choice-matters-more-than-it-looks","Why collection choice matters more than it looks",[15,16,17,18,22,23,26,27,30,31,34,35,38],"p",{},"Picking the wrong collection type is one of the most common sources of subtle .NET\nperformance bugs — ",[19,20,21],"code",{},"List.Contains"," when you should use ",[19,24,25],{},"HashSet",", loading a full\ndictionary value when ",[19,28,29],{},"TryGetValue"," does one lookup, or calling ",[19,32,33],{},"Count()"," on an\n",[19,36,37],{},"IEnumerable\u003CT>"," that executes a database query. This guide gives you a framework\nfor choosing correctly.",[10,40,42],{"id":41},"the-collection-hierarchy","The collection hierarchy",[44,45,50],"pre",{"className":46,"code":48,"language":49},[47],"language-text","IEnumerable\u003CT>           — iterate only (foreach)\n  ICollection\u003CT>         — + Count, Add, Remove, Contains\n    IList\u003CT>             — + indexer [], Insert, RemoveAt\n\n  IReadOnlyCollection\u003CT> — Count + enumeration (no mutation)\n    IReadOnlyList\u003CT>     — + indexer []\n","text",[19,51,48],{"__ignoreMap":52},"",[15,54,55],{},[56,57,58],"strong",{},"Accept the most general type your method needs; return the most specific the caller needs.",[44,60,64],{"className":61,"code":62,"language":63,"meta":52,"style":52},"language-csharp shiki shiki-themes github-light github-dark","\u002F\u002F Accept IEnumerable\u003CT> when you only iterate:\nint Sum(IEnumerable\u003Cint> items) => items.Sum();\nSum(new[] { 1, 2, 3 });           \u002F\u002F array — ok\nSum(new List\u003Cint> { 1, 2 });      \u002F\u002F list — ok\nSum(Enumerable.Range(1, 100));     \u002F\u002F lazy — ok\n\n\u002F\u002F Return IReadOnlyList\u003CT> when the caller needs indexed access but not mutation:\npublic IReadOnlyList\u003COrder> GetOrders() => _orders.AsReadOnly();\n","csharp",[19,65,66,74,80,86,92,98,105,111],{"__ignoreMap":52},[67,68,71],"span",{"class":69,"line":70},"line",1,[67,72,73],{},"\u002F\u002F Accept IEnumerable\u003CT> when you only iterate:\n",[67,75,77],{"class":69,"line":76},2,[67,78,79],{},"int Sum(IEnumerable\u003Cint> items) => items.Sum();\n",[67,81,83],{"class":69,"line":82},3,[67,84,85],{},"Sum(new[] { 1, 2, 3 });           \u002F\u002F array — ok\n",[67,87,89],{"class":69,"line":88},4,[67,90,91],{},"Sum(new List\u003Cint> { 1, 2 });      \u002F\u002F list — ok\n",[67,93,95],{"class":69,"line":94},5,[67,96,97],{},"Sum(Enumerable.Range(1, 100));     \u002F\u002F lazy — ok\n",[67,99,101],{"class":69,"line":100},6,[67,102,104],{"emptyLinePlaceholder":103},true,"\n",[67,106,108],{"class":69,"line":107},7,[67,109,110],{},"\u002F\u002F Return IReadOnlyList\u003CT> when the caller needs indexed access but not mutation:\n",[67,112,114],{"class":69,"line":113},8,[67,115,116],{},"public IReadOnlyList\u003COrder> GetOrders() => _orders.AsReadOnly();\n",[10,118,120,121,123],{"id":119},"listt-vs-array-t","List\u003CT> vs Array (T",[67,122],{},")",[44,125,127],{"className":61,"code":126,"language":63,"meta":52,"style":52},"\u002F\u002F Array — fixed size at creation; fastest indexed access:\nint[] arr = new int[5];\narr[0] = 1;\n\u002F\u002F arr.Add(6); — doesn't exist\n\n\u002F\u002F List\u003CT> — dynamic; backed by an internal array that resizes:\nvar list = new List\u003Cint>(capacity: 5); \u002F\u002F pre-size to avoid early resizes\nlist.Add(1); list.Add(2); \u002F\u002F grows when capacity is exceeded\n\n\u002F\u002F Both support zero-copy Span\u003CT> slicing:\nSpan\u003Cint> spanArr  = arr.AsSpan();\nSpan\u003Cint> spanList = list.AsSpan();       \u002F\u002F .NET 5+\n\n\u002F\u002F API difference:\narr.Length;    \u002F\u002F fixed count\nlist.Count;    \u002F\u002F current element count\nlist.Capacity; \u002F\u002F internal array size (>= Count)\n",[19,128,129,134,139,144,149,153,158,163,168,173,179,185,191,196,202,208,214],{"__ignoreMap":52},[67,130,131],{"class":69,"line":70},[67,132,133],{},"\u002F\u002F Array — fixed size at creation; fastest indexed access:\n",[67,135,136],{"class":69,"line":76},[67,137,138],{},"int[] arr = new int[5];\n",[67,140,141],{"class":69,"line":82},[67,142,143],{},"arr[0] = 1;\n",[67,145,146],{"class":69,"line":88},[67,147,148],{},"\u002F\u002F arr.Add(6); — doesn't exist\n",[67,150,151],{"class":69,"line":94},[67,152,104],{"emptyLinePlaceholder":103},[67,154,155],{"class":69,"line":100},[67,156,157],{},"\u002F\u002F List\u003CT> — dynamic; backed by an internal array that resizes:\n",[67,159,160],{"class":69,"line":107},[67,161,162],{},"var list = new List\u003Cint>(capacity: 5); \u002F\u002F pre-size to avoid early resizes\n",[67,164,165],{"class":69,"line":113},[67,166,167],{},"list.Add(1); list.Add(2); \u002F\u002F grows when capacity is exceeded\n",[67,169,171],{"class":69,"line":170},9,[67,172,104],{"emptyLinePlaceholder":103},[67,174,176],{"class":69,"line":175},10,[67,177,178],{},"\u002F\u002F Both support zero-copy Span\u003CT> slicing:\n",[67,180,182],{"class":69,"line":181},11,[67,183,184],{},"Span\u003Cint> spanArr  = arr.AsSpan();\n",[67,186,188],{"class":69,"line":187},12,[67,189,190],{},"Span\u003Cint> spanList = list.AsSpan();       \u002F\u002F .NET 5+\n",[67,192,194],{"class":69,"line":193},13,[67,195,104],{"emptyLinePlaceholder":103},[67,197,199],{"class":69,"line":198},14,[67,200,201],{},"\u002F\u002F API difference:\n",[67,203,205],{"class":69,"line":204},15,[67,206,207],{},"arr.Length;    \u002F\u002F fixed count\n",[67,209,211],{"class":69,"line":210},16,[67,212,213],{},"list.Count;    \u002F\u002F current element count\n",[67,215,217],{"class":69,"line":216},17,[67,218,219],{},"list.Capacity; \u002F\u002F internal array size (>= Count)\n",[15,221,222,225,226,233,234,237,238,241,242,245,246,249],{},[56,223,224],{},"When to use array:"," size is known up-front, no need to add\u002Fremove, performance\ncritical (marginally faster indexing). ",[56,227,228,229,232],{},"When to use ",[19,230,231],{},"List\u003CT>",":"," size changes,\nneeds ",[19,235,236],{},"Add","\u002F",[19,239,240],{},"Remove",". ",[56,243,244],{},"When to return from a public method:"," return\n",[19,247,248],{},"IReadOnlyList\u003CT>"," to expose indexed access without allowing mutation of your\ninternal list.",[10,251,253],{"id":252},"dictionarytkey-tvalue-how-it-works-and-what-makes-a-good-key","Dictionary\u003CTKey, TValue> — how it works and what makes a good key",[15,255,256,259,260,232],{},[19,257,258],{},"Dictionary\u003CTKey, TValue>"," is a ",[56,261,262],{},"hash table",[264,265,266,274],"ol",{},[267,268,269,270,273],"li",{},"Call ",[19,271,272],{},"key.GetHashCode()"," → determines bucket",[267,275,276,277,280],{},"Walk bucket entries, call ",[19,278,279],{},"key.Equals(entry.Key)"," for each → find exact match",[15,282,283],{},"Average O(1) lookup, insert, remove. Worst case O(n) if all keys hash to the same bucket.",[44,285,287],{"className":61,"code":286,"language":63,"meta":52,"style":52},"\u002F\u002F Safe access — single lookup:\nvar dict = new Dictionary\u003Cstring, int> { [\"a\"] = 1, [\"b\"] = 2 };\n\n\u002F\u002F Two lookups — ContainsKey then indexer:\nif (dict.ContainsKey(\"a\")) { int v = dict[\"a\"]; }\n\n\u002F\u002F One lookup:\nif (dict.TryGetValue(\"a\", out int val)) { \u002F* use val *\u002F }\n\n\u002F\u002F Default if missing:\nint count = dict.GetValueOrDefault(\"missing\", 0);\n\n\u002F\u002F In-place increment (CollectionsMarshal — zero allocation, .NET 6+):\nref int hits = ref CollectionsMarshal.GetValueRefOrAddDefault(dict, \"hits\", out _);\nhits++;\n\n\u002F\u002F Custom comparer — case-insensitive keys:\nvar ci = new Dictionary\u003Cstring, int>(StringComparer.OrdinalIgnoreCase);\nci[\"Hello\"] = 1;\nConsole.WriteLine(ci[\"hello\"]); \u002F\u002F 1\n\n\u002F\u002F Pre-size to avoid rehashing:\nvar big = new Dictionary\u003Cstring, int>(capacity: 10_000);\n",[19,288,289,294,299,303,308,313,317,322,327,331,336,341,345,350,355,360,364,369,375,381,387,392,398],{"__ignoreMap":52},[67,290,291],{"class":69,"line":70},[67,292,293],{},"\u002F\u002F Safe access — single lookup:\n",[67,295,296],{"class":69,"line":76},[67,297,298],{},"var dict = new Dictionary\u003Cstring, int> { [\"a\"] = 1, [\"b\"] = 2 };\n",[67,300,301],{"class":69,"line":82},[67,302,104],{"emptyLinePlaceholder":103},[67,304,305],{"class":69,"line":88},[67,306,307],{},"\u002F\u002F Two lookups — ContainsKey then indexer:\n",[67,309,310],{"class":69,"line":94},[67,311,312],{},"if (dict.ContainsKey(\"a\")) { int v = dict[\"a\"]; }\n",[67,314,315],{"class":69,"line":100},[67,316,104],{"emptyLinePlaceholder":103},[67,318,319],{"class":69,"line":107},[67,320,321],{},"\u002F\u002F One lookup:\n",[67,323,324],{"class":69,"line":113},[67,325,326],{},"if (dict.TryGetValue(\"a\", out int val)) { \u002F* use val *\u002F }\n",[67,328,329],{"class":69,"line":170},[67,330,104],{"emptyLinePlaceholder":103},[67,332,333],{"class":69,"line":175},[67,334,335],{},"\u002F\u002F Default if missing:\n",[67,337,338],{"class":69,"line":181},[67,339,340],{},"int count = dict.GetValueOrDefault(\"missing\", 0);\n",[67,342,343],{"class":69,"line":187},[67,344,104],{"emptyLinePlaceholder":103},[67,346,347],{"class":69,"line":193},[67,348,349],{},"\u002F\u002F In-place increment (CollectionsMarshal — zero allocation, .NET 6+):\n",[67,351,352],{"class":69,"line":198},[67,353,354],{},"ref int hits = ref CollectionsMarshal.GetValueRefOrAddDefault(dict, \"hits\", out _);\n",[67,356,357],{"class":69,"line":204},[67,358,359],{},"hits++;\n",[67,361,362],{"class":69,"line":210},[67,363,104],{"emptyLinePlaceholder":103},[67,365,366],{"class":69,"line":216},[67,367,368],{},"\u002F\u002F Custom comparer — case-insensitive keys:\n",[67,370,372],{"class":69,"line":371},18,[67,373,374],{},"var ci = new Dictionary\u003Cstring, int>(StringComparer.OrdinalIgnoreCase);\n",[67,376,378],{"class":69,"line":377},19,[67,379,380],{},"ci[\"Hello\"] = 1;\n",[67,382,384],{"class":69,"line":383},20,[67,385,386],{},"Console.WriteLine(ci[\"hello\"]); \u002F\u002F 1\n",[67,388,390],{"class":69,"line":389},21,[67,391,104],{"emptyLinePlaceholder":103},[67,393,395],{"class":69,"line":394},22,[67,396,397],{},"\u002F\u002F Pre-size to avoid rehashing:\n",[67,399,401],{"class":69,"line":400},23,[67,402,403],{},"var big = new Dictionary\u003Cstring, int>(capacity: 10_000);\n",[15,405,406],{},[56,407,408],{},"Key design rules:",[410,411,412,422,432],"ul",{},[267,413,414,415,421],{},"Keys must have ",[56,416,417,418],{},"stable ",[19,419,420],{},"GetHashCode"," values — never mutate an object while it is\na dictionary key.",[267,423,424,425,427,428,431],{},"For custom types, override both ",[19,426,420],{}," and ",[19,429,430],{},"Equals",", or use records (which do\nthis automatically via value equality).",[267,433,434,435,437],{},"Never use mutable reference types with default ",[19,436,420],{}," (pointer-based) as keys.",[10,439,441],{"id":440},"hashsett-for-fast-membership","HashSet\u003CT> for fast membership",[44,443,445],{"className":61,"code":444,"language":63,"meta":52,"style":52},"var banned = new HashSet\u003Cstring>(StringComparer.OrdinalIgnoreCase)\n{\n    \"admin\", \"root\", \"superuser\"\n};\n\n\u002F\u002F O(1) lookup — vs O(n) for List\u003CT>.Contains:\nbool isBanned = banned.Contains(username); \u002F\u002F fast regardless of set size\n\n\u002F\u002F Uniqueness — duplicates silently ignored:\nbanned.Add(\"admin\"); \u002F\u002F returns false; set unchanged\n\n\u002F\u002F Set operations:\nvar a = new HashSet\u003Cint> { 1, 2, 3, 4 };\nvar b = new HashSet\u003Cint> { 3, 4, 5, 6 };\n\na.IntersectWith(b);  \u002F\u002F a = { 3, 4 } — in-place\na.UnionWith(b);      \u002F\u002F a = { 3, 4, 5, 6 }\na.ExceptWith(b);     \u002F\u002F a = {} (b is superset here)\n\n\u002F\u002F Remove duplicates from a list:\nvar deduped = new HashSet\u003Cstring>(list).ToList();\n\n\u002F\u002F Sorted variant:\nvar sorted = new SortedSet\u003Cint> { 5, 1, 3, 2 }; \u002F\u002F always ordered: 1, 2, 3, 5\n",[19,446,447,452,457,462,467,471,476,481,485,490,495,499,504,509,514,518,523,528,533,537,542,547,551,556],{"__ignoreMap":52},[67,448,449],{"class":69,"line":70},[67,450,451],{},"var banned = new HashSet\u003Cstring>(StringComparer.OrdinalIgnoreCase)\n",[67,453,454],{"class":69,"line":76},[67,455,456],{},"{\n",[67,458,459],{"class":69,"line":82},[67,460,461],{},"    \"admin\", \"root\", \"superuser\"\n",[67,463,464],{"class":69,"line":88},[67,465,466],{},"};\n",[67,468,469],{"class":69,"line":94},[67,470,104],{"emptyLinePlaceholder":103},[67,472,473],{"class":69,"line":100},[67,474,475],{},"\u002F\u002F O(1) lookup — vs O(n) for List\u003CT>.Contains:\n",[67,477,478],{"class":69,"line":107},[67,479,480],{},"bool isBanned = banned.Contains(username); \u002F\u002F fast regardless of set size\n",[67,482,483],{"class":69,"line":113},[67,484,104],{"emptyLinePlaceholder":103},[67,486,487],{"class":69,"line":170},[67,488,489],{},"\u002F\u002F Uniqueness — duplicates silently ignored:\n",[67,491,492],{"class":69,"line":175},[67,493,494],{},"banned.Add(\"admin\"); \u002F\u002F returns false; set unchanged\n",[67,496,497],{"class":69,"line":181},[67,498,104],{"emptyLinePlaceholder":103},[67,500,501],{"class":69,"line":187},[67,502,503],{},"\u002F\u002F Set operations:\n",[67,505,506],{"class":69,"line":193},[67,507,508],{},"var a = new HashSet\u003Cint> { 1, 2, 3, 4 };\n",[67,510,511],{"class":69,"line":198},[67,512,513],{},"var b = new HashSet\u003Cint> { 3, 4, 5, 6 };\n",[67,515,516],{"class":69,"line":204},[67,517,104],{"emptyLinePlaceholder":103},[67,519,520],{"class":69,"line":210},[67,521,522],{},"a.IntersectWith(b);  \u002F\u002F a = { 3, 4 } — in-place\n",[67,524,525],{"class":69,"line":216},[67,526,527],{},"a.UnionWith(b);      \u002F\u002F a = { 3, 4, 5, 6 }\n",[67,529,530],{"class":69,"line":371},[67,531,532],{},"a.ExceptWith(b);     \u002F\u002F a = {} (b is superset here)\n",[67,534,535],{"class":69,"line":377},[67,536,104],{"emptyLinePlaceholder":103},[67,538,539],{"class":69,"line":383},[67,540,541],{},"\u002F\u002F Remove duplicates from a list:\n",[67,543,544],{"class":69,"line":389},[67,545,546],{},"var deduped = new HashSet\u003Cstring>(list).ToList();\n",[67,548,549],{"class":69,"line":394},[67,550,104],{"emptyLinePlaceholder":103},[67,552,553],{"class":69,"line":400},[67,554,555],{},"\u002F\u002F Sorted variant:\n",[67,557,559],{"class":69,"line":558},24,[67,560,561],{},"var sorted = new SortedSet\u003Cint> { 5, 1, 3, 2 }; \u002F\u002F always ordered: 1, 2, 3, 5\n",[15,563,564,567,568,571,572,575,576,579],{},[56,565,566],{},"Rule:"," Use ",[19,569,570],{},"HashSet\u003CT>"," whenever you're checking \"is X in this set?\" at scale.\n",[19,573,574],{},"List\u003CT>.Contains"," is O(n); ",[19,577,578],{},"HashSet\u003CT>.Contains"," is O(1).",[10,581,583],{"id":582},"concurrentdictionary-for-thread-safe-caching","ConcurrentDictionary for thread-safe caching",[44,585,587],{"className":61,"code":586,"language":63,"meta":52,"style":52},"\u002F\u002F Thread-safe cache:\nvar cache = new ConcurrentDictionary\u003Cstring, UserProfile>();\n\n\u002F\u002F GetOrAdd — returns existing value or adds new one atomically:\nvar profile = cache.GetOrAdd(userId, id => LoadFromDb(id));\n\u002F\u002F WARNING: the factory may run multiple times under contention\n\u002F\u002F Use Lazy\u003CT> to ensure single initialization:\nvar lazyCache = new ConcurrentDictionary\u003Cstring, Lazy\u003CUserProfile>>();\nvar lazy = lazyCache.GetOrAdd(userId, id => new Lazy\u003CUserProfile>(() => LoadFromDb(id)));\nUserProfile p = lazy.Value;\n\n\u002F\u002F AddOrUpdate — atomic add-or-modify:\ncache2.AddOrUpdate(\"hits\", 1, (key, old) => old + 1);\n\n\u002F\u002F TryGetValue, TryAdd, TryRemove — all atomic:\nif (cache.TryGetValue(userId, out var existing))\n    Console.WriteLine(existing.Name);\n\n\u002F\u002F foreach is safe — enumerates a snapshot (may be slightly stale under concurrent writes)\nforeach (var (key, value) in cache)\n    Console.WriteLine(key);\n",[19,588,589,594,599,603,608,613,618,623,628,633,638,642,647,652,656,661,666,671,675,680,685],{"__ignoreMap":52},[67,590,591],{"class":69,"line":70},[67,592,593],{},"\u002F\u002F Thread-safe cache:\n",[67,595,596],{"class":69,"line":76},[67,597,598],{},"var cache = new ConcurrentDictionary\u003Cstring, UserProfile>();\n",[67,600,601],{"class":69,"line":82},[67,602,104],{"emptyLinePlaceholder":103},[67,604,605],{"class":69,"line":88},[67,606,607],{},"\u002F\u002F GetOrAdd — returns existing value or adds new one atomically:\n",[67,609,610],{"class":69,"line":94},[67,611,612],{},"var profile = cache.GetOrAdd(userId, id => LoadFromDb(id));\n",[67,614,615],{"class":69,"line":100},[67,616,617],{},"\u002F\u002F WARNING: the factory may run multiple times under contention\n",[67,619,620],{"class":69,"line":107},[67,621,622],{},"\u002F\u002F Use Lazy\u003CT> to ensure single initialization:\n",[67,624,625],{"class":69,"line":113},[67,626,627],{},"var lazyCache = new ConcurrentDictionary\u003Cstring, Lazy\u003CUserProfile>>();\n",[67,629,630],{"class":69,"line":170},[67,631,632],{},"var lazy = lazyCache.GetOrAdd(userId, id => new Lazy\u003CUserProfile>(() => LoadFromDb(id)));\n",[67,634,635],{"class":69,"line":175},[67,636,637],{},"UserProfile p = lazy.Value;\n",[67,639,640],{"class":69,"line":181},[67,641,104],{"emptyLinePlaceholder":103},[67,643,644],{"class":69,"line":187},[67,645,646],{},"\u002F\u002F AddOrUpdate — atomic add-or-modify:\n",[67,648,649],{"class":69,"line":193},[67,650,651],{},"cache2.AddOrUpdate(\"hits\", 1, (key, old) => old + 1);\n",[67,653,654],{"class":69,"line":198},[67,655,104],{"emptyLinePlaceholder":103},[67,657,658],{"class":69,"line":204},[67,659,660],{},"\u002F\u002F TryGetValue, TryAdd, TryRemove — all atomic:\n",[67,662,663],{"class":69,"line":210},[67,664,665],{},"if (cache.TryGetValue(userId, out var existing))\n",[67,667,668],{"class":69,"line":216},[67,669,670],{},"    Console.WriteLine(existing.Name);\n",[67,672,673],{"class":69,"line":371},[67,674,104],{"emptyLinePlaceholder":103},[67,676,677],{"class":69,"line":377},[67,678,679],{},"\u002F\u002F foreach is safe — enumerates a snapshot (may be slightly stale under concurrent writes)\n",[67,681,682],{"class":69,"line":383},[67,683,684],{},"foreach (var (key, value) in cache)\n",[67,686,687],{"class":69,"line":389},[67,688,689],{},"    Console.WriteLine(key);\n",[15,691,692,693,696,697,700,701,704],{},"Avoid wrapping ",[19,694,695],{},"Dictionary\u003CK,V>"," in ",[19,698,699],{},"lock"," — ",[19,702,703],{},"ConcurrentDictionary"," is more efficient\nfor read-heavy workloads using fine-grained locking per bucket.",[10,706,708],{"id":707},"ireadonlylist-vs-immutablelist","IReadOnlyList vs ImmutableList",[44,710,712],{"className":61,"code":711,"language":63,"meta":52,"style":52},"\u002F\u002F IReadOnlyList\u003CT> — view over a mutable list; caller can't add\u002Fremove\nvar internal = new List\u003Cint> { 1, 2, 3 };\nIReadOnlyList\u003Cint> view = internal;\n\u002F\u002F view.Add(4); — compile error\ninternal.Add(4);\nConsole.WriteLine(view.Count); \u002F\u002F 4 — view reflects the change!\n\n\u002F\u002F ImmutableList\u003CT> — truly unchangeable; modifications return a new list\nvar immutable = ImmutableList.Create(1, 2, 3);\nvar updated = immutable.Add(4);   \u002F\u002F new list\nConsole.WriteLine(immutable.Count); \u002F\u002F 3 — original unchanged\nConsole.WriteLine(updated.Count);   \u002F\u002F 4\n\n\u002F\u002F ImmutableArray\u003CT> — better performance than ImmutableList for indexed access:\nvar arr = ImmutableArray.Create(1, 2, 3);\nvar arr2 = arr.Add(4); \u002F\u002F arr still [1, 2, 3]\n",[19,713,714,719,724,729,734,739,744,748,753,758,763,768,773,777,782,787],{"__ignoreMap":52},[67,715,716],{"class":69,"line":70},[67,717,718],{},"\u002F\u002F IReadOnlyList\u003CT> — view over a mutable list; caller can't add\u002Fremove\n",[67,720,721],{"class":69,"line":76},[67,722,723],{},"var internal = new List\u003Cint> { 1, 2, 3 };\n",[67,725,726],{"class":69,"line":82},[67,727,728],{},"IReadOnlyList\u003Cint> view = internal;\n",[67,730,731],{"class":69,"line":88},[67,732,733],{},"\u002F\u002F view.Add(4); — compile error\n",[67,735,736],{"class":69,"line":94},[67,737,738],{},"internal.Add(4);\n",[67,740,741],{"class":69,"line":100},[67,742,743],{},"Console.WriteLine(view.Count); \u002F\u002F 4 — view reflects the change!\n",[67,745,746],{"class":69,"line":107},[67,747,104],{"emptyLinePlaceholder":103},[67,749,750],{"class":69,"line":113},[67,751,752],{},"\u002F\u002F ImmutableList\u003CT> — truly unchangeable; modifications return a new list\n",[67,754,755],{"class":69,"line":170},[67,756,757],{},"var immutable = ImmutableList.Create(1, 2, 3);\n",[67,759,760],{"class":69,"line":175},[67,761,762],{},"var updated = immutable.Add(4);   \u002F\u002F new list\n",[67,764,765],{"class":69,"line":181},[67,766,767],{},"Console.WriteLine(immutable.Count); \u002F\u002F 3 — original unchanged\n",[67,769,770],{"class":69,"line":187},[67,771,772],{},"Console.WriteLine(updated.Count);   \u002F\u002F 4\n",[67,774,775],{"class":69,"line":193},[67,776,104],{"emptyLinePlaceholder":103},[67,778,779],{"class":69,"line":198},[67,780,781],{},"\u002F\u002F ImmutableArray\u003CT> — better performance than ImmutableList for indexed access:\n",[67,783,784],{"class":69,"line":204},[67,785,786],{},"var arr = ImmutableArray.Create(1, 2, 3);\n",[67,788,789],{"class":69,"line":210},[67,790,791],{},"var arr2 = arr.Add(4); \u002F\u002F arr still [1, 2, 3]\n",[15,793,794,796,797,801,802,805,806,809],{},[19,795,248],{}," prevents the ",[798,799,800],"em",{},"caller"," from mutating but the producer can still change\nthe underlying list. ",[19,803,804],{},"ImmutableList\u003CT>"," (from ",[19,807,808],{},"System.Collections.Immutable",") is truly\nimmutable — safe to share across threads without locking.",[10,811,813],{"id":812},"spant-and-memoryt-zero-allocation-slicing","Span\u003CT> and Memory\u003CT> — zero-allocation slicing",[15,815,816,259,819,822],{},[19,817,818],{},"Span\u003CT>",[56,820,821],{},"stack-only"," struct that represents a contiguous memory region without\ncopying it:",[44,824,826],{"className":61,"code":825,"language":63,"meta":52,"style":52},"int[] array = { 1, 2, 3, 4, 5, 6, 7, 8 };\n\n\u002F\u002F Zero-copy slice:\nSpan\u003Cint> slice = array.AsSpan(2, 4); \u002F\u002F [3, 4, 5, 6]\nslice[0] = 99;                          \u002F\u002F modifies the ORIGINAL array!\n\n\u002F\u002F Stack allocation — no heap involved:\nSpan\u003Cbyte> buf = stackalloc byte[1024];\nbuf.Fill(0);\n\n\u002F\u002F String parsing without allocating substrings:\nReadOnlySpan\u003Cchar> csv = \"Alice,30,London\".AsSpan();\nint first  = csv.IndexOf(',');\nReadOnlySpan\u003Cchar> name = csv[..first]; \u002F\u002F \"Alice\" — zero allocation\n\n\u002F\u002F Memory\u003CT> — heap-allocated Span for async code:\nasync Task ProcessAsync(Memory\u003Cbyte> buffer)\n{\n    await stream.ReadAsync(buffer); \u002F\u002F Memory\u003CT> crosses await boundaries\n    ReadOnlySpan\u003Cbyte> span = buffer.Span; \u002F\u002F access as Span in sync code\n}\n",[19,827,828,833,837,842,847,852,856,861,866,871,875,880,885,890,895,899,904,909,913,918,923],{"__ignoreMap":52},[67,829,830],{"class":69,"line":70},[67,831,832],{},"int[] array = { 1, 2, 3, 4, 5, 6, 7, 8 };\n",[67,834,835],{"class":69,"line":76},[67,836,104],{"emptyLinePlaceholder":103},[67,838,839],{"class":69,"line":82},[67,840,841],{},"\u002F\u002F Zero-copy slice:\n",[67,843,844],{"class":69,"line":88},[67,845,846],{},"Span\u003Cint> slice = array.AsSpan(2, 4); \u002F\u002F [3, 4, 5, 6]\n",[67,848,849],{"class":69,"line":94},[67,850,851],{},"slice[0] = 99;                          \u002F\u002F modifies the ORIGINAL array!\n",[67,853,854],{"class":69,"line":100},[67,855,104],{"emptyLinePlaceholder":103},[67,857,858],{"class":69,"line":107},[67,859,860],{},"\u002F\u002F Stack allocation — no heap involved:\n",[67,862,863],{"class":69,"line":113},[67,864,865],{},"Span\u003Cbyte> buf = stackalloc byte[1024];\n",[67,867,868],{"class":69,"line":170},[67,869,870],{},"buf.Fill(0);\n",[67,872,873],{"class":69,"line":175},[67,874,104],{"emptyLinePlaceholder":103},[67,876,877],{"class":69,"line":181},[67,878,879],{},"\u002F\u002F String parsing without allocating substrings:\n",[67,881,882],{"class":69,"line":187},[67,883,884],{},"ReadOnlySpan\u003Cchar> csv = \"Alice,30,London\".AsSpan();\n",[67,886,887],{"class":69,"line":193},[67,888,889],{},"int first  = csv.IndexOf(',');\n",[67,891,892],{"class":69,"line":198},[67,893,894],{},"ReadOnlySpan\u003Cchar> name = csv[..first]; \u002F\u002F \"Alice\" — zero allocation\n",[67,896,897],{"class":69,"line":204},[67,898,104],{"emptyLinePlaceholder":103},[67,900,901],{"class":69,"line":210},[67,902,903],{},"\u002F\u002F Memory\u003CT> — heap-allocated Span for async code:\n",[67,905,906],{"class":69,"line":216},[67,907,908],{},"async Task ProcessAsync(Memory\u003Cbyte> buffer)\n",[67,910,911],{"class":69,"line":371},[67,912,456],{},[67,914,915],{"class":69,"line":377},[67,916,917],{},"    await stream.ReadAsync(buffer); \u002F\u002F Memory\u003CT> crosses await boundaries\n",[67,919,920],{"class":69,"line":383},[67,921,922],{},"    ReadOnlySpan\u003Cbyte> span = buffer.Span; \u002F\u002F access as Span in sync code\n",[67,924,925],{"class":69,"line":389},[67,926,927],{},"}\n",[15,929,930,932,933,936,937,940],{},[19,931,818],{}," cannot cross ",[19,934,935],{},"await"," boundaries (it's a stack ref type). Use ",[19,938,939],{},"Memory\u003CT>"," when\nyou need to store the reference or pass it across async calls.",[10,942,944],{"id":943},"queue-stack-and-sorted-collections","Queue, Stack, and sorted collections",[44,946,948],{"className":61,"code":947,"language":63,"meta":52,"style":52},"\u002F\u002F Queue\u003CT> — FIFO; O(1) Enqueue\u002FDequeue\nvar queue = new Queue\u003CTask>();\nqueue.Enqueue(task1);\nvar next = queue.Dequeue();\n\n\u002F\u002F Stack\u003CT> — LIFO; O(1) Push\u002FPop\nvar history = new Stack\u003CCommand>();\nhistory.Push(cmd);\nvar undone = history.Pop();\n\n\u002F\u002F SortedDictionary\u003CK,V> — red-black tree; O(log n) all ops; iteration in key order\nvar sorted = new SortedDictionary\u003Cint, string>();\nsorted[3] = \"c\"; sorted[1] = \"a\"; sorted[2] = \"b\";\nforeach (var kv in sorted) Console.Write(kv.Key); \u002F\u002F 1 2 3\n\n\u002F\u002F SortedList\u003CK,V> — parallel arrays; O(log n) lookup, O(n) insert\n\u002F\u002F Supports index-based access: sl.Keys[0], sl.Values[0]\n\u002F\u002F More memory-efficient than SortedDictionary for read-heavy, loaded-once data\n",[19,949,950,955,960,965,970,974,979,984,989,994,998,1003,1008,1013,1018,1022,1027,1032],{"__ignoreMap":52},[67,951,952],{"class":69,"line":70},[67,953,954],{},"\u002F\u002F Queue\u003CT> — FIFO; O(1) Enqueue\u002FDequeue\n",[67,956,957],{"class":69,"line":76},[67,958,959],{},"var queue = new Queue\u003CTask>();\n",[67,961,962],{"class":69,"line":82},[67,963,964],{},"queue.Enqueue(task1);\n",[67,966,967],{"class":69,"line":88},[67,968,969],{},"var next = queue.Dequeue();\n",[67,971,972],{"class":69,"line":94},[67,973,104],{"emptyLinePlaceholder":103},[67,975,976],{"class":69,"line":100},[67,977,978],{},"\u002F\u002F Stack\u003CT> — LIFO; O(1) Push\u002FPop\n",[67,980,981],{"class":69,"line":107},[67,982,983],{},"var history = new Stack\u003CCommand>();\n",[67,985,986],{"class":69,"line":113},[67,987,988],{},"history.Push(cmd);\n",[67,990,991],{"class":69,"line":170},[67,992,993],{},"var undone = history.Pop();\n",[67,995,996],{"class":69,"line":175},[67,997,104],{"emptyLinePlaceholder":103},[67,999,1000],{"class":69,"line":181},[67,1001,1002],{},"\u002F\u002F SortedDictionary\u003CK,V> — red-black tree; O(log n) all ops; iteration in key order\n",[67,1004,1005],{"class":69,"line":187},[67,1006,1007],{},"var sorted = new SortedDictionary\u003Cint, string>();\n",[67,1009,1010],{"class":69,"line":193},[67,1011,1012],{},"sorted[3] = \"c\"; sorted[1] = \"a\"; sorted[2] = \"b\";\n",[67,1014,1015],{"class":69,"line":198},[67,1016,1017],{},"foreach (var kv in sorted) Console.Write(kv.Key); \u002F\u002F 1 2 3\n",[67,1019,1020],{"class":69,"line":204},[67,1021,104],{"emptyLinePlaceholder":103},[67,1023,1024],{"class":69,"line":210},[67,1025,1026],{},"\u002F\u002F SortedList\u003CK,V> — parallel arrays; O(log n) lookup, O(n) insert\n",[67,1028,1029],{"class":69,"line":216},[67,1030,1031],{},"\u002F\u002F Supports index-based access: sl.Keys[0], sl.Values[0]\n",[67,1033,1034],{"class":69,"line":371},[67,1035,1036],{},"\u002F\u002F More memory-efficient than SortedDictionary for read-heavy, loaded-once data\n",[10,1038,1040],{"id":1039},"key-performance-tips","Key performance tips",[44,1042,1044],{"className":61,"code":1043,"language":63,"meta":52,"style":52},"\u002F\u002F 1. Pre-size when count is known:\nvar list = new List\u003COrder>(capacity: orders.Length);\nvar dict = new Dictionary\u003Cstring, int>(capacity: 1000);\n\n\u002F\u002F 2. TryGetValue over ContainsKey + indexer:\nif (dict.TryGetValue(key, out int v)) Use(v); \u002F\u002F one lookup\n\n\u002F\u002F 3. CollectionsMarshal for in-place dict value update (.NET 6+):\nref int counter = ref CollectionsMarshal.GetValueRefOrAddDefault(dict, \"key\", out _);\ncounter++;\n\n\u002F\u002F 4. Span over LINQ for hot-path buffer ops:\nvar data = largeArray.AsSpan(offset, length);\nforeach (var n in data) Process(n); \u002F\u002F no enumerator allocation\n\n\u002F\u002F 5. AsReadOnly() to expose List\u003CT> safely:\npublic IReadOnlyList\u003COrder> Orders => _orders.AsReadOnly();\n",[19,1045,1046,1051,1056,1061,1065,1070,1075,1079,1084,1089,1094,1098,1103,1108,1113,1117,1122],{"__ignoreMap":52},[67,1047,1048],{"class":69,"line":70},[67,1049,1050],{},"\u002F\u002F 1. Pre-size when count is known:\n",[67,1052,1053],{"class":69,"line":76},[67,1054,1055],{},"var list = new List\u003COrder>(capacity: orders.Length);\n",[67,1057,1058],{"class":69,"line":82},[67,1059,1060],{},"var dict = new Dictionary\u003Cstring, int>(capacity: 1000);\n",[67,1062,1063],{"class":69,"line":88},[67,1064,104],{"emptyLinePlaceholder":103},[67,1066,1067],{"class":69,"line":94},[67,1068,1069],{},"\u002F\u002F 2. TryGetValue over ContainsKey + indexer:\n",[67,1071,1072],{"class":69,"line":100},[67,1073,1074],{},"if (dict.TryGetValue(key, out int v)) Use(v); \u002F\u002F one lookup\n",[67,1076,1077],{"class":69,"line":107},[67,1078,104],{"emptyLinePlaceholder":103},[67,1080,1081],{"class":69,"line":113},[67,1082,1083],{},"\u002F\u002F 3. CollectionsMarshal for in-place dict value update (.NET 6+):\n",[67,1085,1086],{"class":69,"line":170},[67,1087,1088],{},"ref int counter = ref CollectionsMarshal.GetValueRefOrAddDefault(dict, \"key\", out _);\n",[67,1090,1091],{"class":69,"line":175},[67,1092,1093],{},"counter++;\n",[67,1095,1096],{"class":69,"line":181},[67,1097,104],{"emptyLinePlaceholder":103},[67,1099,1100],{"class":69,"line":187},[67,1101,1102],{},"\u002F\u002F 4. Span over LINQ for hot-path buffer ops:\n",[67,1104,1105],{"class":69,"line":193},[67,1106,1107],{},"var data = largeArray.AsSpan(offset, length);\n",[67,1109,1110],{"class":69,"line":198},[67,1111,1112],{},"foreach (var n in data) Process(n); \u002F\u002F no enumerator allocation\n",[67,1114,1115],{"class":69,"line":204},[67,1116,104],{"emptyLinePlaceholder":103},[67,1118,1119],{"class":69,"line":210},[67,1120,1121],{},"\u002F\u002F 5. AsReadOnly() to expose List\u003CT> safely:\n",[67,1123,1124],{"class":69,"line":216},[67,1125,1126],{},"public IReadOnlyList\u003COrder> Orders => _orders.AsReadOnly();\n",[10,1128,1130],{"id":1129},"recap","Recap",[15,1132,1133,1134,1136,1137,1140,1141,1143,1144,1146,1147,1150,1151,1153,1154,1156,1157,1159,1160,1162,1163,1166,1167,427,1169,1171,1172,1174,1175,1177,1178,1180,1181,1184],{},"Start with ",[19,1135,231],{}," for ordered mutable sequences; use ",[19,1138,1139],{},"T[]"," when the size is fixed.\nUse ",[19,1142,258],{}," for O(1) key-based lookup — always call ",[19,1145,29],{},"\ninstead of ",[19,1148,1149],{},"ContainsKey"," + indexer. Use ",[19,1152,570],{}," for membership tests and uniqueness.\nUse ",[19,1155,703],{}," for thread-safe caches. Return ",[19,1158,248],{}," from public\nAPIs to prevent mutation of internal state. Use ",[19,1161,804],{}," or ",[19,1164,1165],{},"ImmutableArray\u003CT>","\nwhen you need a truly immutable, shareable collection. Use ",[19,1168,818],{},[19,1170,939],{}," to\nslice buffers without allocation — ",[19,1173,818],{}," in synchronous hot paths, ",[19,1176,939],{}," across\nasync calls. Pre-size collections when the count is known. In thread-safe code, use\n",[19,1179,703],{}," rather than a lock-wrapped ",[19,1182,1183],{},"Dictionary",".",[1186,1187,1188],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":52,"searchDepth":76,"depth":76,"links":1190},[1191,1192,1193,1195,1196,1197,1198,1199,1200,1201,1202],{"id":12,"depth":76,"text":13},{"id":41,"depth":76,"text":42},{"id":119,"depth":76,"text":1194},"List\u003CT> vs Array (T)",{"id":252,"depth":76,"text":253},{"id":440,"depth":76,"text":441},{"id":582,"depth":76,"text":583},{"id":707,"depth":76,"text":708},{"id":812,"depth":76,"text":813},{"id":943,"depth":76,"text":944},{"id":1039,"depth":76,"text":1040},{"id":1129,"depth":76,"text":1130},"How to pick the right .NET collection for the job — the internals that affect performance, thread-safe options, immutable vs read-only abstractions, and when Span\u003CT> eliminates allocations entirely.","medium","md",".NET Core","dotnet",{},"\u002Fblog\u002Fdotnet-collections-list-dictionary-span","\u002Fdotnet\u002Fcsharp-core\u002Fcollections",{"title":5,"description":1203},"blog\u002Fdotnet-collections-list-dictionary-span","Collections","C# Core","csharp-core","2026-06-23","8azhzbOhVqCn53PXn0S4dTIYIA0aKlrdj_fPj-yExTA",1782244087105]