集合

.NET 运行时提供了许多集合类型,用于存储和管理相关对象的组。 一些集合类型(例如 System.ArraySystem.Span<T>System.Memory<T>)可使用 C# 语言识别。 此外,类似 System.Collections.Generic.IEnumerable<T> 的接口可使用枚举集合元素的语言来识别。

集合提供灵活的方式来使用对象组。 可按以下特征对不同的集合进行分类:

  • 元素访问:可以枚举每个集合以按顺序访问每个元素。 某些集合可通过索引(元素在有序集合中的位置)访问元素。 最常见的示例是 System.Collections.Generic.List<T>。 其他集合可按访问元素,其中与单个相关联。 最常见的示例是 System.Collections.Generic.Dictionary<TKey,TValue>。 可根据应用访问元素的方式在这些集合类型之间进行选择。
  • 性能配置文件:每个集合都有不同的性能配置文件,可用于添加元素、查找元素或移除元素等操作。 可以根据应用中最常用的操作选取集合类型。
  • 动态增长和收缩:大多数集合支持动态添加或移除元素。 需要注意的是,ArraySystem.Span<T>System.Memory<T> 不支持。

除了这些特征之外,运行时还提供专用集合,这些集合可阻止添加或移除元素,或修改集合的元素。 其他专用集合为多线程应用中的并发访问提供安全性。

可以在 .NET API 参考中找到所有集合类型。 有关其他信息,请参阅常用集合类型选择集合类

注意

对于本文中的示例,可能需要为 System.Collections.GenericSystem.Linq 命名空间添加 using directives

数组System.Array 表示,并受 C# 语言语法支持。 此语法为数组变量提供了更简洁的声明。

System.Span<T> 属于 ref struct 类型,它可提供一系列元素的快照,而无需复制这些元素。 编译器强制实施安全规则,以确保在它引用的序列不再存在于作用域内之后无法访问 Span。 它用于许多 .NET API 以提高性能。 Memory<T> 可在无法使用 ref struct 类型时提供类似的行为。

从 C# 12 开始,可以使用集合表达式初始化所有集合类型。

可索引集合

可索引集合 是一个可以使用其索引访问每个元素的集合。 其索引是序列中在它之前的元素数。 因此,按索引 0 引用的元素是第一个元素,索引 1 则是第二个元素,依此而行。 这些示例使用 List<T> 类。 它是最常见的可索引集合。

以下示例会创建和初始化字符串列表、移除元素并将元素添加到列表末尾。 每次修改后,它会使用 foreach 语句或 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

以下示例会从一个泛型列表中按索引移除元素。 它使用以降序进行循环访问的 for 语句,而不是 foreach 语句。 RemoveAt 方法将导致已移除元素后的元素索引值减小。

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

对于 List<T> 中的元素类型,还可以定义自己的类。 在下面的示例中,由 List<T> 使用的 Galaxy 类在代码中定义。

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; }
}

键/值对集合

这些示例使用 Dictionary<TKey,TValue> 类。 这是最常见的字典集合。 使用字典集合,可通过使用每个元素的键访问集合中的元素。 每次对字典的添加都包含一个值和与其关联的键。

以下示例创建 Dictionary 集合并通过使用 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}}
    };

以下示例使用 ContainsKey 方法和 DictionaryItem[] 属性按键快速查找某个项。 使用 Item 属性可通过 C# 中的 elements[symbol] 来访问 elements 集合中的项。

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

与之相反,以下示例使用 TryGetValue 方法按键快速查找某个项。

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

迭代器

迭代器用于对集合执行自定义迭代。 迭代器可以是一种方法,或是一个 get 访问器。 迭代器使用 yield return 语句返回集合的每一个元素,每次返回一个元素。

通过使用 foreach 语句调用迭代器。 foreach 循环的每次迭代都会调用迭代器。 迭代器中到达 yield return 语句时,会返回一个表达式,并保留当前在代码中的位置。 下次调用迭代器时,将从该位置重新开始执行。

有关详细信息,请参阅迭代器 (C#)

下面的示例使用迭代器方法。 迭代器方法具有位于 for 循环中的 yield return 语句。 在 ListEvenNumbers 方法中,foreach 语句体的每次迭代都会创建对迭代器方法的调用,并将继续到下一个 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 和集合

可以使用语言集成查询 (LINQ) 来访问集合。 LINQ 查询提供筛选、排序和分组功能。 有关详细信息,请参阅 C# 中的 LINQ 入门

以下示例运行一个对泛型 List 的 LINQ 查询。 LINQ 查询返回一个包含结果的不同集合。

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}}
    };