IRONSOFTWAREHOME
.NET HELP

C# Span (How It Works For Developers)

Jacob Mellor, Chief Technology Officer @ Team Iron
Jacob Mellor
Updated: August 1, 2026

Span is a type introduced in C# 7.2 as part of the Span<T> struct in the System namespace. It is designed to represent a contiguous region of arbitrary memory. Unlike arrays or collections such as managed heap, Span does not own the stack memory or region of memory it points to; instead, it provides a lightweight view over existing memory blocks. This characteristic makes Span particularly powerful for scenarios where you need to work with memory buffers efficiently without incurring additional overhead and unsafe code scenarios. Later in this article, we shall also see the introduction to the IronPDF library from Iron Software.

Key Characteristics of Span

1. Memory Management

Span in C# allows developers to work with memory directly without resorting to traditional heap allocations. It offers a way to create slices of memory from existing arrays or other memory sources, eliminating the need for additional memory copies.

2. Zero-Copy Abstractions

One of the standout features of C# Span is its zero-copy abstractions. Instead of duplicating data, Span provides a way to reference existing memory efficiently. This is particularly beneficial for scenarios where copying large amounts of data would be impractical or too costly.

3. Pointer-Like Operations

While C# has traditionally been a high-level, safe language, Span introduces a degree of low-level memory manipulation akin to working with pointers in languages like C or C++. Developers can perform pointer-like operations without sacrificing the safety and managed nature of C#.

4. Immutable Nature

Despite its capabilities for low-level memory access, C# Span remains immutable. This means that, while it allows manipulation of memory, it enforces safety by preventing unintended modifications.

Example

using System;

class Program
{
    static void Main()
    {
        int[] array = { 1, 2, 3, 4, 5 };

        // Create a span that points to the entire array
        Span<int> span = array;

        // Modify the data using the span
        span[2] = 10;

        // Print the modified array
        foreach (var item in array)
        {
            Console.WriteLine(item);
        }
    }
}

ReadOnlySpan

While Span<T> is mutable and allows for modifications to the underlying data, ReadOnlySpan<T> is an immutable view of memory. It provides a read-only interface to a contiguous region of memory, making it suitable for scenarios where you only need to read the data without modifying it.

Here are some key points.

1. Read-Only View

As the name suggests, ReadOnlySpan<T> allows you to create a read-only view of a block of memory. This means you cannot modify the elements through a ReadOnlySpan<T>.

2. Memory Representation

Like Span<T>, ReadOnlySpan<T> does not own the memory it points to. It refers to existing memory and can point to arrays, stack-allocated memory, or native memory.

3. Performance Benefits

Like Span<T>, ReadOnlySpan<T> can offer better performance compared to traditional collection types, especially when dealing with large amounts of data, as it reduces the need for copying.

4. No Bounds Checking

As with Span<T>, ReadOnlySpan<T> does not perform bounds checking. It is the developer's responsibility to ensure that operations stay within the bounds of the underlying memory.

5. Usage with Array Slicing

ReadOnlySpan<T> supports slicing, allowing you to create sub-spans that reference a portion of the original memory.

Example

using System;

class Program
{
    static void Main()
    {
        int[] array = { 1, 2, 3, 4, 5 };

        // Create a read-only span that points to the entire array
        ReadOnlySpan<int> readOnlySpan = array;

        // Access and print the data through the read-only span
        foreach (var item in readOnlySpan)
        {
            Console.WriteLine(item);
        }

        // Note: The following line would result in a compilation error since readOnlySpan is read-only.
        // readOnlySpan[2] = 10;
    }
}

There are many different ways to create ReadOnlySpan and work with it. Below are some examples.

1. Creating ReadOnlySpan from String

string msg = "Hello, World!";
ReadOnlySpan<char> span1 = msg.AsSpan();
// Read-only manipulation
char firstChar = span1[0];
Console.WriteLine(firstChar); // Outputs: H

2. Working with Substrings

Use Slice on the ReadOnlySpan<char>

// Example usage of Slice method on ReadOnlySpan<char>
ReadOnlySpan<char> spanFromString = "Sample String".AsSpan();
ReadOnlySpan<char> substringSpan = spanFromString.Slice(7, 6); // Extracts 'String'

3. Passing Substring to a Method

Pass ReadOnlySpan<char> as a parameter to the method.

void ProcessSubstringfromReadOnlySpan(ReadOnlySpan<char> substring)
{
    // Perform operations on the substring
}

