type tipo parametros multiple metodos method lista genericos generico generica generic dato c# generics constraints new-operator

c# - parametros - Restricción de parámetros de un tipo genérico para tener un constructor específico



tipo de dato generico c++ (3)

La cita de Kirk Woll de mí, por supuesto, es toda la justificación que se requiere; No estamos obligados a proporcionar una justificación para características que no existen. Las características tienen costos enormes.

Sin embargo, en este caso específico, sin duda le puedo dar algunas razones por las que rechazaría la característica si surgiera en una reunión de diseño como una posible característica para una versión futura del idioma.

Para empezar: considere la característica más general. Los constructores son métodos . Si espera que haya una forma de decir "el argumento de tipo debe tener un constructor que toma un int", entonces ¿por qué no es razonable decir que "el argumento de tipo debe tener un método público llamado Q que toma dos enteros y devuelve un ¿cuerda?"

string M<T>(T t) where T has string Q(int, int) { return t.Q(123, 456); }

¿Te parece que esto es algo muy genérico ? Parece contrario a la idea de los genéricos tener este tipo de restricción.

Si la característica es una mala idea para los métodos, ¿por qué es una buena idea para los métodos que son constructores ?

Por el contrario, si es una buena idea para los métodos y los constructores, ¿por qué detenerse allí?

string M<T>(T t) where T has a field named x of type string { return t.x; }

Digo que deberíamos hacer la función completa o no hacerlo en absoluto . Si es importante poder restringir los tipos para tener constructores particulares, entonces hagamos toda la característica y restrinjamos los tipos sobre la base de los miembros en general y no solo de los constructores .

Esa característica es, por supuesto, mucho más costosa de diseñar, implementar, probar, documentar y mantener.

Segundo punto: supongamos que decidimos implementar la característica, ya sea la versión "solo constructores" o la versión "cualquier miembro". ¿Qué código generamos? Lo que pasa con genérico codegen es que ha sido diseñado cuidadosamente para que pueda hacer el análisis estático una vez y terminar con él. Pero no hay una manera estándar de describir "llamar al constructor que toma un int" en IL. Tendríamos que agregar un nuevo concepto a IL o generar el código para que el constructor genérico llame a Reflection .

El primero es caro; Cambiar un concepto fundamental en IL es muy costoso. El último es (1) lento, (2) cuadros el parámetro y (3) es el código que podría haber escrito usted mismo. Si vas a usar la reflexión para encontrar un constructor y llamarlo, escribe el código que usa la reflexión para encontrar un constructor y llámalo. Si esta es la estrategia de generación de código, el único beneficio que confiere la restricción es que el error de pasar un argumento de tipo que no tiene un ctor público que toma un int se detecta en tiempo de compilación en lugar de en tiempo de ejecución . No obtiene ninguno de los otros beneficios de los medicamentos genéricos, como evitar la reflexión y las sanciones por boxeo.

Me gustaría saber por qué la nueva restricción en un parámetro de tipo genérico solo se puede aplicar sin parámetros, es decir, se puede restringir el tipo para que tenga el constructor sin parámetros, pero no se puede restringir que la clase tenga, por ejemplo, un constructor que recibe un int como parámetro Conozco formas de evitar esto, usando la reflexión o el patrón de fábrica, que funciona bien, ok. Pero realmente me gustaría saber por qué, porque lo he estado pensando y realmente no puedo pensar en una diferencia entre un constructor sin parámetros y uno con parámetros que justificarían esta restricción en la nueva restricción. ¿Qué me estoy perdiendo? Muchas gracias

Argumento 1: Los constructores son métodos.

@Eric: Déjame ir aquí contigo por un segundo:

Los constructores son métodos.

Entonces supongo que nadie se opondría si yo fuera así:

public interface IReallyWonderful { new(int a); string WonderMethod(int a); }

Pero una vez que tenga eso, entonces iría:

public class MyClass<T> where T : IReallyWonderful { public string MyMethod(int a, int b) { T myT = new T(a); return myT.WonderMethod(b); } }

Que es lo que quería hacer en primer lugar. Entonces, lo siento, pero no, los constructores no son métodos , o al menos no exactamente.

Sobre las dificultades para implementar esta función, no lo sabría, e incluso si lo hiciera, no tendría nada que decir sobre una decisión con respecto al gasto prudente del dinero de los accionistas. Algo así, lo hubiera marcado como respuesta de inmediato.

Desde un punto de vista académico (mi), y eso es, sin tener en cuenta los costos de implementación, la pregunta realmente es (lo he redondeado a esto en las últimas horas):

En caso de que los constructores se consideren como parte de la implementación de una clase, o como parte del contrato semántico (de la misma manera, una interfaz se considera un contrato semántico).

