Colecciones

El entorno de ejecución de .NET proporciona muchos tipos de colección que almacenan y administran grupos de objetos relacionados. Algunos de los tipos de colección, como System.Array, System.Span<T>y System.Memory<T> se reconocen en el lenguaje C#. Además, las interfaces como System.Collections.Generic.IEnumerable<T> se reconocen en el lenguaje para enumerar los elementos de una colección.

Las colecciones proporcionan una manera flexible de trabajar con grupos de objetos. Puede clasificar las distintas colecciones en función de estas características:

  • Acceso a elementos: cada colección se puede enumerar para acceder a cada elemento en orden. Algunas colecciones acceden a elementos mediante el índice, la posición del elemento en una colección ordenada. El ejemplo más común es System.Collections.Generic.List<T>. Otras colecciones acceden a los elementos mediante la clave, donde un valor está asociado a una sola clave. El ejemplo más común es System.Collections.Generic.Dictionary<TKey,TValue>. Elige entre estos tipos de colección en función de cómo la aplicación accede a los elementos.
  • Perfil de rendimiento: cada colección tiene perfiles de rendimiento diferentes para acciones como agregar un elemento, buscar un elemento o quitar un elemento. Puede elegir un tipo de colección en función de las operaciones que se usan más en la aplicación.
  • Aumentar y reducir dinámicamente: la mayoría de las colecciones admiten la adición o eliminación de elementos de forma dinámica. En particular, Array, System.Span<T> y System.Memory<T> no lo hacen.

Además de esas características, el tiempo de ejecución proporciona colecciones especializadas que impiden agregar o quitar elementos o modificar los elementos de la colección. Otras colecciones especializadas proporcionan seguridad para el acceso simultáneo en aplicaciones multiproceso.

Puede encontrar todos los tipos de colección en la referencia de API de .NET. Para obtener más información, consulte Tipos de colección usados habitualmente y Seleccionar una clase de colección.

Nota:

Para ver los ejemplos de este artículo, es posible que tenga que agregar directivas using para los espacios de nombres System.Collections.Generic y System.Linq.

Las matrices se representan mediante System.Array y presentan compatibilidad con la sintaxis en el lenguaje C#. Esta sintaxis proporciona declaraciones más concisas para las variables de matriz.

System.Span<T> es un tipo ref struct que proporciona una instantánea sobre una secuencia de elementos sin copiar esos elementos. El compilador aplica reglas de seguridad para garantizar que no se pueda acceder a Span después de que la secuencia a la que hace referencia ya no esté en el ámbito. Se usa en muchas API de .NET para mejorar el rendimiento. Memory<T> proporciona un comportamiento similar cuando no se puede usar un tipo ref struct.

A partir de C# 12, todos los tipos de colección se pueden inicializar mediante una expresión de colección.

Colecciones indexables

Una colección indexable es una en la que puede acceder a cada elemento mediante su índice. Su índice es el número de elementos que le preceden en la secuencia. Por lo tanto, la referencia de elemento por índice 0 es el primer elemento, el índice 1 es el segundo, etc. Estos ejemplos usan la clase List<T>. Es la colección indexable más común.

En el ejemplo siguiente se crea e inicializa una lista de cadenas, se quita un elemento y se agrega un elemento al final de la lista. Después de cada modificación, itera a través de las cadenas utilizando una instrucción foreach o un bucle for:

// Create a list of strings by using a
// collection initializer.
List<string> salmons = ["chinook", "coho", "pink", "sockeye"];

// Iterate through the list.
foreach (var salmon in salmons)
{
    Console.Write(salmon + " ");
}
// Output: chinook coho pink sockeye

// Remove an element from the list by specifying
// the object.
salmons.Remove("coho");


// Iterate using the index:
for (var index = 0; index < salmons.Count; index++)
{
    Console.Write(salmons[index] + " ");
}
// Output: chinook pink sockeye

// Add the removed element
salmons.Add("coho");
// Iterate through the list.
foreach (var salmon in salmons)
{
    Console.Write(salmon + " ");
}
// Output: chinook pink sockeye coho

En el ejemplo siguiente se quitan elementos de una lista por índice. En lugar de una instrucción foreach, usa una instrucción for que itera en orden descendente. El método RemoveAt hace que los elementos después de un elemento quitado tengan un valor de índice inferior.

List<int> numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

// Remove odd numbers.
for (var index = numbers.Count - 1; index >= 0; index--)
{
    if (numbers[index] % 2 == 1)
    {
        // Remove the element by specifying
        // the zero-based index in the list.
        numbers.RemoveAt(index);
    }
}

// Iterate through the list.
// A lambda expression is placed in the ForEach method
// of the List(T) object.
numbers.ForEach(
    number => Console.Write(number + " "));
// Output: 0 2 4 6 8

Para el tipo de elementos de List<T>, también puede definir su propia clase. En el ejemplo siguiente, la clase Galaxy que usa List<T> se define en el código.

private static void IterateThroughList()
{
    var theGalaxies = new List<Galaxy>
    {
        new (){ Name="Tadpole", MegaLightYears=400},
        new (){ Name="Pinwheel", MegaLightYears=25},
        new (){ Name="Milky Way", MegaLightYears=0},
        new (){ Name="Andromeda", MegaLightYears=3}
    };

    foreach (Galaxy theGalaxy in theGalaxies)
    {
        Console.WriteLine(theGalaxy.Name + "  " + theGalaxy.MegaLightYears);
    }

    // Output:
    //  Tadpole  400
    //  Pinwheel  25
    //  Milky Way  0
    //  Andromeda  3
}

