Typy ogólne i metody

Wskazówka

Dopiero zaczynasz programować oprogramowanie? Najpierw zacznij od samouczków Wprowadzenie . Napotkasz typy ogólne zaraz po użyciu kolekcji, takich jak List<T>.

Czy masz doświadczenie w pracy w innym języku? Typy generyczne w języku C# są podobne do tych w Javie lub szablonach C++, ale z pełną informacją o typach w czasie wykonywania i bez wymazywania typów. Przejrzyj sekcje dotyczące wyrażeń kolekcji oraz kowariancji i kontrawariancji, aby zidentyfikować wzorce specyficzne dla języka C#.

Szablony typów umożliwiają pisanie kodu, który działa z dowolnym typem przy pełnym zachowaniu typowania. Zamiast pisać oddzielne klasy lub metody dla int, stringi każdego innego typu, należy napisać jedną wersję z co najmniej jednym parametrem typu (takimi jak T, lub TKey i TValue) i określić rzeczywiste typy podczas korzystania z niego. Kompilator sprawdza typy w czasie kompilacji, więc nie potrzebujesz rzutów środowiska uruchomieniowego ani ryzyka InvalidCastException.

Spotykasz rodzajowe stale w codziennym języku C#. Kolekcje, typy asynchronicznych wyników, delegaty i LINQ opierają się na typach ogólnych.

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

W każdym przypadku argument typu w nawiasach kątowych (<int>, <string>, <Product>) informuje typ ogólny, jakiego rodzaju dane przechowuje lub na których działa. Kompilator wymusza bezpieczeństwo typu. Nie można przypadkowo dodać elementu string do elementu List<int>.

Korzystanie z typów ogólnych

Częściej korzystasz z typów ogólnych z biblioteki klas .NET zamiast tworzyć własne. W poniższych sekcjach przedstawiono najbardziej typowe typy ogólne, których będziesz używać.

Kolekcje ogólne

System.Collections.Generic Przestrzeń nazw zapewnia klasy kolekcji zapewniające bezpieczeństwo typów. Zawsze używaj tych kolekcji zamiast kolekcji niegenerycznych, takich jak 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

Kolekcje ogólne uniemożliwiają błędy typu w czasie wykonywania, ponieważ zamiast tego występują błędy w czasie kompilacji. Te kolekcje unikają również tworzenia pól dla typów wartości, co zwiększa wydajność.

Metody ogólne

Metoda ogólna deklaruje własny parametr typu. Kompilator często wywnioskuje argument typu z przekazanych wartości, więc nie trzeba ich jawnie określać:

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

W wywołaniu Print(42) kompilator wnioskuje T jako int z argumentu. Możesz napisać Print<int>(42) jawnie, ale wnioskowanie typu sprawia, że kod jest czytelniejszy.

Wyrażenia kolekcji

Wyrażenia kolekcji (C# 12) zapewniają zwięzłą składnię tworzenia kolekcji. Użyj nawiasów kwadratowych zamiast wywołań konstruktora lub składni inicjatora:

// 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)}");

Operator spreadu (..) wskazuje elementy jednej kolekcji na drugą, co jest przydatne do łączenia sekwencji:

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

Wyrażenia kolekcji działają z tablicami, List<T>, Span<T>, ImmutableArray<T>i dowolnym typem obsługującym wzorzec konstruktora kolekcji. Aby uzyskać pełne odniesienie do składni, zobacz Wyrażenia kolekcji.

Inicjowanie słownika

Słowniki można zainicjować zwięźle za pomocą inicjalizatorów indeksatorów. Składnia ta używa nawiasów kwadratowych do definiowania par klucz-wartość.

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

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

Słowniki można scalić, kopiując jeden i stosując przesłonięcia:

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

Ograniczenia typu

Ograniczenia ograniczają, które argumenty typu akceptuje typ ogólny lub metoda. Ograniczenia umożliwiają wywoływanie metod lub uzyskiwanie dostępu do właściwości parametru typu, które nie byłyby dostępne tylko za pomocą 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

Najbardziej typowe ograniczenia to:

Ograniczenie Meaning
where T : class T musi być typem odwołania
where T : struct T musi być typem wartości nienullowalnym
where T : new() T musi mieć publiczny konstruktor bez parametrów
where T : BaseClass T musi pochodzić z BaseClass
where T : IInterface T musi implementować IInterface

Ograniczenia można łączyć: where T : class, IComparable<T>, new(). Mniej typowe ograniczenia obejmują where T : System.Enum, where T : System.Delegatei where T : unmanaged dla wyspecjalizowanych scenariuszy. Aby uzyskać pełną listę, zobacz Ograniczenia dotyczące parametrów typu.

Kowariancja i kontrawariancja

Kowariancja i kontrawariancja opisują zachowanie typów ogólnych z dziedziczeniem. Określają, czy można użyć bardziej pochodnego lub mniej pochodnego argumentu typu niż pierwotnie określony:

// 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"));
  • Kowariancja (out T): IEnumerable<Animal> można użyć tam, gdzie oczekiwane jest IEnumerable<Dog>, ponieważ Dog pochodzi z Animal. Słowo out kluczowe w parametrze typu umożliwia to. Kowariantne parametry typu mogą występować tylko w miejscach zwracania (typach zwracanych).
  • Kontrawariancja (in T): Action<Animal> można użyć tam, gdzie Action<Dog> jest oczekiwana, ponieważ każda obsługująca akcja Animal może również obsługiwać Dog. Słowo in kluczowe umożliwia to. Kontrawariantne parametry typu mogą być wyświetlane tylko w pozycjach wejściowych (parametrach).

Wiele wbudowanych interfejsów i delegatów jest już wariantami: IEnumerable<out T>, IReadOnlyList<out T>, Func<out TResult>, i Action<in T>. Podczas pracy z tymi typami korzystasz automatycznie z wariancji. Aby zapoznać się ze szczegółowym traktowaniem projektowania interfejsów wariantów i delegatów, zobacz Kowariancja i kontrawariancja.

Tworzenie własnych typów ogólnych

Możesz zdefiniować własne klasy ogólne, struktury, interfejsy i metody. W poniższym przykładzie przedstawiono prostą ogólną listę połączoną na potrzeby ilustracji. W praktyce użyj List<T> lub innej wbudowanej kolekcji:

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

Typy ogólne nie są ograniczone do klas. Można zdefiniować typy ogólne interface, structi record . Aby uzyskać więcej informacji na temat projektowania ogólnych algorytmów i złożonych kombinacji ograniczeń, zobacz Generics w .NET.

Zobacz także