2012-02-29 16 views
10

Estoy tratando de encontrar una mejor manera de manejar algunas construcciones if en crecimiento para manejar clases de diferentes tipos. Estas clases son, en última instancia, envoltorios de tipos de valores dispares (int, DateTime, etc.) con información de estado adicional. Entonces, la principal diferencia entre estas clases es el tipo de datos que contienen. Si bien implementan interfaces genéricas, también deben mantenerse en colecciones homogéneas, por lo que también implementan una interfaz no genérica. Las instancias de clase se manejan de acuerdo con el tipo de datos que representan y su propagación continúa o no continúa en función de eso.Double-dispatch y alternativas

Si bien esto no es necesariamente un problema de .NET o C#, mi código está en C#.

clases Ejemplo:

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

he llegado con dos opciones:

doble Despacho

Hace poco supe del patrón del visitante y su uso del doble de despacho de manejar solo tal caso. Esto es atractivo porque permitiría que los datos no deseados no se propaguen (si solo queremos manejar un int, podemos manejarlo de manera diferente a DateTime). Además, los comportamientos de cómo se manejan los diferentes tipos se limitarán a la única clase que maneja el envío. Pero hay un poco de mantenimiento si/cuando un nuevo tipo de valor tiene que ser compatible.

Unión Clase

Una clase que contiene una propiedad para cada tipo de valor con el apoyo podría ser lo que cada una de estas clases de tienda. Cualquier operación en un valor afectaría el componente apropiado. Esto es menos complejo y menos mantenimiento que la estrategia de doble envío, pero significaría que cada pieza de datos se propagaría innecesariamente, ya que no se puede discriminar a lo largo de las líneas de "No opero con ese tipo de datos". ". Sin embargo, si/cuando los tipos nuevos necesitan soporte, solo necesitan ingresar a esta clase (más las clases adicionales que deban crearse para admitir el nuevo tipo de datos).

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

¿Hay mejores opciones? ¿Hay algo en cualquiera de estas dos opciones que no consideré que debería?

+0

¿Qué podría ocurrir en el método 'Evaluate' de' NumericEvaluator' que funcionaría en un 'DateTime' * o * a' float'? –

+0

en un móvil en este momento, así que no puedo escribir una respuesta adecuada, pero trate de buscar en Google el uso de la dinámica para despacho doble (que reduce en gran medida el estándar requerido por el patrón de visitante) o la implementación de tipos de unión en C# (recuerdo una hermosa implementación de @Juliet en algún lugar aquí en SO) –

+0

@Chris Shain: Eso es parte del beneficio de la solución de doble despacho, no debería ser así. Para un 'DateTime', la solución de doble envío lo ignora o la solución variante lo consume como un valor 0. – redman

Respuesta

3

método 1, usando dinámico para despacho doble (crédito va a http://blogs.msdn.com/b/curth/archive/2008/11/15/c-dynamic-and-multiple-dispatch.aspx). Básicamente, usted puede tener su patrón de Visitantes simplificada así:

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

} 

muestra de uso:

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 

método 2 usaría tipos Unión en C# como propone @Juliet here (implementación alternativa here)

+0

Me alegro de que hayas vuelto y publicado esto, no pude encontrarlos. Y gracias por corregir mi uso incorrecto del término "variante". – redman

+0

Los he visto denominados tipos de variantes también. Y tipos de suma y (tal vez incorrectamente) tipos de datos algebraicos. De ninguna manera soy un experto, así que no estaba tratando de corregirte: "tipos de unión" resulta ser la forma en que fueron llamados en la respuesta a la que intentaba señalarte. :) –

0

Te digo que he resuelto una situación similar - está almacenando el Ticks de un DateTime o TimeSpan como el doble en la colección y usando IComparable como una restricción where en el parámetro de tipo. La conversión a doble/doble se realiza por una clase auxiliar.

Consulte this previous question.

Curiosamente, esto lleva a otros problemas, como el boxeo y el desempaquetado. La aplicación en la que estoy trabajando requiere un rendimiento extremadamente alto, así que debo evitar el boxeo. Si puede pensar en una excelente forma de manipular genéricamente diferentes tipos de datos (incluido DateTime) ¡entonces soy todo oídos!

0

¿Por qué no simplemente implementar la interfaz que realmente desea, y permitir que el tipo de implementación defina cuál es el valor?Por ejemplo:

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

Si desea que el comportamiento de la aplicación DateTime a variar en función del uso particular (por ejemplo, las implementaciones alternativas de evaluar las funciones), entonces, por definición, deben ser conscientes de ITimedValue<DateTime>. Puede obtener una buena solución estáticamente tipada proporcionando uno o más delegados Converter, por ejemplo.

Por último, si en realidad sólo quiere manejar los casos NumericValue sólo tiene que filtrar a cabo cualquier cosa que no es una instancia NumericValue:

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

Realmente consideré usar 'OfType', pero no podía recordar por qué no lo hice. Buscando en notas, encontré un caso en el que se esperaba que los datos de un tipo diferente aparecieran ocasionalmente como marcador. Podría enumerar la colección varias veces para dar cuenta de eso, pero preferiría evitar la ineficiencia (este es un sistema en tiempo real). – redman

+0

¿Qué tal 'values.Where (v => v es X || v es Y)'? Aunque de nuevo pregunto qué es lo que posiblemente harías con una enumeración heterogénea de X e Y, si no tienen un tipo de base común o una interfaz. –

+0

En este caso, estaban intentando inyectar una métrica temporal en los datos fuera de la marca de tiempo que ya está presente. – redman

0

Buena pregunta. Lo primero que me vino a la mente fue un algoritmo de estrategia reflexivo. El tiempo de ejecución puede indicarle, de forma estática o dinámica, el tipo de referencia más derivado, independientemente del tipo de variable que utilice para contener la referencia. Sin embargo, desafortunadamente, no elegirá automáticamente una sobrecarga basada en el tipo derivado, solo el tipo de variable. Por lo tanto, debemos preguntar en tiempo de ejecución cuál es el tipo verdadero y, en base a eso, seleccionar manualmente una sobrecarga en particular. Usando la reflexión, podemos construir dinámicamente una colección de métodos identificados como manejo de un subtipo particular, luego interrogar a la referencia para su tipo genérico y buscar la implementación en el diccionario en función de eso.

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

Observe que el Evaluador principal es el único que puede manejar un IEnumerable; cada implementación de ITimedValueEvaluator debe manejar valores uno a la vez. Si esto no es factible (digamos que necesita considerar todos los valores de un tipo particular), entonces esto se vuelve realmente fácil; simplemente recorra cada implementación en el Diccionario, pasándole el IEnumerable completo, y haga que esas implementaciones filtren la lista solo a objetos del tipo genérico cerrado particular utilizando el método OfType() Linq. Esto requerirá que ejecute todas las implementaciones de ITimedValueEvaluator que encuentre en la lista, lo cual es esfuerzo desperdiciado si no hay elementos de un tipo particular en una lista.

La belleza de esto es su extensibilidad; para admitir un nuevo cierre genérico de ITimedValue, simplemente agregue una nueva implementación de ITimedValueEvaluator del mismo tipo. La clase Evaluator lo encontrará, instanciará una copia y la usará. Al igual que la mayoría de los algoritmos reflexivos, es lento, pero la parte reflexiva real es un trato de una sola vez.