CodeGym /Courses /C# SELF /Introduction to Span<T&...

Introduction to Span<T> and ReadOnlySpan<T>

C# SELF
Level 65 , Lesson 2
Available

1. Introduction

When you work with arrays, strings and byte buffers, you often need to “look” at part of that data. For example, pick a substring, take a slice of an array, or process a part of an incoming stream. In older .NET versions you had to either copy data (create a new array/substring) or write code that iterates the array from one index to another. None of that is great for performance or readability.

Here’s an example of the old approach: we need to pass only part of a big array to a method:

// Old approach — we copy part of the array (inefficient!)
int[] source = new int[] { 1, 2, 3, 4, 5, 6, 7, 8 };
int[] subArray = source.Skip(2).Take(4).ToArray(); // a new array is created

So, if you need to efficiently pass a “slice” of an array (or even a piece of a string) without creating extra objects, old C# tools clearly lose, especially with large amounts of data.

This is where the hero of the day shows up — Span<T>!

2. What is Span<T>? The main idea

Span<T> is a type that represents a contiguous region of memory of the same type T. Its purpose is to give you a fast, safe and efficient way to work with parts of arrays, strings, structs, and even unmanaged memory (for example, memory allocated outside the managed .NET heap).

The main trick of Span<T> is “slices without creating new arrays”. Imagine a ruler that lets you measure any section of the same array without copying data and with minimal risk of messing up indices.

In short:

  • Span<T> — a “window” or “view” on a chunk of memory you can work with conveniently and safely.
  • No new memory allocation — saves resources and less work for the GC.
  • Works not only with arrays but with string segments, stackalloc blocks and even unmanaged memory.
  • Can't be stored in normal class fields: it's a stack-only struct.

Why it matters?

In high-performance tasks (file parsing, large buffer processing, cryptography, serialization) saving even a couple copy operations can give a huge speed boost and reduce garbage collection pressure. And you'll also show your teammates you keep up with modern C# and .NET!

3. Basic usage of Span<T>: first slice

int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// Create a Span over part of the array (e.g., elements 2 through 5 inclusive)
Span<int> middle = new Span<int>(numbers, 2, 4); // indices: 2, 3, 4, 5

// We see the subarray: 3, 4, 5, 6
Console.WriteLine(string.Join(", ", middle.ToArray())); // 3, 4, 5, 6

// Modifying the Span modifies the original array!
middle[1] = 999;
Console.WriteLine(numbers[3]); // 999

Important! Span<T> does not copy data, it only points to a “slice” of the array. All changes are visible both in the original array and in the Span.

4. Main ways to create Span<T>

From an array:

int[] arr = { 10, 20, 30, 40, 50 };
Span<int> span = arr; // full length
Span<int> slice = arr.AsSpan(1, 3); // elements 20, 30, 40

From part of an array:

Span<int> part = new Span<int>(arr, 2, 2); // elements 30, 40

stackalloc: allocate memory on the stack (extremely fast and doesn't go to the “heap”):

Span<byte> buffer = stackalloc byte[128];
buffer[0] = 42;

Using the .Slice() method:

Span<int> subSpan = span.Slice(1, 2); // elements 20, 30

Visual scheme of a “slice”


Original array:  1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10
                         <--- Span: 3, 4, 5, 6 --->

5. Limitations and specifics of Span<T>

  • Stack-only! Can't be stored in regular class fields or be part of a closure — it's a stack type.
  • Can't be used as a class field or returned from async methods (the compiler will error).
  • Can't be captured in lambdas/anonymous methods — use it “here and now”.
  • Can't be directly serialized or passed between threads.

That's because Span can point to any memory region, and if it suddenly “moves” to the heap you can end up in unsafe states.

6. Immutability: ReadOnlySpan<T>

Sometimes you need to “look” at part of memory but not modify it. For that there's the immutable variant — ReadOnlySpan<T>.

string text = "Hello, Span!";
ReadOnlySpan<char> letters = text.AsSpan(7, 4); // 'S', 'p', 'a', 'n'
Console.WriteLine(string.Join(", ", letters.ToArray())); // S, p, a, n

// letters[0] = 'Z'; // Error: indexer is read-only!

The classic scenario — safely passing a “piece” of a string or array somewhere where it shouldn't (and shouldn't be able to) be modified.

7. Practical example: slicing arrays and strings

Suppose this is a data analyzer that extracts a substring from a large string, finds numbers in it and returns their sum (without extra copies for the initial slice):

using System;

class Program
{
    static void Main()
    {
        // Suppose the user entered a long string of numbers separated by spaces
        string input = "12 34 56 78 90 123 456 789";
        // We need to calculate the sum of numbers only from the "center", e.g. 56 78 90

        // Take a substring (but don't copy it!)
        ReadOnlySpan<char> center = input.AsSpan(6, 8); // indices can be computed dynamically

        // Parse numbers via Split (creates a temporary array)
        string[] numbers = center.ToString().Split(' ');
        int sum = 0;
        foreach (var str in numbers)
        {
            if (int.TryParse(str, out int num))
                sum += num;
        }
        Console.WriteLine($"Sum of central numbers: {sum}");
    }
}

Modern parsing libraries for CSV and JSON use Span for high-speed processing of large string data — now you know what underpins their “magic”.

8. Useful nuances

Span vs copying arrays

// Old way: copy a chunk of the array
int[] arr = Enumerable.Range(0, 1000000).ToArray();
int[] firstThousand = arr.Take(1000).ToArray(); // created a new array of 1000 elements

// New way: Span
Span<int> bestThousand = arr.AsSpan(0, 1000); // no copying at all!
bestThousand[0] = 42; // changes arr as well

The difference is especially noticeable in intensive parsing, network buffer handling, and binary data work.

Real-world uses: why know about Span

  • High-performance parsing and processing of text/binary data. Modern serialization libraries (for example, System.Text.Json, Span in Microsoft docs) use Span to speed things up.
  • Buffering and file reading (slice big buffers without copying).
  • Memory-constrained environments (embedded, IoT) — official Memory/Spans docs.
  • Image and audio algorithms where speed and avoiding allocations matter.
  • Speeding up CSV, JSON, XML parsing with Span — especially in .NET 8/9.

Interviewers started asking about Span as soon as it landed in .NET Core 2.1+, and in .NET 9 it's increasingly expected knowledge.

Visual scheme: where Span lives and where the array lives


+--------------------+
|   int[]  array     |
|  1 2 3 4 5 6 7 8   |
+--------------------+
       ^       ^
       |       |
   [ 2, 3, 4, 5 ]  <-- Span<int> "memory window" (slice)

Span<T> is not a separate array but a “transparent lens” over part of the data.

Difference from other collections: comparison table

Type Holds data? Can modify elements? Can change size? Copies on slice? Where lives?
int[]
Yes Yes No Yes (via .Take) Heap
List<int>
Yes Yes Yes Yes Heap
Span<int>
No Yes No No Stack
ReadOnlySpan<int>
No No No No Stack

9. Common mistakes when working with Span/ReadOnlySpan

Mistake #1: trying to store a Span as a class field. The compiler will show “Span type may not be used in this context”. This is intentional: storing a Span in a field is unsafe.

Mistake #2: returning a Span from an async method. Don't do that, because async methods can get moved to the heap. Use an array or another type instead.

Mistake #3: forgetting that changes through a Span reflect in the original array. That can unexpectedly mutate data “outside” and lead to surprising behavior.

2
Task
C# SELF, level 65, lesson 2
Locked
Modification via `Span`
Modification via `Span`
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION