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>, <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 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ò gestireDoganche . 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.