2012-02-29 17 views
10

Sto cercando di trovare un modo migliore per gestire alcuni costrutti if in crescita per gestire classi di tipi diversi. Queste classi sono, in definitiva, wrapper attorno a diversi tipi di valore (int, DateTime, ecc.) Con alcune informazioni aggiuntive sullo stato. Quindi la differenza principale tra queste classi è il tipo di dati che contengono. Mentre implementano interfacce generiche, devono anche essere conservate in raccolte omogenee, quindi implementano anche un'interfaccia non generica. Le istanze di classe vengono gestite in base al tipo di dati che rappresentano e la loro propagazione continua o non continua in base a ciò.Doppia-dispatch e alternative

Sebbene questo non sia necessariamente un problema .NET o C#, il mio codice è in C#.

classi Esempio:

interface ITimedValue { 
TimeSpan TimeStamp { get; } 
} 

interface ITimedValue<T> : ITimedValue { 
T Value { get; } 
} 

class NumericValue : ITimedValue<float> { 
public TimeSpan TimeStamp { get; private set; } 
public float Value { get; private set; } 
} 

class DateTimeValue : ITimedValue<DateTime> { 
public TimeSpan TimeStamp { get; private set; } 
public DateTime Value { get; private set; } 
} 

class NumericEvaluator { 
public void Evaluate(IEnumerable<ITimedValue> values) ... 
} 

ho fornito con due opzioni:

doppio spedizione

recente ho appreso del modello visitatore e il suo uso della doppia spedizione per gestire solo un caso del genere. Questo fa appello perché permette ai dati indesiderati di non propagarsi (se vogliamo solo gestire un int, possiamo gestirlo in modo diverso rispetto a un DateTime). Inoltre, i comportamenti di come vengono gestiti i diversi tipi sarebbero limitati alla singola classe che sta gestendo la spedizione. Ma c'è un bel po 'di manutenzione se/quando un nuovo tipo di valore deve essere supportato.

dell'Unione Classe

Una classe che contiene una proprietà per ogni tipo di valore supportato potrebbe essere quello che ciascuna di queste classi negozio. Qualsiasi operazione su un valore influenzerebbe il componente appropriato. Questo è meno complesso e meno di manutenzione rispetto alla strategia di doppia spedizione, ma significherebbe che tutti i dati verrebbero propagandati inutilmente in quanto non è più possibile discriminare sulla falsariga di "Non operare su quel tipo di dati ". Tuttavia, se/quando i nuovi tipi devono essere supportati, devono solo entrare in questa classe (più qualsiasi altra classe aggiuntiva che deve essere creata per supportare il nuovo tipo di dati).

class UnionData { 
public int NumericValue; 
public DateTime DateTimeValue; 
} 

Ci sono opzioni migliori? C'è qualcosa in queste due opzioni che non ho ritenuto che dovrei?

+0

Cosa potrebbe accadere nel metodo 'Evaluate' di' NumericEvaluator' che opererebbe su un 'DateTime' * o * a' float'? –

+0

su un cellulare in questo momento, quindi non posso scrivere una risposta adeguata, ma provare a cercare su google l'uso della dinamica per la doppia spedizione (che riduce notevolmente il boilerplate richiesto dal pattern del visitatore) o l'implementazione dei tipi di unione in C# (Ricordo una bella implementazione di @Juliet da qualche parte qui su SO) –

+0

@Chris Shain: Questo è parte del vantaggio della soluzione a doppia spedizione - non dovrebbe. Per un 'DateTime', la soluzione a doppia spedizione la ignora o la soluzione variante la consuma come valore 0. – redman

risposta

3

metodo 1, utilizzo dinamico per doppia spedizione (il credito va a http://blogs.msdn.com/b/curth/archive/2008/11/15/c-dynamic-and-multiple-dispatch.aspx). Fondamentalmente si può avere il vostro modello Visitor semplificato in questo modo:

class Evaluator { 
public void Evaluate(IEnumerable<ITimedValue> values) { 
    foreach(var v in values) 
    { 
     Eval((dynamic)(v)); 
    } 
} 

private void Eval(DateTimeValue d) { 
    Console.WriteLine(d.Value.ToString() + " is a datetime"); 
} 

private void Eval(NumericValue f) { 
    Console.WriteLine(f.Value.ToString() + " is a float"); 
} 

} 

campione di utilizzo:

var l = new List<ITimedValue>(){ 
    new NumericValue(){Value= 5.1F}, 
    new DateTimeValue() {Value= DateTime.Now}}; 

new Evaluator() 
    .Evaluate(l); 
     // output: 
     // 5,1 is a float 
     // 29/02/2012 19:15:16 is a datetime 

metodo 2 userebbe tipi dell'Unione in C#, come proposto dal @Juliet here (implementazione alternativa here)

+0

Sono contento che tu sia tornato e abbia postato questi messaggi, non sono riuscito a trovarli. E grazie per aver corretto il mio uso scorretto del termine "variante". – redman

+0

Li ho visti anche chiamati tipi di varianti. E i tipi di somma e (forse erroneamente?) I tipi di dati algebrici. Non sono affatto un esperto, quindi non stavo cercando di correggerti: i "tipi di unione" sembrano essere il modo in cui sono stati chiamati nella risposta che stavo cercando di indicarti. :) –

0

Dico di aver risolto una situazione simile: memorizzando Ticks di un DateTime o TimeSpan come doppio nella raccolta e utilizzando IComparable come un vincolo where sul parametro type. La conversione in double/from double viene eseguita da una classe helper.

Vedere this previous question.

Stranamente questo porta ad altri problemi, come il pugilato e l'unboxing. L'applicazione su cui sto lavorando richiede prestazioni estremamente elevate, quindi ho bisogno di evitare la boxe. Se riesci a pensare a un ottimo modo per gestire genericamente diversi tipi di dati (incluso DateTime), allora sono tutto orecchie!

0

Perché non solo implementare l'interfaccia che si desidera effettivamente e consentire al tipo di implementazione di definire qual è il valore?Per esempio:

class NumericValue : ITimedValue<float> { 
public TimeSpan TimeStamp { get; private set; } 
public float Value { get; private set; } 
} 

class DateTimeValue : ITimedValue<DateTime>, ITimedValue<float> { 
public TimeSpan TimeStamp { get; private set; } 
public DateTime Value { get; private set; } 
public Float ITimedValue<Float>.Value { get { return 0; } } 
} 

class NumericEvaluator { 
public void Evaluate(IEnumerable<ITimedValue<float>> values) ... 
} 

Se si desidera che il comportamento della realizzazione DateTime variano in base al particolare uso (ad esempio, le implementazioni alternative di valutare le funzioni), allora, per definizione, devono essere consapevoli di ITimedValue<DateTime>. È possibile ottenere una buona soluzione tipizzata staticamente fornendo uno o più delegati Converter, ad esempio.

Infine, se è davvero solo desidera gestire le istanze NumericValue, basta filtrare tutto ciò che non è un'istanza NumericValue:

class NumericEvaluator { 
    public void Evaluate(IEnumerable<ITimedValue> values) { 
     foreach (NumericValue value in values.OfType<NumericValue>()) { 
      .... 
     } 
    } 
} 
+0

In realtà ho preso in considerazione l'utilizzo di "OfType", ma non riuscivo a ricordare perché non l'avessi fatto. Scavando tra le note, ho trovato un caso in cui ci si aspettava che i dati di un tipo diverso si presentassero occasionalmente come marker. Potrei enumerare la raccolta più volte per tenerne conto, ma preferirei evitare l'inefficienza (questo è un sistema in tempo reale). – redman

+0

E riguardo 'values.Where (v => v è X || v è Y)'? Anche se di nuovo chiedo che cosa potresti fare con un'enumerazione eterogenea di X e Y, se non hanno un tipo di base o un'interfaccia comune. –

+0

In questo caso, stavano tentando di iniettare una metrica temporale nei dati al di fuori del timestamp che è già presente. – redman

0

Buona domanda. La prima cosa che mi è venuta in mente era un algoritmo di strategia riflessiva. Il runtime può dire, sia staticamente che dinamicamente, il tipo più derivato del riferimento, indipendentemente dal tipo di variabile che si sta utilizzando per conservare il riferimento. Tuttavia, sfortunatamente, non sceglierà automaticamente un overload basato sul tipo derivato, solo il tipo di variabile. Quindi, dobbiamo chiedere in fase di esecuzione quale sia il vero tipo e, in base a ciò, selezionare manualmente un particolare sovraccarico. Usando il reflection, possiamo costruire dinamicamente una collezione di metodi identificati come gestire un particolare sottotipo, quindi interrogare il riferimento per il suo tipo generico e cercare l'implementazione nel dizionario basato su questo.

public interface ITimedValueEvaluator 
{ 
    void Evaluate(ITimedValue value); 
} 

public interface ITimedValueEvaluator<T>:ITimedValueEvaluator 
{ 
    void Evaluate(ITimedValue<T> value); 
} 

//each implementation is responsible for implementing both interfaces' methods, 
//much like implementing IEnumerable<> requires implementing IEnumerable 
class NumericEvaluator: ITimedValueEvaluator<int> ... 

class DateTimeEvaluator: ITimedValueEvaluator<DateTime> ... 

public class Evaluator 
{ 
    private Dictionary<Type, ITimedValueEvaluator> Implementations; 

    public Evaluator() 
    { 
     //find all implementations of ITimedValueEvaluator, instantiate one of each 
     //and store in a Dictionary 
     Implementations = (from t in Assembly.GetCurrentAssembly().GetTypes() 
     where t.IsAssignableFrom(typeof(ITimedValueEvaluator<>) 
     and !t.IsInterface 
     select new KeyValuePair<Type, ITimedValueEvaluator>(t.GetGenericArguments()[0], (ITimedValueEvaluator)Activator.CreateInstance(t))) 
     .ToDictionary(kvp=>kvp.Key, kvp=>kvp.Value);  
    } 

    public void Evaluate(ITimedValue value) 
    { 
     //find the ITimedValue's true type's GTA, and look up the implementation 
     var genType = value.GetType().GetGenericArguments()[0]; 

     //Since we're passing a reference to the base ITimedValue interface, 
     //we will call the Evaluate overload from the base ITimedValueEvaluator interface, 
     //and each implementation should cast value to the correct generic type. 
     Implementations[genType].Evaluate(value); 
    } 

    public void Evaluate(IEnumerable<ITimedValue> values) 
    { 
     foreach(var value in values) Evaluate(value); 
    } 
} 

Si noti che il valutatore principale è l'unico in grado di gestire un oggetto IEnumerable; ogni implementazione ITimedValueEvaluator dovrebbe gestire i valori uno alla volta. Se questo non è fattibile (diciamo che è necessario considerare tutti i valori di un particolare tipo), allora questo diventa veramente facile; basta scorrere tutte le implementazioni nel Dizionario, passandole l'intero IEnumerable, e fare in modo che tali implementazioni filtrino l'elenco solo agli oggetti del particolare tipo generico chiuso usando il metodo Linq OfType(). Ciò richiederà che tu esegua tutte le implementazioni ITimedValueEvaluator che trovi nella lista, il che è uno sforzo inutile se non ci sono elementi di un particolare tipo in una lista.

La bellezza di questa è la sua estensibilità; per supportare una nuova chiusura generica di ITimedValue, è sufficiente aggiungere una nuova implementazione di ITimedValueEvaluator dello stesso tipo. La classe Evaluator la troverà, istanzia una copia e la userà. Come la maggior parte degli algoritmi riflessivi, è lento, ma l'effettiva parte riflessiva è un affare occasionale.