[Everyday C#] Phase E, PR 14a: Statements: collections + LINQ#54807
[Everyday C#] Phase E, PR 14a: Statements: collections + LINQ#54807BillWagner wants to merge 6 commits into
Conversation
Phase E, PR 14a. Adds two example-heavy Fundamentals concept articles under fundamentals/statements/ with compiling net10.0 snippet projects: - collections.md: arrays, List<T>, Dictionary<TKey,TValue>, adding/ removing/searching elements, collection expressions (C# 12), indexes and ranges (C# 8). - linq.md: query syntax, method syntax, filter/map/reduce/sort/group, lambda expressions, deferred vs eager evaluation. Adds Collections and LINQ entries to the Expressions and statements TOC node. Advanced topics (providers, IQueryable, expression trees, PLINQ, performance, custom collections/operators) are intentionally left to the Language Reference and LINQ sections and linked out. Closes dotnet#53556 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7f9f7f68-82b8-43d2-a511-1a4f07138403
There was a problem hiding this comment.
Pull request overview
Adds two new “Everyday C#” Fundamentals concept articles under fundamentals/statements/ (Collections and LINQ), backed by new compiling snippet projects, and wires both pages into the C# TOC.
Changes:
- Adds new Fundamentals articles: Collections and LINQ queries.
- Adds new snippet projects (
collections-statementsandlinq-statements) that supply all referenced:::coderegions. - Updates
docs/csharp/toc.ymlto include the new pages under Fundamentals → Expressions and statements.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| docs/csharp/toc.yml | Adds TOC entries for the new Collections and LINQ Fundamentals pages. |
| docs/csharp/fundamentals/statements/collections.md | New concept article introducing arrays, List<T>, dictionaries, collection expressions, and indexes/ranges with snippet-backed examples. |
| docs/csharp/fundamentals/statements/linq.md | New concept article introducing LINQ query/method syntax, common operators, grouping, and deferred execution with snippet-backed examples. |
| docs/csharp/fundamentals/statements/snippets/collections-statements/Program.cs | Snippet source for the Collections article :::code regions. |
| docs/csharp/fundamentals/statements/snippets/collections-statements/collections-statements.csproj | New net10.0 snippet project for Collections examples. |
| docs/csharp/fundamentals/statements/snippets/linq-statements/Program.cs | Snippet source for the LINQ article :::code regions. |
| docs/csharp/fundamentals/statements/snippets/linq-statements/linq-statements.csproj | New net10.0 snippet project for LINQ examples. |
Rename collections.md H1/title to "Arrays, lists, and dictionaries" to resolve the duplicate-H1 build warning, and collapse multiple-blank-line runs (MD012) in both statements articles. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7f9f7f68-82b8-43d2-a511-1a4f07138403
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7f9f7f68-82b8-43d2-a511-1a4f07138403
This is looking a little better.
adegeo
left a comment
There was a problem hiding this comment.
Very good! I wrote my comments as if talking to LLM, so they're curt.
| Console.WriteLine($"Array: {string.Join(", ", sprintPlan)}"); // => Array: design, code, test | ||
| Console.WriteLine($"List count: {backlog.Count}"); // => List count: 3 | ||
| Console.WriteLine($"Priority for docs: {priorities["docs"]}"); // => Priority for docs: 2 |
There was a problem hiding this comment.
Is this the way you actually wanted the comments? Or did you mean to do what the other docs do
Console.WriteLine($"Array: {string.Join(", ", sprintPlan)}"); // =>
Console.WriteLine($"List count: {backlog.Count}"); // =>
Console.WriteLine($"Priority for docs: {priorities["docs"]}"); // =>
// This example produces the following output:
//
// Array: design, code, test
// List count: 3
// Priority for docs: 2
//|
|
||
| ## 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. |
There was a problem hiding this comment.
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="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. |
There was a problem hiding this comment.
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?
| 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"::: |
There was a problem hiding this comment.
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.
|
|
||
| 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"::: |
There was a problem hiding this comment.
There should be a comment in this referenced code that calls out the spread. People may not know what spread looks like.
|
|
||
| :::code language="csharp" source="./snippets/linq-statements/Program.cs" id="LambdaExpressions"::: | ||
|
|
||
| In `name => name.Length == 3`, `name` is the input element and `name.Length == 3` is the Boolean expression that decides whether the element belongs in the result. For more information about lambda expressions and delegate types, see [Lambda expressions, delegates, and events](../types/delegates-lambdas.md) in the fundamentals. For detailed information about lambda expression syntax, see [Lambda expressions - Lambda expressions and anonymous functions](../../language-reference/operators/lambda-expressions.md) in the language reference. |
There was a problem hiding this comment.
Having two big links feel like too much. Is one better than the other? For this small paragraph, 70% of it is text about "for more information" 😁
|
|
||
| Query-syntax clauses use lambda expressions too. Clauses such as `where`, `orderby`, and `select` compile to method calls that take lambda expressions. The range variable becomes the lambda parameter, and the clause expression becomes the lambda body. Query syntax is a concise way to write those same lambdas: | ||
|
|
||
| :::code language="csharp" source="./snippets/linq-statements/Program.cs" id="QuerySyntaxLambda"::: |
There was a problem hiding this comment.
I think in this reference code example, having both query and method syntax console output being the same exact values would be helpful. This way it demonstrates that both versions of the syntax produced the same results.
|
|
||
| ## Common LINQ methods | ||
|
|
||
| Use <xref:System.Linq.Enumerable.Where*> to keep only the elements that match a condition. Use <xref:System.Linq.Enumerable.Select*> to transform each element into a new value; C# calls this operation a *projection*. Use <xref:System.Linq.Enumerable.OrderBy*> to sort elements. Use aggregation methods such as <xref:System.Linq.Enumerable.Sum*>, <xref:System.Linq.Enumerable.Count*>, and <xref:System.Linq.Enumerable.Aggregate*> to produce a single value from all elements in the data source. These methods are in the <xref:System.Linq> namespace and work with sequences such as <xref:System.Collections.Generic.IEnumerable`1>. |
There was a problem hiding this comment.
This should be a small intro paragraph and a bullet point list of items.
GroupBy is a subsection, which is OK, but it should be listed here too.
|
|
||
| ## Run a query | ||
|
|
||
| Many LINQ operators use *deferred execution*. Deferred execution means operators that return a sequence, such as <xref:System.Linq.Enumerable.Where*>, <xref:System.Linq.Enumerable.Select*>, and <xref:System.Linq.Enumerable.OrderBy*>, don't run when you define them. They build the recipe for producing results. A `foreach` loop is one way to run that recipe: |
There was a problem hiding this comment.
I think this is really important to also call out at the start of the article. If someone doesn't make it this far, hopefully they got it up front.
| :::code language="csharp" source="./snippets/linq-statements/Program.cs" id="DeferredExecution"::: | ||
|
|
||
| Other operations run a query immediately. Operators that return a single value, such as <xref:System.Linq.Enumerable.Count*>, <xref:System.Linq.Enumerable.Sum*>, <xref:System.Linq.Enumerable.First*>, and <xref:System.Linq.Enumerable.Any*>, must read the elements when you call them so they can produce that value. | ||
|
|
||
| :::code language="csharp" source="./snippets/linq-statements/Program.cs" id="ImmediateExecution"::: |
There was a problem hiding this comment.
I think these code snippets should call out in a comment when the deferred evaluation happens.
Everyday C# — Phase E, PR 14a: Statements: collections + LINQ
Closes #53556
Adds two example-heavy Fundamentals concept articles under
fundamentals/statements/, each backed by a compilingnet10.0snippet project, and wires them into the TOC. Every code sample is included from an external snippet via:::coderegions and shows its result as a trailing// =>comment; the samples saturate the everyday feature set (collection expressions, file-scoped namespaces, nullable enabled, target-typednew).New articles
fundamentals/statements/collections.md— arrays,List<T>,Dictionary<TKey,TValue>; adding, removing, and searching elements; collection expressions (C# 12); indexes and ranges (C# 8) applied to collections. Introduces collection, element, sequence vs map, generic type / type safety, index, range, and collection expression at first use.fundamentals/statements/linq.md— query syntax and method (fluent) syntax; the functional trio filter / map / reduce mapped toWhere/Select/Aggregate·Sum·Count, plusOrderByandGroupBy; lambda (anonymous function) expressions in LINQ; deferred execution vs eager evaluation. Names LINQ providers as a first-class use and links out for provider-based queries.TOC
CollectionsandLINQadded under Fundamentals → Expressions and statements, afterIteration statements.Scope discipline — deliberately left out of Fundamentals (link-out targets)
No existing content was cut (both articles are new); the following advanced topics are intentionally kept out of Fundamentals and linked to their proper homes, per Goal 11 / Filter A:
collections.md→language-reference/builtin-types/arrays.md/collections.md→ multidimensional arrays, covariance, collection taxonomy are reference-level.collections.md→language-reference/operators/collection-expressions.md/member-access-operators.md→ conversion/spread rules and full[]/^/..operator rules are reference-level.collections.md→standard/collections/*→ choosing among specialized/concurrent/immutable collections and custom comparers fail universality.linq.md→linq/*(LINQ section) → joins, providers/IQueryable, deferred-execution mechanics, custom operators, XML/EF behavior are LINQ-focus material.linq.md→ PLINQ / expression trees / performance → parallel and advanced-representation material.Validation
net10.0; every// =>output verified by running the programs.#indexer-operator-anchor, and all<xref:...>UIDs verified to resolve; version claims (collection expressions → C# 12,^/..→ C# 8) confirmed.programming-guide/*link destinations (that area is slated for retirement).Opened as draft for review.
Internal previews