2010-05-23 16 views
18

Sto lavorando per riscrivere la mia interfaccia fluente per la mia libreria di classi IoC, e quando ho refactored del codice per condividere alcune funzionalità comuni attraverso una classe base, ho trovato un problema.Inferenza di tipo generico parziale possibile in C#?

Nota: Questo è qualcosa che voglio da fare, non qualcosa che devo da fare. Se dovessi accontentarmi di una sintassi diversa, lo farò, ma se qualcuno ha un'idea su come compilare il mio codice nel modo in cui lo voglio, sarebbe molto gradito.

Desidero che alcuni metodi di estensione siano disponibili per una classe base specifica e questi metodi dovrebbero essere generici, con un tipo generico, relativi a un argomento del metodo, ma i metodi dovrebbero anche restituire un tipo specifico relativo a il particolare discendente su cui sono invocati.

Meglio con un esempio di codice rispetto alla descrizione di cui sopra methinks.

Ecco un esempio semplice e completa di ciò che non lavoro:

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 
     } 
    } 
} 

Se si compila questo, si otterrà:

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

Quello che voglio è per l'estensione metodo (Parameter<T>) per poter essere richiamato su entrambi ConcreteTypeRegistration e DelegateRegistration e, in entrambi i casi, il tipo di reso deve corrispondere al tipo in cui è stata richiamata l'estensione.

Il problema è come segue:

desidero scrivere:

ct.Parameter<string>("name", "Lasse") 
      ^------^ 
      notice only one generic argument 

ma anche che Parameter<T> restituisce un oggetto dello stesso tipo è stato richiamato sopra, che significa:

ct.Parameter<string>("name", "Lasse").SomethingConcrete(); 
^          ^-------+-------^ 
|            | 
+---------------------------------------------+ 
    .SomethingConcrete comes from the object in "ct" 
    which in this case is of type ConcreteTypeRegistration 

C'è un modo per ingannare il compilatore nel fare questo salto per me?

Se aggiungo due argomenti di tipo generico al metodo Parameter, inferenza di tipo mi costringe a uno fornire sia, o nessuno, che significa che questo:

public static TReg Parameter<TReg, T>(
    this TReg p, string name, T value) 
    where TReg : ParameterizedRegistrationBase 

mi dà questo:

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 

Che è altrettanto brutto.

Posso facilmente ristrutturare le classi, o anche rendere i metodi metodi non di estensione introducendoli nella gerarchia, ma la mia domanda è se posso evitare di dover duplicare i metodi per i due discendenti, e in qualche modo dichiarali solo una volta, per la classe base.

Lasciatemi riformulare. C'è un modo per cambiare le classi nel primo esempio di codice sopra, in modo che la sintassi nel metodo Main possa essere mantenuta, senza duplicare i metodi in questione?

Il codice dovrà essere compatibile sia con C# 3.0 che con 4.0.


Edit: Il motivo per cui preferisco non lasciare entrambi gli argomenti di tipo generico per inferenza è che per alcuni servizi, voglio specificare un valore di parametro per un parametro costruttore che è di un tipo, ma passaggio in un valore che è un discendente. Per il momento, la corrispondenza dei valori degli argomenti specificati e il costruttore corretto da chiamare viene eseguita utilizzando sia il nome che il tipo dell'argomento.

Vi faccio un esempio:

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 

Se lascio sia per inferenza di tipo, il tipo di parametro sarà FileStream, non Stream.

risposta

11

Se avete solo due tipi specifici di registrazione (che sembra essere il caso nella vostra domanda), si può semplicemente implementare due metodi di estensione:

public static DelegateRegistration Parameter<T>( 
    this DelegateRegistration p, string name, T value); 

public static ConcreteTypeRegistration Parameter<T>( 
    this ConcreteTypeRegistration p, string name, T value); 

Allora non avrebbe bisogno di specificare il tipo di argomento, quindi l'inferenza di tipo funzionerebbe nell'esempio che hai citato. Nota che puoi implementare entrambi i metodi di estensione solo per delega a un singolo metodo di estensione generico con due parametri di tipo (quello nella tua domanda).


In generale, C# non supporta nulla di simile o.Foo<int, ?>(..) di dedurre solo il secondo parametro di tipo (sarebbe caratteristica piacevole - F # ha ed è abbastanza utile :-)). Probabilmente si potrebbe implementare una soluzione che permetterebbe di scrivere questo (in pratica, separando la chiamata in due chiamate di metodo, per ottenere due posti in cui si può applicare il tipo inferrence):

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

