Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions docs/csharp/fundamentals/statements/collections.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
---
title: "Common collection types in C#"
description: Store, update, search, and read groups of values by using arrays, lists, dictionaries, collection expressions, indexes, and ranges.
ms.date: 07/15/2026
ms.topic: concept-article
ai-usage: ai-assisted
---

# Common collection types

> [!TIP]
> This article is part of the **Fundamentals** section for developers who already know at least one programming language and are learning C#. If you're new to programming, start with the [Get started](../../tour-of-csharp/tutorials/index.md) tutorials first. For more information about collections, see [Collections](../../language-reference/builtin-types/collections.md) in the language reference. For more information about arrays, see [The array reference type](../../language-reference/builtin-types/arrays.md) in the language reference. For more information about collection expressions, see [Collection expressions (Collection literals)](../../language-reference/operators/collection-expressions.md) in the language reference.
>
> **Coming from another language?** C# arrays are fixed-size ordered collections, like arrays in Java or C++. <xref:System.Collections.Generic.List`1> is the everyday growable sequence, like Java's `ArrayList`, JavaScript's arrays, or Python's lists. <xref:System.Collections.Generic.Dictionary`2> stores values by key, like maps or dictionaries in many languages.

A *collection* is an object that stores multiple related values. Each value in a collection is an *element*. Choose an array when the set is fixed, such as the status names your work-item tracker always uses. Choose a <xref:System.Collections.Generic.List`1> when items come and go, such as a backlog that gains new work and drops completed items. Choose a <xref:System.Collections.Generic.Dictionary`2> when you look up values by a key, such as finding a work item by ID or a setting by name. The next section turns those examples into a quick decision model.

## Choose a collection shape

