CodeGym /Cursos /C# SELF /Nuevas posibilidades Span&...

Nuevas posibilidades Span<T>

C# SELF
Nivel 65 , Lección 3
Disponible

1. Introducción

Imagínate que tienes un gran array de datos, por ejemplo bytes leídos desde un archivo. Quieres procesar un fragmento: leer una parte como cadena o simplemente pasar una porción de datos a un método. En el estilo "antiguo" copiarías esos bytes a un nuevo array — eso es lento y consume memoria. ¿Y si hay decenas o cientos de esos fragmentos?

Aquí aparece el héroe — Span<T>. Es una estructura especial que describe una vista (view) sobre un array (o cualquier bloque contiguo de memoria) sin copiar datos, simplemente apuntando a la región necesaria. Importante: Span<T> no es una colección nueva, sino una ventana segura sobre un array existente.

Características principales y limitaciones de Span<T>

  • Span<T> es una estructura (value type) que representa una "ventana" en la memoria y no copia los elementos.
  • Apunta sólo a una región contigua de memoria: un array, parte de un array, un bloque de stackalloc, la memoria de una cadena vía métodos especiales, o memoria dentro de código unsafe.
  • Span<T> vive en el stack. No puedes almacenarlo en un campo de clase ni devolverlo directamente desde métodos async.
  • Garantiza seguridad de tipos: el acceso a memoria es sin manipular punteros manualmente (si no usas unsafe).

Breve sobre nuevas características de C# 14

C# 14 amplía el trabajo con slices: ranges cómodos .. e índices desde el final ^1, patrones de matching mejorados para arrays y spans y otros "azúcares" sintácticos. Volveremos a esto al final.

2. ¿Cómo crear un Span<T>?

Empecemos con un array sencillo y creemos un span:

int[] numbers = { 10, 20, 30, 40, 50, 60 };
Span<int> midSpan = new Span<int>(numbers, 2, 3); // tomar 3 elementos desde el 3º elemento (índice 2)

Ahora midSpan apunta a los elementos 30, 40, 50 — no es una copia, sino una vista sobre los elementos "vivos" del array. Al modificarlos vía el span, cambias el array original.

midSpan[0] = 100;
Console.WriteLine(numbers[2]); // Mostrará 100

¿Por qué no usar slices de arrays directamente?

El slice clásico con LINQ crea un nuevo array:

int[] slice = numbers.Skip(2).Take(3).ToArray(); // <-- aquí se crea una copia!

Eso es costoso en tiempo y memoria. Span resuelve el problema de copias innecesarias — y funciona no solo con números.

3. Más formas de crear Span<T>

Array existente

Span<int> mySpan = numbers; // Conversión implícita de array a Span

Parte de un array

int[] numbers = {10, 20, 30, 40, 50, 60};
Span<int> part = numbers.AsSpan(1, 4); // 4 elementos empezando en índice 1: {20, 30, 40, 50}

Memoria en el stack (stackalloc)

Span<T> permite alocar arrays en el stack:

Span<byte> buffer = stackalloc byte[128];
for (int i = 0; i < buffer.Length; i++)
    buffer[i] = (byte)i;

La memoria del stack es rápida y se libera automáticamente al salir del método. Pero el tamaño debe ser razonable — megabytes en el stack no son aceptables.

Cadenas y ReadOnlySpan<char>

Las cadenas en .NET son inmutables, así que usamos ReadOnlySpan<char>:

string greeting = "Hello, C# world!";
ReadOnlySpan<char> span = greeting.AsSpan(7, 8);   // "C# world"
Console.WriteLine(span.ToString());                // C# world

4. ¡Armemos un ejemplo!

using System;

class Program
{
    static void Main()
    {
        int[] orderTotals = { 100, 200, 300, 400, 500, 600, 700 };
        Console.WriteLine("Todo el historial de pedidos: ");
        foreach (int total in orderTotals)
            Console.Write(total + " ");
        Console.WriteLine();
        
        Console.WriteLine("Mostrar pedidos del 2º al 4º (índices 1-3):");
        Span<int> recentOrders = orderTotals.AsSpan(1, 3);
        foreach (int t in recentOrders)
            Console.Write(t + " ");
        Console.WriteLine();
        
        // Mutamos datos vía Span
        recentOrders[1] = 999;
        Console.WriteLine("Después del cambio vía Span:");
        foreach (int total in orderTotals)
            Console.Write(total + " ");
        Console.WriteLine();
    }
}

La vista recentOrders trabaja directamente con el array — no es una copia.

5. Seguridad, rendimiento y verificaciones

  • Uso eficiente de memoria: sin copias innecesarias.
  • Comprobaciones de límites protegen contra accesos fuera del array.
  • Optimizaciones del JIT dan acceso muy rápido (sin unsafe).

Interacción con métodos

Los métodos pueden aceptar Span<T> o ReadOnlySpan<T>. Si no vas a modificar datos — usa ReadOnlySpan<T>.

static int Sum(Span<int> slice)
{
    int sum = 0;
    foreach (var item in slice)
        sum += item;
    return sum;
}

int[] data = { 1, 2, 3, 4, 5, 6, 7 };
Console.WriteLine(Sum(data.AsSpan(2, 3))); // 3+4+5=12

6. Uso de ranges

Desde C# 8 están disponibles los ranges .. y los índices desde el final ^ — funcionan de maravilla con slices.

int[] arr = { 10, 20, 30, 40, 50, 60 };

Span<int> span = arr[2..5]; // índices 2,3,4 - es decir 30, 40, 50

En un range el índice de inicio se incluye, el final no.

Índices desde el final

int lastElement = arr[^1];     // último elemento (60)
Span<int> lastTwo = arr[^2..]; // dos últimos elementos (50, 60)

Ranges y cadenas

string code = "SpanMagic!";
var mid = code[4..9]; // "Magic"

Ese slice es un ReadOnlySpan<char>; para obtener una cadena, llama ToString().

7. Patrones modernos para trabajar con slices (C# 14)

Los nuevos patrones simplifican el análisis de arrays y slices.

if (arr is [10, 20, .. var rest]) // .. captura la "cola" del array
{
    Console.WriteLine("El inicio coincide, la cola:");
    foreach (var x in rest)
        Console.WriteLine(x);
}
if (arr is [.., 50, 60])
    Console.WriteLine("El array termina en 50, 60");

Comparación: Array, ArraySegment y Span

Tipo Mutable ¿Copia datos? ¿Se puede en stack? Clase/estructura ¿Se puede como campo de clase?
T[]
- no clase
ArraySegment<T>
no no estructura
Span<T>
no estructura no
ReadOnlySpan<T>
no no estructura no

En otras palabras, Span<T> es la evolución de ArraySegment<T>: más rendimiento y más seguro.

8. Errores típicos y trampas

Intentar almacenar un Span<T> en un campo de clase. No puedes: los campos viven en el heap, y Span<T> debe vivir en el stack. Usa ArraySegment<T> o índices para referenciar.

Devolver/almacenar Span<T> desde métodos async. No se puede: la asincronía rompe el stack. Pasa los datos de otra forma — por ejemplo como array, Memory<T>/ReadOnlyMemory<T>.

Rango inválido al crear un slice. Salir de los límites lanza una excepción en tiempo de ejecución. Siempre verifica longitud y límites antes de formar el slice.

Olvidar que el span refleja los datos originales. Cualquier cambio vía Span<T> modifica el array original. Si necesitas una copia independiente — copia los datos explícitamente.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION