You've written an extension method that takes a ReadOnlySpan<T>. It's clean, it's fast, and it works beautifully — until someone tries to call it on an array. The compiler shrugs. The conversion exists at runtime, but C# 13 doesn't consider user-defined conversions when resolving extension method receivers. So you duplicate the method signature for T[], then again for Span<T>, and suddenly you've got three overloads doing exactly the same thing.

This has been the reality of span-based API design since Span<T> landed in C# 7.2. The types were powerful, but the language treated them as second-class citizens. Library authors compensated with overload sprawl. Application developers reached for .AsSpan() more often than they should have needed to.

C# 14 puts an end to this with first-class span types — a set of implicit conversions, type inference rules, and overload resolution changes that make Span<T>, ReadOnlySpan<T>, and T[] interchangeable where they logically should be. It's the kind of change that doesn't add new syntax but fundamentally reshapes how you design and consume APIs.

The new implicit span conversions

At the heart of this feature is a new category of implicit conversion — the implicit span conversion — added to the language specification alongside identity, reference, and boxing conversions. These aren't user-defined operators that the compiler ignores in certain contexts. They're built-in conversions that the compiler understands at a fundamental level.

The rules are straightforward:

Notice the covariance support on ReadOnlySpan<T>. A string[] can now implicitly convert to ReadOnlySpan<object>, because string is reference-convertible to object. This mirrors how array covariance already works in .NET, but extends it to the span types that the runtime has historically kept at arm's length.

Example.cs
string[] names = ["Alice", "Bob", "Charlie"];

// All of these now work without explicit conversion
Span<string> nameSpan = names;
ReadOnlySpan<string> readOnlyNames = names;
ReadOnlySpan<object> objectView = names;        // covariant conversion
ReadOnlySpan<object> fromSpan = nameSpan;        // Span<string> -> ReadOnlySpan<object>
ReadOnlySpan<char> chars = "hello";              // string -> ReadOnlySpan<char>

These conversions were technically possible before through user-defined operators on Span<T> and ReadOnlySpan<T>, but user-defined conversions have limited reach. The compiler doesn't consider them for extension method receivers, doesn't use them during type inference, and doesn't compose them with other conversions. By making span conversions first-class, C# 14 unlocks all of these scenarios.

Extension methods that actually work

This is where the practical impact hits hardest. In C# 13, an extension method with a ReadOnlySpan<T> receiver simply wasn't applicable when called on a T[]. The compiler required identity, reference, or boxing conversions for extension receivers — and the array-to-span conversion was none of those.

C# 14 adds implicit span conversions to the list of acceptable conversions for extension method receivers. The result is that a single extension method can now serve arrays, spans, and read-only spans without duplication:

Extensions/SpanExtensions.cs
public static class SpanExtensions
{
    public static bool StartsWith<T>(this ReadOnlySpan<T> span, T value)
        where T : IEquatable<T>
        => span.Length != 0 && span[0].Equals(value);
}
Example.cs
int[] numbers = [1, 2, 3];
Span<int> span = numbers;
ReadOnlySpan<int> readOnly = numbers;

// All three calls resolve to the same method — no overloads needed
bool a = numbers.StartsWith(1);    // array receiver
bool b = span.StartsWith(1);       // Span receiver
bool c = readOnly.StartsWith(1);   // ReadOnlySpan receiver

In C# 13, only the third call would compile. The first two would require explicit .AsSpan() or .AsReadOnlySpan() calls, or you'd need to write two additional overloads.

This is particularly impactful for the BCL itself. The MemoryExtensions class already defines many high-performance methods on ReadOnlySpan<T> and Span<T>. In C# 14, all of those methods become directly callable on arrays:

Example.cs
int[] data = [3, 1, 4, 1, 5, 9];

// These MemoryExtensions methods now bind directly to arrays
bool contains = data.Contains(5);
int index = data.IndexOf(4);

// WARNING

There is one important exception. Implicit span conversions are not considered for extension receivers in method group conversions. This prevents breaking scenarios where a delegate needs to capture a reference type receiver.

Smarter type inference

The type inference engine has been updated to understand the relationships between arrays, Span<T>, and ReadOnlySpan<T>. When a generic method parameter is ReadOnlySpan<T> and you pass a T[], the compiler now infers T correctly without explicit type arguments.

Example.cs
public static T[] FindMatches<T>(ReadOnlySpan<T> source, Func<T, bool> predicate)
{
    var results = new List<T>();
    foreach (var item in source)
    {
        if (predicate(item))
            results.Add(item);
    }
    return results.ToArray();
}

// C# 13: FindMatches<int>(numbers.AsSpan(), n => n > 2)
// C# 14: type inference works directly
int[] numbers = [1, 2, 3, 4, 5];
int[] matches = FindMatches(numbers, n => n > 2);

The inference rules also handle the covariant cases. If you pass a string[] to a method expecting ReadOnlySpan<object>, the compiler infers the type parameter correctly by understanding the covariance relationship.

This matters for library APIs that previously needed array overloads purely because type inference couldn't bridge the gap between T[] and Span<T>.

Overload resolution: spans win

When both a span-based overload and a non-span overload are applicable, the compiler now has explicit betterness rules that prefer the span conversion. This prevents ambiguity errors that would otherwise arise from making span conversions implicit.

The rules work as follows:

  1. If one conversion is an implicit span conversion and the other is not, the span conversion wins
  2. ReadOnlySpan<T> is preferred over Span<T> when both are applicable
  3. These preferences only apply when the expression doesn't exactly match either parameter type
