usando - ¿Cuáles son las buenas razones para desear que los genéricos.NET puedan heredar uno de los tipos de parámetros genéricos?
tipos de genericos (5)
Aunque veo a lo que te refieres, parece un caso especial de un problema más general de soporte deficiente para la construcción de tipo a través de la composición en .NET. ¿Sería suficiente con algo como el enfoque descrito en https://connect.microsoft.com/VisualStudio/feedback/details/526307/add-automatic-generation-of- interface-implementation-via- implementing-member?
Esta publicación es en continuación de esta .
Estoy tratando de entender si soy el único que falla y necesita la capacidad de un tipo genérico .NET para heredar uno de sus tipos de parámetros genéricos.
El desafío es recopilar razones convincentes a favor de esta característica o, alternativamente, llegar a saber que no hay ninguna.
Doy mi razón para tenerlo como respuesta a esta pregunta - ver abajo.
Les pido a las personas que agreguen las suyas como respuestas a esta publicación.
Si no está de acuerdo con que la función sea útil o no tenga buenos motivos a favor, por favor absténgase de publicar algo aquí, aunque puede hacerlo en la publicación original que lo inició todo, aquí .
PD
Algunos patrones C ++ son irrelevantes en .NET. Por ejemplo, en su excelente libro Modern C ++ Design, Andrei Alexandrescu describe cómo crear una lista de tipos evaluados en el momento de la compilación. Naturalmente, este patrón es irrelevante para .NET, donde si necesito una lista de tipos, simplemente creo List<Type>
y lo lleno con tipos. Entonces, intentemos encontrar razones pertinentes al framework .NET y no solo traducir ciegamente las técnicas de codificación C ++ a C #.
PPS
Por supuesto, esta discusión es estrictamente académica. Incluso si se descubren un centenar de razones convincentes para la característica en cuestión, nunca se implementará.
De vez en cuando tropiezo con un problema de implementación, donde lamento profundamente que C<T>
no pueda heredar T
Lamentablemente, nunca he registrado estos problemas y, por lo tanto, puedo describir el más reciente, el que me he encontrado en este momento. Asi que aqui esta:
Nuestro sistema es extensible a través de metadatos, que están disponibles en tiempo de ejecución. Los metadatos se traducen a un tipo generado dinámicamente en tiempo de ejecución utilizando Reflection.Emit. Desafortunadamente, el tipo generado dinámicamente debe cumplir las siguientes condiciones:
- Debe derivarse de algún otro tipo, provisto como un parámetro para el creador de tipo dinámico. Este tipo se llama tipo ancestro y siempre es un tipo compilado estáticamente.
- Debe implementar varias interfaces, por ejemplo
IDynamicObject
(nuestra interfaz),System.ComponentModel.INotifyPropertyChanged
yCsla.Core.IMobileObject
. Tenga en cuenta que esta condición establece ciertas restricciones en el tipo de ancestro. Por ejemplo, el tipo ancestro no puede implementar la interfazIDynamicObject
, excepto si todos los métodos de interfaz se implementan de manera abstracta. Hay otras restricciones, todas las cuales deben ser verificadas. - Debería (y lo hace) anular los métodos del
object
Equals
,ToString
yGetHashCode
.
Actualmente, tuve que usar Reflection.Emit para emitir todo el código IL para satisfacer estas condiciones. Por supuesto, algunas tareas pueden ser enviadas al código compilado estáticamente. Por ejemplo, la anulación del método object.Equals(object)
invoca DynamicObjectHelper(IDynamicObject, IDynamicObject)
que es un código C # compilado estáticamente que realiza la mayor parte del trabajo de comparación de dos objetos dinámicos para la igualdad. Pero esto es más una excepción que la regla: se emite la mayor parte de la implementación, que es un dolor en el trasero.
¿No sería genial poder escribir un tipo genérico, como DynamicObjectBase<T>
que se creará una instancia con el tipo ancestro y servirá como el tipo de base real del tipo generado dinámicamente? Según lo veo, el tipo genérico DynamicObjectBase<T>
podría implementar muchos de los requisitos de tipo dinámico en un código C # compilado estáticamente. El tipo emitido dinámicamente lo heredaría y probablemente anule algunos métodos virtuales simples.
Para concluir, mi razón de peso es que dejar que C<T>
herede de T
simplificaría en gran medida la tarea de emitir tipos dinámicos.
Estoy usando GTK # 3. Hay una clase ''Widget'' definida que es la base para todos los demás widgets GTK #, por ejemplo, Window, Label, Frame. Desafortunadamente, el marco hace que sea un poco complicado cambiar el color de fondo de un widget, en el sentido de que debe anular un método del widget para hacerlo (por Dios ...).
Entonces, si quiero tener, por ejemplo, una etiqueta para la cual pueda establecer arbitrariamente un color de fondo, debería hacer algo como esto:
class BGLabel : Label
{
private Color _bgColor;
public Color BackgroundColor
{
get { return this._bgColor; }
set { this._bgColor = value; this.QueueDraw(); /* triggers OnDraw */ }
}
protected override void OnDraw(...)
{
... /* here we can use _bgColor to paint the background */
}
}
Ahora, si quisiera tener esta bonita función para más widgets , tendría que hacer lo anterior para cada uno de ellos . Si "clase C <T>: T" sería compilable, podría simplemente hacer:
class C<T> : T where T : Widget
{
private Color _bgColor;
public Color BackgroundColor
{
get { return this._bgColor; }
set { this._bgColor = value; this.QueueDraw(); /* triggers OnDraw */ }
}
protected override void OnDraw(...)
{
... /* here we can use _bgColor to paint the background */
}
}
y en lugar de "BGxxx bgw = new BGxxx ();" Podría usar "C <xxx> bgw = new C <xxx> ();".
La regla básica de los genéricos que previene esto es "el contenido de un tipo genérico debe estar bien definido en el argumento genérico". Veamos cómo esto se aplica al siguiente código:
public abstract class AbstractBase
{
public abstract string MyMethod();
}
public class SomeType<T> : T
{
}
public class SomeUsage
{
void Foo()
{
// SomeType<AbstractBase> does not implement AbstractBase.MyMethod
SomeType<AbstractBase> b = new SomeType<AbstractBase>();
}
}
Entonces tratamos de implementar MyMethod()
:
public class SomeType<T> : T
{
public override string MyMethod()
{
return "Some return value";
}
}
public class SomeUsage
{
void Foo()
{
// SomeType<string> does not inherit a virtual method MyMethod()
SomeType<string> b = new SomeType<string>();
}
}
Entonces hagamos un requerimiento que T
se derive de AbstractBase
:
public abstract class DerivedAbstractBase : AbstractBase
{
public abstract string AnotherMethod();
}
public class SomeType<T> : T
where T : AbstractBase
{
public override string MyMethod()
{
return "Some return value";
}
}
public class SomeUsage
{
void Foo()
{
// SomeType<DerivedAbstractBase> does not implement DerivedAbstractBase.AnotherMethod()
SomeType<DerivedAbstractBase> b = new SomeType<DerivedAbstractBase>();
}
}
Resumen:
Para cuando contabiliza todas las restricciones en los tipos básicos, está tan limitado que derivar de un parámetro genérico no tiene sentido.
Trataré de explicar con un ejemplo simple por qué necesitamos la herencia de los tipos genéricos.
La motivación en resumen: es facilitar mucho el desarrollo de algo que yo llamo Compile Time Order Enforcement , que es muy popular en ORM Frameworks.
Supongamos que estamos construyendo Database Framework.
Aquí está el ejemplo de cómo construir la transacción con dicho marco:
public ITransaction BuildTransaction(ITransactionBuilder builder)
{
/* Prepare the transaction which will update specific columns in 2 rows of table1, and one row in table2 */
ITransaction transaction = builder
.UpdateTable("table1")
.Row(12)
.Column("c1", 128)
.Column("c2", 256)
.Row(45)
.Column("c2", 512)
.UpdateTable("table2")
.Row(33)
.Column("c3", "String")
.GetTransaction();
return transaction;
}
Como cada línea devuelve alguna interfaz, nos gustaría devolverlas de esa manera, el desarrollador no puede cometer un error en el orden de las operaciones, y el uso válido se aplicará en tiempo de compilación, lo que simplifica la implementación y el uso de TransactionBuilder, porque desarrollador simplemente no podrá cometer errores como:
{
ITransaction transaction = builder
.UpdateTable("table1")
.UpdateTable("table2") /*INVALID ORDER: Table can''t come after Table, because at least one Row should be set for previous Table */
}
// OR
{
ITransaction transaction = builder
.UpdateTable("table1")
.Row(12)
.Row(45) /* INVALID ORDER: Row can''t come after Row, because at least one column should be set for previous row */
}
Ahora veamos la interfaz ITransactionBuilder hoy, SIN heredar de genérico, que hará cumplir el orden deseado en tiempo de compilación:
interface ITransactionBuilder
{
IRowBuilder UpdateTable(string tableName);
}
interface IRowBuilder
{
IFirstColumnBuilder Row(long rowid);
}
interface IFirstColumnBuilder
{
INextColumnBuilder Column(string columnName, Object columnValue);
}
interface INextColumnBuilder : ITransactionBuilder, IRowBuilder, ITransactionHolder
{
INextColumnBuilder Column(string columnName, Object columnValue);
}
interface ITransactionHolder
{
ITransaction GetTransaction();
}
interface ITransaction
{
void Execute();
}
Como puede ver, tenemos 2 interfaces para el generador de columnas "IFirstColumnBuilder" y "INextColumnBuilder" que en realidad no son necesarios, y recuerde que este es un ejemplo muy simple de la máquina de estado de tiempo de compilación, mientras que en un problema más complejo, la cantidad de interfaces innecesarias Crece dramáticamente.
Ahora supongamos que podemos heredar de los genéricos y preparamos tales interfaces
interface Join<T1, T2> : T1, T2 {}
interface Join<T1, T2, T3> : T1, T2, T3 {}
interface Join<T1, T2, T3, T4> : T1, T2, T3, T4 {} //we use only this one in example
Entonces, podemos reescribir nuestras interfaces para un estilo más intuitivo y con un generador de columnas individuales, y sin afectar el orden
interface ITransactionBuilder
{
IRowBuilder UpdateTable(string tableName);
}
interface IRowBuilder
{
IColumnBuilder Row(long rowid);
}
interface IColumnBuilder
{
Join<IColumnBuilder, IRowBuilder, ITransactionBuilder, ITransactionHolder> Column(string columnName, Object columnValue);
}
interface ITransactionHolder
{
ITransaction GetTransaction();
}
interface ITransaction
{
void Execute();
}
Entonces usamos Join <...> para combinar las interfaces existentes (o los "próximos pasos"), lo cual es muy útil en el desarrollo de máquinas de estados.
Por supuesto, este problema específico puede resolverse agregando posibilidades en C # para "unir" interfaces, pero está claro que si fue posible heredar de genéricos, el problema no existía en absoluto, y está claro que la compilación de orden cronológica es algo muy útil.
Por cierto. Para sintaxis como
interface IInterface<T> : T {}
No hay ningún caso de "qué pasa si" excepto el ciclo de herencia, que puede detectarse en tiempo de compilación.
Creo que AL MENOS para interfaces esta característica es 100% necesaria
Saludos