2010-02-15 18 views
9

Si consideri il seguente codice (pesantemente semplificato):tipizzazione condizionale nel metodo generico

public T Function<T>() { 
    if (typeof(T) == typeof(string)) { 
     return (T) (object) "hello"; 
    } 
    ... 
} 

E 'una specie di assurdo primo cast a object, poi a T. Ma il compilatore non ha modo di sapere che il test precedente assicurato T è di tipo string.

Qual è il modo più elegante e idiomatico per ottenere questo comportamento in C# (che include l'eliminazione dello stupido typeof(T) == typeof(string), dal momento che T is string non può essere utilizzato)?


Addendum: Non v'è alcun tipo di ritorno varianza .net, quindi non è possibile fare un sovraccarico di funzioni di tipo stringa (che, tra l'altro, è solo un esempio, ma uno dei motivi per cui un'associazione la ridefinizione finale nel polimorfismo, ad es. UML, non può essere eseguita in C#). Ovviamente, il seguente sarebbe grande, ma non funziona:

public T Function<T>() { 
    ... 
} 

public string Function<string>() { 
    return "hello"; 
} 

Calcestruzzo Esempio 1: Perché c'è stato diversi attacchi al fatto che una funzione generica che mette alla prova per i tipi specifici ISN' t generico, cercherò di fornire un esempio più completo. Considera il modello di design di tipo quadrato. Qui di seguito un frammento:

public class Entity { 
    Dictionary<PropertyType, object> properties; 

