C # Generics - Come evitare il metodo ridondante?

27

Supponiamo di avere due classi simili a questa (il primo blocco di codice e il problema generale sono relativi a C #):

class A 
{
    public int IntProperty { get; set; }
}

class B 
{
    public int IntProperty { get; set; }
}

Queste classi non possono essere modificate in alcun modo (fanno parte di un gruppo di terze parti). Pertanto, non posso costringerli a implementare la stessa interfaccia o ereditare la stessa classe che conterrà quindi IntProperty.

Voglio applicare qualche logica sulla proprietà IntProperty di entrambe le classi, e in C ++ potrei usare una classe template per farlo abbastanza facilmente:

template <class T>
class LogicToBeApplied
{
    public:
        void T CreateElement();

};

template <class T>
T LogicToBeApplied<T>::CreateElement()
{
    T retVal;
    retVal.IntProperty = 50;
    return retVal;
}

E quindi potrei fare qualcosa di simile:

LogicToBeApplied<ClassA> classALogic;
LogicToBeApplied<ClassB> classBLogic;
ClassA classAElement = classALogic.CreateElement();
ClassB classBElement = classBLogic.CreateElement();   

In questo modo ho potuto creare una singola classe generica factory che avrebbe funzionato sia per ClassA che per ClassB.

Tuttavia, in C #, devo scrivere due classi con due diverse clausole where anche se il codice per la logica è esattamente lo stesso:

public class LogicAToBeApplied<T> where T : ClassA, new()
{
    public T CreateElement()
    {
        T retVal = new T();
        retVal.IntProperty = 50;
        return retVal;
    }
}

public class LogicBToBeApplied<T> where T : ClassB, new()
{
    public T CreateElement()
    {
        T retVal = new T();
        retVal.IntProperty = 50;
        return retVal;
    }
}

So che se voglio avere classi diverse nella clausola where , devono essere correlate, cioè ereditare la stessa classe, se voglio applicare lo stesso codice a loro nel senso che ho descritto sopra . È solo che è molto fastidioso avere due metodi completamente identici. Inoltre, non voglio utilizzare il reflection a causa dei problemi di prestazioni.

Qualcuno può suggerire un approccio in cui questo può essere scritto in modo più elegante?

    
posta Vladimir Stokic 03.11.2016 - 09:00
fonte

8 risposte

48

Aggiungi un'interfaccia proxy (talvolta chiamata adattatore , a volte con sottili differenze), implementa LogicToBeApplied in termini di proxy, quindi aggiungi un modo per costruire un'istanza di questo proxy da due lambda : uno per la proprietà get e uno per il set.

interface IProxy
{
    int Property { get; set; }
}
class LambdaProxy : IProxy
{
    private Function<int> getFunction;
    private Action<int> setFunction;
    int Property
    {
        get { return getFunction(); }
        set { setFunction(value); }
    }
    public LambdaProxy(Function<int> getter, Action<int> setter)
    {
        getFunction = getter;
        setFunction = setter;
    }
}

Ora, ogni volta che devi passare un IProxy ma hai un'istanza delle classi di terze parti, puoi semplicemente passare alcuni lambda:

A a = new A();
B b = new B();
IProxy proxyA = new LambdaProxy(() => a.Property, (val) => a.Property = val);
IProxy proxyB = new LambdaProxy(() => b.Property, (val) => b.Property = val);
proxyA.Property = 12; // mutates the proxied 'a' as well

Inoltre, puoi scrivere semplici helper per costruire istanze LamdaProxy da istanze di A o B. Possono anche essere metodi di estensione per darti uno stile "fluente":

public static class ProxyExtension
{
    public static IProxy Proxied(this A a)
    {
      return new LambdaProxy(() => a.Property, (val) => a.Property = val);
    }

    public static IProxy Proxied(this B b)
    {
      return new LambdaProxy(() => b.Property, (val) => b.Property = val);
    }
}

E ora la costruzione dei proxy è simile a questa:

IProxy proxyA = new A().Proxied();
IProxy proxyB = new B().Proxied();

Per quanto riguarda la tua fabbrica, vedrei se puoi refactoring in un metodo factory "principale" che accetta un IProxy ed esegue tutta la logica su di esso e altri metodi che passano solo in new A().Proxied() o new B().Proxied() :

public class LogicToBeApplied
{
    public A CreateA() {
      A a = new A();
      InitializeProxy(a.Proxied());
      return a; // or maybe return the proxy if you'd rather use that
    }

    public B CreateB() {
      B b = new B();
      InitializeProxy(b.Proxied());
      return b;
    }

    private void InitializeProxy(IProxy proxy)
    {
        proxy.IntProperty = 50;
    }
}

Non c'è modo di fare l'equivalente del tuo codice C ++ in C # perché i modelli C ++ si basano sulla tipizzazione structural . Finché due classi hanno lo stesso nome e firma del metodo, in C ++ è possibile chiamarlo genericamente su entrambi. C # ha nominale digitando - il nome di una classe o di un'interfaccia è parte del suo tipo. Pertanto, le classi A e B non possono essere trattate allo stesso modo in qualsiasi capacità, a meno che una relazione esplicita "sia una" sia definita attraverso l'ereditarietà o l'implementazione dell'interfaccia.

Se lo standard di implementazione di questi metodi per classe è eccessivo, puoi scrivere una funzione che accetta un oggetto e reflectively crea un LambdaProxy cercando un nome di proprietà specifico:

public class ReflectiveProxier 
{
    public object proxyReflectively(object proxied)
    {
        PropertyInfo prop = proxied.GetType().GetProperty("Property");
        return new LambdaProxy(
            () => prop.GetValue(proxied),
            (val) => prop.SetValue(proxied, val));
     }
}

Questo fallisce in modo abissale quando vengono dati oggetti di tipo errato; la riflessione introduce intrinsecamente la possibilità di guasti che il sistema di tipo C # non può impedire. Fortunatamente è possibile evitare la riflessione fino a quando il carico di manutenzione degli helper diventa troppo grande perché non è necessario modificare l'interfaccia IProxy o l'implementazione LambdaProxy per aggiungere lo zucchero riflettente.

Parte del motivo per cui questo funziona è che LambdaProxy è "al massimo generico"; può adattare qualsiasi valore che implementa lo "spirito" del contratto IProxy perché l'implementazione di LambdaProxy è completamente definita dalle funzioni getter e setter fornite. Funziona anche se le classi hanno nomi diversi per la proprietà, o tipi diversi che sono sensibilmente e tranquillamente rappresentabili come int s, o se c'è un modo per mappare il concetto che Property dovrebbe rappresentare per qualsiasi altra caratteristica di la classe. L'indirezione fornita dalle funzioni ti dà la massima flessibilità.

    
risposta data 03.11.2016 - 10:09
fonte
10

È possibile adattare ClassA e ClassB tramite un'interfaccia comune. In questo modo il tuo codice in LogicAToBeApplied rimane lo stesso. Non molto diverso da quello che hai però.

class A
{
    public int Property { get; set; }
}
class B
{
    public int Property { get; set; }
}

interface IAdapter
{
    int Property { get; set; }
}

class LogicToBeApplied<T> where T : IAdapter, new()
{
    public T Create()
    {
        var ret = new T();
        ret.Property = 50;
        return ret;
    }
}

class AAdapter : A, IAdapter { }

class BAdapter : B, IAdapter { }
    
risposta data 03.11.2016 - 09:23
fonte
10

Ecco uno schema su come utilizzare gli adattatori senza ereditare da A e / o B, con la possibilità di utilizzarli per gli oggetti A e B esistenti:

interface IAdapter
{
    int Property { get; set; }
}

class LogicToBeApplied<T> where T : IAdapter, new()
{
    public T Create()
    {
        var ret = new T();
        ret.Property = 50;
        return ret;
    }
}

class AAdapter : IAdapter
{
    A _a;

    public AAdapter()  // use this if you want to have the "logic" part create new objects
    {
        _a=new A();
    }

    public AAdapter(A a) // if you need an adapter for an existing object afterwards
    {
       _a=a;
    }

    public int Property
    {
        get { return _a.Property; }
        set { _a.Property = value; }
    }

    public A {get{return _a; } } // to provide access for non-generic code
}

class BAdapter 
{
     // analogously
}

Normalmente preferisco questo tipo di adattatore di oggetti sui proxy di classe, evitano problemi brutti che puoi incontrare con l'ereditarietà. Ad esempio, questa soluzione funzionerà anche se A e B sono classi sigillate.

    
risposta data 03.11.2016 - 10:27
fonte
8

La versione C ++ funziona solo perché i suoi modelli usano "static typing typ" - qualsiasi cosa compila purché il tipo fornisca i nomi corretti. È più simile a un sistema macro. Il sistema generico di C # e altri linguaggi funziona in modo molto diverso.

Le risposte di Devnull e Doc Brown mostrano come il pattern dell'adattatore può essere usato per mantenere l'algoritmo generale e operare comunque su tipi arbitrari ... con un paio di restrizioni. In particolare, ora stai creando un tipo diverso da quello che vuoi realmente.

Con un po 'di inganno, è possibile utilizzare esattamente il tipo desiderato senza modifiche. Tuttavia, ora è necessario estrarre tutte le interazioni con il tipo di destinazione in un'interfaccia separata. Qui, queste interazioni sono costruzione e assegnazione di proprietà:

interface IInteractions<T> {
  T Instantiate();
  void AssignProperty(T target, int value);
}

In un'interpretazione OOP, questo sarebbe un esempio del modello di strategia , anche se combinato con i generici.

Possiamo quindi riscrivere la tua logica per utilizzare queste interazioni:

public class LogicBToBeApplied<T>
{
    public T CreateElement(IInteractions<T> interactions)
    {
        T retVal = interactions.Instantiate();
        interactions.AssignProperty(retVal, 50);
        return retVal;
    }
}

Le definizioni di interazione saranno:

class Interactions_ClassA : IInteractions<ClassA> {
  public override ClassA Instantiate() { return new ClassA(); }
  public override void AssignProperty(ClassA target, int value) { target.IntProperty = value; }
}

Il grande svantaggio di questo approccio è che il programmatore deve scrivere e passare un'istanza di interazione quando chiama la logica. Questo è abbastanza simile alle soluzioni basate su un adattatore, ma è leggermente più generale.

Nella mia esperienza, questo è il più vicino possibile alle funzioni dei modelli in altre lingue. Tecniche simili sono utilizzate in Haskell, Scala, Go e Rust per implementare interfacce al di fuori della definizione di un tipo. Tuttavia, in questi linguaggi il compilatore interviene e seleziona implicitamente l'istanza di interazione corretta in modo da non visualizzare effettivamente l'argomento extra. Questo è anche simile ai metodi di estensione di C #, ma non è limitato ai metodi statici.

    
risposta data 03.11.2016 - 10:31
fonte
8

Se vuoi veramente lanciare attenzione al vento, puoi usare "dinamico" per fare in modo che il compilatore si prenda cura di tutta la cattiveria del riflesso per te. Ciò si tradurrà in un errore di runtime se si passa un oggetto a SetSomeProperty che non ha una proprietà denominata SomeProperty.

using System;

namespace ConsoleApplication3
{
    class A
    {
        public int SomeProperty { get; set; }
    }

    class B
    {
        public int SomeProperty { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var a = new A();
            var b = new B();

            SetSomeProperty(a, 7);
            SetSomeProperty(b, 12);

            Console.WriteLine($"a.SomeProperty = {a.SomeProperty}, b.SomeProperty = {b.SomeProperty}");
        }

        static void SetSomeProperty(dynamic obj, int value)
        {
            obj.SomeProperty = value;
        }
    }
}
    
risposta data 03.11.2016 - 19:30
fonte
4

Le altre risposte identificano correttamente il problema e forniscono soluzioni praticabili. C # non supporta (generalmente) "digitazione anatra" ("Se cammina come un'anatra ..."), quindi non c'è modo di forzare ClassA e ClassB ad essere intercambiabili se non sono stati progettati in questo modo.

Tuttavia, se sei già disposto ad accettare il rischio di un errore di runtime, allora c'è una risposta più semplice rispetto all'utilizzo di Reflection.

C # ha la parola chiave dynamic che è perfetta per situazioni come questa. Indica al compilatore "Non saprò di che tipo si tratta fino al runtime (e forse nemmeno allora), quindi permettimi di fare qualsiasi cosa ad esso".

Usando questo, puoi costruire esattamente la funzione che desideri:

public class LogicToBeApplied<T> where T : new()
{
    public static T CreateElement()
    {
        dynamic retVal = new T(); // This doesn't care what type T is.
        retVal.IntProperty = 50;  // This will fail at runtime if there is no "IntProperty" 
                                  // or it doesn't accept an int.
        return retVal;            // Once again, we don't care what it is.
    }
}

Nota anche l'uso della parola chiave static . Questo ti permette di usare questo come:

A classAElement = LogicToBeApplied<A>.CreateElement();
B classBElement = LogicToBeApplied<B>.CreateElement();

Non ci sono implicazioni sulle prestazioni di grandi dimensioni dell'uso di dynamic , il modo in cui vi è il successo occasionale (e la complessità aggiunta) dell'uso di Reflection. La prima volta che il tuo codice raggiunge la chiamata dinamica con un tipo specifico avrà una piccola quantità di spese generali , ma le chiamate ripetute saranno altrettanto veloce del codice standard. Tuttavia, otterrà un RuntimeBinderException se proverai a passare qualcosa che non ha quella proprietà, e non c'è un buon modo per verificarlo prima del tempo. Potresti voler gestire in modo specifico quell'errore in modo utile.

    
risposta data 04.11.2016 - 00:43
fonte
2

Puoi usare reflection per estrarre la proprietà per nome.

public class logic 
{
    public object getNew<T>() where T : new()
    {
        T ret = new T();
        try
        {
            var property = typeof(T).GetProperty("IntProperty");
            if (property != null && property.PropertyType == typeof(int))
            {
                property.SetValue(ret, 50);
            }
        }
        catch (AmbiguousMatchException)
        {
            //hmm..
        }
        return ret;
    }
}

Ovviamente si rischia un errore di runtime con questo metodo. Qual è ciò che C # sta cercando di smettere di fare.

Ho letto da qualche parte che una versione futura di C # ti permetterà di passare oggetti come un'interfaccia che non ereditano ma che corrispondono. Quale risolverebbe anche il tuo problema.

(Proverò a cercare l'articolo)

Un altro metodo, anche se non sono sicuro che ti salvi alcun codice, sarebbe quello di creare sottoclassi sia A che B e anche ereditare un'interfaccia con IntProperty.

public interface IIntProp {
    public int IntProperty {get, set}
}

public class A2 : A, IIntProp {}

public class B2 : B, IIntProp {}
    
risposta data 03.11.2016 - 09:20
fonte
0

Volevo solo usare implicit operator conversioni insieme all'approccio delegato / lambda della risposta di Jack. A e B sono come presupposti:

// A and B are mutable reference types

class A
{
  public int IntProperty { get; set; }
}

class B
{
  public int IntProperty { get; set; }
}

Quindi è facile ottenere una bella sintassi con conversioni implicite definite dall'utente (non sono necessari metodi di estensione o simili):

// Adapter is an immutable type. However, the delegate instances have a captured reference to an A or a B (closure semantics)
struct Adapter
{
  readonly Func<int> getter;
  readonly Action<int> setter;

  Adapter(Func<int> getter, Action<int> setter)
  {
    this.getter = getter;
    this.setter = setter;
  }

  public int IntProperty
  {
    get { return getter(); }
    set { setter(value); }
  }

  public static implicit operator Adapter(A a) => new Adapter(() => a.IntProperty, x => a.IntProperty = x);
  public static implicit operator Adapter(B b) => new Adapter(() => b.IntProperty, x => b.IntProperty = x);

  public A CloneToA() => new A { IntProperty = getter(), };
  public B CloneToB() => new B { IntProperty = getter(), };
}

Illustrazione di utilizzo:

class LogicToBeApplied
{
  public static A CreateA()
  {
    var a = new A();
    Initialize(a);
    return a;
  }
  public static B CreateB()
  {
    var b = new B();
    Initialize(b);
    return b;
  }

  static void Initialize(Adapter a)
  {
    a.IntProperty = 50;
  }
}

Il metodo Initialize mostra come puoi lavorare con Adapter senza preoccuparti se è un A o un B o qualcos'altro. Le invocazioni del metodo Initialize mostrano che non abbiamo bisogno di alcun cast (visibile) o .AsProxy() o simile per trattare il cemento A o B come Adapter .

Considera se vuoi lanciare un ArgumentNullException nelle conversioni definite dall'utente se l'argomento passato è un riferimento nullo, oppure no.

    
risposta data 04.11.2016 - 14:26
fonte

Leggi altre domande sui tag