Genéricos vs interfaz común?

20

No recuerdo cuándo escribí la clase genérica la última vez. Cada vez que creo que lo necesito después de pensarlo, llego a una conclusión que no.

La segunda respuesta a esta pregunta me hizo pedir una aclaración (ya que aún no puedo comentar, hice una nueva pregunta).

Así que tomemos el código dado como un ejemplo de caso donde uno necesita genéricos:

public class Repository<T> where T : class, IBusinessOBject
{
  T Get(int id)
  void Save(T obj);
  void Delete(T obj);
}

Tiene restricciones de tipo: IBusinessObject

Mi forma habitual de pensar es: la clase está obligada a usar IBusinessObject, al igual que las clases que usan esto Repository. El repositorio almacena estos IBusinessObjects, los clientes más probables de esto Repositoryquerrán obtener y usar objetos a través de la IBusinessObjectinterfaz. Entonces, ¿por qué no solo

public class Repository
{
  IBusinessOBject Get(int id)
  void Save(IBusinessOBject obj);
  void Delete(IBusinessOBject obj);
}

Aunque, el ejemplo no es bueno, ya que es solo otro tipo de colección y la colección genérica es clásica. En este caso, la restricción de tipo también parece extraña.

De hecho, el ejemplo se class Repository<T> where T : class, IBusinessbBjectparece bastante class BusinessObjectRepositorya mí. Que es lo que los genéricos están hechos para arreglar.

El punto es: ¿los genéricos son buenos para cualquier cosa excepto las colecciones y no las restricciones de tipo hacen que genérico sea especializado, como lo hace el uso de esta restricción de tipo en lugar del parámetro de tipo genérico dentro de la clase?

selva_mole
fuente

Respuestas:

24

Primero hablemos sobre el polimorfismo paramétrico puro y luego veamos el polimorfismo acotado.

Polimorfismo paramétrico

¿Qué significa el polimorfismo paramétrico? Bueno, significa que un tipo, o más bien constructor de tipos es parametrizado por un tipo. Como el tipo se pasa como parámetro, no puede saber de antemano qué podría ser. No puede hacer ninguna suposición basada en ello. Ahora, si no sabes lo que podría ser, ¿de qué sirve? ¿Qué puedes hacer con eso?

Bueno, podrías almacenarlo y recuperarlo, por ejemplo. Ese es el caso que ya mencionaste: colecciones. Para almacenar un artículo en una lista o matriz, no necesito saber nada sobre el artículo. La lista o matriz puede ser completamente ajena al tipo.

¿Pero qué hay del Maybetipo? Si no está familiarizado con él, Maybees un tipo que tal vez tenga un valor y tal vez no. ¿Dónde lo usarías? Bueno, por ejemplo, al sacar un elemento de un diccionario: el hecho de que un elemento no esté en el diccionario no es una situación excepcional, por lo que realmente no debería lanzar una excepción si el elemento no está allí. En su lugar, devuelve una instancia de un subtipo de Maybe<T>, que tiene exactamente dos subtipos: Noney Some<T>. int.Parsees otro candidato de algo que realmente debería devolver un en Maybe<int>lugar de lanzar una excepción o todoint.TryParse(out bla) baile.

Ahora, podrías argumentar que Maybe es algo así como una lista que solo puede tener cero o un elemento. Y así, más o menos, una colección.

Entonces que hay de Task<T> ? Es un tipo que promete devolver un valor en algún momento en el futuro, pero no necesariamente tiene un valor en este momento.

¿O qué tal Func<T, …>? ¿Cómo representaría el concepto de una función de un tipo a otro si no puede abstraer sobre tipos?

O, más generalmente: considerando que la abstracción y la reutilización son las dos operaciones fundamentales de la ingeniería de software, ¿por qué no querría poder abstraer sobre tipos?

Polimorfismo limitado

Entonces, hablemos ahora del polimorfismo acotado. El polimorfismo limitado es básicamente donde se encuentran el polimorfismo paramétrico y el polimorfismo de subtipo: en lugar de que un constructor de tipo sea completamente ajeno a su parámetro de tipo, puede vincular (o restringir) el tipo para que sea un subtipo de algún tipo especificado.