// Usage
ReadOnlySpan<char> spanFromString = "Sample String".AsSpan();
ProcessSubstringfromReadOnlySpan(spanFromString.Slice(7, 6));

4. Searching within a String

ReadOnlySpan<char> for searching within a string with IndexOf().

ReadOnlySpan<char> stringSpan = "Hello, World!".AsSpan();
int index = stringSpan.IndexOf('W');
Console.WriteLine(index); // Outputs: 7

5. Using Memory-Mapped Files

ReadOnlySpan<char> can be more efficient with memory-mapped files.

using System;
using System.IO.MemoryMappedFiles;

class Program
{
    static void ProcessData(ReadOnlySpan<byte> data)
    {
        // Process data directly from the memory-mapped file
    }

    static void Main()
    {
        using (var memmf = MemoryMappedFile.CreateFromFile("data.bin"))
        {
            using (var accessor = memmf.CreateViewAccessor())
            {
                byte[] buffer = new byte[accessor.Capacity];
                accessor.ReadArray(0, buffer, 0, buffer.Length);

                ReadOnlySpan<byte> dataSpan = new ReadOnlySpan<byte>(buffer);
                ProcessData(dataSpan);
            }
        }
    }
}

6. Efficient String Manipulation

ReadOnlySpan<char> can be used for efficient string manipulation.

Span<char> newSpan = new char[6];
ReadOnlySpan<char> spanFromString = "Sample String".AsSpan().Slice(7, 6);
spanFromString.CopyTo(newSpan);
Console.WriteLine(new string(newSpan)); // Outputs: String

7. Passing Substring to APIs

When working with external libraries or APIs that operate on character spans.

void ExternalApiMethod(ReadOnlySpan<char> data)
{
    // Call the external API with the character span
}

// Usage
ReadOnlySpan<char> spanFromString = "Sample String".AsSpan();
ExternalApiMethod(spanFromString.Slice(7, 6));

ReadOnlySpan<char> provides a way to work with strings more efficiently, especially in scenarios where memory allocations and copying should be minimized. It's a powerful tool for optimizing performance-critical code and can be particularly beneficial when dealing with large amounts of string data.

Span Limitations

While Span in C# is a powerful feature with numerous advantages, it does come with certain limitations and considerations, particularly in the context of contiguous and non-contiguous memory. Let's explore these limitations:

1. Contiguous Memory Buffers

1.1 No Automatic Memory Management

Span<T> does not manage the memory it points to. This means that if the underlying managed memory is released or goes out of scope, using the Span<T> will result in undefined behavior or potential crashes. Developers need to ensure that the underlying memory is still valid when using a Span<T>.

1.2 No Garbage Collection

Since Span<T> does not own memory, it is not subject to garbage collection. Therefore, you need to be careful when working with stack-allocated memory or memory that has a shorter lifetime than the Span<T> itself.

1.3 Bounds Checking is Disabled

Span<T> and ReadOnlySpan<T> do not perform bounds checking by default. This can lead to accessing invalid memory locations if not used carefully. Developers need to manually ensure that the operations on a Span<T> stay within the bounds of the underlying memory.

1.4 No Support for Non-Contiguous Memory

Span<T> is designed to work with contiguous memory. If you have non-contiguous memory or need to represent more complex data structures, Span<T> may not be the most appropriate choice.

1.5 Not All Operations are Supported

While Span<T> supports many common operations like slicing, indexing, and iterating, not all operations are supported. For example, you cannot resize a Span<T>, and certain operations that involve changing the length of the underlying memory are not allowed.

1.6 Limited Platform Compatibility

While Span<T> is part of the .NET Standard and .NET Core, it may not be available on all platforms or environments. It is crucial to ensure that your target platform supports Span<T> if you plan to use it in your code.

2. Non-Contiguous Memory Buffers

2.1 Limited Support for Non-Contiguous Memory

ReadOnlySpan<T> is primarily designed to work seamlessly with contiguous memory blocks or buffers. It may not be the most suitable choice for scenarios where non-contiguous memory buffers or structures with gaps in memory are involved.

2.2 Structural Limitations

Certain data structures or scenarios that rely on non-contiguous memory may not align well with ReadOnlySpan<T>. For instance, data structures like linked lists or graph structures might not be well-suited due to the contiguous memory requirement of ReadOnlySpan<T>.

2.3 Complex Pointer Operations