    public T GetTypedProperty<T>(PropertyType p) { 
    var val = properties[p]; 

    if (typeof(T) == typeof(string) { 
     (T) (object) p.ToString(this); // magic going here 
    } 

    return (T) TypeDescriptor.GetConverter(typeof(T)).ConvertFrom(val); 
    } 
} 

Calcestruzzo Esempio 2: consideri il modello di progettazione Interprete:

public class Expression { 
    public virtual object Execute() { } 
} 

public class StringExpression: Expression { 
    public override string Execute() { } // Error! Type variance not allowed... 
} 

Ora cerchiamo di usare farmaci generici in esecuzione per consentire al chiamante di forzare un tipo di ritorno:

public class Expression { 
    public virtual T Execute<T>() { 
    if(typeof(T) == typeof(string)) { // what happens when I want a string result from a non-string expression? 
     return (T) (object) do_some_magic_and_return_a_string(); 
    } else if(typeof(T) == typeof(bool)) { // what about bools? any number != 0 should be True. Non-empty lists should be True. Not null should be True 
     return (T) (object) do_some_magic_and_return_a_bool(); 
    } 
    } 
} 

public class StringExpression: Expressiong { 
    public override T Execute<T>() where T: string { 
    return (T) string_result; 
    } 
} 
+0

Sono curioso .. si può spiegare un po 'che tipo di valori che si sta per tornare, per esempio da dove vengono e ci sono dei vincoli espliciti o espliciti sulla funzione? dobbiamo supporre che sono previsti solo tipi intrinseci? –

+0

Come ho sottolineato in un commento qui sotto, ci sono situazioni in cui il chiamante si aspetta perfettamente una semantica diversa per la funzione tra diversi tipi. Ad esempio, diverse lingue supportano l'operatore + per numeri interi e stringhe; e tuttavia, per i numeri interi funge da somma, mentre per le stringhe funge da concatenatore. –

+0

Se ti interessa una situazione ho bisogno di questo modello (badate bene, ho visto questo accadere diverse volte in contesti diversi), ho un 'Valuta ' di una classe chiamata 'Expression', che mappa l'AST di un DSL. La lingua sottostante è digitata dinamicamente, quindi il chiamante non conosce esattamente il tipo 'Valuta '. Ma se il chiamante forza un tipo specifico, ad es. 'Valuta ', quindi la funzione può aggiungere un po 'di "zucchero semantico", come test per 'nulls', numeri maggiori di 0, liste non vuote, ecc. Il chiamante aspetta un' bool', indipendentemente da cosa succede all'interno. –

risposta

6

Se stai facendo questi tipi di assegni in un metodo generico, ripenserò il tuo design. Il metodo ovviamente non è veramente generico - se lo fosse, non avresti bisogno di un controllo specifico del tipo ...

Situazioni come questa possono essere gestite in modo più pulito con una riprogettazione. Un'alternativa è spesso quella di fornire un sovraccarico del tipo appropriato. Esistono anche altre alternative di progettazione che evitano il comportamento specifico del tipo, come ad esempio Richard Berg's suggestion of passing in a delegate.

+0

Mi dispiace. Comprendo il tuo reclamo, ma ci sono usi legittimi per questo modello, quando si desidera modificare la logica interna nel caso in cui appaia un tipo specifico. E i sovraccarichi non funzionano perché non si ha varianza nel tipo di ritorno di una funzione. –

+2

Sono d'accordo in un senso molto generale/vago, ma questo non risponde affatto alla domanda. A volte la realtà impone che funzionalità generiche e specifiche coesistano nello stesso stack frame. (si potrebbe sempre copiare/incollare le parti generiche, suppongo, ma è peggio di qualsiasi delle proposte qui IMO) Fortunatamente, C# supporta l'esecuzione differita, che consente schemi hacky-but-workable come si vede nella mia risposta. –

+2

@Richard: Penso che questa sia la * sola * risposta valida alla domanda così come è stata scritta. Come mai un chiamante dovrebbe comprendere la semantica di un metodo generico che si comporta in modo diverso per tipi specifici?Forse se l'OP avesse spiegato cosa stava davvero cercando di fare, qualcuno sarebbe stato in grado di offrire e spiegare un'alternativa migliore. – Aaronaught

1

Puoi usare as qui?

T s = "hello" as T; 
if(s != null) 
    return s; 
+1

Non senza un vincolo generico per la classe ... –

+0

Buon punto. Questo può o non può essere un affare qui. –

+0

Ci dispiace, ma "ciao" dovrebbe essere "condizionalmente" eseguito quando T è una stringa, non ogni volta e solo testata per la stringa. Certamente, "ciao" qui è solo un esempio; Non voglio davvero andare fino in fondo solo per restituirmi un po ';-) –

1

Non riesco a pensare a un modo "elegante" per farlo. Come dici tu, il compilatore non può sapere che il condizionale ha assicurato che il tipo di T sia string. Di conseguenza, si deve supporre che, poiché non esiste un modo generalizzato per convertire da string a T, si tratta di un errore. object a T potrebbe riuscire, quindi il compilatore lo consente.

Non sono sicuro che vorrei un modo elegante per esprimere questo. Anche se riesco a vedere dove sarebbe necessario fare controlli di tipo espliciti come questo in alcune situazioni, penso che vorrei essere ingombrante perché è davvero un po 'un trucco. E vorrei che sporgesse: "Ehi, sto facendo qualcosa di strano qui!"

0

Ok, ho eseguito una corsa da diversi punti di vista e sono venuto fuori corto: dovrei concludere che se la tua attuale implementazione ha portato a termine il lavoro dovresti prendere la vittoria e andare avanti. A corto di alcune emissioni arcane quello che hai è quello che si ottiene.

Ma il compilatore non ha modo di sapere che il test precedente assicurato T è di tipo stringa .

Umm .... Se non mi sbaglio, i generici sono solo codice gen. Il compilatore genera un metodo di corrispondenza per ogni tipo distinto trovato nei metodi di chiamata. Quindi il compilatore fa conosce l'argomento di tipo per il sovraccarico chiamato. Ancora; Se non mi sbaglio.

Ma nel complesso, penso che in questo caso si stia abusando del generico, da quello che vedo, e come altri hanno affermato, ci sono soluzioni più appropriate ..... che sono innominabili se non si pubblica un codice che specifica completamente le tue esigenze

solo il mio 2 pesos ...

+0

Ah! C'è un problema: i generici non sono solo codice gen in C#. Sono una bestia completamente diversa dai template in C++. Cercherò di fornire un esempio migliore di "buon utilizzo", ma dubito che ciò renderebbe più semplice suggerire disegni alternativi (anche se la gente probabilmente si astiene dal fare accigliare i loro occhi ;-) –

+0

Umm .. a meno che non mi sbagli, sei sbaglia. ;-). Ecco una definizione rapida, non eccessivamente prolissa che ritengo sia corretta: http://www.codeproject.com/KB/cs/generics_explained.aspx in particolare la sezione "Come i generici vengono gestiti dalla sezione runtime .NET". Mi sto perdendo qualcosa? –

+0

Ok ... Vedo il tuo punto .... I generici sono generati in fase di esecuzione. Lapsus. quindi ora che mi sono totalmente voltato ... lasciami provare a riprendere la testa da questo punto di vista da un'altra angolazione. –

3
using System; 
using System.Collections.Generic; 
using System.Linq; 

namespace SimpleExamples 
{ 
    /// <summary> 
    /// Compiled but not run. Copypasta at your own risk! 
    /// </summary> 
    public class Tester 
    { 
     public static void Main(string[] args) 
     { 
      // Contrived example #1: pushing type-specific functionality up the call stack 
      var strResult = Example1.Calculate<string>("hello", s => "Could not calculate " + s); 
      var intResult = Example1.Calculate<int>(1234, i => -1); 

      // Contrived example #2: overriding default behavior with an alternative that's optimized for a certain type 
      var list1 = new List<int> { 1, 2, 3 }; 
      var list2 = new int[] { 4, 5, 6 }; 
      Example2<int>.DoSomething(list1, list2); 

      var list1H = new HashSet<int> { 1, 2, 3 }; 
      Example2<int>.DoSomething<HashSet<int>>(list1H, list2, (l1, l2) => l1.UnionWith(l2)); 
     } 
    } 

    public static class Example1 
    { 
     public static TParam Calculate<TParam>(TParam param, Func<TParam, TParam> errorMessage)    
     { 
      bool success; 
      var result = CalculateInternal<TParam>(param, out success); 
      if (success) 
       return result; 
      else 
       return errorMessage(param); 
     } 

     private static TParam CalculateInternal<TParam>(TParam param, out bool success) 
     { 
      throw new NotImplementedException(); 
     } 
    } 

    public static class Example2<T> 
    { 
     public static void DoSomething(ICollection<T> list1, IEnumerable<T> list2) 
     { 
      Action<ICollection<T>, IEnumerable<T>> genericUnion = (l1, l2) => 
      { 
       foreach (var item in l2) 
       { 
        l1.Add(item); 
       } 
       l1 = l1.Distinct().ToList(); 
      }; 
      DoSomething<ICollection<T>>(list1, list2, genericUnion); 
     } 

     public static void DoSomething<TList>(TList list1, IEnumerable<T> list2, Action<TList, IEnumerable<T>> specializedUnion) 
      where TList : ICollection<T> 
     { 
      /* stuff happens */ 

      specializedUnion(list1, list2); 

      /* other stuff happens */    
     } 
    } 
} 

/// I confess I don't completely understand what your code was trying to do, here's my best shot 
namespace TypeSquarePattern 
{ 
    public enum Property 
    { 
     A, 
     B, 
     C, 
    } 

    public class Entity 
    { 
     Dictionary<Property, object> properties; 
     Dictionary<Property, Type> propertyTypes; 

     public T GetTypedProperty<T>(Property p) 
     { 
      var val = properties[p]; 
      var type = propertyTypes[p]; 

      // invoke the cast operator [including user defined casts] between whatever val was stored as, and the appropriate type as 
      // determined by the domain model [represented here as a simple Dictionary; actual implementation is probably more complex] 
      val = Convert.ChangeType(val, type); 

      // now create a strongly-typed object that matches what the caller wanted 
      return (T)val; 
     } 
    } 
} 

/// Solving this one is a straightforward application of the deferred-execution patterns I demonstrated earlier 
namespace InterpreterPattern 
{ 
    public class Expression<TResult> 
    { 
     protected TResult _value;    
     private Func<TResult, bool> _tester; 
     private TResult _fallback; 

     protected Expression(Func<TResult, bool> tester, TResult fallback) 
     { 
      _tester = tester; 
      _fallback = fallback; 
     } 

     public TResult Execute() 
     { 
      if (_tester(_value)) 
       return _value; 
      else 
       return _fallback; 
     } 
    } 

    public class StringExpression : Expression<string> 
    { 
     public StringExpression() 
      : base(s => string.IsNullOrEmpty(s), "something else") 
     { } 
    } 

    public class Tuple3Expression<T> : Expression<IList<T>> 
    { 
     public Tuple3Expression() 
      : base(t => t != null && t.Count == 3, new List<T> { default(T), default(T), default(T) }) 
     { } 
    } 
} 
+0

Richard: +1 Mi piace questo, come un disegno , ma secondo me è una domanda diversa dall'OP. Sei (correttamente, credo) che estrae di proposito le informazioni specifiche del tipo dalle routine generiche, passando un delegato per gestirlo dal lato del chiamante. Tuttavia, il metodo generico qui non fa nulla di "non generico" - non ha conoscenza del tipo. Questa è una buona opzione per un design che evita i problemi che ho sollevato, tuttavia. –

+1

Punto giusto. A rigor di termini, anch'io sto suggerendo che Hugo ridisegna la sua interfaccia, proprio come hai fatto tu. Tuttavia: (1) con ogni probabilità, la mia soluzione non è troppo lontana da ciò che Hugo aveva in mente [specialmente se proviene da template C++, o da un linguaggio funzionale] per la sua funzione (2) se non, è ancora di più utile che dire "non farlo". –

+0

Soluzione interessante, ma 'DoSomething ' restituisce 'void', e il problema che sto avendo è legato alla restituzione della varianza del tipo. A meno che non stia modificando un oggetto esistente (nel tuo esempio, argomento 'TList' list1), non posso estrapolarlo a una situazione in cui vuoi che il tipo restituito cambi. –