¿Cuál es la razón de usar una interfaz versus un tipo genéricamente restringido?

15

En los lenguajes orientados a objetos que admiten parámetros de tipo genérico (también conocidos como plantillas de clase y polimorfismo paramétrico, aunque, por supuesto, cada nombre tiene connotaciones diferentes), a menudo es posible especificar una restricción de tipo en el parámetro de tipo, de modo que descienda de otro tipo Por ejemplo, esta es la sintaxis en C #:

//for classes:
class ExampleClass<T> where T : I1 {

}
//for methods:
S ExampleMethod<S>(S value) where S : I2 {
        ...
}

¿Cuáles son las razones para usar tipos de interfaz reales sobre tipos restringidos por esas interfaces? Por ejemplo, ¿cuáles son las razones para hacer la firma del método I2 ExampleMethod(I2 value)?

GregRos
fuente
44
Las plantillas de clase (C ++) son algo completamente diferente y mucho más poderoso que los genéricos miserables. Aunque los idiomas que tienen genéricos tomaron prestada la sintaxis de la plantilla para ellos.
Deduplicador
Los métodos de interfaz son llamadas indirectas, mientras que los métodos de tipo pueden ser llamadas directas. Por lo tanto, este último puede ser más rápido que el primero y, en el caso de los refparámetros de tipo de valor, en realidad podría modificar el tipo de valor.
user541686
@Deduplicator: Teniendo en cuenta que los genéricos son más antiguos que las plantillas, no veo cómo los genéricos podrían haber tomado prestado algo de las plantillas, sintaxis o de otro tipo.
Jörg W Mittag
3
@ JörgWMittag: sospecho que por "lenguajes orientados a objetos que admiten genéricos", Deduplicator podría haber entendido "Java y C #" en lugar de "ML y Ada". Entonces, la influencia de C ++ en el primero es clara, a pesar de que no todos los lenguajes que tienen polimorfismos genéricos o paramétricos se tomaron prestados de C ++.
Steve Jessop
2
@SteveJessop: ML, Ada, Eiffel, Haskell son anteriores a las plantillas de C ++, Scala, F #, OCaml vinieron después, y ninguno de ellos comparte la sintaxis de C ++. (Curiosamente, incluso D, que toma mucho prestado de C ++, especialmente las plantillas, no comparte la sintaxis de C ++). Creo que "Java y C #" es una visión bastante limitada de "lenguajes que tienen genéricos".
Jörg W Mittag

Respuestas:

21

Usar la versión paramétrica da

  1. Más información para los usuarios de la función.
  2. Restringe la cantidad de programas que puede escribir (verificación de errores gratuita)

Como ejemplo aleatorio, supongamos que tenemos un método que calcula las raíces de una ecuación cuadrática

int solve(int a, int b, int c) {
  // My 7th grade math teacher is laughing somewhere
}

Y luego quieres que funcione en otros tipos de números como cosas además int. Puedes escribir algo como

Num solve(Num a, Num b, Num c){
  ...
}

El problema es que esto no dice lo que quieres. Dice

Dame 3 cosas que sean números (no necesariamente de la misma manera) y te devolveré algún tipo de número

¡No podemos hacer algo como int sol = solve(a, b, c)if a,, by care ints porque no sabemos que el método devolverá intal final! Esto lleva a un baile incómodo con abatimiento y oración si queremos usar la solución en una expresión más amplia.

Dentro de la función, alguien podría entregarnos un float, un bigint y grados, y tendríamos que sumarlos y multiplicarlos. Nos gustaría rechazar estáticamente esto porque las operaciones entre estas 3 clases van a ser un galimatías. Los grados son mod 360, por lo que no será el caso a.plus(b) = b.plus(a)y surgirán hilaridades similares.

Si usamos el polimorfismo paramétrico con subtipado, podemos descartar todo esto porque nuestro tipo realmente dice lo que queremos decir

<T : Num> T solve(T a, T b, T c)

O en palabras "Si me das algún tipo que sea un número, puedo resolver ecuaciones con esos coeficientes".

Esto aparece en muchos otros lugares también. Otra fuente buena de ejemplos son funciones que abstracto sobre algún tipo de contenedor, ala reverse, sort, map, etc.

Daniel Gratzer
fuente
8
En resumen, la versión genérica garantiza que las tres entradas (y la salida) serán del mismo tipo de número.
MathematicalOrchid
Sin embargo, esto se queda corto cuando no controla el tipo en cuestión (y, por lo tanto, no puede agregarle una interfaz). Para obtener la máxima generalidad, deberá aceptar una interfaz parametrizada por el tipo de argumento (por ejemplo Num<int>) como argumento adicional. Siempre puede implementar la interfaz para cualquier tipo a través de la delegación. Esto es esencialmente lo que son las clases de tipo de Haskell, excepto que es mucho más tedioso de usar ya que tienes que pasar explícitamente por la interfaz.
Doval
16

¿Cuáles son las razones para usar tipos de interfaz reales sobre tipos restringidos por esas interfaces?

Porque eso es lo que necesitas ...

IFoo Fn(IFoo x);
T Fn<T>(T x) where T: IFoo;

son dos firmas decididamente diferentes. El primero toma cualquier tipo que implemente la interfaz y la única garantía que ofrece es que el valor de retorno satisface la interfaz.

El segundo toma cualquier tipo que implemente la interfaz y garantiza que devolverá al menos ese tipo nuevamente (en lugar de algo que satisfaga la interfaz menos restrictiva).

A veces, quieres la garantía más débil. A veces quieres el más fuerte.

Telastyn
fuente
¿Puede dar un ejemplo de cómo usaría la versión de garantía más débil?
GregRos
44
@GregRos: por ejemplo, en algún código de analizador que escribí. Tengo una función Orque toma dos Parserobjetos (una clase base abstracta, pero el principio se cumple) y devuelve una nueva Parser(pero con un tipo diferente). El usuario final no debe saber o importar cuál es el tipo concreto.
Telastyn
En C #, me imagino que devolver una T diferente de la que se pasó es casi imposible (sin dolor de reflexión) sin la nueva restricción, lo que hace que su garantía sea bastante inútil por sí misma.
NtscCobalt
1
@NtscCobalt: es más útil cuando combina programación genérica tanto paramétrica como de interfaz. Por ejemplo, qué hace LINQ todo el tiempo (acepta un IEnumerable<T>, devuelve otro IEnumerable<T>que es, por ejemplo, en realidad un OrderedEnumerable<T>)
Ben Voigt
2

El uso de genéricos restringidos para los parámetros del método puede permitir que un método tenga su tipo de retorno basado en el de la cosa que se pasa. En .NET también pueden tener ventajas adicionales. Entre ellos:

  1. Un método que acepta un constreñidos genérico como una refo outparámetro puede hacerse pasar una variable que satisface la restricción; por el contrario, un método no genérico con un parámetro de tipo de interfaz se limitaría a aceptar variables de ese tipo de interfaz exacto.

  2. Un método con el parámetro de tipo genérico T puede aceptar colecciones genéricas de T. Un método que acepte un IList<T> where T:IAnimalserá capaz de aceptar un List<SiameseCat>, pero un método que quisiera un IList<Animal>no podría hacerlo.

  3. Una restricción a veces puede especificar una interfaz en términos del tipo genérico, por ejemplo where T:IComparable<T>.

  4. Una estructura que implementa una interfaz puede mantenerse como un tipo de valor cuando se pasa a un método que acepta un parámetro genérico restringido, pero debe encuadrarse cuando se pasa como un tipo de interfaz. Esto puede tener un gran efecto en la velocidad.

  5. Un parámetro genérico puede tener múltiples restricciones, mientras que no hay otra forma de especificar un parámetro de "algún tipo que implemente tanto IFoo como IBar". A veces, esto puede ser un arma de doble filo, ya que el código que ha recibido un parámetro de tipo IFooencontrará muy difícil pasarlo a un método que espere un genérico de doble restricción, incluso si la instancia en cuestión satisfaría todas las restricciones.

Si en una situación particular no hubiera ventaja en usar un genérico, simplemente acepte un parámetro del tipo de interfaz. El uso de un genérico obligará al sistema de tipos y JITter a realizar un trabajo adicional, por lo que si no hay ningún beneficio, uno no debería hacerlo. Por otro lado, es muy común que se aplique al menos una de las ventajas anteriores.

Super gato
fuente