-
Notifications
You must be signed in to change notification settings - Fork 6.1k
[Everyday C#] Phase E, PR 14a: Statements: collections + LINQ #54807
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
95335bf
ac8c6ba
10fcf53
38ec8ab
5607cf7
5d0e89b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. | ||
|
|
||
| :::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. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"::: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using |
||
|
|
||
| ## Read from positions with indexes and ranges | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? 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 |
||
|
|
||
| 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. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||
|
|
||
| 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`. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment.
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.