Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
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>, <Product>) 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 (C# 12) 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 controvarianza descrivono il comportamento dei tipi generici con l'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 doveIEnumerable<Animal>è previsto perchéDogderiva daAnimal. Laoutparola 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 doveAction<Dog>è previsto perché qualsiasi azione che gestisceAnimalpuò gestire ancheDog. Lainparola 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.