restricciones - usando generics c#
Método genérico con restricciones de tipo o parámetro de clase base (5)
Si escribo un método que acepta un parámetro que se deriva de una BaseClass
(o una interfaz), que yo sepa hay dos formas de lograrlo:
void MyMethod<T>(T obj) where T : BaseClass { ... }
y
void MyMethod(BaseClass obj) { ... }
¿Qué ventajas / desventajas tiene usar el uno sobre el otro?
Como se dijo, solo importa una vez que obtiene un valor de retorno. Considere estos casos:
BaseClass MyMethod(BaseClass)
DervivedClass temp = new DervivedClass();
//Error. My Method always returns a BaseClass. No implicit casting available
temp = MyMethod(temp);
Compáralo con esto:
T MyMethod<T>(T) where T : BaseClass
DervivedClass temp = new DerivedClass();
temp = MyMethod<DerivedClass>(temp);
Strong Typification es uno de los mejores amigos que tienes en .NET. Abrázalo. Nunca trates de evitarlo. Lo contrario sería los casos que tenemos en PHP y JavaScript: http://www.sandraandwoo.com/2015/12/24/0747-melodys-guide-to-programming-languages/
Cuando T
está restringido a una clase base, no hay realmente mucha diferencia aparte de lo que ya se ha indicado.
Cuando T
está restringido a una interfaz, la diferencia puede ser enorme:
int FrobNonGeneric(IFrobbable frob) { //... }
int Frob<T>(T frob) where T: IFrobbable { //... }
struct Frob: IFrobbable { ... }
FrobNonGeneric(new Frob()); //boxing!
Frob(new Frob()); //no boxing
Definitivamente, el ejemplo que citó no hace mucha diferencia más que el rendimiento de ejecución en tiempo de ejecución como se menciona en otras respuestas.
Dejando de lado los beneficios de las colecciones genéricas (mejora del rendimiento al evitar el boxeo / desempaquetado, por ejemplo) que todos conocemos y usamos con frecuencia: los genéricos también funcionan muy bien desde la perspectiva del consumidor. Por ejemplo, el siguiente fragmento de código se explica por sí mismo para visualizar la flexibilidad de uso de API desde la perspectiva del consumidor:
interface IEntity
{
int Id {get;set;}
}
class Student : IEntity
{
int Id {get;set;}
string SubjectOpted {get;set;}
}
class Employee : IEntity
{
int Id {get;set;}
string DepartmentName{get;set;}
}
interface INonGenericRepository
{
IEntity Get(int id)
}
interface IGenericRepository<T> where T:Entity
{
T Get(int id)
}
class NonGenericRepository : IRepository
{
public IEntity Get(int id) {/*implementation goes here */
}
class GenericRepository<T> : IRepository<T>
{
public T Get(int id) {/*implementation goes here */
}
Class NonGenericStudentConsumer
{
IEntity student = new NonGenericFRepository().Get(5);
var Id = student.Id
var subject = student.SubjectOpted /*does not work, you need to cast */
}
Class GenericStudentConsumer
{
var student = new GenericFRepository<Student>().Get(5);
var Id = student.Id
var subject = student.SubjectOpted /*works perfect and clean */
}
Un par de otros casos de uso que promueven la flexibilidad al usar genéricos junto con restricciones son:
Digamos que quiero asegurar que el parámetro pasado a los implementos de métodos IAdd
e IMultiply
y tengo una clase que implementa tanto IAdd
, como de forma IMulitply
:
public class BusinessOpeartion<T> where T : IAdd, IMultiply{
void SomeBusinessOpeartion(T obj) { /*implementation */}
}
Si necesito un enfoque no genérico, me veo obligado a crear una interfaz ficticia redundante como:
interface IDummy : IAdd, IMultiply
public class BusinessOpeartion{
void SomeBusinessOpeartion(IDummy obj) { /*implementation */}
}
¿No es el primer enfoque más limpio?
También una cosa más pequeña acaba de aparecer al escribir respuesta. En caso de que lo necesite, ¿cómo obtendría una nueva instancia para el tipo de parámetro dentro del método?
tú no puedes hacer
IDummy dummy = new IDummy(); /*illegal*/
Pero con genérico podrías tener; T temp = new T();
siempre que haya una restricción de new()
Además, ¿qué sucede si necesita un valor predeterminado para el tipo de parámetro?
tú no puedes hacer
var default = default(IDummy); /*illegal*/
Pero con genérico podrías tener; var default = default(T)
En este ejemplo, no hay una gran diferencia entre los dos, puede acceder a los mismos miembros dentro del método y puede llamarlo con las mismas clases derivadas. Existe una diferencia de tiempo de ejecución, ya que se compila un método genérico para cada tipo con el que se invoca.
Donde los genéricos son útiles sería si devolviera un valor dependiendo de T
Con los genéricos podrías hacer lo siguiente.
T MyMethod<T>(T obj) where T : BaseClass { ... }
MyMethod(derivedInstance).derivedProperty
Sin esto sería un error:
BaseClass MyMethod(BaseClass obj) { ... }
MyMethod(derivedInstance).derivedProperty // error
Nota Aunque mencione la restricción a una clase base, vale la pena mencionar que si no se restringe a una clase, sino a una interfaz, se producirá un boxeo adicional si la implementación se realiza mediante una estructura en la versión no genérica, esto puede tener un rendimiento severo trascendencia.
En los ejemplos incluidos en su pregunta, no hay mucha diferencia entre la versión genérica y la no genérica. Pero aquí hay algunos otros ejemplos de firmas de métodos que no se pueden expresar sin los genéricos:
T MyMethod<T>(T obj) where T : BaseClass { ... }
void MyMethod<T>(T obj1, T obj2) where T : BaseClass { ... }
void MyMethod<T>(T obj, List<T> list) where T : BaseClass { ... }