Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 11 de junio de 2019
.NET CoreEn el post anterior vimos que la estructura Index, junto con alguna cortesía del compilador, permitía la especificación de índices en arrays de forma muy sencilla. Veíamos cómo podíamos acceder a elementos concretos utilizando su posición en la colección, tanto contando desde el principio como desde el final:
var primes = new[] { 2, 3, 5, 7, 11, 13, 17, 19 };
Index fromStart = 2;  // = Index.FromStart(2) - conversión implícita
Index fromEnd = ^2;   // = Index.FromEnd(2)

Console.WriteLine(primes[fromStart]); // 5
Console.WriteLine(primes[fromEnd]); // 17
Sin embargo, puede que a Index por sí mismo tampoco le veáis demasiada utilidad... y así es. De hecho, su finalidad es más bien el dar soporte a rangos, una nueva característica de C#8 que nos permitirá referirnos a "porciones" de arrays o colecciones similares usando una sintaxis compacta e integrada en el lenguaje.

La estructura Range

La estructura Range ofrece una vía para la especificación de rangos en colecciones indexadas mediante el almacenamiento de sus índices iniciales y finales. O en otras palabras, Range permite definir un rango del interior de un array o similar mediante la indicación de un Index inicial y un Index final.

Veamos un ejemplo de uso:
var primes = new[] { 2, 3, 5, 7, 11, 13, 17, 19 };

var firstTwoItemsRange = new Range(Index.FromStart(0), Index.FromStart(2));
// O Más simple, utilizando conversiones implícitas:
// var firstTwoItemsRange = new Range(0, 2);
var firstTwoItems = primes[firstTwoItemsRange];
Console.WriteLine(string.Join(",", firstTwoItems)); // 2,3
Un aspecto sumamente importante en este punto, del que seguro que algunos os habéis dado cuenta, es el índice final en un objeto Range es no inclusivo (¡ojo con esto!). Por esa razón, el resultado mostrado por consola es "2,3" y no "2,3,5", a pesar de haber indicado como índices iniciales y finales 0 y 2, respectivamente.

Si quisiéramos incluir el último elemento, deberíamos especificar un índice superior por encima del mismo, como en el siguiente código, donde utilizamos Index.From() para obtener el índice del elemento que se encuentra virtualmente más allá del último ítem del array:
var primes = new[] { 2, 3, 5, 7, 11, 13, 17, 19 };

var lastThreeItemsRange = new Range(5, Index.FromEnd(0));
var lastThreeItems = primes[lastThreeItemsRange];
Console.WriteLine(string.Join(",", lastThreeItems)); // 13,17,19
Como ya sabemos gracias al post anterior, podemos utilizar el edulcorante sintáctico que nos ofrece C# para crear rangos de forma más compacta efectiva, gracias al uso de las conversiones implícitas y el hat operator:
var primes = new[] { 2, 3, 5, 7, 11, 13, 17, 19 };

var lastThreeItemsRange = new Range(^3, ^0);
var lastThreeItems = primes[lastThreeItemsRange];
Console.WriteLine(string.Join(",", lastThreeItems)); // 13,17,19

var allPrimesRange = new Range(0, ^0);
var allPrimes = primes[allPrimesRange];
Console.WriteLine(string.Join(",", allPrimes)); // 2, 3, 5, 7, 11, 13, 17, 19
Y como ocurría con la estructura Index, Ranage también ofrece algunos miembros estáticos para simplificar la codificación de rangos frecuentes, como Range.StartAt(), Range.EndAt() o Range.All:
var primes = new[] { 2, 3, 5, 7, 11, 13, 17, 19 };

var firstTwoItemsRange = Range.EndAt(2);
var firstTwoItems = primes[firstTwoItemsRange];
Console.WriteLine(string.Join(",", firstTwoItems)); // 2,3

var lastThreeItemsRange = Range.StartAt(5);
var lastThreeItems = primes[lastThreeItemsRange];
Console.WriteLine(string.Join(",", lastThreeItems)); // 13, 17, 19