Volvamos a las colecciones. Toma una tabla hash. Dijimos anteriormente que una lista no necesita saber nada sobre sus elementos. Bueno, una tabla hash sí: necesita saber que puede hacer hash. (Nota: en C #, todos los objetos son hashable, al igual que todos los objetos se pueden comparar por igualdad. Sin embargo, eso no es cierto para todos los lenguajes, y a veces se considera un error de diseño incluso en C #).

Por lo tanto, desea restringir su parámetro de tipo para que el tipo de clave en la tabla hash sea una instancia de IHashable:

class HashTable<K, V> where K : IHashable
{
  Maybe<V> Get(K key);
  bool Add(K key, V value);
}

Imagina si en cambio tuvieras esto:

class HashTable
{
    object Get(IHashable key);
    bool Add(IHashable key, object value);
}

¿Qué harías con una valuesalida de allí? No puedes hacer nada con él, solo sabes que es un objeto. Y si lo repites, todo lo que obtienes es un par de algo que sabes que es un IHashable(que no te ayuda mucho porque solo tiene una propiedad Hash) y algo que sabes que es un object(que te ayuda aún menos).

O algo basado en tu ejemplo:

class Repository<T> where T : ISerializable
{
    T Get(int id);
    void Save(T obj);
    void Delete(T obj);
}

El elemento debe ser serializable porque se almacenará en el disco. Pero qué pasa si tienes esto en su lugar:

class Repository
{
    ISerializable Get(int id);
    void Save(ISerializable obj);
    void Delete(ISerializable obj);
}

Con el caso genérico, si se pone un BankAccounten, se obtiene una BankAccountvuelta, con los métodos y propiedades como Owner, AccountNumber, Balance, Deposit, Withdraw, etc. Algo que puede trabajar. Ahora, el otro caso? Pones un BankAccountpero obtienes un Serializable, que tiene una sola propiedad:AsString . ¿Qué vas a hacer con eso?

También hay algunos trucos geniales que puedes hacer con el polimorfismo acotado:

Polimorfismo limitado por F

La cuantificación limitada por F es básicamente donde la variable de tipo aparece nuevamente en la restricción. Esto puede ser útil en algunas circunstancias. Por ejemplo, ¿cómo se escribe una ICloneableinterfaz? ¿Cómo se escribe un método donde el tipo de retorno es el tipo de la clase implementadora? En un idioma con una función MyType , eso es fácil:

interface ICloneable
{
    public this Clone(); // syntax I invented for a MyType feature
}

En un lenguaje con polimorfismo acotado, puede hacer algo como esto en su lugar:

interface ICloneable<T> where T : ICloneable<T>
{
    public T Clone();
}

class Foo : ICloneable<Foo>
{
    public Foo Clone()
    {
        // …
    }
}

Tenga en cuenta que esto no es tan seguro como la versión MyType, porque no hay nada que impida que alguien simplemente pase la clase "incorrecta" al constructor de tipos:

class EvilBar : ICloneable<SomethingTotallyUnrelatedToBar>
{
    public SomethingTotallyUnrelatedToBar Clone()
    {
        // …
    }
}

Miembros de tipo abstracto

Resulta que, si tiene miembros de tipo abstracto y subtipos, en realidad puede sobrevivir completamente sin polimorfismo paramétrico y seguir haciendo las mismas cosas. Scala se dirige en esta dirección, siendo el primer lenguaje principal que comenzó con los genéricos y luego tratando de eliminarlos, que es exactamente lo contrario de, por ejemplo, Java y C #.

Básicamente, en Scala, al igual que puede tener campos, propiedades y métodos como miembros, también puede tener tipos. Y al igual que los campos, las propiedades y los métodos pueden dejarse abstractos para su posterior implementación en una subclase, los miembros de tipo también pueden dejarse abstractos. Volvamos a las colecciones, una simple List, que se vería así, si fuera compatible con C #:

class List
{
    T; // syntax I invented for an abstract type member
    T Get(int index) { /* … */ }
    void Add(T obj) { /* … */ }
}

class IntList : List
{
    T = int;
}
// this is equivalent to saying `List<int>` with generics
Jörg W Mittag
fuente
Entiendo que la abstracción sobre los tipos es útil. Simplemente no veo su uso en la "vida real". Func <> y Task <> y Action <> son tipos de biblioteca. y gracias también me acordé interface IFoo<T> where T : IFoo<T>. obviamente es una aplicación de la vida real. El ejemplo es genial. pero por alguna razón no me siento satisfecho. Prefiero tener una idea de cuándo es apropiado y cuándo no. Las respuestas aquí tienen alguna contribución a este proceso, pero todavía me siento incómodo con todo esto. es extraño porque los problemas de nivel de lenguaje ya no me molestan por tanto tiempo.
jungle_mole
Gran ejemplo Sentí que estaba de vuelta en la sala de clase. +1
Chef_Code
1
@Chef_Code: espero que sea un cumplido :-P
Jörg W Mittag el
Sí lo es. Más tarde pensé en cómo podría percibirse después de que ya había comentado. Así que para confirmar la sinceridad ... Sí, es un cumplido nada más.
Chef_Code
14

El punto es: ¿los genéricos son buenos para cualquier cosa excepto las colecciones y no las restricciones de tipo hacen que genérico sea especializado, como lo hace el uso de esta restricción de tipo en lugar del parámetro de tipo genérico dentro de la clase?

No. Estás pensando demasiado Repository, donde es más o menos lo mismo. Pero para eso no están los genéricos. Están ahí para los usuarios. .

El punto clave aquí no es que el repositorio en sí sea más genérico. Es que los usuarios son más specialized- es decir, que Repository<BusinessObject1>y Repository<BusinessObject2>son diferentes tipos, y por otra parte, que si tomo una Repository<BusinessObject1>, yo que voy a llegar BusinessObject1a salir deGet .

No puede ofrecer esta tipificación fuerte por simple herencia. La clase de repositorio propuesta no hace nada para evitar que las personas confundan repositorios para diferentes tipos de objetos comerciales o para garantizar que salga el tipo correcto de objeto comercial.

DeadMG
fuente
Gracias, eso tiene sentido. Pero, ¿el objetivo de utilizar esta característica de lenguaje tan elogiada es tan simple como ayudar a los usuarios que tienen IntelliSense desactivado? (Estoy exagerando un poco, pero estoy seguro de que entiendes)
jungle_mole
@zloidooraque: IntelliSense tampoco sabe qué tipo de objetos se almacenan en un repositorio. Pero sí, podría hacer cualquier cosa sin genéricos si está dispuesto a usar yesos.
Gexicida
@gexicide ese es el punto: no veo dónde necesito usar yesos si uso una interfaz común. Nunca he dicho "uso Object". También entiendo por qué usar genéricos al escribir colecciones (principio DRY). Probablemente, mi pregunta inicial debería haber sido algo sobre el uso de genéricos fuera del contexto de las colecciones ..
jungle_mole
@zloidooraque: No tiene nada que ver con el medio ambiente. Intellisense no puede decirte si un IBusinessObjectes a BusinessObject1o a BusinessObject2. No puede resolver sobrecargas basadas en el tipo derivado que no conoce. No puede rechazar el código que pasa del tipo incorrecto. Hay un millón de bits de mecanografía más fuerte que Intellisense no puede hacer absolutamente nada. Un mejor soporte de herramientas es un buen beneficio, pero realmente no tiene nada que ver con las razones principales.
DeadMG
@DeadMG y ese es mi punto: intellisense no puede hacerlo: usar genérico, ¿podría? ¿importa? cuando obtienes el objeto por su interfaz, ¿por qué rechazarlo? si lo haces, es un mal diseño, ¿no? y por qué y qué es "resolver sobrecargas"? el usuario no debe decidir si el método de llamada o no se basa en el tipo derivado si delega la llamada del método correcto al sistema (que es el polimorfismo). y esto nuevamente me lleva a una pregunta: ¿son genéricos útiles fuera de los contenedores? No estoy discutiendo contigo, realmente necesito entender eso.
jungle_mole
13

"los clientes más probables de este repositorio querrán obtener y usar objetos a través de la interfaz IBusinessObject".

No, no lo harán.

Consideremos que IBusinessObject tiene la siguiente definición:

public interface IBusinessObject
{
  public int Id { get; }
}

Simplemente define el Id porque es la única funcionalidad compartida entre todos los objetos comerciales. Y tiene dos objetos comerciales reales: Persona y Dirección (dado que las Personas no tienen calles y las Direcciones no tienen nombres, no puede restringirlos a ambos a una interfaz común con funcionalidad de ambos. Eso sería un diseño terrible, violando el Principio de Segragación de Interfaz , el "I" en SÓLIDO )

public class Person : IBusinessObject
{
  public int Id { get; private set; }
  public string Name { get; private set; }
}

public class Address : IBusinessObject
{
  public int Id { get; private set; }
  public string City { get; private set; }
  public string StreetName { get; private set; }
  public int Number { get; private set; }
}

Ahora, qué sucede cuando usa la versión genérica del repositorio:

public class Repository<T> where T : class, IBusinessObject
{
  T Get(int id)
  void Save(T obj);
  void Delete(T obj);
}

Cuando llame al método Get en el repositorio genérico, el objeto devuelto se escribirá fuertemente, lo que le permitirá acceder a todos los miembros de la clase.

Person p = new Repository<Person>().Get(1);
int id = p.Id;
string name = p.Name;

Address a = new Repository<Address>().Get(1);
int id = a.Id;
string cityName = a.City;
int houseNumber = a.Number;

Por otro lado, cuando usa el repositorio no genérico:

public class Repository
{
  IBusinessOBject Get(int id)
  void Save(IBusinessOBject obj);
  void Delete(IBusinessOBject obj);
}

Solo podrá acceder a los miembros desde la interfaz IBusinessObject:

IBussinesObject p = new Repository().Get(1);
int id = p.Id; //OK
string name = p.Name; //Oooops, you dont have "Name" defined on the IBussinesObject interface.

Entonces, el código anterior no se compilará debido a las siguientes líneas:

string name = p.Name;
string cityName = a.City;
int houseNumber = a.Number;

Claro, puede lanzar el objeto IBussines a la clase real, pero perderá toda la magia de tiempo de compilación que permiten los genéricos (lo que lleva a InvalidCastExceptions en el futuro), sufrirá una sobrecarga innecesariamente ... E incluso si no lo hace no importa el tiempo de compilación ni la comprobación del rendimiento (debe hacerlo), el lanzamiento después definitivamente no le dará ningún beneficio sobre el uso de genéricos en primer lugar.

Lucas Corsaletti
fuente