c# generics crtp self-reference type-constraints

c# - Restricciones de parámetros de tipo reflexivo: X<T> donde T: X<T>: ¿alguna alternativa más simple?



generics crtp (2)

De vez en cuando estoy haciendo que una interfaz simple sea más complicada al agregarle una restricción de parámetro de tipo autorreferenciada ("reflexiva"). Por ejemplo, podría convertir esto:

interface ICloneable { ICloneable Clone(); } class Sheep : ICloneable { ICloneable Clone() { … } } //^^^^^^^^^^ Sheep dolly = new Sheep().Clone() as Sheep; //^^^^^^^^

dentro:

interface ICloneable<TImpl> where TImpl : ICloneable<TImpl> { TImpl Clone(); } class Sheep : ICloneable<Sheep> { Sheep Clone() { … } } //^^^^^ Sheep dolly = new Sheep().Clone();

Principal ventaja: un tipo de implementación (como Sheep ) ahora puede referirse a sí mismo en lugar de a su tipo base, lo que reduce la necesidad de la conversión de tipos (como lo demuestra la última línea de código).

Si bien esto es muy bueno, también he notado que estas restricciones de parámetros de tipo no son intuitivas y tienen la tendencia a ser realmente difíciles de comprender en escenarios más complejos. *)

Pregunta: ¿Alguien sabe de otro patrón de código C # que logre el mismo efecto o algo similar, pero de una manera más fácil de entender?

*) Este patrón de código puede ser poco intuitivo y difícil de entender, por ejemplo, de estas maneras:

  • La declaración X<T> where T : X<T> parece ser recursiva, y uno podría preguntarse por qué el compilador no se queda atascado en un bucle infinito , razonando: "Si T es una X<T> , entonces X<T> es realmente una X<X<…<T>…>> . " (Pero las restricciones obviamente no se resuelven así).

  • Para los implementadores, podría no ser obvio qué tipo se debe especificar en lugar de TImpl . (La restricción eventualmente se hará cargo de eso).

  • Una vez que agrega más parámetros de tipo y relaciones de subtipo entre varias interfaces genéricas a la mezcla, las cosas se vuelven inmanejables con bastante rapidez.


Principal ventaja: un tipo de implementación ahora puede referirse a sí mismo en lugar de a su tipo base, lo que reduce la necesidad de la conversión de tipos

Aunque podría parecer que la restricción de tipo que se refiere a misma obliga al tipo de implementación a hacer lo mismo, en realidad no es lo que hace. La gente usa este patrón para tratar de expresar patrones de la forma "una anulación de este método debe devolver el tipo de la clase dominante", pero esa no es realmente la restricción expresada o impuesta por el sistema de tipos. Doy un ejemplo aquí:

http://blogs.msdn.com/b/ericlippert/archive/2011/02/03/curiouser-and-curiouser.aspx

Si bien esto es muy bueno, también he notado que estas restricciones de parámetros de tipo no son intuitivas y tienen la tendencia a ser realmente difíciles de comprender en escenarios más complejos.

Sí. Intento evitar este patrón. Es difícil razonar acerca de

¿Alguien sabe de otro patrón de código C # que logre el mismo efecto o algo similar, pero de una manera más fácil de entender?

No en C #, no. Podría considerar mirar el sistema de tipos de Haskell si le interesa este tipo de cosas; Los "tipos superiores" de Haskell pueden representar ese tipo de patrones de tipo.

La declaración X<T> where T : X<T> parece ser recursiva, y uno podría preguntarse por qué el compilador no se queda atascado en un bucle infinito, razonando: "Si T es una X<T> , entonces X<T> es realmente una X<X<…<T>…>> . "

El compilador nunca se mete en bucles infinitos al razonar sobre relaciones tan simples. Sin embargo, el subtipo nominal de tipos genéricos con contravarianza es, en general, indecible . Hay formas de forzar al compilador a realizar regresiones infinitas, y el compilador de C # no las detecta y las evita antes de embarcarse en el viaje infinito. (Aún así, espero agregar detección para esto en el compilador de Roslyn, pero ya veremos).

Vea mi artículo sobre el tema si esto le interesa. Usted querrá leer el papel vinculado también.

http://blogs.msdn.com/b/ericlippert/archive/2008/05/07/covariance-and-contravariance-part-twelve-to-infinity-but-not-beyond.aspx


Desafortunadamente, no hay una manera de evitarlo por completo, y una ICloneable<T> genérica ICloneable<T> sin restricciones de tipo es suficiente. Su restricción solo limita los parámetros posibles a las clases que ellos mismos la implementan, lo que no significa que sean los que se están implementando actualmente.

En otras palabras, si una Cow implementa ICloneable<Cow> , aún así fácilmente hará que el implemento Sheep ICloneable<Cow> .

Simplemente usaría ICloneable<T> sin restricciones por dos razones:

  1. Dudo seriamente que alguna vez cometa un error al utilizar un parámetro de tipo incorrecto.

  2. Las interfaces están destinadas a ser contratos para otras partes del código, que no deben usarse para codificar en el piloto automático. Si una parte de un código espera ICloneable<Cow> y pasa una Sheep que puede hacer eso, parece perfectamente válido desde ese punto.