Cuando trabajamos con colecciones o buffers de datos, es muy común necesitar operar solo sobre una parte de ellos: una porción de un array, un segmento de texto, o un rango de bytes. La solución más directa suele ser copiar esa parte a una nueva estructura… pero eso introduce sobrecarga innecesaria de memoria y CPU. Si queremos trabajar con partes de una colección (por ejemplo, un array, un string o un buffer de bytes) sin crear copias, es decir, tener una vista o referencia a un fragmento de la colección original.
Es decir, queremos qu esta solución sea:
- Ligera en memoria (sin asignaciones adicionales)
- Segura (sin acceder fuera de los límites)
- Eficiente (idealmente sin costo en tiempo de ejecución)
Muchos lenguajes modernos han ido incorporando construcciones para resolver este problema. Veamos cómo lo hacen C#, Go y Rust.
A partir de C# 7.2, se introdujo Span<T>, una estructura de tipo ref struct que representa una ventana sobre memoria contigua.
int[] datos = { 10, 20, 30, 40 };
Span<int> segmento = datos.AsSpan(1, 2); // contiene {20, 30}
Podés usar Span<T> para:
- Evitar copiar arrays o strings.
- Trabajar con memoria en el stack (stackalloc).
- Reutilizar buffers en pipelines o parsers.
- Procesar archivos grandes en trozos.
Span<byte> buffer = stackalloc byte[1024]; // sin heap
Limitaciones:
- No puede usarse como campo de clases (sólo en structs).
- No se puede usar con async/await ni capturar en lambdas.
- Solo dentro del alcance del stack (por diseño).
Go tiene slices desde siempre: son una capa por encima de los arrays. Un slice guarda un puntero al array subyacente, longitud y capacidad.
arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:4] // contiene {2, 3, 4}
Ventajas:
- Livianos y eficientes.
- Se puede modificar el contenido (afecta al array original).
- Permiten crecer mediante append si hay capacidad.
s[0] = 99 // también cambia arr[1]
La semántica de slicing en Go es natural y permite componer operaciones sin asignar memoria.
Rust maneja este problema con referencias segmentadas: &[T] para vistas inmutables y &mut [T] para mutables.
let arr = [1, 2, 3, 4];
let slice = &arr[1..3]; // &[2, 3]
Características:
- Completamente seguras en tiempo de compilación.
- El borrow checker impide aliasing mutable.
- Altamente eficientes, sin sobrecarga.
- Son la base de muchas APIs estándar.
fn print_slice(s: &[i32]) {
for val in s {
println!("{}", val);
}
}
¿Y cuál conviene?
- Si estás en un lenguaje GC-friendly como C#, Span<T> te da poder sin pagar costo de GC.
- Si buscás simplicidad y velocidad de desarrollo, Go es imbatible con su slicing natural.
- Si necesitás seguridad al máximo y performance nativa, Rust con slices es lo más robusto.
Es decir, depende del lenguaje que estes usando ...
Y Otros lenguajes tambien tenemos cosas parecidas:
- C++20: std::span<T> cumple un rol casi idéntico a Span<T> de C#, y también es zero-copy.
- Python: memoryview permite trabajar con buffers sin copiar, aunque menos seguro.
- Java: No tiene slices como tal, pero ByteBuffer puede simularlos.
- Nim, Zig, D: Todos ofrecen slices como vistas eficientes sobre datos.
En la práctica, estas estructuras son fundamentales para escribir código eficiente, especialmente en procesamiento de datos, parsers, sistemas embebidos o cualquier aplicación donde el rendimiento importa.