Tipi e metodi generici

Suggerimento

Novità dello sviluppo di software? Iniziare prima con le esercitazioni introduttive . I generics verranno visualizzati non appena si usano raccolte come List<T>.

Esperienza in un'altra lingua? I generics C# sono simili ai generics in Java o modelli in C++, ma con informazioni complete sul tipo di runtime e nessuna cancellazione dei tipi. Eseguire una rapida lettura delle espressioni di raccolta e delle sezioni di covarianza e controvarianza alla ricerca di modelli specifici di C#.

I generics consentono di scrivere codice che funziona con qualsiasi tipo mantenendo la sicurezza completa dei tipi. Anziché scrivere classi o metodi separati per int, stringe ogni altro tipo necessario, scrivere una versione con uno o più parametri di tipo (ad esempio T, o TKey e TValue) e specificare i tipi effettivi quando lo si usa. Il compilatore controlla i tipi in fase di compilazione, quindi non sono necessari cast di runtime o rischi InvalidCastException.

I generics si riscontrano costantemente in C#. Raccolte, tipi di ritorno asincroni, delegati e LINQ si basano su tipi generici:

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

In ogni caso, l'argomento di tipo tra parentesi angolari (<int>, <string>, <decimal>) indica al tipo generico il tipo di dati su cui contiene o opera. Il compilatore impone la sicurezza dei tipi. Non è possibile aggiungere accidentalmente un oggetto string a un oggetto List<int>.

Uso di tipi generici

Di solito, si consumano tipi generici dalla libreria di classi .NET anziché creare i tuoi. Le sezioni seguenti illustrano i tipi generici più comuni che verranno usati.

Raccolte generiche

Lo System.Collections.Generic spazio dei nomi fornisce classi di raccolta sicure per i tipi. Usare sempre queste raccolte anziché raccolte non generiche, ad esempio 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

Le raccolte generiche impediscono errori di tipo in fase di esecuzione perché gli errori vengono visualizzati in fase di compilazione. Queste raccolte evitano anche il boxing per i tipi di valore, migliorando così le prestazioni.

Metodi generici

Un metodo generico dichiara il proprio parametro di tipo. Il compilatore spesso deduce l'argomento di tipo dai valori passati, quindi non è necessario specificarlo in modo esplicito:

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

Nella chiamata Print(42), il compilatore deduce T come int dall'argomento . È possibile scrivere Print<int>(42) in modo esplicito, ma l'inferenza del tipo mantiene il codice più pulito.

Espressioni di raccolta

Le espressioni di raccolta forniscono una sintassi concisa per la creazione di raccolte. Usare le parentesi quadre anziché le chiamate al costruttore o la sintassi dell'inizializzatore:

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

L'operatore spread (..) inserisce inline gli elementi di una raccolta in un'altra, utile per combinare sequenze:

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

Le espressioni di raccolta funzionano con matrici, List<T>, Span<T>ImmutableArray<T>, e qualsiasi tipo che supporta il modello di generatore di raccolte. Per informazioni complete sulla sintassi, vedere Espressioni di raccolta.

Inizializzazione del dizionario

È possibile inizializzare i dizionari in modo conciso con gli inizializzatori dell'indicizzatore. Questa sintassi usa le parentesi quadre per impostare coppie chiave-valore:

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

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

È possibile unire i dizionari copiandone uno e applicando le sostituzioni:

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

Vincoli di tipo

I vincoli limitano gli argomenti di tipo accettati da un tipo o un metodo generico. I vincoli consentono di chiamare metodi o accedere alle proprietà nel parametro di tipo che non sarebbero disponibili object da soli:

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

I vincoli più comuni sono:

Constraint Meaning
where T : class T deve essere un tipo di riferimento
where T : struct T deve essere un tipo di valore non nullable
where T : new() T deve avere un costruttore pubblico senza parametri
where T : BaseClass T deve derivare da BaseClass
where T : IInterface T deve implementare IInterface

È possibile combinare vincoli: where T : class, IComparable<T>, new(). I vincoli meno comuni includono where T : System.Enum, where T : System.Delegatee where T : unmanaged per scenari specializzati. Per l'elenco completo, vedere Vincoli sui parametri di tipo.

Covarianza e controvarianza

Covarianza e contravarianza descrivono il comportamento dei tipi generici rispetto all'ereditarietà. Determinano se è possibile usare un argomento di tipo più derivato o meno derivato di quanto specificato originariamente:

// 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"));
  • Covarianza (out T): IEnumerable<Dog> può essere usato dove IEnumerable<Animal> è previsto perché Dog deriva da Animal. La out parola chiave nel parametro di tipo abilita questa opzione. I parametri di tipo covariante possono apparire solo in posizioni di output (tipi restituiti).
  • Controvarianza (in T): Action<Animal> può essere usato dove Action<Dog> è previsto perché qualsiasi azione che gestisce Animal può gestire Doganche . La in parola chiave abilita questa opzione. I parametri di tipo controvariante possono essere visualizzati solo nelle posizioni di input (parametri).

Molte interfacce e delegati predefiniti sono già varianti: IEnumerable<out T>, IReadOnlyList<out T>, Func<out TResult>e Action<in T>. È possibile trarre vantaggio dalla varianza automaticamente quando si lavora con questi tipi. Per un trattamento approfondito della progettazione di interfacce e delegati varianti, vedere Covarianza e controvarianza.

Creare tipi generici personalizzati

È possibile definire classi, struct, interfacce e metodi generici personalizzati. Nell'esempio seguente viene illustrato un semplice elenco collegato generico per l'illustrazione. In pratica, usare List<T> o un'altra raccolta predefinita:

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

I tipi generici non sono limitati alle classi. È possibile definire interfacetipi generici , structe record . Per altre informazioni sulla progettazione di algoritmi generici e combinazioni di vincoli complessi, vedere Generics in .NET.

Vedere anche