Choose the collection that matches how your code uses the data. A *sequence* stores elements in order so you can reach them by position. Use an *array*, which is represented by <xref:System.Array>, when you know the number of positions the sequence needs. An array's length can't change after you create it, but you can replace the element stored at an existing position. Use <xref:System.Collections.Generic.List`1> when you need a sequence that can add or remove elements. A *map* stores values that you reach by key instead of by position. Use <xref:System.Collections.Generic.Dictionary`2> when each element has a lookup key, such as a name, ID, or code. The examples use a *collection expression*, which creates a collection from expressions between square brackets; [later in this article](#create-collections-with-collection-expressions), you learn the syntax in more detail.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is "map" what we want people to use when referring to dictionaries? Because we're using it here like it is. In the note near the intro we say "Map or dictionary" to at least bring the user into the thought of "I know what a map is, oh it's called dictionary here." Continuing to use Map seems wrong.


:::code language="csharp" source="./snippets/collections-statements/Program.cs" id="ChooseCollection":::

<xref:System.Collections.Generic.List`1> and <xref:System.Collections.Generic.Dictionary`2> are *generic types*. A generic type uses a type argument, such as `string` or `int`, to say what kind of values it stores. Generics let you reuse the same collection shape, such as a list or dictionary, with different element types, such as `string`, `int`, or a custom type. They also provide *type safety*: the compiler guarantees every element is the declared type, so you don't need casts and can't accidentally store the wrong type. For more information about generic types, see [Generic types and methods](../types/generics.md) in the fundamentals.

## Store fixed-size data in arrays

An array is an ordered collection with a fixed length. You access an array element by its *index*, which is its zero-based position in the array. Index `0` is the first element, index `1` is the second element, and so on.

:::code language="csharp" source="./snippets/collections-statements/Program.cs" id="Arrays":::

Use `foreach` when you want to read every element in order. Use an index when the position matters, such as when you need the first element, the last element, or the position returned by <xref:System.Array.IndexOf*?displayProperty=nameWithType>.

The length of an array is fixed, but the elements in that array can change. Assign a new value to an existing index when the position stays the same but the stored value needs an update:

:::code language="csharp" source="./snippets/collections-statements/Program.cs" id="ArrayElementUpdate":::

## Grow and shrink a sequence with `List<T>`

A <xref:System.Collections.Generic.List`1> stores elements in order and can grow or shrink as your program runs. Use <xref:System.Collections.Generic.List`1.Add*> to append an element, <xref:System.Collections.Generic.List`1.Remove*> to remove a matching element, <xref:System.Collections.Generic.List`1.Contains*> to test whether an element exists, and <xref:System.Collections.Generic.List`1.IndexOf*> to find an element's position. Use the list indexer to replace the value at an existing position.

:::code language="csharp" source="./snippets/collections-statements/Program.cs" id="ListChanges":::

A <xref:System.Collections.Generic.List`1> keeps the remaining elements in order when you add or remove items. When you remove an element, later elements move to lower indexes.

Use <xref:System.Collections.Generic.List`1.Insert*> to add one element at a specific position, <xref:System.Collections.Generic.List`1.InsertRange*> to add several elements at a position, and <xref:System.Collections.Generic.List`1.RemoveAt*> to remove an element by index:

:::code language="csharp" source="./snippets/collections-statements/Program.cs" id="ListInsertRemove":::

Adding or removing at the end of a <xref:System.Collections.Generic.List`1> is fast. Adding with <xref:System.Collections.Generic.List`1.Add*> is an O(1) operation on average, and removing the last element with <xref:System.Collections.Generic.List`1.RemoveAt*> is O(1). Inserting or removing at the front or middle is O(n) because every later element shifts to a new index. Big-O notation describes how the work grows as the collection size, `n`, grows. O(1), pronounced "order one," means the operation takes about the same amount of work no matter how many elements the collection contains. O(n), pronounced "order n," means the work grows roughly in proportion to the number of elements. If your code frequently inserts or removes at the front, a <xref:System.Collections.Generic.List`1> might be the wrong collection shape.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Big-O notation should be a tip note.

Is the description of how O(n) is working correct? Wouldn't it be that it grows roughly in proportion to how many elements are ahead of the insertion index?


## Associate a value by key with `Dictionary<TKey,TValue>`

A <xref:System.Collections.Generic.Dictionary`2> stores key/value pairs. A *key/value pair* is one key and the value associated with that key; .NET represents one pair with <xref:System.Collections.Generic.KeyValuePair`2>. Use the dictionary indexer to add or update a value by key, and use <xref:System.Collections.Generic.Dictionary`2.TryGetValue*> when the key might not exist.

:::code language="csharp" source="./snippets/collections-statements/Program.cs" id="DictionaryLookup":::

An *indexer* lets you use bracket syntax to access a value from an object. The dictionary indexer is useful when the key must exist or when you're assigning a value. <xref:System.Collections.Generic.Dictionary`2.TryGetValue*> is safer for reads when the key might be missing because it reports both outcomes without throwing an exception. For more information about the indexer operator, see [Member access operators](../../language-reference/operators/member-access-operators.md#indexer-operator-) in the language reference.

You can also change the value associated with a key that already exists. When your code knows the dictionary contains the key, assign through the indexer. When the key might or might not exist and you need to react to the current value, use <xref:System.Collections.Generic.Dictionary`2.TryGetValue*> first, then assign the new value:

:::code language="csharp" source="./snippets/collections-statements/Program.cs" id="DictionaryUpdates":::
Comment on lines +58 to +62

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about all of this. It's kind of repeating (content and code) from above.

Also, it says "it's safer to use TryGetValue because it does XYZ" but it doesn't say safer than what. So does it add value? Is it that important to call out? The TryGet pattern is there to be helpful and more efficient than the Contains+Get combination. And Contains+Get is there to prevent you from using Get when the key doesn't exist and thus crashing or having to handle the exception. But then you need to explain all of that and it seems more than the scope of this article. I suggest just demonstrating TryGetValue in the code, but not going into details as to why this was chosen.


## Create collections with collection expressions

A *collection expression* creates a collection from expressions between square brackets. Beginning with C# 12, collection expressions can create arrays, lists, and other collection types. A *spread element* copies the elements from another collection into the new collection.

:::code language="csharp" source="./snippets/collections-statements/Program.cs" id="CollectionExpressions":::

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be a comment in this referenced code that calls out the spread. People may not know what spread looks like.


Collection expressions keep initialization concise. A collection expression has no type of its own. Because `[...]` is typeless by itself, the compiler converts the expression into the collection shape the context calls for. The receiving variable or parameter can turn it into an array, a <xref:System.Collections.Generic.List`1>, or other supported collection type.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using [...] could be confusing considering the spread is used in the previous code snippet.


## Read from positions with indexes and ranges

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole section is a bunch of small paragraphs detailing how index/range work. However, index is only talked about once and the rest are ranges. Would a different presentation style be better?


An <xref:System.Index> can count from the end of a sequence with `^`, and a <xref:System.Range> can select a slice with `..` (a sequence of 2 dots). Arrays and `List<T>` support both indexes and ranges.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this say that index can count from either the beginning or the end of a sequence?
Should this call out that Range .. isn't the spread operator?

Update: After I finished reading this a few times, the code really showcases the examples well. I wonder if the range stuff can be grouped into more conversational prose instead of fact-based paragraphs. For example, you could group all of the ..3, 3.., .. into a single paragraph talking about omitting the one or both indexes.


Use a from-end index with `^` when you want to count backward from the end. The expression `phases[^1]` reads the last element, and `phases[^2]` reads the next-to-last element.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think because this section is trying to describe things from the context of "what's new" in C#, it's omitting basic info, such as that you can use a number to count from the start. I don't think the difference needs to be talked about, but just giving an example will communicate that. It currently sounds like index is only useful for from-end.

Something like

An index can count from the start or the end of a collection. Use the index number directly when counting from the start. the expression phases[0] reads the first element, and phases[1] reads the second element. Use ^ to count from the end of the collection. The expression phases[^1] reads the...


Use a range `a..b` when you want elements from position `a` up to, but not including, position `b`. The expression `phases[1..3]` creates a new array that contains positions `1` and `2`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's very important to call out that it DOES NOT INCLUDE the last item. Bold+italic perhaps?

Also, the array doesn't contain the positions, it contains the elements that were in positions 1 and 2. And is "position" correct? Should it be "index?"


Use an open-start range `..b` when the slice starts at the beginning. The expression `phases[..2]` returns the first two elements.

Use an open-end range `a..` when the slice continues through the last element. The expression `phases[2..]` returns the elements from position `2` to the end.

Use the full range `..` when you want a copy of the whole array. The expression `phases[..]` creates a new array with all the same elements.

:::code language="csharp" source="./snippets/collections-statements/Program.cs" id="IndexesAndRanges":::

Indexes answer "which single element?" Ranges answer "which contiguous slice?" Use indexes when your code needs one position and ranges when the subsection is part of the data you're working with. For more information about indexes and ranges, see the [Explore ranges of data using indices and ranges](../../tutorials/ranges-indexes.md) tutorial.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this statement about what index and range are for should be at the start of this section.


## See also

- [Iteration statements](iteration.md)
- [LINQ queries](linq.md)
- [Collections](../../language-reference/builtin-types/collections.md)
- [Arrays](../../language-reference/builtin-types/arrays.md)
- [Collection expressions](../../language-reference/operators/collection-expressions.md)
- [Member access operators](../../language-reference/operators/member-access-operators.md)
- [Explore indexes and ranges](../../tutorials/ranges-indexes.md)
Loading
Loading