In situations involving non-contiguous memory, particularly those requiring intricate pointer arithmetic, ReadOnlySpan<T> might not offer the same low-level control and flexibility as raw pointers in languages like C++. In such cases, utilizing unsafe code with pointers could be more appropriate.

2.4 Lack of Direct Support in Some APIs

Similar to contiguous memory, it's important to note that not all APIs or libraries may directly support non-contiguous memory represented by ReadOnlySpan<T>. Adapting to such scenarios might necessitate additional intermediate steps or conversions to ensure compatibility.

Span and Unmanaged Memory

In C#, Span can be effectively used with unmanaged memory to perform memory-related operations in a controlled and efficient manner. Unmanaged memory refers to memory that is not managed by the .NET runtime's garbage collector, and it often involves using native memory allocations and deallocations. Here's how Span can be utilized with unmanaged memory in C#.

Allocating Unmanaged Memory

To allocate unmanaged memory, you can use the System.Runtime.InteropServices.MemoryMarshal class. The Marshal.AllocHGlobal method allocates memory and returns a pointer to the allocated block. The memory allocated or memory address is held in an unmanagedMemory pointer and will have read-write access. The contiguous regions of memory can be easily accessed.

using System;
using System.Runtime.InteropServices;

class Program
{
    static void Main()
    {
        const int bufferSize = 100;
        IntPtr unmanagedMemory = Marshal.AllocHGlobal(bufferSize);

        // Create a Span from the unmanaged memory
        Span<byte> span = new Span<byte>(unmanagedMemory.ToPointer(), bufferSize);

        // Use the Span as needed...
        
        // Don't forget to free the unmanaged memory when done
        Marshal.FreeHGlobal(unmanagedMemory);
    }
}

In the above source code, we allocate a block of unmanaged memory using Marshal.AllocHGlobal and then create a Span<byte> using the pointer obtained from the unmanaged memory. This allows us to work with unmanaged memory using the familiar Span API. It's important to note that when working with unmanaged memory, you are responsible for managing the allocation and deallocation of the memory.

Copying Data to and from Unmanaged Memory

Span provides methods like Slice, CopyTo, and ToArray that can be used for copying data between managed and unmanaged memory efficiently.

using System;
using System.Runtime.InteropServices;

class Program
{
    static void Main()
    {
        // Managed array to copy data from
        int[] sourceArray = { 1, 2, 3, 4, 5 };
        
        // Allocate unmanaged memory for the destination data
        IntPtr destinationPointer = Marshal.AllocHGlobal(sourceArray.Length * sizeof(int));
        try
        {
            // Create a Span<int> from the source array
            Span<int> sourceSpan = sourceArray;
            
            // Create a Span<int> from the allocated unmanaged memory
            Span<int> destinationSpan = new Span<int>(destinationPointer.ToPointer(), sourceArray.Length);
            
            // Copy data from the source Span<int> to the destination Span<int>
            sourceSpan.CopyTo(destinationSpan);
            
            // Print the values in the destination memory
            Console.WriteLine("Values in the destination memory:");
            foreach (var value in destinationSpan)
            {
                Console.Write($"{value} ");
            }
        }
        finally
        {
            // Deallocate the unmanaged memory when done
            Marshal.FreeHGlobal(destinationPointer);
        }
    }
}

In this example:

  • Marshal.AllocHGlobal allocates unmanaged memory for the destination data.
  • new Span<int>(destinationPointer.ToPointer(), sourceArray.Length) creates a Span<int> from the allocated unmanaged memory.
  • The sourceSpan.CopyTo(destinationSpan) method copies the data from the managed array to the unmanaged memory.
  • The values in the destination memory are printed to verify the copy operation.
  • The Marshal.FreeHGlobal(destinationPointer) method is used to deallocate the unmanaged memory when done.

Using Unsafe Code

When dealing with unmanaged memory, you may also use unsafe code with pointers. In such cases, you can obtain a pointer from the Span using the Unsafe.AsPointer() method.

using System;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;

class Program
{
    static void Main()
    {
        const int bufferSize = 100;
        IntPtr unmanagedMemory = Marshal.AllocHGlobal(bufferSize);

        // Create a Span from the unmanaged memory
        Span<byte> span = new Span<byte>(unmanagedMemory.ToPointer(), bufferSize);

        // Use unsafe code to work with pointers
        unsafe
        {
            byte* pointer = (byte*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(span));
            // Use the pointer as needed...
        }

        // Don't forget to free the unmanaged memory when done
        Marshal.FreeHGlobal(unmanagedMemory);
    }
}

