tutorial - interfaces generics c#
¿Por qué las restricciones de tipo genérico no son heredables ni se aplican jerárquicamente? (4)
A continuación se muestra un escenario donde la naturaleza implícita de este comportamiento causa un comportamiento diferente al esperado:
Reconozco que este escenario puede parecer extravagante en cuanto a la cantidad de configuración, pero este es solo un ejemplo de dónde este comportamiento podría causar un problema. Las aplicaciones de software pueden ser complicadas, por lo que aunque este escenario puede parecer complicado, no diría que esto no puede suceder.
En este ejemplo, hay una clase Operator que implementa dos interfaces similares: IMonitor y IProcessor. Ambos tienen un método de inicio y una propiedad IsStarted, pero el comportamiento para cada interfaz dentro de la clase Operator es independiente. Es decir, hay una variable _MonitorStarted y una variable _ProcessorStarted dentro de la clase Operator.
MyClass<T>
deriva de ClassBase<T>
. ClassBase tiene una restricción de tipo en T que debe implementar la interfaz IProcessor y, de acuerdo con el comportamiento sugerido, MyClass hereda esa restricción de tipo.
MyClass<T>
tiene un método Check, que se basa en la suposición de que puede obtener el valor de la propiedad IProcessor.IsStarted del objeto interno de IProcessor.
Supongamos que alguien cambia la implementación de ClassBase para eliminar la restricción de tipo de IProcessor en el parámetro genérico T y la reemplaza con un tipo de contrición de IMonitor. Este código funcionará silenciosamente, pero producirá un comportamiento diferente. La razón es porque el método Check en MyClass<T>
ahora está llamando a la propiedad IMonitor.IsStarted en lugar de a la propiedad IProcessor.IsStarted, aunque el código para MyClass<T>
no ha cambiado en absoluto.
public interface IMonitor
{
void Start();
bool IsStarted { get; }
}
public interface IProcessor
{
void Start();
bool IsStarted { get; }
}
public class Operator : IMonitor, IProcessor
{
#region IMonitor Members
bool _MonitorStarted;
void IMonitor.Start()
{
Console.WriteLine("IMonitor.Start");
_MonitorStarted = true;
}
bool IMonitor.IsStarted
{
get { return _MonitorStarted; }
}
#endregion
#region IProcessor Members
bool _ProcessorStarted;
void IProcessor.Start()
{
Console.WriteLine("IProcessor.Start");
_ProcessorStarted = true;
}
bool IProcessor.IsStarted
{
get { return _ProcessorStarted; }
}
#endregion
}
public class ClassBase<T>
where T : IProcessor
{
protected T Inner { get; private set; }
public ClassBase(T inner)
{
this.Inner = inner;
}
public void Start()
{
this.Inner.Start();
}
}
public class MyClass<T> : ClassBase<T>
//where T : IProcessor
{
public MyClass(T inner) : base(inner) { }
public bool Check()
{
// this code was written assuming that it is calling IProcessor.IsStarted
return this.Inner.IsStarted;
}
}
public static class Extensions
{
public static void StartMonitoring(this IMonitor monitor)
{
monitor.Start();
}
public static void StartProcessing(this IProcessor processor)
{
processor.Start();
}
}
class Program
{
static void Main(string[] args)
{
var @operator = new Operator();
@operator.StartMonitoring();
var myClass = new MyClass<Operator>(@operator);
var result = myClass.Check();
// the value of result will be false if the type constraint on T in ClassBase<T> is where T : IProcessor
// the value of result will be true if the type constraint on T in ClassBase<T> is where T : IMonitor
}
}
Clase de artículo
public class Item
{
public bool Check(int value) { ... }
}
Clase abstracta base con restricción de tipo genérico
public abstract class ClassBase<TItem>
where TItem : Item
{
protected IList<TItem> items;
public ClassBase(IEnumerable<TItem> items)
{
this.items = items.ToList();
}
public abstract bool CheckAll(int value);
}
Clase heredada sin restricciones
public class MyClass<TItem> : ClassBase<TItem>
{
public override bool CheckAll(int value)
{
bool result = true;
foreach(TItem item in this.items)
{
if (!item.Check(value)) // this doesn''t work
{
result = false;
break;
}
}
return result;
}
}
Me gustaría saber por qué las restricciones de tipo genérico no son heredables. Porque si mi clase heredada hereda de la clase base y pasa su tipo genérico que tiene una restricción en la clase base, significa automáticamente que el tipo genérico en la clase heredada debe tener la misma restricción sin definirla explícitamente. ¿No debería?
¿Estoy haciendo algo mal, entendiéndolo mal o, en realidad, esa restricción de tipo genérico no es heredable? Si esto último es cierto, ¿por qué en el mundo es eso ?
Un poco de explicación adicional
¿Por qué creo que las restricciones de tipo genérico definidas en una clase deberían heredarse o imponerse en las clases secundarias? Déjame darte un código adicional para hacerlo un poco menos obvio.
Supongamos que tenemos las tres clases como arriba. Entonces también tenemos esta clase:
public class DanteItem
{
public string ConvertHellLevel(int value) { ... }
}
Como podemos ver, esta clase no hereda de Item
por lo que no puede usarse como una clase concreta como ClassBase<DanteItem>
(olvidemos el hecho de que ClassBase
es abstracto por ahora. Podría ser también una clase regular). Como MyClass
no define ninguna restricción para su tipo genérico, parece perfectamente válido tener MyClass<DanteItem>
...
Pero. Esta es la razón por la que creo que las restricciones de tipo genérico deben heredarse / imponerse en clases heredadas al igual que con las restricciones de tipo genérico de miembro porque si miramos la definición de MyClass
dice:
MyClass<T> : ClassBase<T>
Cuando T
es DanteItem
, podemos ver que no se puede usar automáticamente con MyClass
porque se hereda de ClassBase<T>
y DanteItem
no cumple con su restricción de tipo genérico. Podría decir que ** el tipo genérico en MyClass
depende de las restricciones de tipo genérico de ClassBase
porque, de lo contrario, MyClass
podría MyClass
una instancia con cualquier tipo. Pero sabemos que no puede ser.
Sería por supuesto diferente cuando tuviera MyClass
definido como:
public class MyClass<T> : ClassBase<Item>
en este caso, T no tiene nada que ver con el tipo genérico de la clase base, por lo que es independiente de él.
Esta es una explicación / razonamiento un poco largo. Podría simplemente resumirlo de la siguiente manera:
Si no proporcionamos restricción de tipo genérico en
MyClass
, implica implícitamente que podemos instanciarMyClass
con cualquier tipo concreto . Pero sabemos que no es posible, dado queMyClass
se hereda deClassBase
y que tiene una restricción de tipo genérico.
Espero que esto tenga mucho más sentido ahora.
Creo que estás confundido porque estás declarando que TItem
clase con TItem
también.
Si lo piensas si estuvieras usando Q
entonces sí.
public class MyClass<Q> : BaseClass<Q>
{
...
}
Entonces, ¿cómo se determina que Q
es del tipo de item
?
Debe agregar la restricción al tipo genérico de las clases derivadas, así
public class MyClass<Q> : BaseClass<Q> were Q : Item { ... }
Debido a que ClassBase tiene una restricción en su plantilla (debe por tipo de elemento), también debe agregar esta restricción a MyClass. Si no lo hace, podría crear una nueva instancia de MyClass, donde la plantilla no es un tipo de artículo. Al crear la clase base, fallará.
[edit] Hmm ahora vuelve a leer tu pregunta, y veo que tu código se compila? De acuerdo.
Bueno, en MyClass no conoces el tipo básico de estos elementos, por lo que no puedes llamar al método Check. this.items es del tipo IList, y en tu clase, TItem no está especificado, por eso la clase no entiende el método Check.
Déjame responder tu pregunta, ¿por qué no quieres agregar la restricción a tu clase MyClass? Dado cualquier otro tipo de clase como plantilla para esta clase, resultaría en un error. ¿Por qué no evitar estos errores agregando una restricción por lo que fallará el tiempo de compilación?
OTRA ACTUALIZACIÓN:
Esta pregunta fue el tema de mi blog en julio de 2013 . Gracias por la gran pregunta!
ACTUALIZAR:
He pensado un poco más sobre esto y creo que el problema es que no quieres la herencia en absoluto. Más bien, lo que quiere es para todas las restricciones que deben colocarse en un parámetro de tipo para que ese parámetro de tipo se use como un argumento de tipo en otro tipo para deducirse automáticamente y agregarse de manera invisible a la declaración del parámetro de tipo. ¿Sí?
Algunos ejemplos simplificados:
class B<T> where T:C {}
class D<U> : B<U> {}
U es un parámetro de tipo que se utiliza en un contexto donde debe ser C. Por lo tanto, en su opinión, el compilador debe deducir eso y colocar automáticamente una restricción de C en U.
¿Qué hay de esto?
class B<T, U> where T : X where U : Y {}
class D<V> : B<V, V> {}
Ahora V es un parámetro de tipo utilizado en un contexto donde debe ser tanto X como Y. Por lo tanto, en su opinión, el compilador debe deducir eso y colocar automáticamente una restricción de X e Y en V. ¿Sí?
¿Qué hay de esto?
class B<T> where T : C<T> {}
class C<U> : B<D<U>> where U : IY<C<U>> {}
class D<V> : C<B<V>> where V : IZ<V> {}
Me lo inventé, pero te aseguro que es una jerarquía de tipos perfectamente legal. Describa una regla clara y consistente que no entre en bucles infinitos para determinar cuáles son todas las restricciones en T, U y V. No olvide tratar los casos en que se sabe que los parámetros de tipo son tipos de referencia y las restricciones de interfaz tienen anotaciones de covarianza o contravarianza! Además, el algoritmo debe tener la propiedad de que da exactamente los mismos resultados, sin importar en qué orden aparecen B, C y D en el código fuente.
Si la inferencia de restricciones es la característica que desea, entonces el compilador debe ser capaz de manejar casos como este y dar mensajes de error claros cuando no puede.
¿Qué tienen de especial los tipos de base? ¿Por qué no implementar la función en todos los sentidos?
class B<T> where T : X {}
class D<V> { B<V> bv; }
V es un parámetro de tipo utilizado en un contexto donde debe ser convertible a X; por lo tanto, el compilador debe deducir este hecho y poner una restricción de X en V. ¿Sí? ¿O no?
¿Por qué los campos son especiales? ¿Qué tal esto?
class B<T> { static public void M<U>(ref U u) where U : T {} }
class D<V> : B<int> { static V v; static public void Q() { M(ref v); } }
V es un parámetro de tipo utilizado en un contexto donde solo puede ser int. Por lo tanto, el compilador de C # debe deducir este hecho y colocar automáticamente una restricción de int en V.
¿Sí? ¿No?
¿Ves a dónde va esto? ¿Dónde se detiene? Para implementar correctamente la función deseada, el compilador debe realizar un análisis completo del programa.
El compilador no hace este nivel de análisis porque eso es colocar el carro antes que el caballo. Cuando construyes un genérico, debes probar al compilador que has cumplido la restricción. No es tarea del compilador averiguar qué quiere decir y determinar qué conjunto adicional de restricciones satisface la restricción original.
Por razones similares, el compilador tampoco intenta inferir automáticamente las anotaciones de varianza en las interfaces en su nombre. Vea mi artículo sobre ese tema para más detalles.
Respuesta original:
Me gustaría saber por qué las restricciones de tipo genérico no son heredables.
Solo los miembros son heredados. Una restricción no es un miembro.
si mi clase heredada hereda de la clase base y pasa su tipo genérico que tiene una restricción en la clase base, significa automáticamente que el tipo genérico en la clase heredada debe tener la misma restricción sin definirla explícitamente. ¿No debería?
Simplemente estás afirmando cómo debería ser algo, sin dar ninguna explicación de por qué debería ser así. Explíquenos por qué cree que el mundo debería ser de esa manera; ¿Cuáles son los beneficios y cuáles son los inconvenientes y cuáles son los costos ?
¿Estoy haciendo algo mal, entendiéndolo mal o, en realidad, esa restricción de tipo genérico no es heredable?
Las restricciones genéricas no son heredadas.
Si esto último es cierto, ¿por qué en el mundo es eso?
Las características no están "implementadas" de manera predeterminada. ¡No tenemos que proporcionar una razón por la cual una característica no está implementada! Cada función no se implementa hasta que alguien gasta el dinero para implementarla.
Ahora, me apresuro a señalar que las restricciones de tipo genérico se heredan en los métodos . Los métodos son miembros, los miembros se heredan y la restricción es parte del método (aunque no forma parte de su firma ). Entonces la restricción viene junto con el método cuando es heredado. Cuando tu dices:
class B<T>
{
public virtual void M<U>() where U : T {}
}
class D<V> : B<IEnumerable<V>>
{
public override void M<U>() {}
}
Entonces D<V>.M<U>
hereda la restricción y sustituye IEnumerable<V>
por T; por lo tanto, la restricción es que U debe ser convertible a IEnumerable<V>
. Tenga en cuenta que C # no le permite replantear la restricción. Esto es en mi opinión un error; Me gustaría poder replantear la restricción para mayor claridad.
Pero D no hereda ningún tipo de restricción en T de B; No entiendo cómo podría ser. M es un miembro de B, y es heredado por D junto con su restricción. Pero T no es miembro de B en primer lugar, entonces, ¿qué hay para heredar?
Realmente no entiendo en absoluto qué función es la que quieres aquí. ¿Puedes explicarlo con más detalles?