Ecco una pseudo il codice per dimostrare la struttura:

// 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ì, sembra la soluzione migliore. Ne sto testando uno ora dove ho introdotto anche i generici nella classe base, e ho appena eliminato i metodi di estensione, ma il tuo esempio mi consente di mantenerli come metodi di estensione, che io preferisco in questo caso. Cioè, i due metodi di estensione che chiamano solo uno comune specificando i tipi corretti. –

2

Perché non si specificano i parametri di tipo zero? Entrambi possono essere dedotti nel tuo campione. Se questa non è una soluzione accettabile per te, spesso sto incontrando anche questo problema e non esiste un modo semplice per risolvere il problema "dedurre un solo parametro di tipo". Quindi andrò con i metodi duplicati.

+0

Preferisco evitare che, quando si registra un tipo concreto nella mia implementazione IoC, possa fornire un valore di parametro per un parametro costruttore, e devo specificare il nome del parametro e il suo tipo, quindi fornire anche un valore per passare ad esso. Il tipo specificato deve corrispondere, ma il valore effettivo passato può essere un discendente. Ad esempio: '.Parametro (" origine ", nuovo FileStream (...))'. Qui, se lascio entrambi per l'inferenza, il tipo del parametro sarà FileStream, e quindi devo implementare la corrispondenza dei discendenti. Potrei farlo, preferirei non farlo. –

0

Che dire quanto segue:

Utilizzare la definizione forniscono: public static TReg Parameter<TReg, T>( this TReg p, string name, T value) where TReg : ParameterizedRegistrationBase

poi gettato il parametro in modo che il motore di inferenza ottiene il giusto tipo:

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

Penso che avete bisogno di dividere i due parametri di tipo tra due espressioni diverse; fai in modo che l'esplicito faccia parte del tipo di parametro al metodo di estensione, quindi l'inferenza può quindi prenderlo.

Supponiamo che si dichiarato una classe wrapper:

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

    public TValue Value { get; private set; } 
} 

Allora il tuo metodo di estensione come:

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; 
    } 
} 

Più un sovraccarico di più semplice (quanto sopra potrebbe infatti chiamare questo uno):

public static class Extensions 
{ 
    public static TReg Parameter<TValue, TReg>(
     this TReg p, string name, TValue value) 
     where TReg : ParameterizedRegistrationBase 
    { 
     return p; 
    } 
} 

Ora nel caso semplice in cui si desume il tipo di valore del parametro:

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

Ma nel caso in cui è necessario dichiarare esplicitamente il tipo, è possibile farlo:

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

sembra brutto, ma raramente, si spera che il semplice tipo completamente dedotto.

Nota che si potrebbe semplicemente avere il sovraccarico di non-wrapper e scrivere:

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

Ma questo, naturalmente, ha il difetto di non riuscire a runtime se si ottiene qualcosa di sbagliato. Purtroppo lontano dal mio compilatore C# in questo momento, quindi scuse se questo è lontano.

13

Volevo creare un metodo di estensione che potesse enumerare un elenco di cose e restituire un elenco di quelle che erano di un certo tipo. Sarebbe come questo:

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

Purtroppo, questo non è possibile. La firma proposto per questo metodo di estensione avrebbe guardato come:

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

... e la chiamata a thatare <> fallisce perché entrambi gli argomenti di tipo devono essere specificati, anche se TSource può dedurre dal loro utilizzo.

Seguendo i consigli contenuti in altre risposte, ho creato due funzioni: una che cattura la fonte, e un altro che permette ai chiamanti di esprimere il risultato:

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; 
     } 
    } 
} 

Il risultato è il seguente codice chiamante:

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

... che non è male.

Si noti che a causa dei vincoli di tipo generico, il seguente codice:

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

non riuscirà a compilare nella fase sei(), dal momento che i camion non sono frutti. Questo batte la condizione .OfType <> funzione:

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

Questo compila, ma dà sempre zero risultati e in effetti non ha alcun senso per provare. È molto più bello lasciare che il compilatore ti aiuti a individuare queste cose.

+2

Penso che tu abbia detto la stessa cosa di Thomas sopra, ma ho capito molto meglio la tua risposta. –

+1

Per curiosità, perché non hai usato '.OfType ()'? – recursive

+2

@recursive: perché .OfType non impone la restrizione che il tipo che si sta tentando di trasmettere debba essere compatibile con l'origine. Modificato la risposta, ultimo paragrafo. – CSJ