You are viewing a preview of this lesson. Sign in to start learning
Back to Mastering Memory Management and Garbage Collection in .NET

Span<T> and ReadOnlySpan<T>

Stack-only ref structs for zero-allocation slicing

Last generated

Span<T> and ReadOnlySpan<T>

Master modern .NET memory management with Span<T> and ReadOnlySpan<T>, complete with free flashcards and spaced repetition practice. This lesson covers stack-only allocation principles, memory slicing techniques, and safe high-performance buffer manipulationβ€”essential concepts for building efficient .NET applications that minimize heap allocations and garbage collection pressure.

Welcome πŸ’»

Welcome to one of the most transformative features introduced in modern .NET! Span<T> and ReadOnlySpan<T> represent a paradigm shift in how we work with contiguous memory regions. These types enable zero-allocation slicing, provide unified access to arrays, stack memory, and native memory, and deliver performance that rivals unsafe codeβ€”all while maintaining type safety.

πŸ€” Did you know? The introduction of Span<T> in .NET Core 2.1 led to massive performance improvements across the entire framework. The ASP.NET Core team reduced allocations by over 50% in some scenarios simply by adopting these types!

Core Concepts

What Are Span<T> and ReadOnlySpan<T>? 🎯

Span<T> is a ref struct that provides a type-safe, memory-safe representation of a contiguous region of arbitrary memory. Think of it as a "window" into memoryβ€”whether that memory lives on the stack, heap, or even in unmanaged memory.

ReadOnlySpan<T> is the immutable counterpart, preventing modifications to the underlying data. It's similar to the relationship between List<T> and IReadOnlyList<T>, but with far greater performance implications.

// Traditional approach - creates new arrays
int[] original = { 1, 2, 3, 4, 5, 6, 7, 8 };
int[] firstHalf = original.Take(4).ToArray();  // ❌ Heap allocation!
int[] secondHalf = original.Skip(4).ToArray(); // ❌ Another allocation!

// Modern approach - zero allocations
Span<int> span = original;
Span<int> firstHalfSpan = span.Slice(0, 4);    // βœ… Just a view!
Span<int> secondHalfSpan = span.Slice(4, 4);   // βœ… Another view!

The ref struct Constraint ⚑

Both Span<T> and ReadOnlySpan<T> are declared as ref struct, which means they can only live on the stack. This is a deliberate design decision that enables their incredible performance characteristics:

<table> <tr><th>Can Do βœ…</th><th>Cannot Do ❌</th></tr> <tr><td>Use as local variables</td><td>Be fields in classes</td></tr> <tr><td>Pass as method parameters</td><td>Be boxed to object</td></tr> <tr><td>Return from methods</td><td>Be used in async methods</td></tr> <tr><td>Use in stackalloc scenarios</td><td>Be captured by lambdas</td></tr> <tr><td>Store in other ref structs</td><td>Implement interfaces</td></tr> </table>

🧠 Memory Device: Think "STACK ONLY" - Span stays on the STACK, Only No exceptions, Lambdas Yield problems!

Memory Safety and the Compiler's Role πŸ”’

The C# compiler performs extensive safety analysis to ensure Span<T> instances never outlive the memory they reference:

// ❌ Compiler error: Cannot use local variable in method that returns to caller
Span<int> DangerousMethod()
{
    Span<int> local = stackalloc int[10];
    return local;  // ERROR: Would reference invalid stack memory!
}

// βœ… Safe: Span references heap memory with longer lifetime
Span<int> SafeMethod()
{
    int[] array = new int[10];
    return array.AsSpan();  // OK: Array outlives the method
}

πŸ’‘ Tip: The compiler's safety analysis is called ref safety rules. These rules track the lifetime scope of memory and ensure references never dangle.

Unified Memory Access Pattern 🌍

One of Span<T>'s superpowers is providing a single API for working with memory from different sources:

<pre> β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ UNIFIED SPAN<T> API β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ πŸ“¦ Array πŸ”§ stackalloc β”‚ β”‚ ↓ ↓ β”‚ β”‚ └──→ Span<T> β†β”€β”€β”˜ β”‚ β”‚ ↑ β”‚ β”‚ β”‚ β”‚ β”‚ πŸ”— Native Memory β”‚ β”‚ β”‚ β”‚ Single API for slice, indexing, β”‚ β”‚ iteration, comparison, searching β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ </pre>

void ProcessData(Span<byte> data)
{
    // Same code works regardless of source!
    for (int i = 0; i < data.Length; i++)
    {
        data[i] = (byte)(data[i] ^ 0xFF);  // XOR operation
    }
}

// All these work with the same method:
byte[] heapArray = new byte[100];
ProcessData(heapArray);  // From heap

Span<byte> stackSpan = stackalloc byte[100];
ProcessData(stackSpan);  // From stack

unsafe
{
    byte* ptr = (byte*)Marshal.AllocHGlobal(100);
    ProcessData(new Span<byte>(ptr, 100));  // From native memory
    Marshal.FreeHGlobal((IntPtr)ptr);
}

Slicing Without Allocating πŸ”ͺ

The killer feature of Span<T> is zero-allocation slicing. Traditional substring and array operations create new objects; Span<T> just adjusts pointers:

string text = "Hello, World! Welcome to Span<T>.";

// ❌ Old way - allocates new strings
string part1 = text.Substring(0, 5);      // Allocates "Hello"
string part2 = text.Substring(7, 5);      // Allocates "World"

// βœ… New way - zero allocations
ReadOnlySpan<char> span = text.AsSpan();
ReadOnlySpan<char> hello = span.Slice(0, 5);     // Just a view
ReadOnlySpan<char> world = span.Slice(7, 5);     // Another view

// Even better: range syntax (C# 8.0+)
ReadOnlySpan<char> hello2 = span[0..5];   // Equivalent to Slice(0, 5)
ReadOnlySpan<char> world2 = span[7..12];  // Equivalent to Slice(7, 5)

<table> <tr><th>Operation</th><th>Traditional</th><th>With Span<T></th><th>Savings</th></tr> <tr><td>Substring</td><td>New string allocation</td><td>Pointer + length</td><td>~40 bytes + data</td></tr> <tr><td>Array segment</td><td>New array allocation</td><td>Pointer + length</td><td>~24 bytes + data</td></tr> <tr><td>Memory copy</td><td>Array.Copy</td><td>Direct memory access</td><td>Function call overhead</td></tr> </table>

Performance Characteristics ⚑

Understanding the internal structure explains Span<T>'s performance:

// Simplified conceptual representation
public readonly ref struct Span<T>
{
    private readonly ref T _reference;  // Pointer to first element
    private readonly int _length;       // Number of elements
    
    // Just 16 bytes on 64-bit systems!
    // 8 bytes for reference + 8 bytes for length
}

<pre> MEMORY LAYOUT COMPARISON

Array (heap object): β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Sync Block | Type Pointer | Length β”‚ 24 bytes overhead β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ Element 0 | Element 1 | Element 2 ...β”‚ + actual data β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ↑ β”‚ Heap allocation required

Span<T> (stack structure): β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Reference β”‚ Length β”‚ 16 bytes total (stack) β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ └──→ Points to existing memory (no new allocation) </pre>

Conversion and Interoperability πŸ”„

Span<T> provides rich conversion capabilities:

// From array to Span
int[] array = { 1, 2, 3, 4, 5 };
Span<int> span1 = array;                    // Implicit conversion
Span<int> span2 = array.AsSpan();           // Explicit method
Span<int> span3 = new Span<int>(array);     // Constructor

// From array segment
Span<int> partial = array.AsSpan(1, 3);     // Elements [1], [2], [3]

// From stackalloc
Span<int> stack = stackalloc int[10];

// To array (requires allocation)
int[] backToArray = span1.ToArray();        // Creates new array

// ReadOnlySpan conversions
ReadOnlySpan<int> readOnly = array;         // Implicit
ReadOnlySpan<int> fromSpan = span1;         // Span<T> β†’ ReadOnlySpan<T>
// Span<int> notAllowed = readOnly;        // ❌ Cannot convert back!