Si consideramos a los constructores como parte de la implementación, entonces, restringir el constructor de un parámetro de tipo genérico no es algo muy genérico, ya que eso sería vincular su tipo genérico con una implementación concreta, y casi podría decir por qué usar genéricos en absoluto?

Ejemplo de constructor como parte de la implementación (no tiene sentido en especificar cualquiera de los siguientes constructores como parte del contrato semántico definido por ITransformer ):

public interface ITransformer { //Operates with a and returns the result; int Transform(int a); } public class PlusOneTransformer : ITransformer { public int Transform(int a) { return a + 1; } } public class MultiplyTransformer : ITransformer { private int multiplier; public MultiplyTransformer(int multiplier) { this.multiplier = multiplier; } public int Transform(int a) { return a * multiplier; } } public class CompoundTransformer : ITransformer { private ITransformer firstTransformer; private ITransformer secondTransformer; public CompoundTransformer(ITransformer first, ITransformer second) { this.firstTransformer = first; this.secondTransformer = second; } public int Transform(int a) { return secondTransformer.Transform(firstTransformer.Transform(a)); } }

El problema es que los constructores también se pueden considerar como parte del contrato semántico, de esta manera:

public interface ICollection<T> : IEnumerable<T> { new(IEnumerable<T> tees); void Add(T tee); ... }

Esto significa que siempre es posible crear una colección a partir de una secuencia de elementos, ¿verdad? Y eso haría una parte muy válida de un contrato semántico, ¿verdad?

Yo, sin tener en cuenta ninguno de los aspectos relacionados con el gasto inteligente del dinero de los accionistas, favorecería que los constructores formen parte de los contratos semánticos. Algunos desarrolladores lo arruinan y limitan un cierto tipo a tener un constructor semánticamente incorrecto, bueno, ¿cuál es la diferencia de que el mismo desarrollador agregue una operación semánticamente incorrecta? Después de todo, los contratos semánticos son eso, porque todos acordamos que lo son, y porque todos documentamos muy bien nuestras bibliotecas;)

Argumento 2: Supuestos problemas al resolver constructores

@supercat está tratando de establecer algunos ejemplos como cómo (cita de un comentario)

También sería difícil definir exactamente cómo deberían funcionar las restricciones de los constructores, sin dar lugar a comportamientos sorprendentes.

Pero realmente debo estar en desacuerdo. En C # (bueno, en .NET) sorpresas como "¿Cómo hacer que un pingüino vuele?" simplemente no suceda Existen reglas bastante sencillas sobre cómo el compilador resuelve las llamadas de método, y si el compilador no puede resolverlo, bueno, no pasará, no compilará.

Su último ejemplo fue:

Si son contravariantes, entonces uno tiene problemas para resolver a qué constructor se debe llamar si un tipo genérico tiene una restricción nueva (Cat, ToyotaTercel), y el tipo real solo tiene constructores nuevos (Animal, ToyotaTercel) y nuevo (Cat, Automóvil).

Bueno, intentemos esto (que en mi opinión es una situación similar a la propuesta por @supercat)

class Program { static void Main(string[] args) { Cat cat = new Cat(); ToyotaTercel toyota = new ToyotaTercel(); FunnyMethod(cat, toyota); } public static void FunnyMethod(Animal animal, ToyotaTercel toyota) { Console.WriteLine("Takes an Animal and a ToyotaTercel"); } public static void FunnyMethod(Cat cat, Automobile car) { Console.WriteLine("Takes a Cat and an Automobile"); } } public class Automobile { } public class ToyotaTercel : Automobile { } public class Animal { } public class Cat : Animal { }

Y, wow, no se compilará con el error.

La llamada es ambigua entre los siguientes métodos o propiedades: ''TestApp.Program.FunnyMethod (TestApp.Animal, TestApp.ToyotaTercel)'' y ''TestApp.Program.FunnyMethod (TestApp.Cat, TestApp.Automobile)''

No veo por qué el resultado debería ser diferente si el mismo problema surge de una solución con restricciones de constructor parametrizadas, como por ejemplo:

class Program { static void Main(string[] args) { GenericClass<FunnyClass> gc = new GenericClass<FunnyClass>(); } } public class Automobile { } public class ToyotaTercel : Automobile { } public class Animal { } public class Cat : Animal { } public class FunnyClass { public FunnyClass(Animal animal, ToyotaTercel toyota) { } public FunnyClass(Cat cat, Automobile car) { } } public class GenericClass<T> where T: new(Cat, ToyotaTercel) { }

Ahora, por supuesto, el compilador no puede manejar la restricción en el constructor, pero si pudiera, ¿por qué no podría ser el error, en la línea GenericClass<FunnyClass> gc = new GenericClass<FunnyClass>(); similar al obtenido al intentar compilar el primer ejemplo, el de FunnyMethod .

