C# Language
Generics
Ricerca…
Sintassi
-
public void SomeMethod <T> () { }
-
public void SomeMethod<T, V>() { }
-
public T SomeMethod<T>(IEnumerable<T> sequence) { ... }
-
public void SomeMethod<T>() where T : new() { }
-
public void SomeMethod<T, V>() where T : new() where V : struct { }
-
public void SomeMethod<T>() where T: IDisposable { }
-
public void SomeMethod<T>() where T: Foo { }
-
public class MyClass<T> { public T Data {get; set; } }
Parametri
Parametro (s) | Descrizione |
---|---|
TV | Digitare segnaposto per dichiarazioni generiche |
Osservazioni
Generics in C # sono supportati fino al runtime: i tipi generici creati con C # conservano la loro semantica generica anche dopo essere stati compilati in CIL .
Ciò significa in effetti che, in C #, è possibile riflettere su tipi generici e vederli come sono stati dichiarati o controllare se un oggetto è un'istanza di un tipo generico, ad esempio. Ciò è in contrasto con la cancellazione del tipo , in cui le informazioni di tipo generico vengono rimosse durante la compilazione. È anche in contrasto con l'approccio del modello ai generici, in cui più tipi generici concreti diventano più tipi non generici in fase di runtime e tutti i metadati necessari per un'ulteriore definizione delle definizioni del tipo generico originale vengono persi.
Prestare attenzione, tuttavia, quando si riflette su tipi generici: i nomi dei tipi generici verranno alterati nella compilazione, sostituendo le parentesi angolate ei nomi dei parametri di tipo con un apice seguito dal numero di parametri di tipo generico. Pertanto, un Dictionary<TKey, Tvalue>
sarà tradotto nel Dictionary`2
.
Tipo Parametri (Classi)
Dichiarazione:
class MyGenericClass<T1, T2, T3, ...>
{
// Do something with the type parameters.
}
Inizializzazione:
var x = new MyGenericClass<int, char, bool>();
Utilizzo (come il tipo di un parametro):
void AnotherMethod(MyGenericClass<float, byte, char> arg) { ... }
Tipo Parametri (metodi)
Dichiarazione:
void MyGenericMethod<T1, T2, T3>(T1 a, T2 b, T3 c)
{
// Do something with the type parameters.
}
Invocazione:
Non è necessario fornire argomentazioni di tipo a un metodo genrico, poiché il compilatore può implicitamente inferire il tipo.
int x =10;
int y =20;
string z = "test";
MyGenericMethod(x,y,z);
Tuttavia, se c'è un'ambiguità, i metodi generici devono essere chiamati con tipo arguemnts come
MyGenericMethod<int, int, string>(x,y,z);
Tipo Parametri (interfacce)
Dichiarazione:
interface IMyGenericInterface<T1, T2, T3, ...> { ... }
Uso (in eredità):
class ClassA<T1, T2, T3> : IMyGenericInterface<T1, T2, T3> { ... }
class ClassB<T1, T2> : IMyGenericInterface<T1, T2, int> { ... }
class ClassC<T1> : IMyGenericInterface<T1, char, int> { ... }
class ClassD : IMyGenericInterface<bool, char, int> { ... }
Utilizzo (come il tipo di un parametro):
void SomeMethod(IMyGenericInterface<int, char, bool> arg) { ... }
Inferenza implicita del tipo (metodi)
Quando si passano argomenti formali a un metodo generico, argomenti di tipo generico rilevanti possono essere normalmente dedotti implicitamente. Se è possibile dedurre tutti i tipi generici, specificarli nella sintassi è facoltativo.
Si consideri il seguente metodo generico. Ha un parametro formale e un parametro di tipo generico. Esiste una relazione molto evidente tra di essi: il tipo passato come argomento al parametro di tipo generico deve essere uguale al tipo di tempo di compilazione dell'argomento passato al parametro formale.
void M<T>(T obj)
{
}
Queste due chiamate sono equivalenti:
M<object>(new object());
M(new object());
Queste due chiamate sono anche equivalenti:
M<string>("");
M("");
E così sono queste tre chiamate:
M<object>("");
M((object) "");
M("" as object);
Si noti che se non è possibile dedurre almeno un argomento di tipo, è necessario specificarli tutti.
Si consideri il seguente metodo generico. Il primo argomento di tipo generico è lo stesso del tipo dell'argomento formale. Ma non esiste una tale relazione per il secondo argomento di tipo generico. Pertanto, il compilatore non ha modo di inferire il secondo argomento di tipo generico in qualsiasi chiamata a questo metodo.
void X<T1, T2>(T1 obj)
{
}
Questo non funziona più:
X("");
Anche questo non funziona, perché il compilatore non è sicuro se stiamo specificando il primo o il secondo parametro generico (entrambi sarebbero validi come object
):
X<object>("");
Siamo tenuti a digitare entrambi, in questo modo:
X<string, object>("");
Vincoli di tipo (classi e interfacce)
I vincoli di tipo sono in grado di forzare un parametro di tipo per implementare una determinata interfaccia o classe.
interface IType;
interface IAnotherType;
// T must be a subtype of IType
interface IGeneric<T>
where T : IType
{
}
// T must be a subtype of IType
class Generic<T>
where T : IType
{
}
class NonGeneric
{
// T must be a subtype of IType
public void DoSomething<T>(T arg)
where T : IType
{
}
}
// Valid definitions and expressions:
class Type : IType { }
class Sub : IGeneric<Type> { }
class Sub : Generic<Type> { }
new NonGeneric().DoSomething(new Type());
// Invalid definitions and expressions:
class AnotherType : IAnotherType { }
class Sub : IGeneric<AnotherType> { }
class Sub : Generic<AnotherType> { }
new NonGeneric().DoSomething(new AnotherType());
Sintassi per più vincoli:
class Generic<T, T1>
where T : IType
where T1 : Base, new()
{
}
I vincoli di tipo funzionano allo stesso modo dell'ereditarietà, in quanto è possibile specificare più interfacce come vincoli sul tipo generico, ma solo una classe:
class A { /* ... */ }
class B { /* ... */ }
interface I1 { }
interface I2 { }
class Generic<T>
where T : A, I1, I2
{
}
class Generic2<T>
where T : A, B //Compilation error
{
}
Un'altra regola è che la classe deve essere aggiunta come primo vincolo e quindi le interfacce:
class Generic<T>
where T : A, I1
{
}
class Generic2<T>
where T : I1, A //Compilation error
{
}
Tutti i vincoli dichiarati devono essere soddisfatti simultaneamente affinché una determinata istanza generica funzioni. Non c'è modo di specificare due o più insiemi alternativi di vincoli.
Vincoli di tipo (classe e struct)
È possibile specificare se l'argomento type debba essere un tipo di riferimento o un tipo di valore utilizzando la rispettiva class
vincoli o struct
. Se questi vincoli vengono utilizzati, devono essere definiti prima di poter elencare tutti gli altri vincoli (ad esempio un genitore o un new()
).
// TRef must be a reference type, the use of Int32, Single, etc. is invalid.
// Interfaces are valid, as they are reference types
class AcceptsRefType<TRef>
where TRef : class
{
// TStruct must be a value type.
public void AcceptStruct<TStruct>()
where TStruct : struct
{
}
// If multiple constraints are used along with class/struct
// then the class or struct constraint MUST be specified first
public void Foo<TComparableClass>()
where TComparableClass : class, IComparable
{
}
}
Vincoli di tipo (nuova parola chiave)
Utilizzando il new()
vincolo new()
, è possibile applicare i parametri del tipo per definire un costruttore vuoto (predefinito).
class Foo
{
public Foo () { }
}
class Bar
{
public Bar (string s) { ... }
}
class Factory<T>
where T : new()
{
public T Create()
{
return new T();
}
}
Foo f = new Factory<Foo>().Create(); // Valid.
Bar b = new Factory<Bar>().Create(); // Invalid, Bar does not define a default/empty constructor.
La seconda chiamata a Create()
restituirà l'errore di compilazione con il seguente messaggio:
'Bar' deve essere un tipo non astratto con un costruttore pubblico senza parametri per utilizzarlo come parametro 'T' nel tipo generico o nel metodo 'Factory'
Non esiste alcun vincolo per un costruttore con parametri, sono supportati solo costruttori senza parametri.
Inferenza di tipo (classi)
Gli sviluppatori possono essere colti dal fatto che l'inferenza di tipo non funziona per i costruttori:
class Tuple<T1,T2>
{
public Tuple(T1 value1, T2 value2)
{
}
}
var x = new Tuple(2, "two"); // This WON'T work...
var y = new Tuple<int, string>(2, "two"); // even though the explicit form will.
Il primo modo di creare un'istanza senza specificare esplicitamente i parametri di tipo causerà errori di compilazione che direbbero:
L'utilizzo del tipo generico 'Tuple <T1, T2>' richiede due argomenti di tipo
Una soluzione alternativa è aggiungere un metodo helper in una classe statica:
static class Tuple
{
public static Tuple<T1, T2> Create<T1, T2>(T1 value1, T2 value2)
{
return new Tuple<T1, T2>(value1, value2);
}
}
var x = Tuple.Create(2, "two"); // This WILL work...
Riflettendo sui parametri del tipo
L'operatore typeof
funziona sui parametri del tipo.
class NameGetter<T>
{
public string GetTypeName()
{
return typeof(T).Name;
}
}
Parametri di tipo esplicito
Esistono diversi casi in cui è necessario specificare esplicitamente i parametri del tipo per un metodo generico. In entrambi i casi seguenti, il compilatore non è in grado di dedurre tutti i parametri del tipo dai parametri del metodo specificato.
Un caso è quando non ci sono parametri:
public void SomeMethod<T, V>()
{
// No code for simplicity
}
SomeMethod(); // doesn't compile
SomeMethod<int, bool>(); // compiles
Il secondo caso è quando uno (o più) dei parametri del tipo non fa parte dei parametri del metodo:
public K SomeMethod<K, V>(V input)
{
return default(K);
}
int num1 = SomeMethod(3); // doesn't compile
int num2 = SomeMethod<int>("3"); // doesn't compile
int num3 = SomeMethod<int, string>("3"); // compiles.
Utilizzo del metodo generico con un'interfaccia come tipo di vincolo.
Questo è un esempio di come usare il tipo generico TFood all'interno del metodo Eat sulla classe Animal
public interface IFood
{
void EatenBy(Animal animal);
}
public class Grass: IFood
{
public void EatenBy(Animal animal)
{
Console.WriteLine("Grass was eaten by: {0}", animal.Name);
}
}
public class Animal
{
public string Name { get; set; }
public void Eat<TFood>(TFood food)
where TFood : IFood
{
food.EatenBy(this);
}
}
public class Carnivore : Animal
{
public Carnivore()
{
Name = "Carnivore";
}
}
public class Herbivore : Animal, IFood
{
public Herbivore()
{
Name = "Herbivore";
}
public void EatenBy(Animal animal)
{
Console.WriteLine("Herbivore was eaten by: {0}", animal.Name);
}
}
Puoi chiamare il metodo Eat in questo modo:
var grass = new Grass();
var sheep = new Herbivore();
var lion = new Carnivore();
sheep.Eat(grass);
//Output: Grass was eaten by: Herbivore
lion.Eat(sheep);
//Output: Herbivore was eaten by: Carnivore
In questo caso, se provi a chiamare:
sheep.Eat(lion);
Non sarà possibile perché l'oggetto leone non implementa l'interfaccia IFood. Tentare di effettuare la chiamata sopra genererà un errore del compilatore: "Il tipo 'Carnivore' non può essere utilizzato come parametro di tipo 'TFood' nel tipo generico o nel metodo 'Animal.Eat (TFood)'. Non c'è conversione implicita di riferimento da ' Carnivore 'a' IFood '. "
covarianza
Quando è un IEnumerable<T>
un sottotipo di un IEnumerable<T1>
diverso? Quando T
è un sottotipo di T1
. IEnumerable
è covariante nel suo parametro T
, il che significa che la relazione del sottotipo di IEnumerable
va nella stessa direzione di T
class Animal { /* ... */ }
class Dog : Animal { /* ... */ }
IEnumerable<Dog> dogs = Enumerable.Empty<Dog>();
IEnumerable<Animal> animals = dogs; // IEnumerable<Dog> is a subtype of IEnumerable<Animal>
// dogs = animals; // Compilation error - IEnumerable<Animal> is not a subtype of IEnumerable<Dog>
Un'istanza di un tipo generico covariante con un determinato parametro di tipo è implicitamente convertibile nello stesso tipo generico con un parametro di tipo meno derivato.
Questa relazione vale perché IEnumerable
produce T
s ma non li consuma. Un oggetto che produce Dog
s può essere usato come se producesse Animal
s.
I parametri di tipo Covariant vengono dichiarati utilizzando la parola chiave out
, poiché il parametro deve essere utilizzato solo come output .
interface IEnumerable<out T> { /* ... */ }
Un parametro di tipo dichiarato come covariante potrebbe non apparire come input.
interface Bad<out T>
{
void SetT(T t); // type error
}
Ecco un esempio completo:
using NUnit.Framework;
namespace ToyStore
{
enum Taste { Bitter, Sweet };
interface IWidget
{
int Weight { get; }
}
interface IFactory<out TWidget>
where TWidget : IWidget
{
TWidget Create();
}
class Toy : IWidget
{
public int Weight { get; set; }
public Taste Taste { get; set; }
}
class ToyFactory : IFactory<Toy>
{
public const int StandardWeight = 100;
public const Taste StandardTaste = Taste.Sweet;
public Toy Create() { return new Toy { Weight = StandardWeight, Taste = StandardTaste }; }
}
[TestFixture]
public class GivenAToyFactory
{
[Test]
public static void WhenUsingToyFactoryToMakeWidgets()
{
var toyFactory = new ToyFactory();
//// Without out keyword, note the verbose explicit cast:
// IFactory<IWidget> rustBeltFactory = (IFactory<IWidget>)toyFactory;
// covariance: concrete being assigned to abstract (shiny and new)
IFactory<IWidget> widgetFactory = toyFactory;
IWidget anotherToy = widgetFactory.Create();
Assert.That(anotherToy.Weight, Is.EqualTo(ToyFactory.StandardWeight)); // abstract contract
Assert.That(((Toy)anotherToy).Taste, Is.EqualTo(ToyFactory.StandardTaste)); // concrete contract
}
}
}
controvarianza
Quando è un IComparer<T>
un sottotipo di un altro IComparer<T1>
? Quando T1
è un sottotipo di T
IComparer
è controvariante nel suo parametro T
, il che significa che la relazione del sottotipo di IComparer
va nella direzione opposta a quella di T
class Animal { /* ... */ }
class Dog : Animal { /* ... */ }
IComparer<Animal> animalComparer = /* ... */;
IComparer<Dog> dogComparer = animalComparer; // IComparer<Animal> is a subtype of IComparer<Dog>
// animalComparer = dogComparer; // Compilation error - IComparer<Dog> is not a subtype of IComparer<Animal>
Un'istanza di un tipo generico controvariante con un determinato parametro di tipo è implicitamente convertibile nello stesso tipo generico con un parametro di tipo più derivato.
Questa relazione vale perché IComparer
consuma T
s ma non li produce. Un oggetto che può confrontare due Animal
può essere usato per confrontare due Dog
.
Parametri di tipo controvarianti vengono dichiarati utilizzando la in
parola, poiché il parametro deve essere utilizzato solo come ingresso.
interface IComparer<in T> { /* ... */ }
Un parametro di tipo dichiarato come controvariante potrebbe non apparire come output.
interface Bad<in T>
{
T GetT(); // type error
}
invarianza
IList<T>
non è mai un sottotipo di un diverso IList<T1>
. IList
è invariante nel suo parametro di tipo.
class Animal { /* ... */ }
class Dog : Animal { /* ... */ }
IList<Dog> dogs = new List<Dog>();
IList<Animal> animals = dogs; // type error
Non esiste una relazione di sottotipo per gli elenchi poiché è possibile inserire valori in un elenco e ricavare valori da un elenco.
Se IList
fosse covariante, saresti in grado di aggiungere elementi del sottotipo sbagliato a una data lista.
IList<Animal> animals = new List<Dog>(); // supposing this were allowed...
animals.Add(new Giraffe()); // ... then this would also be allowed, which is bad!
Se IList
fosse controverso, saresti in grado di estrarre i valori del sottotipo errato da una lista data.
IList<Dog> dogs = new List<Animal> { new Dog(), new Giraffe() }; // if this were allowed...
Dog dog = dogs[1]; // ... then this would be allowed, which is bad!
I parametri di tipo invariante sono dichiarati omettendo sia le parole chiave in
e out
.
interface IList<T> { /* ... */ }
Interfacce varianti
Le interfacce possono avere parametri di tipo variante.
interface IEnumerable<out T>
{
// ...
}
interface IComparer<in T>
{
// ...
}
ma le classi e le strutture non possono
class BadClass<in T1, out T2> // not allowed
{
}
struct BadStruct<in T1, out T2> // not allowed
{
}
né dichiarazioni di metodi generici
class MyClass
{
public T Bad<out T, in T1>(T1 t1) // not allowed
{
// ...
}
}
L'esempio seguente mostra più dichiarazioni di varianza sulla stessa interfaccia
interface IFoo<in T1, out T2, T3>
// T1 : Contravariant type
// T2 : Covariant type
// T3 : Invariant type
{
// ...
}
IFoo<Animal, Dog, int> foo1 = /* ... */;
IFoo<Dog, Animal, int> foo2 = foo1;
// IFoo<Animal, Dog, int> is a subtype of IFoo<Dog, Animal, int>
Delegati varianti
I delegati possono avere parametri di tipo variante.
delegate void Action<in T>(T t); // T is an input
delegate T Func<out T>(); // T is an output
delegate T2 Func<in T1, out T2>(); // T1 is an input, T2 is an output
Ciò deriva dal Principio di sostituzione di Liskov , che stabilisce (tra le altre cose) che un metodo D può essere considerato più derivato di un metodo B se:
- D ha un tipo di ritorno uguale o più derivato di B
- D ha i tipi di parametro corrispondenti uguali o più generali di B
Pertanto i seguenti compiti sono tutti di tipo sicuro:
Func<object, string> original = SomeMethod;
Func<object, object> d1 = original;
Func<string, string> d2 = original;
Func<string, object> d3 = original;
Tipi varianti come parametri e valori di ritorno
Se un tipo covariante viene visualizzato come output, il tipo contenente è covariante. Produrre un produttore di T
s è come produrre T
s.
interface IReturnCovariant<out T>
{
IEnumerable<T> GetTs();
}
Se un tipo controvariante appare come output, il tipo contenente è controverso. Produrre un consumatore di T
s è come consumare T
s.
interface IReturnContravariant<in T>
{
IComparer<T> GetTComparer();
}
Se un tipo covariante appare come un input, il tipo contenente è controverso. Consumare un produttore di T
s è come consumare T
s.
interface IAcceptCovariant<in T>
{
void ProcessTs(IEnumerable<T> ts);
}
Se un tipo controvariante appare come input, il tipo contenente è covariante. Consumare un consumatore di T
s è come produrre T
s.
interface IAcceptContravariant<out T>
{
void CompareTs(IComparer<T> tComparer);
}
Controllo dell'uguaglianza dei valori generici.
Se la logica di classe generica o metodo richiede il controllo parità di valore aventi tipo generico, utilizzare EqualityComparer<TType>.Default
proprietà :
public void Foo<TBar>(TBar arg1, TBar arg2)
{
var comparer = EqualityComparer<TBar>.Default;
if (comparer.Equals(arg1,arg2)
{
...
}
}
Questo approccio è meglio che semplicemente chiamando Object.Equals()
metodo, perché i controlli di implementazione predefinita di confronto, se TBar
tipo implementa IEquatale<TBar>
Interfaccia e se sì, le chiamate IEquatable<TBar>.Equals(TBar other)
metodo. Ciò consente di evitare il boxing / unboxing dei tipi di valore.
Tipo generico casting
/// <summary>
/// Converts a data type to another data type.
/// </summary>
public static class Cast
{
/// <summary>
/// Converts input to Type of default value or given as typeparam T
/// </summary>
/// <typeparam name="T">typeparam is the type in which value will be returned, it could be any type eg. int, string, bool, decimal etc.</typeparam>
/// <param name="input">Input that need to be converted to specified type</param>
/// <param name="defaultValue">defaultValue will be returned in case of value is null or any exception occures</param>
/// <returns>Input is converted in Type of default value or given as typeparam T and returned</returns>
public static T To<T>(object input, T defaultValue)
{
var result = defaultValue;
try
{
if (input == null || input == DBNull.Value) return result;
if (typeof (T).IsEnum)
{
result = (T) Enum.ToObject(typeof (T), To(input, Convert.ToInt32(defaultValue)));
}
else
{
result = (T) Convert.ChangeType(input, typeof (T));
}
}
catch (Exception ex)
{
Tracer.Current.LogException(ex);
}
return result;
}
/// <summary>
/// Converts input to Type of typeparam T
/// </summary>
/// <typeparam name="T">typeparam is the type in which value will be returned, it could be any type eg. int, string, bool, decimal etc.</typeparam>
/// <param name="input">Input that need to be converted to specified type</param>
/// <returns>Input is converted in Type of default value or given as typeparam T and returned</returns>
public static T To<T>(object input)
{
return To(input, default(T));
}
}
usi:
std.Name = Cast.To<string>(drConnection["Name"]);
std.Age = Cast.To<int>(drConnection["Age"]);
std.IsPassed = Cast.To<bool>(drConnection["IsPassed"]);
// Casting type using default value
//Following both ways are correct
// Way 1 (In following style input is converted into type of default value)
std.Name = Cast.To(drConnection["Name"], "");
std.Marks = Cast.To(drConnection["Marks"], 0);
// Way 2
std.Name = Cast.To<string>(drConnection["Name"], "");
std.Marks = Cast.To<int>(drConnection["Marks"], 0);
Lettore di configurazione con casting di tipo generico
/// <summary>
/// Read configuration values from app.config and convert to specified types
/// </summary>
public static class ConfigurationReader
{
/// <summary>
/// Get value from AppSettings by key, convert to Type of default value or typeparam T and return
/// </summary>
/// <typeparam name="T">typeparam is the type in which value will be returned, it could be any type eg. int, string, bool, decimal etc.</typeparam>
/// <param name="strKey">key to find value from AppSettings</param>
/// <param name="defaultValue">defaultValue will be returned in case of value is null or any exception occures</param>
/// <returns>AppSettings value against key is returned in Type of default value or given as typeparam T</returns>
public static T GetConfigKeyValue<T>(string strKey, T defaultValue)
{
var result = defaultValue;
try
{
if (ConfigurationManager.AppSettings[strKey] != null)
result = (T)Convert.ChangeType(ConfigurationManager.AppSettings[strKey], typeof(T));
}
catch (Exception ex)
{
Tracer.Current.LogException(ex);
}
return result;
}
/// <summary>
/// Get value from AppSettings by key, convert to Type of default value or typeparam T and return
/// </summary>
/// <typeparam name="T">typeparam is the type in which value will be returned, it could be any type eg. int, string, bool, decimal etc.</typeparam>
/// <param name="strKey">key to find value from AppSettings</param>
/// <returns>AppSettings value against key is returned in Type given as typeparam T</returns>
public static T GetConfigKeyValue<T>(string strKey)
{
return GetConfigKeyValue(strKey, default(T));
}
}
usi:
var timeOut = ConfigurationReader.GetConfigKeyValue("RequestTimeout", 2000);
var url = ConfigurationReader.GetConfigKeyValue("URL", "www.someurl.com");
var enabled = ConfigurationReader.GetConfigKeyValue("IsEnabled", false);