Span es un de esas funcionalidades o características que pasaron desapercibida en C#. Span es una estructura introducida en las primeras versiones de .net Core de C# que nos permite representar una región contigua de memoria arbitraria como un objeto de clase primaria.
Span es especialmente útil cuando nos encontramos trabajando con un gran volumen de datos permitiéndonos hacer un uso más eficiente de la memoria logrando un excelente rendimiento.
Utilizaremos Span pasta para declarar un variable del tipo Span indicando a T como el tipo de elemento en el Span. Digamos que queremos declarar y array de números, pero en su lugar utilizaremos Span.
Span<int> numbers = new int[] { 3, 4, 9, 11, 15 }; |
Una gran característica es que permite usar la memoria más eficientemente evitando copiar datos innecesarios. En algunos casos, nos permite acceder directamente a los datos en memoria, sin tener que extraerlos o copiar a una matriz o array temporalmente administrada e independiente. Esto es fundamental cuando tenemos mucha cantidad de datos ya que dependiendo de lo que necesitemos tomar, ocuparemos más memoria.
Por otro lado, hacemos mejor uso en la manipulación de los datos. Por ejemplo, cuando usamos el método Slice(), que nos permite tomar un segmento a partir de un índice, creará un subconjunto del conjunto de datos original. Es útil cuando debemos acceder a partes específicas de un conjunto de datos sin tener que copiar nuevamente los datos a otro lugar.
Span<int> lastThreeNumbers = numbers.Slice(1, 3); |
Además de slice tenemos varias propiedades, métodos y funciones de comparación disponibles.
Propiedades | |
Empty | Devuelve un objeto Span<T> vacío. |
IsEmpty | Devuelve un valor que indica si el elemento Span<T> actual está vacío. |
Item[Int32] | Obtiene el elemento en el índice basado en cero especificado. |
Length | Devuelve la longitud del intervalo actual. |
Métodos | |
Clear() | Borra el contenido de este objeto Span<T>. |
CopyTo(Span<T>) | Copia el contenido de este elemento Span<T> en un elemento Span<T> de destino. |
Equals(Object) | Obsoleto.No se admiten llamadas a este método. |
Fill(T) | Rellena los elementos de este intervalo con un valor especificado. |
GetEnumerator() | Devuelve un enumerador para este elemento Span<T>. |
GetHashCode() | Obsoleto.Produce una excepción NotSupportedException. |
GetPinnableReference() | Devuelve una referencia a un objeto de tipo T que se puede usar para anclar.Este método está diseñado para admitir compiladores de .NET y no está diseñado para que el código de usuario lo llame. |
Slice(Int32) | Forma un segmento fuera del intervalo actual que comienza en un índice especificado. |
Slice(Int32, Int32) | Forma un segmento fuera del intervalo actual a partir de un índice especificado durante una longitud determinada. |
ToArray() | Copia el contenido de este intervalo en una nueva matriz. |
ToString() | Devuelve la representación en forma de cadena de este objeto Span<T>. |
TryCopyTo(Span<T>) | Intenta copiar el elemento Span<T> actual en un destino Span<T> y devuelve un valor que indica si la operación de copia se ha realizado correctamente. |
Operadores | |
Equality(Span<T>, Span<T>) | Devuelve un valor que indica si dos objetos Span<T> son iguales. |
Implicit(ArraySegment<T> to Span<T>) | Define una conversión implícita de un elemento ArraySegment<T> en Span<T>. |
Implicit(Span<T> to ReadOnlySpan<T>) | Define una conversión implícita de un elemento Span<T> en ReadOnlySpan<T>. |
Implicit(T[] to Span<T>) | Define una conversión implícita de una matriz en Span<T>. |
Inequality(Span<T>, Span<T>) | Devuelve un valor que indica si dos objetos Span<T> no son iguales. |
Puedes consultar las extensión de métodos desde este link.
Ejemplos
Veamos un ejemplo de uso. Como comentamos, cuando trabajamos con grandes volúmenes de datos Span nos permitirá acceder y manipular los datos de manera eficiente. Si tenemos una matriz de números enteros y deseamos realizar una operación cada 5 elementos podemos utilizar Slice(). Este nos ayudará a crear un nuevo Span que contenga solo los elementos que deseamos y luego realizar la operación requerida.
// Crear el array
int[] bigArray = new int[500000];
// Llenamos nuestro array con los numeros
for (int i = 0; i < bigArray.Length; i++)
{
bigArray[i] = i;
}
// Creamos el Span
Span<int> lbigArraySpan = largeArray;
// Creamos el subconjunto de datos
Span<int> _elements = bigArraySpan.Slice(9, bigeArraySpan.Length / 5);
// Perform some operation on every tenth element
for (int i = 0; i < _elements .Length; i++)
{
_elements [i] *= 2;
}
Cuando trabajamos con datos almacenados en memoria de forma administrada Span nos será útil. Si estamos trabajando con una matriz de bytes que está relacionada con un archivo de disco podemos acceder los datos y manipularlos directamente sin intermediarios y sin la necesidad de volverlos a copiar.
byte[] _file = File.ReadAllBytes("C:\\bigFile.dat");
Span<byte> _fileSpan = _file;
for (int i = 0; i < _fileSpan.Length; i++)
{
_fileSpan[i] ^= 0xFF;
}
Si tenemos algoritmos críticos que necesitan un excelente rendimiento accediendo directamente a regiones contiguas de memoria Span es una excelente opción. Si implementamos un algoritmo de ordenación, podemos representar los datos que se ordenarán y luego, manipular los datos directamente.
int[] _data = { 3, 4, 2, 7, 3, 2, 6, 9, 0 };
Span<int> _dataSpan = _data;
for (int i = 0; i < _dataSpan.Length - 1; i++)
{
for (int j = i + 1; j < _dataSpan.Length; j++)
{
if (_dataSpan[i] > _dataSpan[j])
{
int temp = _dataSpan[i];
_dataSpan[i] = _dataSpan[j];
_dataSpan[j] = temp;
}
}
}
Limitaciones
No todo es color de rosa. Ten presente que es una estructura de tipo ref que se asigna en el pila en lugar de en managed heap. Al ser ref, posee una serie de restricciones que garantizan que no se puedan pasar al managed heap. Tampoco es posible convertir el tipo de valor a cualquier otro tipo de la interfaz que implementa el tipo, no se pueden asignar objetos y no es posible usar tipos dynamic.
Como su base es de tipo pila, es la mejor opción para escenarios donde requerimos referencias a un bugger. Por ejemplo, podemos complementar su uso con los tipos System.Mermory<T> y System.ReadOnlyMemory<T>.
Conclusiones
En la empresa en la que actualmente trabajo, hemos implementado exitosamente esta estructura para gestionar las geoposiciones de los vehículos de nuestra flota. Cada vehículo realiza informes de su posición cada minuto, generando así un considerable volumen de datos que registra sus rutas diariamente. Mediante la implementación de esta estructura en C#, hemos logrado reducir, en promedio, un 25% del tiempo necesario para resolver las rutas, alcanzando incluso reducciones de hasta un 80% en algunos casos particulares.
La optimización de la resolución de rutas ha tenido un impacto significativo en nuestra eficiencia operativa y nos ha permitido gestionar de manera más efectiva la logística de nuestra flota. Si bien los resultados han sido notables para nosotros, estoy interesado en conocer tu experiencia al probar esta estructura en tu contexto. Te invito a compartir tus comentarios y cualquier observación que tengas después de implementarla. ¡Espero que encuentres útil esta sugerencia y que sea un recurso valioso para tu aplicación en C#!.”