public class Galaxy
{
    public string Name { get; set; }
    public int MegaLightYears { get; set; }
}

Colecciones de pares clave-valor

Estos ejemplos usan la clase Dictionary<TKey,TValue>. Es la colección de diccionarios más común. Una colección de diccionarios permite acceder a los elementos de la colección mediante la clave de cada elemento. Cada adición al diccionario consta de un valor y de su clave asociada.

En el ejemplo siguiente se crea una colección Dictionary y se recorre en iteración el diccionario usando una instrucción foreach.

private static void IterateThruDictionary()
{
    Dictionary<string, Element> elements = BuildDictionary();

    foreach (KeyValuePair<string, Element> kvp in elements)
    {
        Element theElement = kvp.Value;

        Console.WriteLine("key: " + kvp.Key);
        Console.WriteLine("values: " + theElement.Symbol + " " +
            theElement.Name + " " + theElement.AtomicNumber);
    }
}

public class Element
{
    public required string Symbol { get; init; }
    public required string Name { get; init; }
    public required int AtomicNumber { get; init; }
}

private static Dictionary<string, Element> BuildDictionary() =>
    new ()
    {
        {"K",
            new (){ Symbol="K", Name="Potassium", AtomicNumber=19}},
        {"Ca",
            new (){ Symbol="Ca", Name="Calcium", AtomicNumber=20}},
        {"Sc",
            new (){ Symbol="Sc", Name="Scandium", AtomicNumber=21}},
        {"Ti",
            new (){ Symbol="Ti", Name="Titanium", AtomicNumber=22}}
    };

En el ejemplo siguiente se usa el método ContainsKey y la propiedad Item[] de Dictionary para encontrar rápidamente un elemento por clave. La propiedad Item le permite tener acceso a un elemento de la colección elements usando elements[symbol] en C#.

if (elements.ContainsKey(symbol) == false)
{
    Console.WriteLine(symbol + " not found");
}
else
{
    Element theElement = elements[symbol];
    Console.WriteLine("found: " + theElement.Name);
}

En el ejemplo siguiente se usa en su lugar el método TryGetValue para encontrar rápidamente un elemento por clave.

if (elements.TryGetValue(symbol, out Element? theElement) == false)
    Console.WriteLine(symbol + " not found");
else
    Console.WriteLine("found: " + theElement.Name);

Iterators

Los iteradores se usan para efectuar una iteración personalizada en una colección. Un iterador puede ser un método o un descriptor de acceso get. Un iterador usa una instrucción yield return para devolver cada elemento de la colección a la vez.

Llame a un iterador mediante una instrucción foreach. Cada iteración del bucle foreach llama al iterador. Cuando se alcanza una instrucción yield return en el iterador, se devuelve una expresión y se conserva la ubicación actual en el código. La ejecución se reinicia desde esa ubicación la próxima vez que se llama al iterador.

Para obtener más información, vea Iteradores (C#).

El siguiente ejemplo usa el método del iterador. El método del iterador tiene una instrucción yield return que se encuentra dentro de un bucle for. En el método ListEvenNumbers, cada iteración del cuerpo de la instrucción foreach crea una llamada al método iterador, que continúa con la siguiente instrucción yield return.

private static void ListEvenNumbers()
{
    foreach (int number in EvenSequence(5, 18))
    {
        Console.Write(number.ToString() + " ");
    }
    Console.WriteLine();
    // Output: 6 8 10 12 14 16 18
}

private static IEnumerable<int> EvenSequence(
    int firstNumber, int lastNumber)
{
    // Yield even numbers in the range.
    for (var number = firstNumber; number <= lastNumber; number++)
    {
        if (number % 2 == 0)
        {
            yield return number;
        }
    }
}

LINQ y colecciones

Language Integrated Query (LINQ) se puede usar para acceder a las colecciones. Las consultas LINQ proporcionan capacidades de filtrado, ordenación y agrupación. Para obtener más información, vea Introducción a LINQ en C#.

El ejemplo siguiente ejecuta una consulta LINQ en una List genérica. La consulta LINQ devuelve otra colección que contiene los resultados.

private static void ShowLINQ()
{
    List<Element> elements = BuildList();

    // LINQ Query.
    var subset = from theElement in elements
                 where theElement.AtomicNumber < 22
                 orderby theElement.Name
                 select theElement;

    foreach (Element theElement in subset)
    {
        Console.WriteLine(theElement.Name + " " + theElement.AtomicNumber);
    }

    // Output:
    //  Calcium 20
    //  Potassium 19
    //  Scandium 21
}

private static List<Element> BuildList() => new()
    {
        { new(){ Symbol="K", Name="Potassium", AtomicNumber=19}},
        { new(){ Symbol="Ca", Name="Calcium", AtomicNumber=20}},
        { new(){ Symbol="Sc", Name="Scandium", AtomicNumber=21}},
        { new(){ Symbol="Ti", Name="Titanium", AtomicNumber=22}}
    };