una - tipos genericos en c#
¿Inferencia de tipo genérico parcial posible en C#? (5)
Estoy trabajando en la reescritura de mi interfaz fluida para mi biblioteca de clases de IoC, y cuando refactoreé un código para compartir algunas funcionalidades comunes a través de una clase base, encontré un inconveniente.
Nota : Esto es algo que quiero hacer, no es algo que deba hacer. Si tengo que conformarme con una sintaxis diferente, lo haré, pero si alguien tiene una idea sobre cómo hacer que mi código se compile de la manera que yo quiero, sería muy bienvenido.
Quiero que algunos métodos de extensión estén disponibles para una clase base específica, y estos métodos deben ser genéricos, con un tipo genérico, relacionado con un argumento para el método, pero los métodos también deben devolver un tipo específico relacionado con el descendiente particular que ellos se invoca en.
Mejor con un ejemplo de código que la descripción anterior.
Aquí hay un ejemplo simple y completo de lo que no funciona:
using System;
namespace ConsoleApplication16
{
public class ParameterizedRegistrationBase { }
public class ConcreteTypeRegistration : ParameterizedRegistrationBase
{
public void SomethingConcrete() { }
}
public class DelegateRegistration : ParameterizedRegistrationBase
{
public void SomethingDelegated() { }
}
public static class Extensions
{
public static ParameterizedRegistrationBase Parameter<T>(
this ParameterizedRegistrationBase p, string name, T value)
{
return p;
}
}
class Program
{
static void Main(string[] args)
{
ConcreteTypeRegistration ct = new ConcreteTypeRegistration();
ct
.Parameter<int>("age", 20)
.SomethingConcrete(); // <-- this is not available
DelegateRegistration del = new DelegateRegistration();
del
.Parameter<int>("age", 20)
.SomethingDelegated(); // <-- neither is this
}
}
}
Si compila esto, obtendrá:
''ConsoleApplication16.ParameterizedRegistrationBase'' does not contain a definition for ''SomethingConcrete'' and no extension method ''SomethingConcrete''...
''ConsoleApplication16.ParameterizedRegistrationBase'' does not contain a definition for ''SomethingDelegated'' and no extension method ''SomethingDelegated''...
Lo que quiero es que el método de extensión ( Parameter<T>
) pueda invocarse tanto en Registro de tipo ConcreteTypeRegistration
como en ConcreteTypeRegistration
DelegateRegistration
, y en ambos casos, el tipo de retorno debe coincidir con el tipo en que se invocó la extensión.
El problema es el siguiente:
Me gustaría escribir:
ct.Parameter<string>("name", "Lasse")
^------^
notice only one generic argument
sino también que el Parameter<T>
devuelve un objeto del mismo tipo sobre el que se invocó, lo que significa:
ct.Parameter<string>("name", "Lasse").SomethingConcrete();
^ ^-------+-------^
| |
+---------------------------------------------+
.SomethingConcrete comes from the object in "ct"
which in this case is of type ConcreteTypeRegistration
¿Hay alguna manera de engañar al compilador para que haga este salto por mí?
Si agrego dos argumentos de tipo genérico al método Parameter
, escriba inference me obliga a proporcionar both, or none, que significa esto:
public static TReg Parameter<TReg, T>(
this TReg p, string name, T value)
where TReg : ParameterizedRegistrationBase
me da esto:
Using the generic method ''ConsoleApplication16.Extensions.Parameter<TReg,T>(TReg, string, T)'' requires 2 type arguments
Using the generic method ''ConsoleApplication16.Extensions.Parameter<TReg,T>(TReg, string, T)'' requires 2 type arguments
Lo cual es igual de malo.
Puedo reestructurar fácilmente las clases, o incluso hacer que los métodos no sean de extensión introduciéndolos en la jerarquía, pero mi pregunta es si puedo evitar tener que duplicar los métodos para los dos descendientes, y de alguna manera declararlos solo una vez , para la clase base.
Déjame reformular eso. ¿Hay alguna manera de cambiar las clases en el primer ejemplo de código anterior, para que se pueda conservar la sintaxis en el método principal, sin duplicar los métodos en cuestión?
El código deberá ser compatible con C # 3.0 y 4.0.
Editar : La razón por la que prefiero no dejar ambos argumentos de tipo genérico a la inferencia es que para algunos servicios, quiero especificar un valor de parámetro para un parámetro constructor que sea de un tipo, pero pase un valor que sea un descendiente. Por el momento, la coincidencia de los valores de argumento especificados y el constructor correcto para llamar se hace utilizando tanto el nombre como el tipo del argumento.
Déjame dar un ejemplo:
ServiceContainerBuilder.Register<ISomeService>(r => r
.From(f => f.ConcreteType<FileService>(ct => ct
.Parameter<Stream>("source", new FileStream(...)))));
^--+---^ ^---+----^
| |
| +- has to be a descendant of Stream
|
+- has to match constructor of FileService
Si dejo ambos para escribir inferencia, el tipo de parámetro será FileStream
, no Stream
.
¿Por qué no especifica los parámetros de tipo cero? Ambos pueden inferirse en tu muestra. Si esta no es una solución aceptable para usted, a menudo me encuentro con este problema y no hay una manera fácil de resolver el problema "inferir solo un parámetro de tipo". Así que iré con los métodos duplicados.
¿Qué hay de lo siguiente?
Use la definición que proporciona: public static TReg Parameter<TReg, T>( this TReg p, string name, T value) where TReg : ParameterizedRegistrationBase
Luego, eche el parámetro para que el motor de inferencia obtenga el tipo correcto:
ServiceContainerBuilder.Register<ISomeService>(r => r
.From(f => f.ConcreteType<FileService>(ct => ct
.Parameter("source", (Stream)new FileStream(...)))));
Creo que debes dividir los dos parámetros de tipo entre dos expresiones diferentes; haga que el explícito sea parte del tipo de parámetro del método de extensión, de modo que la inferencia puede recogerlo.
Supongamos que declaraste una clase contenedora:
public class TypedValue<TValue>
{
public TypedValue(TValue value)
{
Value = value;
}
public TValue Value { get; private set; }
}
Entonces su método de extensión como:
public static class Extensions
{
public static TReg Parameter<TValue, TReg>(
this TReg p, string name, TypedValue<TValue> value)
where TReg : ParameterizedRegistrationBase
{
// can get at value.Value
return p;
}
}
Además de una sobrecarga más simple (lo anterior de hecho podría llamar a esto):
public static class Extensions
{
public static TReg Parameter<TValue, TReg>(
this TReg p, string name, TValue value)
where TReg : ParameterizedRegistrationBase
{
return p;
}
}
Ahora, en el caso simple en el que le complace inferir el tipo de valor del parámetro:
ct.Parameter("name", "Lasse")
Pero en el caso donde necesite indicar explícitamente el tipo, puede hacerlo:
ct.Parameter("list", new TypedValue<IEnumerable<int>>(new List<int>()))
Se ve feo, pero con suerte más raro que el simple tipo completamente inferido.
Tenga en cuenta que podría tener la sobrecarga sin envoltura y escribir:
ct.Parameter("list", (IEnumerable<int>)(new List<int>()))
Pero eso, por supuesto, tiene la desventaja de fallar en el tiempo de ejecución si obtienes algo mal. Desafortunadamente lejos de mi compilador de C # en este momento, así que me disculpo si esto está fuera de lugar.
Si solo tiene dos tipos específicos de registro (que parece ser el caso en su pregunta), simplemente podría implementar dos métodos de extensión:
public static DelegateRegistration Parameter<T>(
this DelegateRegistration p, string name, T value);
public static ConcreteTypeRegistration Parameter<T>(
this ConcreteTypeRegistration p, string name, T value);
Entonces no necesitaría especificar el argumento tipo, por lo que la inferencia tipo funcionaría en el ejemplo que mencionó. Tenga en cuenta que puede implementar ambos métodos de extensión simplemente por delegación en un único método de extensión genérico con dos parámetros de tipo (el de su pregunta).
En general, C # no admite nada como o.Foo<int, ?>(..)
para inferir solo el segundo parámetro de tipo (sería una buena característica, F # lo tiene y es bastante útil :-)). Probablemente pueda implementar una solución alternativa que le permita escribir esto (básicamente, separando la llamada en dos llamadas a método, para obtener dos lugares donde se puede aplicar la infusión de tipo):
FooTrick<int>().Apply(); // where Apply is a generic method
Aquí hay un pseudo-código para demostrar la estructura:
// in the original object
FooImmediateWrapper<T> FooTrick<T>() {
return new FooImmediateWrapper<T> { InvokeOn = this; }
}
// in the FooImmediateWrapper<T> class
(...) Apply<R>(arguments) {
this.InvokeOn.Foo<T, R>(arguments);
}
Quería crear un método de extensión que pudiera enumerar una lista de cosas, y devolver una lista de esas cosas que eran de cierto tipo. Se vería así:
listOfFruits.ThatAre<Banana>().Where(banana => banana.Peel != Color.Black) ...
Lamentablemente, esto no es posible. La firma propuesta para este método de extensión se vería así:
public static IEnumerable<TResult> ThatAre<TSource, TResult>
(this IEnumerable<TSource> source) where TResult : TSource
... y la llamada a ThatAre <> falla porque ambos tipos de argumentos deben ser especificados, aunque se puede inferir TSource del uso.
Siguiendo los consejos de otras respuestas, creé dos funciones: una que captura la fuente y otra que permite a los llamantes expresar el resultado:
public static ThatAreWrapper<TSource> That<TSource>
(this IEnumerable<TSource> source)
{
return new ThatAreWrapper<TSource>(source);
}
public class ThatAreWrapper<TSource>
{
private readonly IEnumerable<TSource> SourceCollection;
public ThatAreWrapper(IEnumerable<TSource> source)
{
SourceCollection = source;
}
public IEnumerable<TResult> Are<TResult>() where TResult : TSource
{
foreach (var sourceItem in SourceCollection)
if (sourceItem is TResult) yield return (TResult)sourceItem;
}
}
}
Esto da como resultado el siguiente código de llamada:
listOfFruits.That().Are<Banana>().Where(banana => banana.Peel != Color.Black) ...
... lo cual no está mal.
Tenga en cuenta que debido a las restricciones de tipo genérico, el siguiente código:
listOfFruits.That().Are<Truck>().Where(truck => truck.Horn.IsBroken) ...
no podrá compilar en el paso Are (), ya que los Camiones no son Frutas. Esto supera a la función .OfType <> proporcionada:
listOfFruits.OfType<Truck>().Where(truck => truck.Horn.IsBroken) ...
Esto compila, pero siempre produce cero resultados y, de hecho, no tiene ningún sentido intentarlo. Es mucho mejor dejar que el compilador te ayude a detectar estas cosas.