πŸ’‘ Tip: Use ReadOnlySpan<T> for method parameters when you don't need to modify the data. This allows both Span<T> and ReadOnlySpan<T> to be passed, following the principle of least privilege.

Practical Examples

Example 1: High-Performance String Parsing πŸ“

Parsing CSV data traditionally creates many temporary string objects. With ReadOnlySpan<char>, we eliminate these allocations:

public static class CsvParser
{
    // ❌ Traditional approach - many allocations
    public static string[] ParseLineOld(string line)
    {
        return line.Split(',');  // Allocates array AND strings
    }
    
    // βœ… Modern approach - zero allocations (except result list)
    public static List<string> ParseLine(ReadOnlySpan<char> line)
    {
        var results = new List<string>();
        
        while (line.Length > 0)
        {
            int commaIndex = line.IndexOf(',');
            
            if (commaIndex == -1)
            {
                // Last field
                results.Add(line.ToString());  // Only allocate final strings
                break;
            }
            
            // Extract field without allocation
            ReadOnlySpan<char> field = line.Slice(0, commaIndex);
            results.Add(field.ToString());
            
            // Move to next field (no allocation)
            line = line.Slice(commaIndex + 1);
        }
        
        return results;
    }
    
    // πŸ”₯ Even better: Parse without string allocation
    public static int SumCsvIntegers(ReadOnlySpan<char> line)
    {
        int sum = 0;
        
        while (line.Length > 0)
        {
            int commaIndex = line.IndexOf(',');
            ReadOnlySpan<char> field = commaIndex == -1 
                ? line 
                : line.Slice(0, commaIndex);
            
            // Parse directly from span (no string allocation)
            if (int.TryParse(field, out int value))
            {
                sum += value;
            }
            
            if (commaIndex == -1) break;
            line = line.Slice(commaIndex + 1);
        }
        
        return sum;
    }
}

// Usage
string csvData = "100,200,300,400,500";
int total = CsvParser.SumCsvIntegers(csvData.AsSpan());
Console.WriteLine($"Total: {total}");  // Output: Total: 1500

Performance impact: In benchmarks, the span-based approach is 3-5x faster and allocates 10-20x less memory than traditional string operations.

Example 2: Safe Buffer Manipulation πŸ›‘οΈ

Working with byte buffers for network protocols or file I/O becomes both safer and faster:

public class PacketProcessor
{
    // Protocol: [2-byte length][4-byte type][payload]
    
    public static bool TryReadPacket(
        ReadOnlySpan<byte> buffer,
        out int packetType,
        out ReadOnlySpan<byte> payload)
    {
        packetType = 0;
        payload = ReadOnlySpan<byte>.Empty;
        
        // Validate minimum size
        if (buffer.Length < 6)
            return false;
        
        // Read length (first 2 bytes) - zero allocation
        short payloadLength = BitConverter.ToInt16(buffer.Slice(0, 2));
        
        // Validate total size
        if (buffer.Length < 6 + payloadLength)
            return false;
        
        // Read type (next 4 bytes) - zero allocation
        packetType = BitConverter.ToInt32(buffer.Slice(2, 4));
        
        // Extract payload - just a view, no copy
        payload = buffer.Slice(6, payloadLength);
        
        return true;
    }
    
    public static void WritePacket(
        Span<byte> buffer,
        int packetType,
        ReadOnlySpan<byte> payload)
    {
        // Write length
        BitConverter.TryWriteBytes(buffer.Slice(0, 2), (short)payload.Length);
        
        // Write type
        BitConverter.TryWriteBytes(buffer.Slice(2, 4), packetType);
        
        // Write payload - efficient copy
        payload.CopyTo(buffer.Slice(6));
    }
}

// Usage
byte[] networkBuffer = new byte[1024];
int bytesReceived = 100;  // From network read

if (PacketProcessor.TryReadPacket(
    networkBuffer.AsSpan(0, bytesReceived),
    out int type,
    out ReadOnlySpan<byte> data))
{
    Console.WriteLine($"Packet type: {type}, Data length: {data.Length}");
    // Process data without any copying
}