In this example, we use the Unsafe.AsPointer method to obtain a pointer from the Span. This allows us to use unsafe code when working with pointers directly.

Remember, when working with unmanaged memory, it's crucial to manage the allocation and deallocation properly to avoid memory leaks. Always free unmanaged memory using appropriate methods, such as Marshal.FreeHGlobal(). Additionally, exercise caution when using unsafe code, as it can introduce potential security risks if not handled properly.

Span and Asynchronous Method Calls

Using Span in conjunction with asynchronous method calls in C# is a powerful combination, especially when dealing with large amounts of data or I/O operations. The goal is to handle asynchronous operations without unnecessary copying of data efficiently. Let's explore how you can leverage Span in asynchronous scenarios:

1. Asynchronous I/O Operations:

When dealing with asynchronous I/O operations, such as reading or writing data to a stream, you can use Memory<T> or Span<T> to efficiently work with the data without creating additional buffers.

using System;
using System.IO;
using System.Threading.Tasks;

class Program
{
    static async Task ProcessDataAsync(Stream stream)
    {
        const int bufferSize = 4096;
        byte[] buffer = new byte[bufferSize];
        while (true)
        {
            int bytesRead = await stream.ReadAsync(buffer.AsMemory());
            if (bytesRead == 0)
                break;

            // Process the data using Span without unnecessary copying
            ProcessData(buffer.AsSpan(0, bytesRead));
        }
    }

    static void ProcessData(Span<byte> data)
    {
        // Perform operations on the data
    }
}

In this example, the ReadAsync method asynchronously reads data from a stream into the buffer. The ProcessData method then processes the data directly from the Span<byte> without copying it to another buffer.

2. Asynchronous File Operations:

Similar to I/O operations, when dealing with asynchronous file operations, you can use Span to efficiently process data without additional copying.

using System;
using System.IO;
using System.Threading.Tasks;

class Program
{
    static async Task ProcessFileAsync(string filePath)
    {
        const int bufferSize = 4096;
        using (FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
        {
            byte[] buffer = new byte[bufferSize];
            while (true)
            {
                int bytesRead = await fileStream.ReadAsync(buffer.AsMemory());
                if (bytesRead == 0)
                    break;

                // Process the data using Span without unnecessary copying
                ProcessData(buffer.AsSpan(0, bytesRead));
            }
        }
    }

    static void ProcessData(Span<byte> data)
    {
        // Perform operations on the data
    }
}

Here, the ReadAsync method reads data from a file stream into the buffer, and the ProcessData method processes the data directly from the Span<byte>.

3. Asynchronous Task Processing:

When working with asynchronous tasks that produce or consume data, you can use Memory<T> or Span<T> to avoid unnecessary copying.

using System;
using System.Linq;
using System.Threading.Tasks;

class Program
{
    static async Task<int> ProcessDataAsync(int[] data)
    {
        // Asynchronous processing of data
        await Task.Delay(1000);
        // Returning the length of the processed data
        return data.Length;
    }

