Modern C# · Lesson 29

Collection expressions need a target

Choose array, list, and read-only interface targets deliberately.

The concept and environment

A collection expression describes elements while its surrounding context supplies a target collection type. In this C# 12 example, the same bracketed values initialize an array, a List, and an IReadOnlyList variable. Those declarations are three separate constructions, not three views obtained from one original collection. Read the declared type before reasoning about operations: an array has a fixed length, a List can grow, and the interface exposes indexed reading without promising a particular implementation. The expression makes creation concise; it does not erase those different contracts or automatically impose deep immutability.

Walk through the portable program

Trace the source before running it. The array contains two and four, so its length is two and its sum is six. The List initially has those same values, then Add appends six, giving a count of three. The independently constructed interface target still has a count of two. Therefore the lines are array:2, list:3, view:2, and sum:6. The final six is the array's sum, not the value appended to the List. Following variable ownership prevents coincidentally equal numbers from hiding which operation produced each observation.

A deeper design boundary

Target typing also applies at method arguments, return expressions, and property initializers. A method accepting an integer array can provide the needed context for a bracketed argument. In contrast, writing var values = [2, 4]; provides no collection target and does not ask the compiler to choose your preferred concrete type. State an appropriate type or call an API whose parameter supplies one. If overloaded APIs accept several collection shapes, inspect overload selection rather than assuming brackets always mean an array. An explicit local can make a difficult call clearer.

Pitfalls and limits

An IReadOnlyList reference restricts available operations through that reference; it is not a universal immutability guarantee. Another program could expose a mutable collection through that interface and retain a writable alias. This sample's interface value has no such source alias because it was constructed independently, but that arrangement is not a promise made by the interface name. Likewise, avoid measuring allocation policy from syntax alone. The compiler can choose storage strategies permitted by the target and language rules, so an observed implementation detail should not become a public ownership assumption.

Practice one: isolate a contract

For a first exercise, add eight only to the array expression. Predict array:3 and sum:14, while the List and interface counts remain three and two. Hint: change only the declaration whose ownership you are testing. Then replace the array with an explicitly declared empty array expression and predict zero for both its length and sum. Keep these expectations separate from the List's behavior. The exercise checks that your prediction comes from the target variable and element values, rather than from a general impression that all three declarations share data.

Practice two: challenge an assumption

For another exercise, create a List first, then assign that same List to an IReadOnlyList variable instead of using a second bracketed expression. Append an element through the List reference and predict that the interface count now changes too. Hint: the assignment copies a reference, whereas the original sample used a separate construction. Draw the two resulting object relationships and explain why identical declared interface types can participate in different ownership arrangements. Do not attempt to demonstrate safety by casting the original interface result to a guessed concrete implementation.

Review the resulting contract

Before using collection expressions in an API, identify the target, the element type, the allowed mutations, and whether any aliases survive. Test an empty input as well as a nonempty one, particularly when a consumer assumes at least one element exists. If ownership independence matters, verify it through a controlled mutation of the actual source, not through the spelling of a type name. These checks make bracket syntax useful in real application code while preserving the distinction between a concise initializer, a fixed-size array, a growable collection, and a restricted interface surface.

A working example

using System;
using System.Collections.Generic;
using System.Linq;
int[] array = [2, 4];
List<int> list = [2, 4];
IReadOnlyList<int> view = [2, 4];
list.Add(6);
Console.WriteLine($"array:{array.Length}");
Console.WriteLine($"list:{list.Count}");
Console.WriteLine($"view:{view.Count}");
Console.WriteLine($"sum:{array.Sum()}");

Expected output

array:2
list:3
view:2
sum:6
Try this
Add a third element to only the array expression and predict its length and sum. The other declarations construct independent collections.

Further reading