Modern C# · Lesson 41

C# 14 span conversions at call boundaries

Compare explicit span calls with generic extension receiver conversions.

External .NET 10 SDK / stable C# 14. This project does not run in SharpRune's .NET 9 playground.

Read or download the complete SDK project, then follow its setup on your computer. The separate C# companion practice below runs in SharpRune’s .NET 9 console environment.

The concept and environment

C# 14 gives span conversions a larger role in language binding, including additional generic inference and extension receiver scenarios. The external project calls one generic ReadOnlySpan extension directly on an integer array and a string. The portable companion uses explicit AsSpan calls and explicit generic arguments in ordinary static invocations. It compares the bounded logic without pretending to reproduce the new binding rules. Arrays could already be viewed as spans in earlier versions; the lesson is about where the compiler now recognizes those relationships, not a claim that spans themselves were invented in C# 14.

Walk through the portable program

The helper first checks that its input is nonempty, then compares the initial element with the expected value. The integer array begins with two, so the first observation is true. The text rune begins with the character r, so the second is true. A separate ReadOnlySpan view is then created over the array, and the owner changes its first element to nine. Reading through the view sees nine. Output is array:True, text:True, and shared:9, demonstrating both the simple predicate and the continued sharing of underlying storage.

Compile the genuine external feature

The actual external extension declares its receiver as ReadOnlySpan of T and constrains T to IEquatable of T. C# 14 resolves the array and string receiver calls using its span conversion rules, with type information also supplied by the expected element. The .NET 10 project explicitly selects language version 14.0 so this is a real compiler exercise. The portable static calls intentionally show the conversion steps rather than presenting the newer fluent form as supported by an older compiler. Both programs preserve the same empty-input guard and equality requirement.

Pitfalls and limits

A read-only span prevents writes through that view, but it does not freeze the owner or copy the viewed elements. The shared observation exists specifically to prevent that misunderstanding. Span lifetime restrictions also remain meaningful; broader conversion support does not allow a span to be captured in an ordinary heap object or retained across arbitrary suspension points. New conversions can affect overload resolution, so inspect real competing signatures when upgrading a library. Avoid casually replacing every collection API with a span API without first understanding ownership, escape constraints, and the consumer's supported compiler environment.

Practice one: isolate a contract

First pass an empty integer array and predict false without an indexing exception. Then use a nonempty array whose first element differs from the expected value and predict false again. Hint: these outcomes challenge the length guard and the equality comparison independently. In the external project, keep direct receiver calls so the real C# 14 binding remains exercised. In the portable project, retain explicit conversions and type arguments rather than making unsupported syntax appear to work by substituting a different hidden implementation at execution time.

Practice two: challenge an assumption

For another exercise, create a copied array and a view over that copy before mutating the original owner. Predict that the view over the original changes while the view over the copy does not. Hint: the independence comes from copying storage, not from adding ReadOnly to the span type. Then compare a synchronous span-consuming helper with an API that must retain data for later asynchronous work. Design that later boundary around an owned array or another suitable lifetime-aware representation, and test the ownership policy separately from conversion convenience.

Review the resulting contract

Review a span-bound API by identifying its accepted sources, generic inference requirements, overload choices, lifetime, and sharing semantics. The external program proves an actual C# 14 call shape; the portable comparison proves the deliberately small predicate in the current engine. Neither result is an allocation benchmark or a guarantee of immutable input. Clear conversion syntax can make low-level APIs easier to call, but reliable design still requires a concrete answer to who owns the storage and whether another reference may change it while the view remains in use.

External .NET 10 SDK / stable C# 14. This project does not run in SharpRune's .NET 9 playground.

C# 14 span conversions at call boundaries lab

Set up the SDK project

  1. Save every listed file at its exact relative path in a fresh directory.
  2. Install a .NET 10 SDK supporting stable C# 14; check dotnet --version.
  3. Run dotnet run --project FeatureLab.csproj. Keep this external code separate from the .NET 9 playground.

Download complete SDK project ↗

Project files

Program.csDownload file ↗
int[] numbers = [2, 4];
Console.WriteLine($"array:{numbers.StartsWithValue(2)}");
Console.WriteLine($"text:{"rune".StartsWithValue('r')}");
ReadOnlySpan<int> view = numbers;
numbers[0] = 9;
Console.WriteLine($"shared:{view[0]}");
SpanRules.csDownload file ↗
public static class SpanRules
{
    public static bool StartsWithValue<T>(this ReadOnlySpan<T> values, T expected) where T : IEquatable<T>
        => values.Length != 0 && values[0].Equals(expected);
}
FeatureLab.csprojDownload file ↗
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <LangVersion>14.0</LangVersion>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>

Expected result

Expected console output, in this order: array:True text:True shared:9 The portable companion compares the same small rule using older syntax; it does not compile or emulate the new feature.

Use the listed SDK and dependencies for this project. Open C# practice runs the separate portable exercise.

C# companion practice

using System;
int[] numbers = [2, 4];
Console.WriteLine($"array:{SpanRules.StartsWithValue<int>(numbers.AsSpan(), 2)}");
Console.WriteLine($"text:{SpanRules.StartsWithValue<char>("rune".AsSpan(), 'r')}");
ReadOnlySpan<int> view = numbers;
numbers[0] = 9;
Console.WriteLine($"shared:{view[0]}");
static class SpanRules
{
    public static bool StartsWithValue<T>(ReadOnlySpan<T> values, T expected) where T : IEquatable<T>
        => values.Length != 0 && values[0].Equals(expected);
}
Open C# practice

C# practice output

array:True
text:True
shared:9
Try this
Use an empty array to test the guard. A read-only view still observes changes through the owning array.

Further reading