2010-05-23 19 views
18

Estoy trabajando en la reescritura de mi interfaz fluida para la 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.Inferencia de tipo genérico parcial posible en C#?

Nota: Esto es algo que quiero que hacer, algo que no tengo que 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 del método, pero los métodos también deben devolver un tipo específico relacionado con el descendiente particular sobre el que se invoca.

Mejor con un ejemplo de código que la descripción anterior.

Aquí está un ejemplo simple y completa de lo que no trabajo:

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, se 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 la extensión método (Parameter<T>) para que se pueda invocar tanto en ConcreteTypeRegistration como en DelegateRegistration, y en ambos casos, el tipo de devolución 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 Parameter<T> devuelve un objeto del mismo tipo que fue invocada en adelante, 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 añado dos argumentos de tipo genérico con el método Parameter, la inferencia de tipos me obliga a cualquiera de proporcionar ambas, o ninguna, lo 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éjenme 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 dejo los dos argumentos de tipo genérico a la inferencia es que para algunos servicios, quiero especificar un valor de parámetro para un parámetro de constructor que es de un tipo, pero pase en un valor que es 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.

te voy a 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 a los dos a la inferencia de tipos, el tipo de parámetro habrá FileStream, no Stream.

Respuesta

11

Si usted tiene sólo 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 tendría que especificar el tipo de argumento, por lo que la inferencia tipo funcionaría en el ejemplo que mencionaste. 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 soporta nada por el estilo o.Foo<int, ?>(..) para inferir el segundo parámetro de tipo (que sería buena característica - F # tiene y que es bastante útil :-)). Probablemente se podría implementar una solución que le permitirá escribir esto (básicamente, mediante la separación de la llamada en dos llamadas de método, para obtener dos lugares donde el inferrence tipo se puede aplicar):

FooTrick<int>().Apply(); // where Apply is a generic method 

Aquí es 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); 
} 
+0

Sí, parece la mejor solución. Estoy probando uno ahora en el que también introduje los genéricos en la clase base, y acabo de abandonar los métodos de extensión, pero su ejemplo me permite mantenerlos como métodos de extensión, que a mi favor en este caso. Es decir, los dos métodos de extensión que solo llaman uno común que especifica los tipos correctos. –

2

¿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.

+0

Prefiero evitar que, al registrar un tipo concreto en mi implementación de IoC, pueda proporcionar un valor de parámetro para un parámetro de constructor, y debo especificar el nombre del parámetro y su tipo, y luego también proporcionar un valor para pasar a él. El tipo especificado tiene que coincidir, pero el valor real pasado puede ser un descendiente. Por ejemplo: '.Parameter (" source ", new FileStream (...))'. Aquí, si los dejo a ambos, el tipo del parámetro será FileStream, y por lo tanto tengo que implementar la coincidencia de descendientes. Aunque podría hacerlo, preferiría no hacerlo. –

0

¿Qué hay de lo siguiente:

utilizar la definición que proporcionan: public static TReg Parameter<TReg, T>( this TReg p, string name, T value) where TReg : ParameterizedRegistrationBase

fundidas entonces el parámetro de modo que el motor de inferencia obtiene el tipo correcto:

ServiceContainerBuilder.Register<ISomeService>(r => r 
.From(f => f.ConcreteType<FileService>(ct => ct 
    .Parameter("source", (Stream)new FileStream(...))))); 
0

creo que es necesario 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 declaró una clase contenedora:

public class TypedValue<TValue> 
{ 
    public TypedValue(TValue value) 
    { 
     Value = value; 
    } 

    public TValue Value { get; private set; } 
} 

A continuación, el 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 simple (lo de arriba puede, de hecho, llamar a éste):

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 está dispuesto a inferir el tipo de valor de parámetro, escriba:

ct.Parameter("name", "Lasse") 

Pero en el caso en que sea necesario declarar explícitamente el tipo, puede hacerlo:

ct.Parameter("list", new TypedValue<IEnumerable<int>>(new List<int>())) 

ve feo, pero es de esperar más raro que la simple naturaleza que se deduce totalmente.

Tenga en cuenta que podría simplemente tener la sobrecarga sin envoltorio y escribir:

ct.Parameter("list", (IEnumerable<int>)(new List<int>())) 

Pero eso, por supuesto, tiene la desventaja de no en tiempo de ejecución si obtiene algo mal. Desafortunadamente lejos de mi compilador de C# en este momento, así que me disculpo si esto está fuera de lugar.

13

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 propone para este método de extensión habría parecido:

public static IEnumerable<TResult> ThatAre<TSource, TResult> 
    (this IEnumerable<TSource> source) where TResult : TSource 

... y la llamada a thatare <> falla porque los dos argumentos de tipo necesitan especificarse, a pesar de que TSource puede inferirse del uso.

Siguiendo el consejo en otras respuestas, he creado dos funciones: una que capta la fuente, y otro que permite a las personas que llaman para 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 se traduce en el siguiente Código de llamada:

listOfFruits.That().Are<Banana>().Where(banana => banana.Peel != Color.Black) ... 

... lo cual no está nada mal.

Tenga en cuenta que debido a las limitaciones de tipo genérico, el siguiente código:

listOfFruits.That().Are<Truck>().Where(truck => truck.Horn.IsBroken) ... 

se producirá un error de compilar en el paso de Are(), ya que no son camiones Frutas. Esto supera el .OfType <> función proporcionada:

listOfFruits.OfType<Truck>().Where(truck => truck.Horn.IsBroken) ... 

Esto compila, pero siempre produce resultados cero y en realidad no tiene ningún sentido intentar. Es mucho mejor dejar que el compilador te ayude a detectar estas cosas.

+2

Creo que has dicho lo mismo que Thomas anteriormente, pero entendí tu respuesta mucho mejor. –

+1

Por curiosidad, ¿por qué no usaste '.OfType ()'? – recursive

+2

@recursive: porque .OfType no impone la restricción de que el tipo al que intentas enviar debe ser compatible con la fuente. Editado la respuesta, último párrafo. – CSJ

Cuestiones relacionadas