var allPrimesRange = Range.All;
var allPrimes = primes[allPrimesRange];
Console.WriteLine(string.Join(",", allPrimes)); // 2, 3, 5, 7, 11, 13, 17, 19

Añadimos más edulcorante: el operador rango ".."

Con lo que sabemos hasta este momento, ya podríamos definir rangos de una forma bastante efectiva y fácil de leer, pero, de nuevo, los diseñadores de C# no podían dejarlo ahí y han añadido un atajo para hacerlo aún más sencillo.

El operador ".." permite definir rangos de forma más compacta, usando índices separados por estos caracteres para determinar los límites de éstos. Como vemos en el siguiente código, estos índices son opcionales, lo que permite especificar muy fácilmente los límites superiores e inferiores del rango:
var primes = new[] { 2, 3, 5, 7, 11, 13, 17, 19 };

var thirdAndFourthItems = primes[2..4];
Console.WriteLine(string.Join(",", thirdAndFourthItems)); // 5,7

var firstTwoItems = primes[..2];
Console.WriteLine(string.Join(",", firstTwoItems)); // 2,3

var lastThreeItems = primes[^3..];
Console.WriteLine(string.Join(",", lastThreeItems)); // 13, 17, 19

var allPrimes = primes[..];
Console.WriteLine(string.Join(",", allPrimes)); // 2, 3, 5, 7, 11, 13, 17, 19

Pero... ¿qué estamos haciendo realmente a obtener un rango de una colección?

Hasta ahora hemos visto cómo se definen los rangos, y cómo podemos usarlos para "extraer" un subconjunto de elementos de un array.

Pero en realidad no nos hemos detenido a pensar qué es lo que estamos haciendo realmente cuando obtenemos un rango. Es decir, la pregunta es: ¿estamos creando un nuevo array copiando los elementos del anterior? ¿O quizás estamos obteniendo un puntero a los elementos existentes inicialmente? Pues, como suele ocurrir, la respuesta es que depende.

Cuando obtenemos un rango desde un array, internamente se copiarán los elementos a un nuevo array. Esto podemos comprobarlo fácilmente con el siguiente código, donde podemos observar que la modificación de un elemento del rango no afecta a la colección original:
var primes = new [] { 2, 3, 5, 7, 11, 13, 17, 19 };

var firstTwoItems = primes[..2];
firstTwoItems[0] = 666;
Console.WriteLine(string.Join(",", firstTwoItems)); // 666,3

var allItems = primes[..];
Console.WriteLine(string.Join(",", allItems)); // 2, 3, 5, 7, 11, 13, 17, 19
Sin embargo, si lo aplicamos sobre zonas de memoria como las referenciadas por Span<T>, el rango representará un subconjunto indexado de las mismas, pero apuntando a los contenidos originales (es decir, no se creará una copia de los datos):
string ToCommaString(Span<int> values) => string.Join(",", values.ToArray());

var primes = new[] { 2, 3, 5, 7, 11, 13, 17, 19 };
var span = primes.AsSpan();

var thirdAndFourthItems = span[2..4]; // Points to ...[5,7]...
thirdAndFourthItems[0] = 666;
Console.WriteLine(ToCommaString(thirdAndFourthItems)); // 666,7

var allItems = span[..];
Console.WriteLine(ToCommaString(allItems)); // 2, 3, 666, 7, 11, 13, 17, 19

var allOriginalItems = primes[..];
Console.WriteLine(string.Join(",", allOriginalItems)); // 2, 3, 666, 7, 11, 13, 17, 19
Toolbelt. Picture by Bert MarshallBueno, y creo que con esto ya hemos dado un buen repaso a Index y Range, así que lo dejamos aquí :)  Espero que os haya resultado interesante para comprender estos nuevos tipos y añadirlos como herramientas a nuestro cinturón de desarrollador.

Publicado en: www.variablenotfound.com.

Aún no hay comentarios, ¡sé el primero!