C# Index and Range — Index, Range, Hat Operator (^), and Range Operator (..)
In this tutorial, you will learn about C# Index and Range. We cover key concepts, practical examples, and best practices to help you master this topic.
C# Index and Range types, with the hat operator (^) and range operator (..), provide concise, readable syntax for accessing elements and slicing collections from the end or by range.
What You'll Learn
You will master Index and Range in C#: the Index type and hat operator for end-relative indexing, the Range type and range operator for slicing, using indexes and ranges with arrays, strings, spans, lists, and custom types in .NET.
Why It Matters
Before C# 8, accessing elements from the end required array[array.Length - 1] and slicing required array.Skip(3).Take(5) or manual loops. Index and Range provide concise, readable, and compiler-optimized syntax for these common operations. They are now idiomatic in modern C# and used throughout .NET documentation.
Real-World Use
String manipulation uses ranges for substring extraction. Data processing uses ranges for batch operations. Binary protocol Parsing uses indexes for header fields. Span slicing uses ranges for zero-allocation views. ASP.NET Core routing uses ranges for URL segment extraction.
Learning Path
graph LR
A["31: Span Memory"] --> B["32: Index Ranges"]
B --> C["33: Top-Level Statements"]
C --> D["34: File I/O"]
D --> E["35: Serialization"]
style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
style E fill:#4a90d9,stroke:#2c5f8a,color:#fff
Index Types
int[] numbers = { 10, 20, 30, 40, 50, 60 };
// Regular index
int first = numbers[0]; // 10
int third = numbers[2]; // 30
// Hat operator (^) counts from the end
int last = numbers[^1]; // 60
int secondLast = numbers[^2]; // 50
int thirdLast = numbers[^3]; // 40
// Index type explicit usage
Index idx1 = 0; // From start
Index idx2 = ^1; // From end
Index idx3 = ^3;
Console.WriteLine($"numbers[idx1] = {numbers[idx1]}"); // 10
Console.WriteLine($"numbers[idx2] = {numbers[idx2]}"); // 60
Console.WriteLine($"numbers[idx3] = {numbers[idx3]}"); // 40
Range Types
int[] numbers = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
// Range from start index to end index (exclusive end)
int[] firstThree = numbers[0..3]; // { 0, 1, 2 }
int[] midSection = numbers[3..7]; // { 3, 4, 5, 6 }
// Open-ended ranges
int[] fromStart = numbers[..3]; // { 0, 1, 2 }
int[] toEnd = numbers[7..]; // { 7, 8, 9 }
int[] all = numbers[..]; // Full copy
// Range with hat operator
int[] lastThree = numbers[^3..]; // { 7, 8, 9 }
int[] exceptLast = numbers[..^1]; // { 0, 1, 2, 3, 4, 5, 6, 7, 8 }
// Range type explicit
Range range = 2..7;
int[] sliced = numbers[range]; // { 2, 3, 4, 5, 6 }
Console.WriteLine($"firstThree: {string.Join(", ", firstThree)}");
Console.WriteLine($"sliced: {string.Join(", ", sliced)}");
Console.WriteLine($"lastThree: {string.Join(", ", lastThree)}");
Ranges with Strings
string text = "Hello, World!";
string hello = text[..5]; // "Hello"
string world = text[7..12]; // "World"
string world2 = text[^6..^1]; // "World"
// Extract domain from email
string email = "user@example.com";
string domain = email[(email.IndexOf('@') + 1)..]; // "example.com"
string localPart = email[..email.IndexOf('@')]; // "user"
// URL path parsing
string url = "/api/users/42/profile";
string path = url[..^"/profile".Length]; // "/api/users/42"
string lastSegment = url[(url.LastIndexOf('/') + 1)..]; // "profile"
Console.WriteLine($"domain: {domain}");
Console.WriteLine($"path: {path}");
Console.WriteLine($"lastSegment: {lastSegment}");
Ranges with Spans
Span<int> numbersSpan = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }.AsSpan();
Span<int> subset = numbersSpan[2..7]; // { 2, 3, 4, 5, 6 } - zero allocation
subset[0] = 99; // Modifies original
Console.WriteLine(numbersSpan[2]); // 99 (original changed)
ReadOnlySpan<char> textSpan = "Hello, World!".AsSpan();
ReadOnlySpan<char> hello = textSpan[..5];
Console.WriteLine(hello.ToString()); // Hello
Ranges with Lists
List<int> list = Enumerable.Range(0, 10).ToList();
// List<T> supports ranges
List<int> firstFive = list[..5]; // Copy
List<int> lastThree = list[^3..];
List<int> middle = list[3..7];
Console.WriteLine($"firstFive: {string.Join(", ", firstFive)}");
Console.WriteLine($"lastThree: {string.Join(", ", lastThree)}");
Custom Type Support
public class CyclicBuffer<T>
{
private readonly T[] _buffer;
private int _head;
public CyclicBuffer(T[] data) => _buffer = data;
public int Length => _buffer.Length;
public T this[Index index] => _buffer[index.GetOffset(_buffer.Length)];
public T[] this[Range range]
{
get
{
var (offset, length) = range.GetOffsetAndLength(_buffer.Length);
return _buffer[offset..(offset + length)];
}
}
}
var buffer = new CyclicBuffer<int>(new[] { 1, 2, 3, 4, 5 });
Console.WriteLine(buffer[^1]); // 5
Console.WriteLine(string.Join(", ", buffer[1..4])); // 2, 3, 4
Index and Range with Custom Collections
// Implementing support in custom types
public class DataCollection<T> : IReadOnlyList<T>
{
private readonly T[] _items;
public DataCollection(T[] items) => _items = items;
public T this[int index] => _items[index];
public int Count => _items.Length;
public T this[Index index] => _items[index.GetOffset(Count)];
public IEnumerable<T> this[Range range]
{
get
{
var (offset, length) = range.GetOffsetAndLength(Count);
for (int i = offset; i < offset + length; i++)
yield return _items[i];
}
}
}
var collection = new DataCollection<string>(new[] { "a", "b", "c", "d", "e" });
Console.WriteLine(collection[^1]); // e
Console.WriteLine(string.Join(", ", collection[1..4])); // b, c, d
Common Mistakes
Mistake 1: Off-by-One with Hat Operator
^1 is the last element, not one past the end. ^0 would be one past the end (same as array.Length), which is valid only as the end of a range.
Mistake 2: Forgetting That Range End Is Exclusive
array[2..5] includes elements at index 2, 3, 4 but NOT 5. This matches the C# convention but differs from inclusive ranges in some other languages.
Mistake 3: Using Ranges with Non-Indexable Types
Ranges work with arrays, strings, spans, and List
Mistake 4: Assuming Range Creates a View (Not a Copy)
For arrays, array[2..5] creates a new array copy. For spans, span[2..5] creates a view (no copy). Use spans for zero-allocation slicing.
Mistake 5: Using ^0 as an Index
^0 is equivalent to array.Length, which is out of range when used as an element index. It is only valid as the end of a range.
Mistake 6: Not Validating Range Bounds
Using an out-of-range index or range throws IndexOutOfRangeException. Validate indices when working with dynamic data.
Practice Questions
- What does
^1mean in C#? How does it differ from^0? - What is the difference between
array[2..5]andarray[2..]? - How do ranges work with strings? Are they zero-alloc?
- How would you extract the file extension from "document.pdf" using ranges?
- Write a method that takes a string and returns the first and last character using Index.
Challenge
Create an UrlParser class that uses Index and Range to extract protocol, domain, path, and query string from a URL. Use spans for zero-allocation parsing where possible.
FAQ
Mini Project
Create a URL path parser using Index and Range:
public class UrlParser
{
public string Protocol { get; }
public string Domain { get; }
public string Path { get; }
public string Query { get; }
public UrlParser(string url)
{
ReadOnlySpan<char> span = url.AsSpan();
// Extract protocol
int protocolEnd = span.IndexOf("://");
Protocol = protocolEnd >= 0
? span[..protocolEnd].ToString()
: "http";
span = protocolEnd >= 0 ? span[(protocolEnd + 3)..] : span;
// Extract path and query
int pathStart = span.IndexOf('/');
int queryStart = span.IndexOf('?');
int domainEnd = pathStart >= 0 ? pathStart : queryStart >= 0 ? queryStart : span.Length;
Domain = span[..domainEnd].ToString();
if (pathStart >= 0)
{
span = span[pathStart..];
queryStart = span.IndexOf('?');
Path = queryStart >= 0 ? span[..queryStart].ToString() : span.ToString();
span = queryStart >= 0 ? span[(queryStart + 1)..] : ReadOnlySpan<char>.Empty;
Query = span.ToString();
}
}
}
var urls = new[]
{
"https://example.com/api/users?page=1",
"https://docs.microsoft.com/en-us/dotnet/csharp",
"http://localhost:5000/health",
};
foreach (var url in urls)
{
var parsed = new UrlParser(url);
Console.WriteLine($"URL: {url}");
Console.WriteLine($" Protocol: {parsed.Protocol}");
Console.WriteLine($" Domain: {parsed.Domain}");
Console.WriteLine($" Path: {parsed.Path}");
Console.WriteLine($" Query: {parsed.Query}");
Console.WriteLine();
}
// Range-based string manipulation
string filePath = "/home/user/documents/report.pdf";
int lastSlash = filePath.LastIndexOf('/');
string fileName = filePath[(lastSlash + 1)..]; // "report.pdf"
string nameWithoutExt = fileName[..fileName.LastIndexOf('.')]; // "report"
string extension = fileName[fileName.LastIndexOf('.')..]; // ".pdf"
Console.WriteLine($"File: {fileName}");
Console.WriteLine($"Name: {nameWithoutExt}");
Console.WriteLine($"Ext: {extension}");
Expected output:
URL: https://example.com/api/users?page=1
Protocol: https
Domain: example.com
Path: /api/users
Query: page=1
URL: https://docs.microsoft.com/en-us/dotnet/csharp
Protocol: https
Domain: docs.microsoft.com
Path: /en-us/dotnet/csharp
Query:
URL: http://localhost:5000/health
Protocol: http
Domain: localhost:5000
Path: /health
Query:
What's Next
You have mastered Index and Range in C#. The next lesson covers top-level statements, file-scoped namespaces, and global usings.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro