span-and-readonlyspan.md

July 11, 2026 · View on GitHub

Span and ReadOnlySpan

Specifying Span<T> and ReadOnlySpan<T> work the same as with the array T[] for immediate constructor or method use.

using Shouldly;
using Pure.DI;

DI.Setup(nameof(Composition))
    .Bind<Point>('a').To(() => new Point(1, 1))
    .Bind<Point>('b').To(() => new Point(2, 2))
    .Bind<Point>('c').To(() => new Point(3, 3))
    .Bind<IPath>().To<Path>()

    // Composition root
    .Root<IPath>("Path");

var composition = new Composition();
var path = composition.Path;
path.PointCount.ShouldBe(3);

readonly struct Point(int x, int y)
{
    public int X { get; } = x;

    public int Y { get; } = y;
}

interface IPath
{
    int PointCount { get; }
}

class Path(ReadOnlySpan<Point> points) : IPath
{
    // The 'points' span is allocated on the stack, so it's very efficient.
    // However, we cannot store it in a field because it's a ref struct.
    // We can process it here in the constructor.
    public int PointCount { get; } = points.Length;
}
Running this code sample locally
dotnet --list-sdk
  • Create a net10.0 (or later) console application
dotnet new console -n Sample
dotnet add package Pure.DI
dotnet add package Shouldly
  • Copy the example code into the Program.cs file

You are ready to run the example 🚀

dotnet run

This scenario is even more efficient in the case of Span<T> or ReadOnlySpan<T> when T is a value type. In this case, there is no heap allocation, and the composition root IPath looks like this:

public IPath Path
{
  get
  {
    ReadOnlySpan<Point> points = stackalloc Point[3] { new Point(1, 1), new Point(2, 2), new Point(3, 3) };
    return new Path(points);
  }
}

Constructor injection into a heap type is available for compatibility and reports warning DIW012. Prefer method injection for new code when the stack-only value is only needed during initialization. Generic root arguments with where T : allows ref struct follow the same rules. Pure.DI treats such T as maybe stack-only and emits scoped T in generated root signatures. When a root has several arguments, scoped is applied only to arguments that are stack-only or maybe stack-only themselves. Heap-safe wrappers such as Wrapper<T> remain regular parameters even when T allows ref structs. Factory bodies may resolve and consume stack-only values immediately via ctx.Inject<T>(...) when the API target supports allows ref struct. Pure.DI reports DIE049 when such values are captured behind a generated delegate or deferred factory. Factory overrides may pass stack-only values with ctx.Override<T>(...) or ctx.Let<T>(...) only inside the current synchronous factory frame or the current delegate invocation. Delegate arguments such as T text can be consumed immediately by method injection, but Pure.DI still reports DIE049 if an outer stack-only value is captured by the delegate or if text is passed into a nested/returned delegate. When manual ctx.Override<T>(...) or ctx.Let<T>(...) calls are used in factory delegates, the usual thread-safety rule still applies: wrap the override and the following injection in lock (ctx.Lock) or disable thread safety for known single-threaded compositions. Otherwise Pure.DI reports DIW013. The generated default Func<ReadOnlySpan<char>, T> binding does not use shared override state and does not require a manual lock; use it when the standard Func shape is enough. Generic interfaces are allowed when the implementation is heap-safe, for example class Parser<T> : IParser<T>. Pure.DI reports DIE048 only when the implementation itself is stack-only, such as ref struct Parser<T> : IParser<T>. Pure.DI reports errors when Span<T>, ReadOnlySpan<T>, or custom ref struct values are injected into fields, properties, stored lifetimes, delegate captures, or stack-only interface conversions.

The following partial class will be generated
partial class Composition
{
  public IPath Path
  {
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    get
    {
      Point transientPoint = new Point(1, 1);
      Point transientPoint1 = new Point(2, 2);
      Point transientPoint2 = new Point(3, 3);
      return new Path(stackalloc Point[3] { transientPoint, transientPoint1, transientPoint2 });
    }
  }
}

Class diagram:

---
 config:
  class:
   hideEmptyMembersBox: true
---
classDiagram
	Path --|> IPath
	Composition ..> Path : IPath Path
	Path *-- ReadOnlySpanᐸPointᐳ : ReadOnlySpanᐸPointᐳ
	ReadOnlySpanᐸPointᐳ *-- Point : 'a' Point
	ReadOnlySpanᐸPointᐳ *-- Point : 'b' Point
	ReadOnlySpanᐸPointᐳ *-- Point : 'c' Point
	namespace Pure.DI.UsageTests.BCL.SpanScenario {
		class Composition {
		<<partial>>
		+IPath Path
		}
		class IPath {
			<<interface>>
		}
		class Path {
				<<class>>
			+Path(ReadOnlySpanᐸPointᐳ points)
		}
		class Point {
				<<struct>>
		}
	}
	namespace System {
		class ReadOnlySpanᐸPointᐳ {
				<<struct>>
		}
	}

See also: