Skip to content

C# Index and Range — Index, Range, Hat Operator (^), and Range Operator (..)

DodaTech Updated 2026-06-28 7 min read

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 out of the box. For other collections (HashSet, Dictionary, Queue), you need explicit LINQ or manual handling.

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

  1. What does ^1 mean in C#? How does it differ from ^0?
  2. What is the difference between array[2..5] and array[2..]?
  3. How do ranges work with strings? Are they zero-alloc?
  4. How would you extract the file extension from "document.pdf" using ranges?
  5. 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

Are ranges always zero-alloc?

For spans (Span, ReadOnlySpan), yes — slice creates a view. For arrays and strings, no — they create new copies. For List, it creates a new list with copied elements.

Can I use Index and Range with LINQ?

Not directly. Use items.Take(..5) or convert with items.ToArray()[..5]. LINQ does not have built-in Index/Range overloads.

What is the difference between `array[^2..]` and `array[^2..^0]`?

Both give the last two elements. ^0 as range end is equivalent to the length. Both represent the same range.

Can I define custom Index and Range behavior for my types?

Yes. Implement an indexer that accepts Index and Range: public T this[Index index] and public IEnumerable<T> this[Range range].

Are Index and Range structs or classes?

Both are structs. Index is a value type with an int value and a bool indicating whether it is from the end. Range is a value type with Start and End Index values.

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