    static async Task Main()
    {
        int[] inputData = Enumerable.Range(1, 1000).ToArray();
        
        // Process the data asynchronously without copying
        int processedLength = await ProcessDataAsync(inputData.AsMemory());
        Console.WriteLine($"Processed data length: {processedLength}");
    }
}

In this example, the ProcessDataAsync method processes the data asynchronously and returns the length of the processed data without requiring additional copies.

Introducing IronPDF

IronPDF library overview is the latest C# PDF library from Iron Software which can be used to generate beautiful PDF documents on the fly dynamically using C# code. IronPDF provides a variety of features such as PDF generation from HTML, converting HTML content to PDF, merging or splitting PDF files, etc.

IronPDF's main feature is its HTML to PDF functionality, which preserves layouts and styles. It can generate PDFs from web content, making it great for reports, invoices, and documentation. This tool supports converting HTML files, URLs, and HTML strings to PDF files.

using IronPdf;

class Program
{
    static void Main(string[] args)
    {
        var renderer = new ChromePdfRenderer();

        // 1. Convert HTML String to PDF
        var htmlContent = "<h1>Hello, IronPDF!</h1><p>This is a PDF from an HTML string.</p>";
        var pdfFromHtmlString = renderer.RenderHtmlAsPdf(htmlContent);
        pdfFromHtmlString.SaveAs("HTMLStringToPDF.pdf");

        // 2. Convert HTML File to PDF
        var htmlFilePath = "path_to_your_html_file.html"; // Specify the path to your HTML file
        var pdfFromHtmlFile = renderer.RenderHtmlFileAsPdf(htmlFilePath);
        pdfFromHtmlFile.SaveAs("HTMLFileToPDF.pdf");

        // 3. Convert URL to PDF
        var url = "http://ironpdf.com"; // Specify the URL
        var pdfFromUrl = renderer.RenderUrlAsPdf(url);
        pdfFromUrl.SaveAs("URLToPDF.pdf");
    }
}

Installation

IronPDF can be installed using the NuGet package manager for IronPDF console or using the Visual Studio package manager.

> dotnet add package IronPdf

C# Span (How It Works For Developers): Figure 1 - Install IronPDF using NuGet Package Manager by searching "IronPDF" in the search bar of NuGet Package Manager

using System;
using IronPdf;

class Program
{
    static void Main()
    {
        Console.WriteLine("Generating PDF using IronPDF.");
        var displayFirstName = "<p>First Name is Joe</p>".AsSpan();
        var displayLastName = "<p>Last Name is Doe</p>".AsSpan();
        var displayAddress = "<p>12th Main, 7Th Cross, New York</p>".AsSpan();
        var start = "<html><body>".AsSpan();
        var end = "</body></html>".AsSpan();
        var content = string.Concat(start.ToString(), displayFirstName.ToString(), displayLastName.ToString(), displayAddress.ToString(), end.ToString());
        var pdfDocument = new ChromePdfRenderer();
        pdfDocument.RenderHtmlAsPdf(content).SaveAs("span.pdf");
    }
}

In this example, we are using Span along with IronPDF to generate a PDF document.

Output:

C# Span (How It Works For Developers): Figure 2 - Console Output

Generated PDF:

C# Span (How It Works For Developers): Figure 3 - PDF Output

Licensing (Free Trial Available)

IronPDF license information. This key needs to be placed in appsettings.json.

"IronPdf.LicenseKey": "your license key"
JSON

Provide your email to get a trial license.

Conclusion

Span<T> in C# provides a powerful and efficient way to work with memory, offering benefits in terms of performance and flexibility. Its non-owning, contiguous nature makes it particularly suitable for scenarios where minimizing memory allocations and copying is crucial. By leveraging Span, developers can achieve better performance in a variety of applications, ranging from string manipulation to high-performance numeric processing. By understanding its features and considering its limitations, developers can leverage Span<T> for various memory manipulation tasks safely and efficiently. Along with IronPDF library overview, it can be used to generate awesome PDF documents without await and yield boundaries.

Please visit IronPDF's Quick-Start Documentation Page page.

Jacob Mellor, Chief Technology Officer @ Team Iron
Chief Technology Officer

Jacob Mellor is Chief Technology Officer at Iron Software and a visionary engineer pioneering C# PDF technology. As the original developer behind Iron Software's core codebase, he has shaped the company's product architecture since its inception, transforming it alongside CEO Cameron Rimington into a 50+ person company serving NASA, Tesla, and global government agencies.

...
Read More

Related Articles

Key in blue circle

Get your free 30-day Trial Key instantly.

Your trial license will be sent to your email address

No limitations. 100% unlocked. No credit card.

OR
bullet_checkedNo credit card or account creation requiredNo limitations. 100% unlocked. No credit card.
  • Logo Aetna
  • Logo NASA
  • Logo GE
  • Logo Porsche
  • Logo USDA
  • Logo Qatar
Join Millions of Engineers who’ve tried Iron Suite
Book your free Live Demo
Booking Badge

Trusted by Millions of Engineers Worldwide

Iron Software's customer logos
Get Your No-Obligation Consult
Complete the form below or email sales@ironsoftware.com
Your details will always be kept confidential.
Trusted by Millions of Engineers Worldwide
Iron Software's customer logos
Get your free 30-day Trial Key instantly.
No credit card or account creation required
C# NuGet Library for PDF
Install with NuGet

Version: 2026.9

PM > Install-Package IronPdf
nuget.org/packages/IronPdf/
  1. In Solution Explorer, right-click References, Manage NuGet Packages
  2. Select Browse and search "IronPdf"
  3. Select the package and install
C# PDF DLL
Download DLL

Version: 2026.9

or download Windows Installer here.

  1. Download and unzip IronPDF to a location such as ~/Libs within your Solution directory
  2. In Visual Studio Solution Explorer, right click References. Select Browse, "IronPdf.dll"

Licenses from $999