Span<T> and Collection Expressions
Collection expressions written as [1, 2, 3], and Span<T> as a window onto memory you already have, widened in C# 14.
Span<int> window = numbers.AsSpan(1, 2);A collection expression is square brackets holding items: [1, 2, 3]. It arrived in C# 12. The compiler looks at the type on the left of the assignment and builds that type, so the same brackets can produce an array, a List<T> or a span. Two dots inside the brackets, [..first, ..second], spread another collection's items into the new one.
Span<T> is a window, not a container
A Span<T> describes a stretch of memory that already exists somewhere else, usually part of an array or a string. It copies nothing. It has Length and an indexer, and writing through it changes the original. ReadOnlySpan<T> is the same window with writing removed. Slicing is the point: numbers.AsSpan(1, 2) gives you two elements of numbers without allocating a second array.
What C# 14 changed
C# 14, which ships with .NET 10, makes these implicit span conversions part of the language itself: array to Span<E>, array to ReadOnlySpan<U>, Span<E> to ReadOnlySpan<U>, ReadOnlySpan<E> to ReadOnlySpan<U> where E converts to U, and string to ReadOnlySpan<char>. Earlier compilers knew about conversion operators on those types but would not apply them in every position. Now they also work for an extension member's receiver and during generic type inference.
Where it goes wrong
A span may only live on the stack, so you cannot store one in a field of a class. There is no span over a List<T> in the everyday API; CollectionsMarshal.AsSpan gives one, and any Add afterwards can leave that span pointing at memory the list no longer uses.
Example
int[] numbers = [4, 8, 15, 16]; // the same array as new int[] { 4, 8, 15, 16 }
List<int> more = [23, 42]; // the same list as new List<int> { 23, 42 }
// .. spreads another collection's items into this one.
int[] all = [..numbers, ..more, 99];
// A window over positions 1 and 2 of numbers. Nothing is copied.
Span<int> window = numbers.AsSpan(1, 2);
window[0] = 80; // writes straight into numbers
Console.WriteLine($"all: {string.Join(", ", all)}");
Console.WriteLine($"numbers now: {string.Join(", ", numbers)}");
Console.WriteLine($"window length: {window.Length}");
// A Span<int> converts to a ReadOnlySpan<int>, so a method that only reads
// takes the read-only form and still accepts a writable span or an array.
Console.WriteLine($"sum of window: {Sum(window)}");
static int Sum(ReadOnlySpan<int> values)
{
int total = 0;
foreach (int value in values)
{
total += value;
}
return total;
}
// Output:
// all: 4, 8, 15, 16, 23, 42, 99
// numbers now: 4, 80, 15, 16
// window length: 2
// sum of window: 95When to use it
- Parsing a fixed-format log line into fields without allocating a string per field.
- Passing part of a large buffer to a method that should not see or change the rest.
- Writing [] instead of new List<string>() when the type is already stated on the left.
- Building one array out of two others plus an extra item, in a single expression.
More examples
The same brackets, four target types
Worth reading once so the syntax is recognisable in other people's code, where [] is now the common way to write a small or empty collection. The spread form replaces the usual AddRange dance when you build one collection out of others.
int[] asArray = [1, 2, 3];
List<int> asList = [1, 2, 3];
Span<int> asSpan = [1, 2, 3];
ReadOnlySpan<char> letters = ['a', 'b'];
// The brackets mean "build the type on the left". The expression has no type
// of its own, which is why var does not work with one.
int[] combined = [0, ..asArray, 4];
Console.WriteLine($"array: {string.Join(", ", asArray)}");
Console.WriteLine($"list count: {asList.Count}");
Console.WriteLine($"span length: {asSpan.Length}");
Console.WriteLine($"letters: {letters[0]}{letters[1]}");
Console.WriteLine($"combined: {string.Join(", ", combined)}");
// Output:
// array: 1, 2, 3
// list count: 3
// span length: 3
// letters: ab
// combined: 0, 1, 2, 3, 4Slicing a string without allocating
This is where spans pay for themselves in a real service: parsing a header, a CSV line or a query string thousands of times a second without creating a short-lived string for every field. Convert to a string only where you need to keep the text.
string line = "name=ana";
int split = line.IndexOf('=');
// Substring makes a new string, with its own copy of the characters.
string valueCopy = line.Substring(split + 1);
// AsSpan makes a window instead: the same characters, no new string.
ReadOnlySpan<char> valueView = line.AsSpan(split + 1);
Console.WriteLine($"copy: {valueCopy}");
Console.WriteLine($"view length: {valueView.Length}");
// A string converts implicitly to ReadOnlySpan<char>, so span methods
// accept a plain string argument.
Console.WriteLine($"starts with a: {valueView.StartsWith("a")}");
Console.WriteLine($"same text: {valueView.SequenceEqual(valueCopy)}");
// Output:
// copy: ana
// view length: 3
// starts with a: True
// same text: TrueThe C# 14 span conversions
The Span<string> to ReadOnlySpan<object> line is the part that is new in C# 14 and .NET 10; earlier compilers reject it. Because these conversions now take part in choosing between overloads, upgrading a project can change which overload of a library method is picked, so run the tests rather than assuming the behaviour is identical.
int[] numbers = [10, 20, 30];
// In C# 14 these are language conversions, called implicit span conversions.
Span<int> writable = numbers; // array to Span<T>
ReadOnlySpan<int> readable = writable; // Span<T> to ReadOnlySpan<T>
string[] names = ["ana", "bo"];
Span<string> nameSpan = names;
ReadOnlySpan<object> asObjects = nameSpan; // allowed in C# 14: a string is an object
writable[0] = 11; // a span is a window, so this changes numbers itself
Console.WriteLine($"numbers[0]: {numbers[0]}");
Console.WriteLine($"readable[0]: {readable[0]}");
Console.WriteLine($"objects: {asObjects.Length}");
Console.WriteLine($"first name: {asObjects[0]}");
// Output:
// numbers[0]: 11
// readable[0]: 11
// objects: 2
// first name: ana
Discussion