C# Language
Generics
Szukaj…
Składnia
-
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; } }
Parametry
Parametr (y) | Opis |
---|---|
TELEWIZJA | Wpisz symbole zastępcze dla ogólnych deklaracji |
Uwagi
Generyczne w C # są obsługiwane aż do środowiska uruchomieniowego: typy ogólne zbudowane w C # zachowają swoją ogólną semantykę nawet po skompilowaniu do CIL .
Oznacza to skutecznie, że w języku C # można zastanowić się nad rodzajami rodzajowymi i zobaczyć je tak, jak zostały zadeklarowane, lub sprawdzić, czy obiekt jest na przykład typem rodzaju ogólnego. Jest to w przeciwieństwie do usuwania typu , w którym ogólne informacje o typie są usuwane podczas kompilacji. Kontrastuje to również z szablonowym podejściem do rodzajów ogólnych, w którym wiele konkretnych typów ogólnych staje się w czasie wykonywania wieloma nietypowymi typami, a wszelkie metadane wymagane do dalszego tworzenia oryginalnych definicji typów ogólnych są tracone.
Zastanów się jednak nad typami rodzajowymi: nazwy typów ogólnych zostaną zmienione podczas kompilacji, zastępując nawiasy klamrowe i nazwy parametrów typu odwrotnym znakiem, a następnie liczbą parametrów typu ogólnego. Tak więc Dictionary<TKey, Tvalue>
zostanie przetłumaczony na Dictionary`2
.
Parametry typu (klasy)
Deklaracja:
class MyGenericClass<T1, T2, T3, ...>
{
// Do something with the type parameters.
}
Inicjalizacja:
var x = new MyGenericClass<int, char, bool>();
Zastosowanie (jako typ parametru):
void AnotherMethod(MyGenericClass<float, byte, char> arg) { ... }
Parametry typu (metody)
Deklaracja:
void MyGenericMethod<T1, T2, T3>(T1 a, T2 b, T3 c)
{
// Do something with the type parameters.
}
Wezwanie:
Nie ma potrzeby dostarczania argumentów typu do metody genetycznej, ponieważ kompilator może domyślnie wnioskować o typie.
int x =10;
int y =20;
string z = "test";
MyGenericMethod(x,y,z);
Jeśli jednak występuje dwuznaczność, należy wywołać metody ogólne z argumentami typu as
MyGenericMethod<int, int, string>(x,y,z);
Parametry typu (interfejsy)
Deklaracja:
interface IMyGenericInterface<T1, T2, T3, ...> { ... }
Zastosowanie (dziedziczenie):
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> { ... }
Zastosowanie (jako typ parametru):
void SomeMethod(IMyGenericInterface<int, char, bool> arg) { ... }
Wnioskowanie typu niejawnego (metody)
Podczas przekazywania argumentów formalnych do metody ogólnej, odpowiednie argumenty typu ogólnego można zwykle wywnioskować w sposób dorozumiany. Jeśli można wywnioskować cały typ ogólny, określenie ich w składni jest opcjonalne.
Rozważ następującą ogólną metodę. Ma jeden parametr formalny i jeden parametr typu ogólnego. Istnieje bardzo oczywisty związek między nimi - typ przekazywany jako argument do ogólnego parametru typu musi być taki sam, jak typ kompilacji argumentu przekazywanego do parametru formalnego.
void M<T>(T obj)
{
}
Te dwa połączenia są równoważne:
M<object>(new object());
M(new object());
Te dwa połączenia są również równoważne:
M<string>("");
M("");
Podobnie są te trzy połączenia:
M<object>("");
M((object) "");
M("" as object);
Zauważ, że jeśli nie można wywnioskować co najmniej jednego argumentu typu, należy podać wszystkie z nich.
Rozważ następującą ogólną metodę. Pierwszy argument typu ogólnego jest taki sam, jak typ argumentu formalnego. Ale nie ma takiej relacji dla argumentu drugiego rodzaju ogólnego. Dlatego kompilator nie ma możliwości wnioskowania drugiego argumentu typu ogólnego w żadnym wywołaniu tej metody.
void X<T1, T2>(T1 obj)
{
}
To już nie działa:
X("");
To też nie działa, ponieważ kompilator nie jest pewien, czy określamy pierwszy czy drugi parametr ogólny (oba byłyby poprawne jako object
):
X<object>("");
Jesteśmy zobowiązani do wpisania obu z nich:
X<string, object>("");
Ograniczenia typu (klasy i interfejsy)
Ograniczenia typu mogą wymusić na parametrze typu wdrożenie określonego interfejsu lub klasy.
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());
Składnia wielu ograniczeń:
class Generic<T, T1>
where T : IType
where T1 : Base, new()
{
}
Ograniczenia typu działają w taki sam sposób jak dziedziczenie, ponieważ można określić wiele interfejsów jako ograniczenia dla typu ogólnego, ale tylko jedną klasę:
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
{
}
Inną zasadą jest to, że klasa musi zostać dodana jako pierwsze ograniczenie, a następnie interfejsy:
class Generic<T>
where T : A, I1
{
}
class Generic2<T>
where T : I1, A //Compilation error
{
}
Wszystkie zadeklarowane ograniczenia muszą być spełnione jednocześnie, aby określona generyczna instancja działała. Nie ma możliwości określenia dwóch lub więcej alternatywnych zestawów ograniczeń.
Ograniczenia typu (klasa i struktura)
Można określić, czy argument typu powinien być typem referencyjnym czy typem wartości, używając odpowiedniej class
lub struct
ograniczeń. Jeśli te ograniczenia są używane, należy je zdefiniować, zanim będzie można wyświetlić wszystkie inne ograniczenia (na przykład typ nadrzędny lub 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
{
}
}
Ograniczenia typu (nowe słowo kluczowe)
Korzystając z new()
ograniczenia new()
, można wymusić parametry typu, aby zdefiniować pusty (domyślny) konstruktor.
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.
Drugie wywołanie funkcji Create()
spowoduje błąd czasu kompilacji z następującym komunikatem:
„Słupek” musi być nieabstrakcyjnym typem z publicznym konstruktorem bez parametrów, aby można go było używać jako parametru „T” w typie ogólnym lub metodzie „Fabryka”
Konstruktor z parametrami nie ma ograniczeń, obsługiwane są tylko konstruktory bez parametrów.
Wnioskowanie typu (klasy)
Deweloperzy mogą zostać przyłapani na tym, że wnioskowanie o typach nie działa w przypadku konstruktorów:
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.
Pierwszy sposób tworzenia instancji bez jawnego określenia parametrów typu spowoduje błąd czasu kompilacji, który powiedziałby:
Użycie typu ogólnego „Tuple <T1, T2>” wymaga 2 argumentów typu
Częstym obejściem jest dodanie metody pomocniczej w klasie statycznej:
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...
Refleksja nad parametrami typu
Operator typeof
działa na parametrach typu.
class NameGetter<T>
{
public string GetTypeName()
{
return typeof(T).Name;
}
}
Jawne parametry typu
Istnieją różne przypadki, w których należy Jawnie określić parametry typu dla metody ogólnej. W obu poniższych przypadkach kompilator nie jest w stanie wywnioskować wszystkich parametrów typu na podstawie określonych parametrów metody.
Jednym z przypadków jest brak parametrów:
public void SomeMethod<T, V>()
{
// No code for simplicity
}
SomeMethod(); // doesn't compile
SomeMethod<int, bool>(); // compiles
Drugi przypadek ma miejsce, gdy jeden (lub więcej) parametrów typu nie jest częścią parametrów metody:
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.
Zastosowanie metody ogólnej z interfejsem jako typu ograniczenia.
To jest przykład użycia ogólnego typu TFood wewnątrz metody Eat w klasie 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);
}
}
Możesz wywołać metodę Eat w następujący sposób:
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
W takim przypadku, jeśli spróbujesz zadzwonić:
sheep.Eat(lion);
Nie będzie to możliwe, ponieważ lew obiektu nie implementuje interfejsu IFood. Próba wykonania powyższego wywołania spowoduje wygenerowanie błędu kompilatora: „Typu„ Carnivore ”nie można użyć jako parametru typu„ TFood ”w ogólnym typie lub metodzie„ Animal.Eat (TFood) ”. Brak jest domyślnej konwersji odniesienia z„ Carnivore ”na„ IFood ”.
Kowariancja
Kiedy IEnumerable<T>
jest podtypem innego IEnumerable<T1>
? Gdy T
jest podtypem T1
. IEnumerable
jest kowariantem w swoim parametrze T
, co oznacza, że zależność podtypu IEnumerable
idzie w tym samym kierunku co 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>
Wystąpienie kowariantnego typu ogólnego z danym parametrem typu jest domyślnie przekształcalne na ten sam typ ogólny z parametrem typu mniej pochodnego.
Ten związek utrzymuje się, ponieważ IEnumerable
produkuje T
ale ich nie konsumuje. Obiekt, który produkuje Dog
może być używany tak, jakby produkował Animal
.
Parametry typu kowariantnego deklarowane są za pomocą słowa kluczowego out
, ponieważ parametru należy użyć tylko jako wyniku .
interface IEnumerable<out T> { /* ... */ }
Parametr typu zadeklarowany jako kowariant może nie pojawić się jako dane wejściowe.
interface Bad<out T>
{
void SetT(T t); // type error
}
Oto kompletny przykład:
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
}
}
}
Kontrawariancja
Kiedy IComparer<T>
jest podtypem innego IComparer<T1>
? Gdy T1
jest podtypem T
IComparer
jest przeciwstawny w swoim parametrze T
, co oznacza, że IComparer
podtypu IComparer
idzie w przeciwnym kierunku niż 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>
Wystąpienie przeciwstawnego typu ogólnego z danym parametrem typu jest domyślnie przekształcalne na ten sam typ ogólny z bardziej pochodnym parametrem typu.
Ten związek utrzymuje się, ponieważ IComparer
zużywa T
ale ich nie produkuje. Obiekt, który może porównać dowolne dwa Animal
może być użyty do porównania dwóch Dog
.
Parametry typu przeciwstawnego deklarowane są za pomocą słowa kluczowego in
, ponieważ parametru należy użyć tylko jako danych wejściowych .
interface IComparer<in T> { /* ... */ }
Parametr typu zadeklarowany jako przeciwwariantowy może nie pojawić się jako wynik.
interface Bad<in T>
{
T GetT(); // type error
}
Niezmienność
IList<T>
nigdy nie jest podtypem innej IList<T1>
. IList
jest niezmienny w swoim parametrze typu.
class Animal { /* ... */ }
class Dog : Animal { /* ... */ }
IList<Dog> dogs = new List<Dog>();
IList<Animal> animals = dogs; // type error
Nie istnieje relacja podtypu dla list, ponieważ można umieszczać wartości na liście i usuwać wartości z listy.
Gdyby IList
był kowariantem, można by dodać elementy niewłaściwego podtypu do danej listy.
IList<Animal> animals = new List<Dog>(); // supposing this were allowed...
animals.Add(new Giraffe()); // ... then this would also be allowed, which is bad!
Jeśli IList
był sprzeczny, można wyodrębnić wartości niewłaściwego podtypu z danej listy.
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!
Parametry Typ niezmiennicze są zadeklarowane przez pominięcie zarówno in
i out
słowami kluczowymi.
interface IList<T> { /* ... */ }
Warianty interfejsów
Interfejsy mogą mieć parametry typu wariantowego.
interface IEnumerable<out T>
{
// ...
}
interface IComparer<in T>
{
// ...
}
ale klasy i struktury mogą nie
class BadClass<in T1, out T2> // not allowed
{
}
struct BadStruct<in T1, out T2> // not allowed
{
}
ani ogólne deklaracje metod
class MyClass
{
public T Bad<out T, in T1>(T1 t1) // not allowed
{
// ...
}
}
Poniższy przykład pokazuje wiele deklaracji wariancji w tym samym interfejsie
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>
Warianty delegatów
Delegaci mogą mieć parametry typu wariantu.
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
Wynika to z zasady substytucji Liskowa , która stwierdza (między innymi), że metodę D można uznać za bardziej pochodną niż metodę B, jeżeli:
- D ma taki sam lub większy typ zwrotu niż B
- D ma równe lub bardziej ogólne odpowiadające typy parametrów niż B
Dlatego wszystkie poniższe zadania są bezpieczne dla typu:
Func<object, string> original = SomeMethod;
Func<object, object> d1 = original;
Func<string, string> d2 = original;
Func<string, object> d3 = original;
Typy wariantów jako parametry i zwracane wartości
Jeśli typ kowariantny pojawia się jako wynik, typem zawierającym jest kowariant. Wytwarzanie producenta T
jest jak wytwarzanie T
interface IReturnCovariant<out T>
{
IEnumerable<T> GetTs();
}
Jeśli typ przeciwny pojawia się jako wynik, typ zawierający jest przeciwny. Wytwarzanie konsumenta T
jest jak konsumowanie T
interface IReturnContravariant<in T>
{
IComparer<T> GetTComparer();
}
Jeśli jako zmienną pojawia się typ kowariantny, typ zawierający jest sprzeczny. Spożywanie producenta T
jest jak konsumowanie T
interface IAcceptCovariant<in T>
{
void ProcessTs(IEnumerable<T> ts);
}
Jeśli jako dane wejściowe pojawia się typ przeciwstawny, to typ zawierający jest kowariantem. Spożywanie konsumenta T
jest jak wytwarzanie T
interface IAcceptContravariant<out T>
{
void CompareTs(IComparer<T> tComparer);
}
Sprawdzanie równości wartości ogólnych.
Jeśli logika ogólnej klasy lub metody wymaga sprawdzenia równości wartości o typie ogólnym, użyj EqualityComparer<TType>.Default
Właściwość EqualityComparer<TType>.Default
:
public void Foo<TBar>(TBar arg1, TBar arg2)
{
var comparer = EqualityComparer<TBar>.Default;
if (comparer.Equals(arg1,arg2)
{
...
}
}
To podejście jest lepsze niż zwykłe wywołanie metody Object.Equals()
, ponieważ domyślna implementacja TBar
sprawdza, czy typ IEquatale<TBar>
implementuje IEquatale<TBar>
, a jeśli tak, wywołuje IEquatable<TBar>.Equals(TBar other)
. Pozwala to uniknąć boksowania / rozpakowywania typów wartości.
Odlewanie typu ogólnego
/// <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));
}
}
Zastosowania:
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);
Czytnik konfiguracji z rzutowaniem typu ogólnego
/// <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));
}
}
Zastosowania:
var timeOut = ConfigurationReader.GetConfigKeyValue("RequestTimeout", 2000);
var url = ConfigurationReader.GetConfigKeyValue("URL", "www.someurl.com");
var enabled = ConfigurationReader.GetConfigKeyValue("IsEnabled", false);