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
- Make sure you have the .NET SDK 10.0 or later installed
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: