Поделиться через


Универсальные типы и методы

Подсказка

Вы новичок в разработке программного обеспечения? Сначала начните с учебников для начинающих. Вы столкнетесь с дженериками, как только используете такие коллекции, как List<T>.

Есть опыт на другом языке? Универсальные шаблоны C# похожи на универсальные шаблоны в Java или шаблонах в C++, но с полными сведениями о типе среды выполнения и без удаления типов. Просмотрите разделы выражений коллекции и ковариации и контравариации для характерных шаблонов C#.

Универсальные шаблоны позволяют писать код, который работает с любым типом при сохранении полной безопасности типов. Вместо написания отдельных классов или методов для int, stringа также каждого другого типа, необходимо написать одну версию с одним или несколькими параметрами типа (например T, или TKey ) TValueи указать фактические типы при его использовании. Компилятор проверяет типы во время компиляции, поэтому не требуется приведение среды выполнения или риск InvalidCastException.

Вы сталкиваетесь с дженериками постоянно в C# в вашей повседневной работе. Коллекции, асинхронные возвращаемые типы, делегаты и LINQ используют универсальные типы:

List<int> scores = [95, 87, 72, 91];
Dictionary<string, decimal> prices = new()
{
    ["Widget"] = 19.99m,
    ["Gadget"] = 29.99m
};
Task<string> greeting = Task.FromResult("Hello, generics!");
Func<int, bool> isPositive = n => n > 0;

Console.WriteLine($"First score: {scores[0]}");
Console.WriteLine($"Widget price: {prices["Widget"]:C}");
Console.WriteLine($"Greeting: {await greeting}");
Console.WriteLine($"Is 5 positive? {isPositive(5)}");

В каждом случае аргумент типа в угловых скобках (<int>, <string>, <Product>) сообщает универсальному типу данных, с которыми он содержит или работает. Компилятор обеспечивает безопасность типов. Нельзя случайно добавить string в List<int>.

Использование универсальных типов

Чаще вы используете универсальные типы из библиотеки классов .NET вместо создания собственных. В следующих разделах показаны наиболее распространенные универсальные типы, которые вы будете использовать.

Универсальные коллекции

Пространство System.Collections.Generic имен предоставляет типобезопасные классы коллекций. Всегда используйте эти коллекции вместо негенерических коллекций, таких как ArrayList:

// A strongly typed list of strings
List<string> names = ["Alice", "Bob", "Carol"];
names.Add("Dave");
// names.Add(42); // Compile-time error: can't add an int to List<string>

// A dictionary mapping string keys to int values
var inventory = new Dictionary<string, int>
{
    ["Apples"] = 50,
    ["Oranges"] = 30
};
inventory["Bananas"] = 25;

// A set that prevents duplicates
HashSet<int> uniqueIds = [1, 2, 3, 1, 2];
Console.WriteLine($"Unique count: {uniqueIds.Count}"); // 3

// A FIFO queue
Queue<string> tasks = new();
tasks.Enqueue("Build");
tasks.Enqueue("Test");
Console.WriteLine($"Next task: {tasks.Dequeue()}"); // Build

Универсальные коллекции предотвращают ошибки типов во время выполнения, так как вместо этого возникают ошибки при компиляции. Эти коллекции также избегают бокса для типов значений, что повышает производительность.

Универсальные методы

Универсальный метод объявляет собственный параметр типа. Компилятор часто вводит аргумент типа из передаваемых значений, поэтому не нужно явно указывать его:

static void Print<T>(T value) =>
    Console.WriteLine($"Value: {value}");

Print(42);        // Compiler infers T as int
Print("hello");   // Compiler infers T as string
Print(3.14);      // Compiler infers T as double

В вызове Print(42) компилятор определяет T как int из аргумента. Вы можете написать Print<int>(42) явно, но вывод типов делает код более чистым.

Выражения коллекции

Выражения коллекции (C# 12) предоставляют краткий синтаксис для создания коллекций. Используйте квадратные скобки вместо вызовов конструктора или синтаксиса инициализатора:

// Create a list with a collection expression
List<string> fruits = ["Apple", "Banana", "Cherry"];

// Create an array
int[] numbers = [1, 2, 3, 4, 5];

// Works with any supported collection type
IReadOnlyList<double> temperatures = [72.0, 68.5, 75.3];

Console.WriteLine($"Fruits: {string.Join(", ", fruits)}");
Console.WriteLine($"Numbers: {string.Join(", ", numbers)}");
Console.WriteLine($"Temps: {string.Join(", ", temperatures)}");

Оператор распространения (..) встраивает элементы одной коллекции в другую, что полезно для объединения последовательностей:

List<int> first = [1, 2, 3];
List<int> second = [4, 5, 6];

// Spread both lists into a new combined list
List<int> combined = [.. first, .. second];
Console.WriteLine(string.Join(", ", combined));
// Output: 1, 2, 3, 4, 5, 6

// Add extra elements alongside spreads
List<int> withExtras = [0, .. first, 99, .. second];
Console.WriteLine(string.Join(", ", withExtras));
// Output: 0, 1, 2, 3, 99, 4, 5, 6

Выражения коллекции работают с массивами, List<T>, Span<T>, ImmutableArray<T>и любым типом, поддерживающим шаблон построителя коллекций. Для полного справочника по синтаксису см. в Выражения коллекций.

Инициализация словаря

Словари можно кратко инициализировать с использованием индексаторных инициализаторов. Этот синтаксис использует квадратные скобки для задания пар "ключ-значение":

Dictionary<string, int> scores = new()
{
    ["Alice"] = 95,
    ["Bob"] = 87,
    ["Carol"] = 92
};

foreach (var (name, score) in scores)
{
    Console.WriteLine($"{name}: {score}");
}

Вы можете объединить словари, скопировав один из них и применив переопределения:

Dictionary<string, int> defaults = new()
{
    ["Timeout"] = 30,
    ["Retries"] = 3
};
Dictionary<string, int> overrides = new()
{
    ["Timeout"] = 60
};

// Merge defaults and overrides into a new dictionary
Dictionary<string, int> config = new(defaults);
foreach (var (key, value) in overrides)
{
    config[key] = value;
}

Console.WriteLine($"Timeout: {config["Timeout"]}");  // 60
Console.WriteLine($"Retries: {config["Retries"]}");   // 3

Ограничения типов

Ограничения задают пределы для аргументов того, какие типы может принимать универсальный тип или метод. Ограничения позволяют вызывать методы или свойства доступа к параметру типа, который не будет доступен только в object :

static T Max<T>(T a, T b) where T : IComparable<T> =>
    a.CompareTo(b) >= 0 ? a : b;

Console.WriteLine(Max(3, 7));          // 7
Console.WriteLine(Max("apple", "banana")); // banana

static T CreateDefault<T>() where T : new() => new T();

var list = CreateDefault<List<int>>(); // Creates an empty List<int>
Console.WriteLine($"Empty list count: {list.Count}"); // 0

Наиболее распространенными ограничениями являются:

Ограничение Значение
where T : class T должен быть ссылочным типом
where T : struct T должен быть типом значения, не допускающего значение NULL
where T : new() T должен иметь открытый конструктор без параметров
where T : BaseClass T должен быть производным от BaseClass
where T : IInterface T должен реализовать IInterface

Ограничения можно объединить: where T : class, IComparable<T>, new() Менее распространенные ограничения включают where T : System.Enumи where T : System.Delegatewhere T : unmanaged для специализированных сценариев. Полный список см. в разделе "Ограничения" для параметров типа.

Ковариантность и контрвариантность

Ковариантность и контравариантность описывают поведение универсальных типов с наследованием. Они определяют, можно ли использовать более производный или менее производный аргумент типа, чем первоначально указанный:

// Covariance: IEnumerable<Dog> can be used as IEnumerable<Animal>
// because IEnumerable<out T> is covariant
List<Dog> dogs = [new("Rex"), new("Buddy")];
IEnumerable<Animal> animals = dogs; // Allowed because Dog derives from Animal

foreach (var animal in animals)
{
    Console.WriteLine(animal.Name);
}

// Contravariance: Action<Animal> can be used as Action<Dog>
// because Action<in T> is contravariant
Action<Animal> printAnimal = a => Console.WriteLine($"Animal: {a.Name}");
Action<Dog> printDog = printAnimal; // Allowed because any Animal handler can handle Dog

printDog(new Dog("Spot"));
  • Ковариантность (out T): IEnumerable<Dog> можно использовать, где IEnumerable<Animal> ожидается, так как Dog производный от Animal. Ключевое out слово в параметре типа включает это. Ковариантные параметры типа могут появляться только в выходных позициях (возвращаемых типах).
  • Контравариантность (in T): Action<Animal> можно использовать, где Action<Dog> ожидается, так как любое действие, которое обрабатывает Animal также может обрабатываться Dog. Ключевое in слово включает это. Параметры контравариантного типа могут отображаться только в входных позициях (параметрах).

Многие встроенные интерфейсы и делегаты уже ковариантны: IEnumerable<out T>, IReadOnlyList<out T>, Func<out TResult>, и Action<in T>. При работе с этими типами вы автоматически получаете преимущество от вариативности. Для детального рассмотрения проектирования вариантных интерфейсов и делегатов см. Ковариация и контравариация.

Создание собственных универсальных типов

Вы можете определить собственные универсальные классы, структуры, интерфейсы и методы. В следующем примере показан простой универсальный связанный список для иллюстрации. На практике используйте List<T> или другую встроенную коллекцию:

public class GenericList<T>
{
    private class Node(T data)
    {
        public T Data { get; set; } = data;
        public Node? Next { get; set; }
    }

    private Node? head;

    public void AddHead(T data)
    {
        var node = new Node(data) { Next = head };
        head = node;
    }

    public IEnumerator<T> GetEnumerator()
    {
        var current = head;
        while (current is not null)
        {
            yield return current.Data;
            current = current.Next;
        }
    }
}
var list = new GenericList<int>();
for (var i = 0; i < 5; i++)
{
    list.AddHead(i);
}

foreach (var item in list)
{
    Console.Write($"{item} ");
}
Console.WriteLine();
// Output: 4 3 2 1 0

Универсальные типы не ограничиваются классами. Вы можете определить универсальные interfaceи structrecord типы. Дополнительные сведения о проектировании универсальных алгоритмов и сложных сочетаний ограничений см. в разделе Generics в .NET.

См. также