Primero hablemos sobre el polimorfismo paramétrico puro y luego veamos el polimorfismo acotado.
¿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 Maybe
tipo? Si no está familiarizado con él, Maybe
es 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: None
y Some<T>
. int.Parse
es 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?
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 value
salida 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 BankAccount
en, se obtiene una BankAccount
vuelta, con los métodos y propiedades como Owner
, AccountNumber
, Balance
, Deposit
, Withdraw
, etc. Algo que puede trabajar. Ahora, el otro caso? Pones un BankAccount
pero 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:
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 ICloneable
interfaz? ¿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
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.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>
yRepository<BusinessObject2>
son diferentes tipos, y por otra parte, que si tomo unaRepository<BusinessObject1>
, yo sé que voy a llegarBusinessObject1
a 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.
fuente
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 ..IBusinessObject
es aBusinessObject1
o aBusinessObject2
. 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."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:
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 )
Ahora, qué sucede cuando usa la versión genérica del repositorio:
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.
Por otro lado, cuando usa el repositorio no genérico:
Solo podrá acceder a los miembros desde la interfaz IBusinessObject:
Entonces, el código anterior no se compilará debido a las siguientes líneas:
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.
fuente