Example 3: Stack Allocation for Temporary Buffers πŸ“š

Combining stackalloc with Span<T> enables extremely fast temporary buffer creation:

public static class StringHelpers
{
    // Reverse a string efficiently
    public static string Reverse(string input)
    {
        if (string.IsNullOrEmpty(input))
            return input;
        
        // Allocate on stack for small strings (very fast!)
        // Fall back to heap for large strings
        Span<char> buffer = input.Length <= 128
            ? stackalloc char[input.Length]
            : new char[input.Length];
        
        // Copy and reverse
        for (int i = 0; i < input.Length; i++)
        {
            buffer[i] = input[input.Length - 1 - i];
        }
        
        return new string(buffer);
    }
    
    // Convert to uppercase without allocation (reading only)
    public static bool EqualsIgnoreCase(
        ReadOnlySpan<char> left,
        ReadOnlySpan<char> right)
    {
        if (left.Length != right.Length)
            return false;
        
        return left.Equals(right, StringComparison.OrdinalIgnoreCase);
    }
    
    // Build a formatted string with minimal allocations
    public static string FormatCoordinates(double x, double y, double z)
    {
        // Stack-allocate buffer for formatting
        Span<char> buffer = stackalloc char[100];
        
        int pos = 0;
        "Point(".AsSpan().CopyTo(buffer.Slice(pos));
        pos += 6;
        
        // Format each coordinate
        x.TryFormat(buffer.Slice(pos), out int written);
        pos += written;
        buffer[pos++] = ',';
        buffer[pos++] = ' ';
        
        y.TryFormat(buffer.Slice(pos), out written);
        pos += written;
        buffer[pos++] = ',';
        buffer[pos++] = ' ';
        
        z.TryFormat(buffer.Slice(pos), out written);
        pos += written;
        buffer[pos++] = ')';
        
        return new string(buffer.Slice(0, pos));
    }
}

// Usage
string reversed = StringHelpers.Reverse("Hello");  // "olleH"
bool same = StringHelpers.EqualsIgnoreCase("Test".AsSpan(), "TEST".AsSpan());
string coords = StringHelpers.FormatCoordinates(1.5, 2.7, 3.9);

🧠 Memory Device: "128 is great" - Use stackalloc for buffers 128 bytes or less to avoid stack overflow risks.

Example 4: Working with Memory<T> for Async πŸ”„

Since Span<T> cannot be used in async methods, .NET provides Memory<T> as a heap-based alternative:

public class AsyncBufferProcessor
{
    // ❌ Cannot use Span in async method
    // public async Task ProcessAsync(Span<byte> data) { ... }
    
    // βœ… Use Memory<T> instead
    public async Task<int> ProcessAsync(Memory<byte> data)
    {
        // Simulate async I/O
        await Task.Delay(100);
        
        // Convert to Span when doing actual work
        Span<byte> span = data.Span;
        
        int sum = 0;
        foreach (byte b in span)
        {
            sum += b;
        }
        
        return sum;
    }
    
    // Pattern: Accept Memory<T>, work with Span<T>
    public async Task WriteToStreamAsync(
        Stream stream,
        Memory<byte> buffer,
        int count)
    {
        // Memory<T> can be stored across await
        await stream.WriteAsync(buffer.Slice(0, count));
        
        // After await, convert to Span for processing
        Span<byte> written = buffer.Span.Slice(0, count);
        LogWrittenData(written);  // Synchronous processing
    }
    
    private void LogWrittenData(Span<byte> data)
    {
        // Work with Span in non-async context
        Console.WriteLine($"Wrote {data.Length} bytes");
    }
}

// Usage
var processor = new AsyncBufferProcessor();
byte[] buffer = new byte[1024];
int result = await processor.ProcessAsync(buffer);