Example.cs
public static class CollectionHelpers
{
    public static void Process<T>(IEnumerable<T> items) { /* slower path */ }
    public static void Process<T>(ReadOnlySpan<T> items) { /* fast path */ }
}

int[] data = [1, 2, 3];
CollectionHelpers.Process(data); // Resolves to ReadOnlySpan<T> overload — no ambiguity

Without the betterness rule, the call above would be ambiguous in C# 14 because both overloads are now applicable (the array converts implicitly to both IEnumerable<T> and ReadOnlySpan<T>). The betterness rule ensures the span overload wins, which is exactly what you want — it's the faster code path.

The preference for ReadOnlySpan<T> over Span<T> is deliberate. It avoids a class of runtime errors with covariant arrays. A string[] stored in an object[] variable can safely convert to ReadOnlySpan<object>, but converting to Span<object> would throw an ArrayTypeMismatchException because Span<T> requires exact type matching for write safety.

Designing APIs with first-class spans

With these changes, the recommended approach for API design shifts. Instead of providing multiple overloads, you can define a single method on ReadOnlySpan<T> and let the compiler handle arrays and spans transparently:

Services/DataProcessor.cs
public class DataProcessor
{
    // One method serves arrays, Span<T>, and ReadOnlySpan<T>
    public double CalculateAverage(ReadOnlySpan<double> values)
    {
        if (values.IsEmpty)
            throw new ArgumentException("Cannot calculate average of empty sequence.");

        double sum = 0;
        foreach (var value in values)
            sum += value;

        return sum / values.Length;
    }

    // If you need to mutate, take Span<T> — arrays and Span<T> will both work
    public void NormaliseInPlace(Span<double> values)
    {
        double max = double.MinValue;
        foreach (var value in values)
        {
            if (value > max)
                max = value;
        }

        for (int i = 0; i < values.Length; i++)
            values[i] /= max;
    }
}
Example.cs
var processor = new DataProcessor();

double[] readings = [1.5, 2.3, 4.1, 3.7];
double avg = processor.CalculateAverage(readings);      // array -> ReadOnlySpan<double>
processor.NormaliseInPlace(readings);                     // array -> Span<double>

Span<double> slice = readings.AsSpan(1, 2);
double sliceAvg = processor.CalculateAverage(slice);     // Span -> ReadOnlySpan

// TIP

Prefer ReadOnlySpan<T> parameters over Span<T> unless your method genuinely needs to mutate the data. This gives callers maximum flexibility and avoids covariant array issues.

Common pitfalls

The Reverse trap

The most widely reported breaking change is with array.Reverse(). In C# 13, calling Reverse() on an array bound to Enumerable.Reverse<T>(), which returns a new IEnumerable<T>. In C# 14, it binds to MemoryExtensions.Reverse<T>(this Span<T>), which reverses in place and returns void.

Example.cs
int[] numbers = [1, 2, 3];

// Bad — this compiled in C# 13 but fails in C# 14
// foreach (var n in numbers.Reverse()) { }

// Good — explicitly call the LINQ version
foreach (var n in numbers.AsEnumerable().Reverse()) { }

// Good — or call it as a static method
foreach (var n in Enumerable.Reverse(numbers)) { }

.NET 10 mitigates this by adding an array-specific Enumerable.Reverse<T>(this T[]) overload, but be aware of this if you're upgrading existing code.

Expression trees and interpretation

Span-based methods like MemoryExtensions.Contains are now preferred over Enumerable.Contains, even inside expression trees. The problem is that ref structs aren't supported by the expression tree interpreter, so code that compiled and ran in C# 13 can throw at runtime in C# 14:

Example.cs
// Bad — binds to MemoryExtensions.Contains in C# 14, fails at runtime with interpretation
// Expression<Func<int[], int, bool>> expr = (arr, n) => arr.Contains(n);
// expr.Compile(preferInterpretation: true);

// Good — explicitly use the Enumerable overload
Expression<Func<int[], int, bool>> expr = (arr, n) => Enumerable.Contains(arr, n);
expr.Compile(preferInterpretation: true);

This also affects LINQ-to-SQL translation engines like Entity Framework Core. If your expression tree visitors expect Enumerable.Contains, they'll now encounter MemoryExtensions.Contains instead. EF Core has been updated to handle this, but custom expression visitors may need attention.

Covariant array pitfalls

Covariant arrays can cause runtime exceptions when converted to Span<T>:

Example.cs
string[] strings = ["hello"];
object[] objects = strings; // covariant array assignment

// Bad — Span<T> requires exact type match, throws ArrayTypeMismatchException
// Span<object> span = objects;

// Good — ReadOnlySpan<T> handles covariance correctly
ReadOnlySpan<object> readOnly = objects;

The betterness rules help here by preferring ReadOnlySpan<T> overloads over Span<T> overloads, but if only a Span<T> overload exists and you're working with covariant arrays, you'll hit runtime errors.

User-defined conversions are suppressed

If a type defines a user-defined conversion operator to or from Span<T>, ReadOnlySpan<T>, or T[] for types where a built-in span conversion now exists, the user-defined conversion is ignored. This is intentional — it prevents ambiguity between the new language-level conversions and existing operator overloads.

Example.cs
// This user-defined conversion is ignored in C# 14
// because the language now has a built-in conversion from T[] to ReadOnlySpan<T>
public static implicit operator ReadOnlySpan<int>(MyCollection c) => c._items;

The BCL's own conversion operators on Span<T> and ReadOnlySpan<T> still exist for backward compatibility and for the compiler to use in code generation, but they're treated as implementation details rather than conversion candidates.

Summary