C# String Slicing with Range
c# string slicing with range: Learn how to slice strings with the C# range operator, including syntax, performance tradeoffs, and practical examples.
The C# range operator .. lets you slice strings using index expressions, offering a concise alternative to the traditional Substring method. This article explains how c# string slicing with range works, its performance implications, and where it fits in your codebase.
Understanding the Index and Range Types
The Index type represents a position in a sequence, either from the start (^ operator) or from the end. For example, ^1 refers to the last element. The Range type represents a start and end index, created with the .. operator.
Index start = 1; Index end = ^1; Range range = start..end;
The Range can be used directly with string indexing: myString[range] returns a new string containing the characters between the start and end positions. The start is inclusive, the end is exclusive.
Here's a minimal example:
string text = "Hello, World"; string sliced = text[7..12]; // "World"
The range operator is not just for strings; it works with arrays, spans, and other types that support indexing and slicing. For strings, the result is always a new string instance.
Using the Range Operator for String Slicing
The syntax str[start..end] is sugar for str[new Range(start, end)]. Both start and end can be Index values, which can be integers or the ^ operator expressions. You can also omit either side to indicate the beginning or end of the string.
string url = "https://example.com"; string domain = url[8..]; // "example.com" string withoutProtocol = url[..^0]; // the whole string
In url[8..], the start index 8 skips https://, and the omitted end defaults to the length of the string. The ^0 denotes the position just past the last character, so url[..^0] is the entire string. In practice, you'll use ^0 rarely; omitting the end is clearer.
Common Slicing Scenarios
Consider extracting file extensions, path components, or fixed-width fields. With ranges, these operations become more readable.
string filename = "report_2024.csv"; string extension = filename[^3..]; // "csv" string baseName = filename[..^4]; // "report_2024"
filename[^3..] takes the last three characters. filename[..^4] removes the last four characters (the dot and the extension). The range operator's expressive nature reduces off-by-one errors common with Substring.
Another example: extracting a substring between two markers.
string log = "[INFO] Starting server"; int start = log.IndexOf("[") + 1; int end = log.IndexOf("]"); string level = log[start..end]; // "INFO"
Here, start and end are computed as integers, and the range operator applies them cleanly. The end index is exclusive, so the closing bracket is not included.
Performance Characteristics
The range operator on a string always allocates a new string, just like Substring. The underlying mechanism copies the selected characters into a fresh heap object. There is no zero-copy slicing for strings because .NET strings are immutable and cannot reference a portion of another string.
For scenarios where you need to read a segment without allocating a new string, consider using ReadOnlySpan<char> with the AsSpan method, then apply the range to the span.
string data = "2024-05-01"; ReadOnlySpan<char> year = data.AsSpan()[..4]; Console.WriteLine(year.ToString()); // "2024"
AsSpan returns a read-only span that points into the original string's memory. Slicing the span with a range does not copy data; it creates a new view. However, you must copy the span to a string (via ToString) if you need to store it beyond the original string's lifetime, because the span is only valid while the original string is alive and unchanged.
In performance-sensitive code, such as parsing large text inputs, avoiding extra allocations can matter. But for typical application logic, the allocation cost of slicing a short string is negligible.
The range operator itself adds no significant runtime overhead beyond the underlying copying. The compiler translates the range syntax into direct method calls, avoiding reflection or dynamic dispatch.
When to Prefer Substring Over Ranges
While the range operator is more readable in many cases, there are situations where Substring is clearer or more compatible.
- Compatibility: The range operator requires C# 8.0 and .NET Core 3.0 or later. If you're targeting .NET Framework or older runtimes,
Substringis the only option. - Dynamic boundaries: When the start and end indices come from runtime calculations that need explicit length validation,
Substringoffers overloads that throw exceptions more naturally. - Complex logic: If you need to conditionally slice based on a predicate, breaking it into separate
Substringcalls might be more straightforward.
Here's an example where Substring might be a better fit:
string header = input.Substring(0, input.IndexOf(':'));
This uses a computed length rather than a range, which can be simpler to reason about when the end index depends on search results.
Handling Out-of-Range Scenarios
Both Substring and the range operator throw exceptions if indices are out of bounds. For ranges, ArgumentOutOfRangeException is thrown when the start index is greater than the end index, or if either index is outside the valid range.
string text = "short"; string bad = text[3..10]; // throws ArgumentOutOfRangeException
The range operator does not automatically clamp to the string's length. You must ensure your indices are valid. If you're parsing inconsistent input, consider checking lengths before slicing.
Slicing with Dynamic Indices
You can combine the range operator with Index values computed at runtime, including using ^ for from-end positions. This is particularly useful when processing data from the end of a string, such as extracting the last log entry.
string logLine = "2024-05-01 12:34:56 ERROR: Disk full"; int colonIndex = logLine.IndexOf(':'); string timestamp = logLine[..colonIndex]; // up to the first ':' string lastPart = logLine[^10..]; // last 10 characters
In this example, colonIndex is an integer, and logLine[..colonIndex] extracts the timestamp. logLine[^10..] takes the last 10 characters, assuming they form a meaningful substring. The range operator composes well with search results, but you still need to validate that indices are within the string's length.
Alternatives and When to Use Them
Beyond Substring and the range operator, consider Span<T> for zero-copy views, and Memory<T> when you need to pass a slice to async methods. For strings, AsSpan is the primary way to get a span.
| Method | Allocation | Use Case |
|---|---|---|
string.Substring | New string | General-purpose slicing |
| Range operator | New string | Readable syntax |
AsSpan() + range | No allocation | Performance-critical parsing |
For most application code, the range operator offers the best balance of readability and performance. When measuring tells you that allocation is a bottleneck, switch to spans. The range operator remains a valuable tool because it makes the intent clear, reducing the chance of off-by-one errors compared to manually computing lengths.