De todos modos, iría un paso más allá. Cuando uno reemplaza un método abstracto o implementa un método definido en una interfaz, se requiere que lo haga con exactamente el mismo tipo de parámetros, no se permiten herederos ni ancestros. Entonces, cuando se requiere un constructor parametrizado, el requisito debe cumplirse con una definición exacta, no con otra cosa. En este caso, la clase FunnyClass nunca se podría especificar como el tipo, para el tipo de parámetro genérico de la clase GenericClass .


Si uno quiere tener un método con un tipo genérico T cuyas instancias se pueden crear usando un solo parámetro int , debe tener el método aceptar, además del tipo T , ya sea un Func<int, T> o bien un adecuado interfaz, posiblemente utilizando algo como:

static class IFactoryProducing<ResultType> { interface WithParam<PT1> { ResultType Create(PT1 p1); } interface WithParam<PT1,PT2> { ResultType Create(PT1 p1, PT2 p2); } }

(El código parecería más agradable si la clase estática externa pudiera declararse como una interfaz, pero IFactoryProducing<T>.WithParam<int> parece más claro que IFactory<int,T> (ya que este último es ambiguo en cuanto a qué tipo es el parámetro y cual es el resultado).

En cualquier caso, cada vez que se pasa a través de un tipo T también se pasa por un delegado o interfaz de fábrica adecuado, se puede lograr el 99% de lo que se podría lograr con restricciones de constructor parametrizadas. El costo de tiempo de ejecución se puede minimizar haciendo que cada tipo construible genere una instancia estática de una fábrica, por lo que no será necesario crear instancias de fábrica en ningún tipo de contexto de bucle.

Por cierto, más allá del costo de la función, casi con toda seguridad habría algunas limitaciones sustanciales que la harían menos versátil que la solución alternativa. Si las restricciones del constructor no son contravariantes con respecto a los tipos de parámetros, puede ser necesario pasar un parámetro de tipo para el tipo exacto requerido para la restricción del constructor, además del tipo real del parámetro que se utilizará; para cuando uno haga eso, también podría pasar por una fábrica. Si son contravariantes, entonces uno tiene problemas para resolver a qué constructor se debe llamar si un tipo genérico tiene una restricción new(Cat, ToyotaTercel) , y el tipo real solo tiene constructores new(Animal, ToyotaTercel) y new(Cat, Automobile) .

PD: para aclarar el problema, las restricciones del constructor contravariante conducen a una variación del problema del "diamante doble". Considerar:

T CreateUsingAnimalAutomobile<T>() where T:IThing,new(Animal,Automobile) { ... } T CreateUsingAnimalToyotaTercel<T>() where T:IThing,new(Animal,ToyotaTercel) { return CreateUsingAnimalAutomobile<T>(); } T CreateUsingCatAutomobile<T>() where T:IThing,new(Cat,Automobile) { return CreateUsingAnimalAutomobile<T>(); } IThing thing1=CreateUsingAnimalToyotaTercel<FunnyClass>(); // FunnyClass defined in question IThing thing2=CreateUsingCatAutomobile<FunnyClass>(); // FunnyClass defined in question

Al procesar la llamada a CreateUsingAnimalToyotaTercel<FunnyClass>() , el constructor "Animal, ToyotaTercel" debe satisfacer la restricción para ese método, y el tipo genérico para ese método debe satisfacer una restricción para CreateUsingAnimalAutomobile<T>() . Al procesar la llamada a CreateUsingCatAutomobile<FunnyClass>() , el constructor "Cat, Automobile" debe satisfacer la restricción para ese método, y el tipo genérico para ese método debe satisfacer la restricción para CreateUsingAnimalAutomobile<T>() .

El problema es que ambas llamadas invocarán una llamada al mismo método CreateUsingAnimalAutomobile<SillyClass>() , y ese método no tiene forma de saber qué constructor debe invocarse. Las ambigüedades relacionadas con la contraparte no son exclusivas de los constructores, pero en la mayoría de los casos se resuelven mediante el enlace en tiempo de compilación.


Resumen

Este es un intento de capturar la información actual y las soluciones alternativas a esta pregunta y presentarla como una respuesta.

Encuentro que los genéricos combinados con las restricciones son uno de los aspectos más poderosos y elegantes de C # (provenientes de un fondo de plantillas de C ++). where T : Foo es excelente, ya que introduce la capacidad en T y al mismo tiempo lo limita a Foo en el momento de la compilación. En muchos casos, ha simplificado mi implementación. Al principio, estaba un poco preocupado ya que el uso de tipos genéricos de esta manera puede hacer que los genéricos crezcan a través del código, pero lo he permitido y los beneficios han superado con creces cualquier desventaja. Sin embargo, la abstracción siempre cae cuando se trata de construir un tipo genérico que toma un parámetro.

El problema

Al restringir una clase genérica, puede indicar que el genérico debe tener un constructor sin parámetros y luego instanciarlo:

public class Foo<T> where T : new() { public void SomeOperation() { T something = new T(); ... } }

El problema es que uno solo es capaz de restringir para constructores sin parámetros. Esto significa que una de las soluciones sugeridas a continuación debe usarse para los constructores que tienen parámetros. Como se describe a continuación, las soluciones tienen inconvenientes que van desde requerir un código adicional hasta ser muy peligroso. Además, si tengo una clase que tiene un constructor público sin parámetros que utiliza un método genérico, pero en algún lugar de la pista se cambia esa clase para que el constructor ahora tenga un parámetro, necesito cambiar el diseño de la plantilla y los alrededores. código para usar una de las soluciones en lugar de new ().

Esto es algo que Microsoft definitivamente conoce, vea estos enlaces en Microsoft Connect solo como una muestra (sin contar a los confusos usuarios de que hacen la pregunta) here here here here .

Todos están cerrados como "No se arreglará" o "Por diseño". Lo triste de esto es que el problema queda bloqueado y ya no es posible votarlos. Sin embargo, puedes votar here por la característica del constructor.

Las soluciones

Hay tres tipos principales de soluciones alternativas, ninguna de las cuales es ideal:

  1. Fábricas de uso . Esto requiere un montón de código repetitivo y gastos generales.
  2. Utilice Activator.CreateInstance (typeof (T), arg0, arg1, arg2, ...) . Este es mi menos favorito ya que el tipo de seguridad se pierde. ¿Qué pasa si en la pista se agrega un parámetro al constructor de tipo T? Obtienes una excepción de tiempo de ejecución.
  3. Utilice el enfoque de función / acción. y here Este es mi favorito, ya que conserva el tipo de seguridad y requiere menos código repetitivo. Sin embargo, todavía no es tan simple como la nueva T (a, b, c) y como una abstracción genérica a menudo abarca muchas clases, la clase que conoce el tipo a menudo está a unas pocas clases de la clase que necesita crear una instancia para que La función se pasa dando como resultado un código innecesario.

Las explicaciones

Se proporciona una respuesta estándar en Microsoft Connect que es:

"Gracias por su sugerencia. Microsoft ha recibido una serie de sugerencias sobre cambios en la semántica de restricciones de los tipos genéricos, además de realizar su propio trabajo en esta área. Sin embargo, en este momento Microsoft no puede asumir ningún compromiso de cambios en esta área. sea ​​parte de una futura versión del producto. Su sugerencia se tomará en cuenta para ayudar a tomar decisiones en esta área. Mientras tanto, el ejemplo de código a continuación ... "

La solución no es realmente mi solución recomendada de todas las opciones, ya que no es de tipo seguro y genera una excepción en tiempo de ejecución si alguna vez agrega otro parámetro al constructor.

Eric Lippert ofrece la mejor explicación que puedo encontrar en este post de desbordamiento de pila . Aprecio mucho esta respuesta, pero creo que se requiere más discusión sobre esto a nivel de usuario y luego a nivel técnico (probablemente por personas que saben más que yo acerca de los aspectos internos).

También descubrí recientemente que Mads Torgersen tiene un detalle bueno y detallado en este enlace (consulte "Publicado por Microsoft el 31 de marzo de 2009 a las 3:29 p.m.").

El problema es que los constructores son diferentes de otros métodos en que ya podemos restringir los métodos tanto como necesitamos mediante restricciones de derivación (interfaz o clase base). Puede haber algunos casos en los que las restricciones de los métodos sean beneficiosas, nunca las he necesitado, sin embargo, continuamente presiono la limitación del constructor sin parámetros. Por supuesto, una here sería ideal y Microsoft tendría que decidir por sí mismos.

Las sugerencias

Con respecto al beneficio discutible frente a la dificultad de implementación, puedo apreciar esto, pero destacaría los siguientes puntos:

  1. Hay un gran valor en la captura de errores en tiempo de compilación en lugar de en tiempo de ejecución (escriba seguridad en este caso).
  2. Parece que hay otras opciones que no son tan terribles. Ha habido algunas sugerencias sobre cómo se podría implementar esto. En particular, Jon Skeet propuso ''interfaces estáticas'' como una forma de resolver esto y parece que ya existen restricciones explícitas de miembros en el CLR, pero no en C #, vea los comentarios here y la discusión aquí . Además, el comentario de kvb en la respuesta de Eric Lippert sobre las restricciones de miembros arbitrarios.

El estado

No voy a pasar en cualquier forma de forma que yo sepa.