<div style="border: 2px solid #4fd1c5; border-radius: 8px; padding: 16px; margin: 16px 0; background: rgba(79, 209, 197, 0.1);"> <h4>πŸ“‹ Span vs Memory Quick Reference</h4> <table> <tr><th>Feature</th><th>Span<T></th><th>Memory<T></th></tr> <tr><td><strong>Storage</strong></td><td>Stack only (ref struct)</td><td>Heap (regular struct)</td></tr> <tr><td><strong>Async support</strong></td><td>❌ No</td><td>βœ… Yes</td></tr> <tr><td><strong>Performance</strong></td><td>⚑ Fastest</td><td>⚑ Fast (converts to Span)</td></tr> <tr><td><strong>Use case</strong></td><td>Synchronous processing</td><td>Async/await scenarios</td></tr> <tr><td><strong>Conversion</strong></td><td>N/A</td><td>memory.Span β†’ Span<T></td></tr> </table> </div>

Common Mistakes ⚠️

Mistake 1: Trying to Store Span in a Class Field

// ❌ WRONG - Compiler error!
public class DataProcessor
{
    private Span<byte> _buffer;  // ERROR: Cannot be a field
    
    public DataProcessor(Span<byte> buffer)
    {
        _buffer = buffer;  // Won't compile
    }
}

// βœ… CORRECT - Use Memory<T> for storage
public class DataProcessor
{
    private Memory<byte> _buffer;  // OK: Memory can be stored
    
    public DataProcessor(Memory<byte> buffer)
    {
        _buffer = buffer;
    }
    
    public void Process()
    {
        Span<byte> span = _buffer.Span;  // Convert when needed
        // Work with span...
    }
}

Mistake 2: Using Span in Async Methods

// ❌ WRONG - Compiler error!
public async Task ProcessAsync(Span<byte> data)
{
    await Task.Delay(100);  // ERROR: Span cannot cross await
    ProcessData(data);
}

// βœ… CORRECT - Use Memory<T> for async
public async Task ProcessAsync(Memory<byte> data)
{
    await Task.Delay(100);  // OK: Memory can cross await
    ProcessData(data.Span);  // Convert to Span after await
}

private void ProcessData(Span<byte> data)
{
    // Synchronous processing with Span
}

Mistake 3: Assuming Span Copies Data

// ⚠️ DANGER - Modifying through span affects original!
int[] original = { 1, 2, 3, 4, 5 };
Span<int> span = original.AsSpan();
span[0] = 999;

Console.WriteLine(original[0]);  // Output: 999 (not 1!)

// βœ… CORRECT - Use ToArray() to create a copy
int[] original = { 1, 2, 3, 4, 5 };
Span<int> span = original.AsSpan();
int[] copy = span.ToArray();  // Create independent copy
copy[0] = 999;

Console.WriteLine(original[0]);  // Output: 1 (unchanged)
Console.WriteLine(copy[0]);      // Output: 999

Mistake 4: Returning Stack-Allocated Spans

// ❌ EXTREMELY DANGEROUS - Compiler prevents this!
Span<int> CreateBuffer()
{
    Span<int> buffer = stackalloc int[10];
    return buffer;  // ERROR: Would reference invalid stack memory
}

// βœ… CORRECT - Return heap-allocated or parameter-based spans
Span<int> GetSlice(int[] array, int start, int length)
{
    return array.AsSpan(start, length);  // OK: array outlives method
}

int[] CreateInitializedArray()
{
    Span<int> buffer = stackalloc int[10];
    buffer.Fill(42);
    return buffer.ToArray();  // OK: Converts to heap array
}

Mistake 5: Capturing Span in Lambdas or Closures

// ❌ WRONG - Compiler error!
void ProcessItems(Span<int> items)
{
    var query = items.Where(x => x > 0);  // ERROR: Cannot capture Span
}

// βœ… CORRECT - Convert to array first, or avoid LINQ
void ProcessItems(Span<int> items)
{
    // Option 1: Manual iteration
    for (int i = 0; i < items.Length; i++)
    {
        if (items[i] > 0)
        {
            ProcessItem(items[i]);
        }
    }
    
    // Option 2: Convert to array (if allocation is acceptable)
    var array = items.ToArray();
    var query = array.Where(x => x > 0);
}

