C# Language
Genéricos
Buscar..
Sintaxis
-
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; } }
Parámetros
Parámetro (s) | Descripción |
---|---|
TELEVISIÓN | Tipo de marcadores de posición para declaraciones genéricas |
Observaciones
Los genéricos en C # son compatibles hasta el tiempo de ejecución: los tipos genéricos creados con C # tendrán su semántica genérica conservada incluso después de compilarse en CIL .
Esto significa efectivamente que, en C #, es posible reflexionar sobre tipos genéricos y verlos como fueron declarados o verificar si un objeto es una instancia de un tipo genérico, por ejemplo. Esto contrasta con el borrado de tipo , donde la información de tipo genérico se elimina durante la compilación. También está en contraste con el enfoque de la plantilla para los genéricos, donde varios tipos genéricos concretos se convierten en múltiples tipos no genéricos en tiempo de ejecución, y se pierde cualquier metadato requerido para crear una instancia más precisa de las definiciones de los tipos genéricos originales.
Sin embargo, tenga cuidado al reflexionar sobre los tipos genéricos: los nombres de los tipos genéricos se modificarán en la compilación, sustituyendo los corchetes angulares y los nombres de los parámetros de tipo por una comilla invertida seguida del número de parámetros de tipo genérico. Por lo tanto, un Dictionary<TKey, Tvalue>
se traducirá al Dictionary`2
.
Parámetros de tipo (clases)
Declaración:
class MyGenericClass<T1, T2, T3, ...>
{
// Do something with the type parameters.
}
Inicialización:
var x = new MyGenericClass<int, char, bool>();
Uso (como el tipo de un parámetro):
void AnotherMethod(MyGenericClass<float, byte, char> arg) { ... }
Tipo de parámetros (métodos)
Declaración:
void MyGenericMethod<T1, T2, T3>(T1 a, T2 b, T3 c)
{
// Do something with the type parameters.
}
Invocación:
No es necesario proporcionar argumentos de tipo a un método genric, porque el compilador puede inferir implícitamente el tipo.
int x =10;
int y =20;
string z = "test";
MyGenericMethod(x,y,z);
Sin embargo, si existe una ambigüedad, los métodos genéricos deben llamarse con argumentos de tipo como
MyGenericMethod<int, int, string>(x,y,z);
Parámetros de tipo (interfaces)
Declaración:
interface IMyGenericInterface<T1, T2, T3, ...> { ... }
Uso (en herencia):
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> { ... }
Uso (como el tipo de un parámetro):
void SomeMethod(IMyGenericInterface<int, char, bool> arg) { ... }
Inferencia de tipo implícita (métodos)
Cuando se pasan argumentos formales a un método genérico, los argumentos de tipo genérico relevantes se pueden inferir implícitamente. Si se puede inferir todo tipo genérico, especificarlo en la sintaxis es opcional.
Considere el siguiente método genérico. Tiene un parámetro formal y un parámetro de tipo genérico. Existe una relación muy obvia entre ellos: el tipo que se pasa como argumento al parámetro de tipo genérico debe ser el mismo que el tipo de tiempo de compilación del argumento que se pasa al parámetro formal.
void M<T>(T obj)
{
}
Estas dos llamadas son equivalentes:
M<object>(new object());
M(new object());
Estas dos llamadas también son equivalentes:
M<string>("");
M("");
Y así son estas tres llamadas:
M<object>("");
M((object) "");
M("" as object);
Observe que si al menos un argumento de tipo no se puede inferir, entonces todos deben especificarse.
Considere el siguiente método genérico. El primer argumento de tipo genérico es el mismo que el tipo del argumento formal. Pero no existe tal relación para el segundo argumento de tipo genérico. Por lo tanto, el compilador no tiene forma de inferir el segundo argumento de tipo genérico en ninguna llamada a este método.
void X<T1, T2>(T1 obj)
{
}
Esto ya no funciona:
X("");
Esto tampoco funciona, porque el compilador no está seguro de si estamos especificando el primer o el segundo parámetro genérico (ambos serían válidos como object
):
X<object>("");
Estamos obligados a escribir ambos, como este:
X<string, object>("");
Tipo de restricciones (clases e interfaces)
Las restricciones de tipo pueden forzar un parámetro de tipo para implementar una determinada interfaz o clase.
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());
Sintaxis para múltiples restricciones:
class Generic<T, T1>
where T : IType
where T1 : Base, new()
{
}
Las restricciones de tipo funcionan de la misma manera que la herencia, ya que es posible especificar múltiples interfaces como restricciones en el tipo genérico, pero solo una clase:
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
{
}
Otra regla es que la clase debe agregarse como la primera restricción y luego las interfaces:
class Generic<T>
where T : A, I1
{
}
class Generic2<T>
where T : I1, A //Compilation error
{
}
Todas las restricciones declaradas deben satisfacerse simultáneamente para que funcione una instanciación genérica particular. No hay manera de especificar dos o más conjuntos alternativos de restricciones.
Tipo de restricciones (clase y estructura)
Es posible especificar si el argumento de tipo debe ser un tipo de referencia o un tipo de valor utilizando la class
o struct
restricciones respectivas. Si se usan estas restricciones, deben definirse antes de que se puedan enumerar todas las demás restricciones (por ejemplo, un tipo principal o 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
{
}
}
Restricciones de tipo (nueva palabra clave)
Al usar la restricción new()
, es posible imponer parámetros de tipo para definir un constructor vacío (predeterminado).
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 segunda llamada a to Create()
dará un error de tiempo de compilación con el siguiente mensaje:
'Barra' debe ser un tipo no abstracto con un constructor público sin parámetros para usarlo como parámetro 'T' en el tipo o método genérico 'Fábrica'
No hay ninguna restricción para un constructor con parámetros, solo se admiten constructores sin parámetros.
Inferencia de tipos (clases)
Los desarrolladores pueden verse atrapados por el hecho de que la inferencia de tipos no funciona para los constructores:
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.
La primera forma de crear una instancia sin especificar explícitamente los parámetros de tipo causará un error de tiempo de compilación que diría:
El uso del tipo genérico 'Tuple <T1, T2>' requiere 2 argumentos de tipo
Una solución común es agregar un método auxiliar en una clase estática:
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...
Reflexionando sobre los parámetros de tipo.
El operador typeof
trabaja en parámetros de tipo.
class NameGetter<T>
{
public string GetTypeName()
{
return typeof(T).Name;
}
}
Parámetros de tipo explícito
Hay diferentes casos en los que debe especificar explícitamente los parámetros de tipo para un método genérico. En los dos casos siguientes, el compilador no puede inferir todos los parámetros de tipo de los parámetros de método especificados.
Un caso es cuando no hay parámetros:
public void SomeMethod<T, V>()
{
// No code for simplicity
}
SomeMethod(); // doesn't compile
SomeMethod<int, bool>(); // compiles
El segundo caso es cuando uno (o más) de los parámetros de tipo no es parte de los parámetros del método:
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.
Utilizando método genérico con una interfaz como tipo de restricción.
Este es un ejemplo de cómo usar el tipo genérico TFood dentro del método Eat en la clase 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);
}
}
Puedes llamar al método Eat así:
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
En este caso si intentas llamar:
sheep.Eat(lion);
No será posible porque el objeto lion no implementa la interfaz IFood. Intentar realizar la llamada anterior generará un error de compilación: "El tipo 'Carnivore' no se puede usar como parámetro de tipo 'TFood' en el tipo genérico o método 'Animal.Eat (TFood)'. No hay una conversión de referencia implícita desde ' Carnívoro 'a' IFood '".
Covarianza
¿Cuándo es un IEnumerable<T>
un subtipo de un IEnumerable<T1>
diferente IEnumerable<T1>
? Cuando T
es un subtipo de T1
. IEnumerable
es covariante en su parámetro T
, lo que significa que la relación de subtipo de IEnumerable
va en la misma dirección que T
's.
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>
Una instancia de un tipo genérico covariante con un parámetro de tipo dado se puede convertir implícitamente al mismo tipo genérico con un parámetro de tipo menos derivado.
Esta relación se mantiene porque IEnumerable
produce T
s pero no las consume. Un objeto que produce Dog
s puede usarse como si produjera Animal
s.
Los parámetros de tipo covariante se declaran usando la palabra clave out
, porque el parámetro debe usarse solo como una salida .
interface IEnumerable<out T> { /* ... */ }
Un parámetro de tipo declarado como covariante puede no aparecer como entrada.
interface Bad<out T>
{
void SetT(T t); // type error
}
Aquí hay un ejemplo 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
}
}
}
Contravarianza
¿Cuándo es un IComparer<T>
de un IComparer<T1>
diferente IComparer<T1>
? Cuando T1
es un subtipo de T
IComparer
es contravariante en su parámetro T
, lo que significa que la relación de subtipo de IComparer
va en la dirección opuesta a la de 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>
Una instancia de un tipo genérico contravariante con un parámetro de tipo dado se puede convertir implícitamente al mismo tipo genérico con un parámetro de tipo más derivado.
Esta relación se mantiene porque IComparer
consume T
s pero no las produce. Un objeto que puede comparar cualesquiera dos Animal
puede usarse para comparar dos Dog
.
Los parámetros de tipo contravariante se declaran usando la palabra clave in
, porque el parámetro se debe usar solo como entrada .
interface IComparer<in T> { /* ... */ }
Un parámetro de tipo declarado como contravariante puede no aparecer como una salida.
interface Bad<in T>
{
T GetT(); // type error
}
Invariancia
IList<T>
nunca es un subtipo de un IList<T1>
diferente IList<T1>
. IList
es invariante en su parámetro de tipo.
class Animal { /* ... */ }
class Dog : Animal { /* ... */ }
IList<Dog> dogs = new List<Dog>();
IList<Animal> animals = dogs; // type error
No hay una relación de subtipo para las listas porque puede poner valores en una lista y sacar valores de una lista.
Si IList
fuera covariante, podría agregar elementos del subtipo incorrecto a una lista determinada.
IList<Animal> animals = new List<Dog>(); // supposing this were allowed...
animals.Add(new Giraffe()); // ... then this would also be allowed, which is bad!
Si IList
fuera contravariante, podría extraer valores del subtipo incorrecto de una lista determinada.
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!
Los parámetros de tipo invariantes se declaran omitiendo tanto el in
y out
palabras clave.
interface IList<T> { /* ... */ }
Interfaces de variante
Las interfaces pueden tener parámetros de tipo variante.
interface IEnumerable<out T>
{
// ...
}
interface IComparer<in T>
{
// ...
}
pero las clases y las estructuras no pueden
class BadClass<in T1, out T2> // not allowed
{
}
struct BadStruct<in T1, out T2> // not allowed
{
}
ni tampoco declaraciones de método genérico
class MyClass
{
public T Bad<out T, in T1>(T1 t1) // not allowed
{
// ...
}
}
El siguiente ejemplo muestra múltiples declaraciones de varianza en la misma interfaz
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>
Delegados variantes
Los delegados pueden tener parámetros de 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
Esto se desprende del Principio de Sustitución de Liskov , que establece (entre otras cosas) que un método D puede considerarse más derivado que un método B si:
- D tiene un tipo de retorno igual o más derivado que B
- D tiene tipos de parámetros correspondientes iguales o más generales que B
Por lo tanto las siguientes asignaciones son todas de tipo seguro:
Func<object, string> original = SomeMethod;
Func<object, object> d1 = original;
Func<string, string> d2 = original;
Func<string, object> d3 = original;
Tipos de variantes como parámetros y valores de retorno.
Si un tipo covariante aparece como una salida, el tipo que contiene es covariante. Producir un productor de T
s es como producir T
s.
interface IReturnCovariant<out T>
{
IEnumerable<T> GetTs();
}
Si un tipo contravariante aparece como una salida, el tipo que contiene es contravariante. Producir un consumidor de T
s es como consumir T
s.
interface IReturnContravariant<in T>
{
IComparer<T> GetTComparer();
}
Si un tipo covariante aparece como una entrada, el tipo que contiene es contravariante. Consumir un productor de T
s es como consumir T
s.
interface IAcceptCovariant<in T>
{
void ProcessTs(IEnumerable<T> ts);
}
Si un tipo contravariante aparece como una entrada, el tipo que contiene es covariante. Consumir un consumidor de T
s es como producir T
s.
interface IAcceptContravariant<out T>
{
void CompareTs(IComparer<T> tComparer);
}
Comprobando la igualdad de valores genéricos.
Si la lógica de una clase o método genérico requiere verificar la igualdad de valores que tienen un tipo genérico, use la propiedad EqualityComparer<TType>.Default
:
public void Foo<TBar>(TBar arg1, TBar arg2)
{
var comparer = EqualityComparer<TBar>.Default;
if (comparer.Equals(arg1,arg2)
{
...
}
}
Este enfoque es mejor que simplemente llamando Object.Equals()
método, debido a los controles de aplicación comparador predeterminado, ya sea TBar
tipo implementa IEquatale<TBar>
interfaz en caso afirmativo, las llamadas y IEquatable<TBar>.Equals(TBar other)
del método. Esto permite evitar el boxeo / unboxing de tipos de valor.
Tipo genérico de fundición
/// <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));
}
}
Usos:
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);
Lector de configuración con conversión de tipo genérico
/// <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));
}
}
Usos:
var timeOut = ConfigurationReader.GetConfigKeyValue("RequestTimeout", 2000);
var url = ConfigurationReader.GetConfigKeyValue("URL", "www.someurl.com");
var enabled = ConfigurationReader.GetConfigKeyValue("IsEnabled", false);