AsSpan Instead Of Range

ID

csharp.asspan_instead_of_range

Severity

low

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

Efficiency

Language

CSharp

Tags

efficiency, performance, string

Description

Reports range slicing on a string receiver (s[a..b]) when the sliced value is passed directly as an argument to a method with a ReadOnlySpan<char> overload (Parse / TryParse of the numeric and date types, StringBuilder.Append). In that position the range indexer allocates a substring that the callee immediately consumes; s.AsSpan(a, length) passes a non-allocating window over the same backing storage and the call compiles unchanged.

Rationale

In hot paths (parsing, log processing, request decoding) substring allocations dominate garbage-collection pressure. Switching the argument to AsSpan removes the allocation and the copy entirely. Slices that flow anywhere else — returned, concatenated, stored in a string variable — are not reported: those sinks require a string, so AsSpan does not apply there.

public class Slicer
{
    public void ParseArguments(string s)
    {
        int.TryParse(s[..5], out var x);           // FLAW — TryParse accepts ReadOnlySpan<char>
        var y = int.Parse(s[2..5]);                // FLAW
    }

    public void ParseSpans(string s)
    {
        int.TryParse(s.AsSpan(0, 5), out var x);   // OK — non-allocating
    }

    public string FirstFive(string s)
    {
        return s[..5];                             // OK — result used as string
    }

    public char ByIndex(string s)
    {
        return s[0];                               // OK, scalar index
    }
}

Remediation

Replace s[a..b] with s.AsSpan(a, b - a) when the caller can consume a ReadOnlySpan<char>. When the caller really needs a string, keep the range indexer but consider whether the allocation is necessary.