Mistake 6: Excessive stackalloc Causing Stack Overflow

// ❌ DANGER - May overflow stack!
void ProcessLargeData()
{
    Span<byte> huge = stackalloc byte[1_000_000];  // 1 MB on stack! πŸ’₯
    // ...
}

// βœ… CORRECT - Use threshold pattern
void ProcessData(int size)
{
    const int StackAllocThreshold = 512;  // Conservative limit
    
    Span<byte> buffer = size <= StackAllocThreshold
        ? stackalloc byte[size]        // Small: use stack
        : new byte[size];              // Large: use heap
    
    // Process buffer...
}

πŸ’‘ Tip: The typical stack size is 1 MB on Windows (64-bit). Keep stackalloc allocations under 128-512 bytes to be safe, especially in recursive or deeply nested code.

Key Takeaways 🎯

βœ… Span<T> and ReadOnlySpan<T> are ref structs that provide type-safe, zero-allocation views into contiguous memory regions

βœ… Stack-only constraint enables incredible performance but prevents use in async methods, as class fields, or with lambdas

βœ… Slicing operations (Slice, indexers) create new views without copying dataβ€”modifications affect the underlying memory

βœ… Memory<T> is the heap-based alternative for scenarios requiring async support or storage in fields

βœ… stackalloc with Span enables ultra-fast temporary buffers, but keep allocations small (≀128-512 bytes)

βœ… Unified API works with arrays, stack memory, and native memory through a single, consistent interface

βœ… ReadOnlySpan<T> provides immutability guarantees and should be preferred for method parameters that don't modify data

βœ… BitConverter and parsing APIs work directly with spans, enabling zero-allocation data transformations

<div style="border: 2px solid #4fd1c5; border-radius: 8px; padding: 16px; margin: 16px 0; background: rgba(79, 209, 197, 0.1);"> <h4>πŸ“‹ Quick Reference Card</h4>

<table> <tr><th>Scenario</th><th>Use This</th><th>Why</th></tr> <tr><td>Synchronous processing</td><td>Span<T></td><td>Maximum performance</td></tr> <tr><td>Read-only parameter</td><td>ReadOnlySpan<T></td><td>Prevents modifications</td></tr> <tr><td>Async/await</td><td>Memory<T></td><td>Can cross await boundaries</td></tr> <tr><td>Store in field</td><td>Memory<T></td><td>Not a ref struct</td></tr> <tr><td>Small temp buffer</td><td>stackalloc + Span</td><td>Zero allocation</td></tr> <tr><td>Large buffer</td><td>ArrayPool or heap</td><td>Avoid stack overflow</td></tr> <tr><td>String operations</td><td>ReadOnlySpan<char></td><td>Avoid substring allocations</td></tr> <tr><td>Byte protocols</td><td>Span<byte></td><td>Direct memory access</td></tr> </table>

<h4>Common Operations</h4> <table> <tr><th>Operation</th><th>Code</th></tr> <tr><td>Create from array</td><td><code>array.AsSpan()</code></td></tr> <tr><td>Slice range</td><td><code>span[start..end]</code></td></tr> <tr><td>Get length</td><td><code>span.Length</code></td></tr> <tr><td>Access element</td><td><code>span[index]</code></td></tr> <tr><td>Copy data</td><td><code>source.CopyTo(dest)</code></td></tr> <tr><td>Fill with value</td><td><code>span.Fill(value)</code></td></tr> <tr><td>Find element</td><td><code>span.IndexOf(value)</code></td></tr> <tr><td>Convert to array</td><td><code>span.ToArray()</code></td></tr> <tr><td>Memory to Span</td><td><code>memory.Span</code></td></tr> </table> </div>

πŸ“š Further Study

  1. Official Documentation: Span<T> Struct - Microsoft Docs - Comprehensive API reference and usage guidelines

  2. Performance Deep Dive: All About Span - MSDN Magazine - Detailed exploration of Span's implementation and performance characteristics

  3. Memory<T> and Async Patterns: Memory<T> and Span<T> usage guidelines - Best practices for choosing between Span and Memory in real-world applications