Parsing Text Efficiently with Span<char> and the 'in' Parameter in C#
Introduction
When I started working on a high‑throughput log‑processing service, I quickly realized that the naive
Why Span and 'in' Matter
Span<T> is a ref‑like, stack‑allocated view over a contiguous block of memory. It can point to either managed arrays,
The
Real‑World Scenario
Imagine you need to parse incoming log lines that follow a simple CSV format:
- Timestamp – ISO‑8601 string
- Level – INFO, WARN, ERROR
- Message – free‑form text
Each line arrives as a
Using Span and thein parameter is not just a micro‑optimization; it changes the way you think about text processing in high‑frequency scenarios.
Implementation Example
Below is a compact, production‑ready utility that splits a log line into its three components without allocating extra strings. The method accepts the raw line as a
using System;
using System.Buffers;
using System.Globalization;
public enum LogLevel { Info, Warning, Error }
public static class LogParser
{
// Helper to trim whitespace from a span
private static ReadOnlySpan<char> Trim(this ReadOnlySpan<char> span)
{
var start = span.Length;
var end = span.Length;
while (start > 0 && char.IsWhiteSpace(span[start - 1])) start--;
while (end > start && char.IsWhiteSpace(span[end - 1])) end--;
return span[start..end];
}
// Main parsing method – note the 'in' parameter
public static (DateTime Timestamp, LogLevel Level, ReadOnlySpan<char> Message) ParseLine(in ReadOnlySpan<char> line)
{
// Split on the first comma only
var comma = line.IndexOf(',');
if (comma < 0) throw new FormatException("Invalid log format");
var timestampSpan = line[..comma].Trim();
var remainder = line[(comma + 1)..];
// Find second comma
var secondComma = remainder.IndexOf(',');
if (secondComma < 0) throw new FormatException("Invalid log format");
var levelSpan = remainder[..secondComma].Trim();
var messageSpan = remainder[(secondComma + 1)..].Trim();
// Parse timestamp – DateTime.ParseExact works with spans
if (!DateTime.TryParseExact(timestampSpan, "yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture, DateTimeStyles.None, out var timestamp))
throw new FormatException("Invalid timestamp");
// Map level string to enum
var levelString = levelSpan.ToString(); // small allocation only for enum lookup
var level = levelString switch
{
"INFO" => LogLevel.Info,
"WARN" or "WARNING" => LogLevel.Warning,
"ERROR" => LogLevel.Error,
_ => throw new FormatException("Unknown log level"),
};
return (timestamp, level, messageSpan);
}
}
The key observations are:
- We accept the line as an
in ReadOnlySpan<char> – no copy is made. - Slicing with
line[..comma] creates newSpan objects that reference the original buffer. - The
Trim extension works directly onSpan and returns another span, again zero‑allocation. - Only the level string is turned into a
string becauseEnum.TryParse does not yet supportReadOnlySpan<char> in all .NET versions. In .NET 6+ you could avoid that allocation by usingEnum.TryParse(levelSpan, true, out var level) .
Best Practices and Gotchas
When you start using
- Stay in the same logical scope. If you need to pass a
Span to a helper that expects astring , be aware of the implicit conversion that allocates. - Prefer
ReadOnlySpan for input parameters. It signals that you will not modify the data and allows the caller to pass bothstring and array without copying. - Benchmark before you optimize. The performance gain is most noticeable when processing many lines; for a handful of operations the extra syntax may not be worth the cognitive load.
- Use
MemoryMarshal when you need to interoperate with native APIs. It lets you get a pointer to the underlying buffer safely.
One common pitfall is assuming that slicing a
Conclusion
The combination of