Génériques C # - Comment éviter les méthodes redondantes?

Supposons que deux classes ressemblent à ceci (le premier bloc de code et le problème général sont liés à C #):

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

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

Ces classes ne peuvent en aucun cas être modifiées (elles font partie d'un assemblage tiers). Par conséquent, je ne peux pas leur faire implémenter la même interface, ni hériter de la même classe qui contiendrait alors IntProperty.

Je souhaite appliquer une logique à la propriété IntProperty des deux classes, et en C ++, je pourrais utiliser une classe de modèle pour le faire assez facilement:

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

};

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

Et puis je pourrais faire quelque chose comme ça:

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

Ainsi, je pouvais créer une seule classe d’usine générique qui fonctionnerait à la fois pour ClassA et ClassB.

Cependant, en C#, je dois écrire deux classes avec deux clauses différentes même si le code de la logique est exactement le même:

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

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

Je sais que si je veux avoir différentes classes dans la clause , elles doivent être liées, c'est-à-dire hériter de la même classe, si je veux leur appliquer le même code dans le sens où je décrit ci-dessus. C'est juste que c'est très ennuyeux d'avoir deux méthodes complètement identiques. Je ne veux pas non plus utiliser la réflexion en raison de problèmes de performances.

Quelqu'un peut-il suggérer une approche permettant d'écrire cela de manière plus élégante?

27
ajouté édité
Vues: 1
@Luaan, il est très raisonnable qu'un programmeur C ++ s'attende à ce que générique en C# résolve le même problème que les modèles en C ++.
ajouté l'auteur user1114, source
@Joshua, je pense plus à cela comme à un problème d'interfaces ne prenant pas en charge le "typage de canard".
ajouté l'auteur user1114, source
@Ian Non, le problème est d'utiliser un sous-typage nominal au lieu d'un sous-typage structurel. Le typage de canard n'est qu'une version faible et non sécurisée du typage structurel.
ajouté l'auteur gardenhead, source
En fait, c’est ma principale plainte à propos des génériques, c’est que cela ne peut pas être fait.
ajouté l'auteur Joshua, source
Pourquoi utilisez-vous des génériques pour cela en premier lieu? Il n'y a rien de générique sur ces deux fonctions.
ajouté l'auteur Luaan, source
Eh bien, vous pouvez faire appel à la réflexion ou à la dynamique si vous êtes certain de ne pas pouvoir le casser dans les versions à venir.
ajouté l'auteur Casey, source
@ Luan Ceci est un exemple simplifié d'une variation de modèle de fabrique abstraite. Imaginez que des dizaines de classes héritent de ClassA ou de ClassB, et que ClassA et ClassB soient des classes abstraites. Les classes héritées ne portent aucune information supplémentaire et doivent être instanciées. Au lieu d’écrire une usine pour chacun d’entre eux, j’ai opté pour des génériques.
ajouté l'auteur Vladimir Stokic, source

8 Réponses

Ajoutez une interface proxy (parfois appelée adaptateur , parfois avec des différences subtiles), implémentez LogicToBeApplied en termes de proxy, puis ajoutez un moyen de créer une instance de ce proxy deux lambdas: un pour la propriété get et un pour l'ensemble.

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

Maintenant, chaque fois que vous avez besoin de passer un IProxy mais que vous avez une instance des classes tierces, vous pouvez simplement passer quelques lambdas:

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

De plus, vous pouvez écrire des aides simples pour construire des instances LamdaProxy à partir d'instances de A ou B. Elles peuvent même être des méthodes d'extension pour vous donner un style "fluide":

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

Et maintenant, la construction de mandataires ressemble à ceci:

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

Pour ce qui est de votre usine, je verrais si vous pouvez la reformuler en une méthode de fabrique "principale" qui accepte un IProxy et exécute toute la logique dessus ainsi que d'autres méthodes qui transmettent simplement au nouveau A (). Proxied() ou nouveau 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;
    }
}

Il n'y a aucun moyen de faire l'équivalent de votre code C ++ en C# car les modèles C ++ reposent sur le typage structurel . Tant que deux classes ont le même nom de méthode et la même signature, en C ++, vous pouvez appeler cette méthode de manière générique sur les deux. C# a saisie nominale - le nom d'une classe ou d'une interface fait partie de son type. Par conséquent, les classes A et B ne peuvent être traitées de la même manière quelle que soit leur capacité, sauf si une relation explicite "est une" est définie via l'implémentation d'héritage ou d'interface.

Si l'implémentation de ces méthodes par classe est trop lourde, vous pouvez écrire une fonction qui prend un objet et de manière réfléchie crée un LambdaProxy en recherchant un nom de propriété spécifique:

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

Cela échoue complètement lorsque des objets de type incorrect sont donnés; la réflexion introduit de manière inhérente la possibilité d'échecs que le système de type C# ne peut pas empêcher. Heureusement, vous pouvez éviter les réflexions jusqu'à ce que la charge de maintenance des assistants devienne trop lourde, car vous n'êtes pas obligé de modifier l'interface IProxy ou l'implémentation LambdaProxy pour ajouter le sucre réfléchissant.

Cela fonctionne en partie parce que LambdaProxy est "générique au maximum"; il peut adapter toute valeur qui implémente "l'esprit" du contrat IProxy car l'implémentation de LambdaProxy est complètement définie par les fonctions de lecture et de définition données. Cela fonctionne même si les classes ont des noms différents pour la propriété, ou différents types qui sont représentables de manière fiable et sûre en tant que int , ou s'il existe un moyen de mapper le concept que Propriété est censé représenter toute autre caractéristique de la classe. L'indirection fournie par les fonctions vous offre une flexibilité maximale.

48
ajouté
@ Jack - Assez bien. J'ai ajouté ma propre réponse pour en faire la démonstration. C'est une fonctionnalité très utile, dans certaines circonstances rares (comme celle-ci).
ajouté l'auteur Bobson, source
Au lieu de ReflectiveProxier , pourriez-vous créer un proxy en utilisant le mot clé dynamic ? Il me semble que vous auriez les mêmes problèmes fondamentaux (c’est-à-dire des erreurs qui ne sont détectées qu’au moment de l’exécution), mais la syntaxe et la facilité de maintenance seraient beaucoup plus simples.
ajouté l'auteur Bobson, source
@Bobson Mes compétences en C# ne sont pas très avancées, alors honnêtement, je n'en ai aucune idée.
ajouté l'auteur Jack, source
@Ewan - Il vous le permet , mais il ne vous nécessite pas . Cela signifie que vous pouvez choisir d'entrer et de sortir lorsque l'élimination de la surcharge justifie l'utilisation de la réflexion.
ajouté l'auteur Jack, source
@VladimirStokic Voir les modifications, j'ai développé un peu sur ce
ajouté l'auteur Jack, source
cette méthode nécessite toujours que vous mappiez explicitement la propriété pour chaque type avec la possibilité supplémentaire d'une erreur d'exécution si votre fonction de mappage est défectueuse
ajouté l'auteur Ewan, source
Une approche très intéressante, et peut certainement être utilisée pour appeler les fonctions, mais peut-elle être utilisée pour une usine, où je dois réellement créer des objets de ClassA et ClassB?
ajouté l'auteur Vladimir Stokic, source
@ Jack Excellente réponse, merci beaucoup! Tous les autres, merci pour vos commentaires de très grande qualité.
ajouté l'auteur Vladimir Stokic, source

Vous pouvez adapter ClassA et ClassB via une interface commune. Ainsi, votre code dans LogicAToBeApplied reste le même. Pas très différent de ce que vous avez cependant.

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

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

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

class AAdapter : A, IAdapter { }

class BAdapter : B, IAdapter { }
10
ajouté
@ amon merci pour la clarification, je n'ai pas trop prêté attention à ce que c'est (adaptateur vs proxy) pour être honnête :)
ajouté l'auteur Ryan McGeary, source
@DocBrown sans entrer dans un débat sur l'agrégation vs l'héritage, pourquoi l'un est-il meilleur que l'autre dans ce cas?
ajouté l'auteur Ryan McGeary, source
Le problème avec cette solution est que vous ne pouvez pas simplement prendre deux objets de types A et B, les convertir en quelque sorte en AProxy et BProxy, puis leur appliquer LogicToBeApplied. Ce problème peut être résolu en utilisant l'agrégation au lieu de l'héritage (resp. Implémentez les objets proxy non pas en dérivant de A et B, mais en prenant une référence aux objets A et B). Encore une fois, un exemple de mauvaise utilisation de l'héritage pose des problèmes.
ajouté l'auteur Peter LeFanu Lumsdaine, source
@Jack: ce type de solution est logique lorsque LogicToBeApplied présente une certaine complexité et ne doit en aucun cas être répété à deux endroits de la base de code. Ensuite, le code passe-partout supplémentaire est souvent négligeable.
ajouté l'auteur Peter LeFanu Lumsdaine, source
@amon: assez bien, dans ce cas précis, où le "nouveau" fait partie de la logique, votre solution est parfaitement valide. Quoi qu'il en soit, j'ai posté un exemple montrant comment créer un adaptateur d'objet.
ajouté l'auteur Peter LeFanu Lumsdaine, source
+1 en utilisant le Adapter Pattern est la solution traditionnelle de programmation orientée objet ici. Il s’agit plus d’un adaptateur que d’un proxy puisque nous adaptons les types A , B à une interface commune. Le gros avantage est que nous n’avons pas à dupliquer la logique commune. L'inconvénient est que la logique instancie maintenant le wrapper/proxy au lieu du type réel.
ajouté l'auteur amon, source
@DocBrown Il existe à la fois le modèle d'adaptateur de classe (illustré ici, suppose une sorte d'héritage multiple) et le modèle plus commun d'objet d'adaptateur. Les deux ont leurs utilisations et sont décrits comme des versions différentes de la même idée dans le livre Design Patterns. Je suis d'accord que cet exemple n'est pas sans problème, mais c'est une solution valable.
ajouté l'auteur amon, source
Plus directement au problème des PO, cela n’évite pas très bien la redondance - vous devez déclarer de manière redondante les classes de proxy dérivées pour chaque A, B et les autres classes devant être unifiées.
ajouté l'auteur Jack, source
@devnull Je pense que vous pouvez omettre les propriétés sur vos objets proxy car ils utiliseront l'implémentation de base?
ajouté l'auteur Ewan, source
@ DocBrown wikipedia n'utilise-t-il pas la version d'héritage comme premier exemple du modèle d'adaptateur?
ajouté l'auteur Ewan, source
@ Jack Où est la redondance? Les deux classes n'ont pas d'interface commune. Vous créez des wrappers qui do ont une interface commune. Vous utilisez cette interface commune pour implémenter votre logique. Ce n'est pas comme si la même redondance n'existait pas dans le code C ++ - c'est juste caché derrière un peu de génération de code. Si vous pensez vraiment à des choses qui ont l’air identiques, même si elles ne sont pas identiques, vous pouvez toujours utiliser des T4 ou un autre système de gabarit.
ajouté l'auteur Luaan, source
Je pense que c'est la voie que je prendrais. Ce n'est pas parfait, mais parfois, le style d'héritage Java/C # rend les choses difficiles. Il suffit de faire le downcasting via le constructeur. classe A2: A, IPropertyHaver {public A2 (A a) {this.IntProperty = a.IntProperty; }} . De plus, vous comprenez que vous utilisez le modèle d’adaptateur, mais le nom de l’interface doit correspondre à ce qu’il est.
ajouté l'auteur Nathan Cooper, source
@DocBrown Comment cela se passerait-il dans ce cas particulier?
ajouté l'auteur Vladimir Stokic, source

Voici un aperçu de la façon d'utiliser des adaptateurs sans hériter de A et/ou B, avec la possibilité de les utiliser pour des objets A et B existants:

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

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

Je préférerais généralement ce type d’adaptateur d’objet aux mandataires de classe, ils évitent les problèmes laids que vous pouvez rencontrer avec l’héritage. Par exemple, cette solution fonctionnera même si A et B sont des classes scellées.

10
ajouté
@ pinkfloydx33: juste une faute de frappe, changé, merci.
ajouté l'auteur Peter LeFanu Lumsdaine, source
Pourquoi new int Property ? vous ne surveillez rien.
ajouté l'auteur pinkfloydx33, source

Si vous voulez vraiment faire preuve de prudence, vous pouvez utiliser «dynamique» pour que le compilateur prenne soin de toute la vilaine réflexion. Cela entraînera une erreur d'exécution si vous passez un objet à SetSomeProperty qui ne possède pas de propriété nommée 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;
        }
    }
}
8
ajouté

La version C ++ ne fonctionne que parce que ses modèles utilisent le «typage statique»: tout est compilé tant que le type fournit les noms corrects. Cela ressemble plus à un système macro. Le système générique de C# et d'autres langages fonctionne très différemment.

Les réponses de devnull et de Doc Brown montrent comment le motif d'adaptateur peut être utilisé pour conserver votre algorithme en général, tout en fonctionnant sur des types arbitraires… avec quelques restrictions. Notamment, vous créez maintenant un type différent de celui que vous souhaitez réellement.

Avec un peu de ruse, il est possible d'utiliser exactement le type prévu sans aucune modification. Cependant, nous devons maintenant extraire toutes les interactions avec le type de cible dans une interface séparée. Ici, ces interactions sont construction et attribution de propriété:

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

Dans une interprétation POO, il s'agirait d'un exemple de modèle de stratégie , bien que mélangé à des génériques.

Nous pouvons ensuite réécrire votre logique pour utiliser ces interactions:

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

Les définitions d'interaction ressembleraient à ceci:

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

Le gros inconvénient de cette approche est que le programmeur doit écrire et transmettre une instance d'interaction lorsqu'il appelle la logique. Ceci est assez similaire aux solutions basées sur un modèle d'adaptateur, mais est légèrement plus général.

D'après mon expérience, c'est ce qui se rapproche le plus des fonctions de modèle dans d'autres langues. Des techniques similaires sont utilisées dans Haskell, Scala, Go et Rust pour implémenter des interfaces en dehors d'une définition de type. Cependant, dans ces langages, le compilateur intervient et sélectionne implicitement l'instance d'interaction correcte afin de ne pas voir l'argument supplémentaire. Ceci est également similaire aux méthodes d'extension de C#, mais n'est pas limité aux méthodes statiques.

8
ajouté
Approche intéressante. Ce n'est pas celui qui serait mon premier choix, mais j'imagine que cela peut présenter certains avantages lors de l'écriture d'un cadre ou quelque chose comme ça.
ajouté l'auteur Peter LeFanu Lumsdaine, source

Les autres réponses identifient correctement le problème et proposent des solutions viables. C# ne prend pas (généralement) en charge la "saisie de canard" ("Si cela marche comme un canard. .. "), il n’ya donc aucun moyen de forcer vos ClassA et ClassB à être interchangeables s’ils ne sont pas conçus de cette façon.

Toutefois, si vous êtes déjà prêt à accepter le risque d'erreur d'exécution, la réponse est plus simple que l'utilisation de Reflection.

C # a le mot-clé dynamique qui convient parfaitement à de telles situations. Il dit au compilateur "Je ne saurai pas de quel type il s'agit avant le runtime (et peut-être même pas à ce moment-là), alors permettez-moi de faire quoi que ce soit ".

En utilisant cela, vous pouvez créer exactement la fonction que vous voulez:

public class LogicToBeApplied 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.
    }
}

Notez également l'utilisation du mot clé static . Cela vous permet d'utiliser ceci comme:

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

L’utilisation de dynamic n’entraîne aucune incidence sur les performances, de la même manière que la simple utilisation (et la complexité accrue) de l’utilisation de Reflection. La première fois que votre code touche l'appel dynamique avec un type spécifique, entraîne un léger surcoût , mais les appels répétés sont répétés. aussi vite que le code standard. Cependant, vous obtiendrez un RuntimeBinderException si vous essayez de transmettre un élément qui ne possède pas cette propriété et qu'il n'existe aucun moyen efficace de le vérifier à l'avance. Vous voudrez peut-être traiter spécifiquement cette erreur de manière utile.

4
ajouté
Souvenez-vous qu'en C ++, les modèles n'ont même pas la surcharge des méthodes virtuelles!
ajouté l'auteur user1114, source
Cela peut être lent, mais souvent le code lent n'est pas un problème.
ajouté l'auteur user1114, source
@ Ian - Bon point. J'ai ajouté un peu plus sur la performance. Ce n'est pas aussi grave qu'on pourrait le penser, à condition de réutiliser les mêmes classes aux mêmes endroits.
ajouté l'auteur Bobson, source

Vous pouvez utiliser la réflexion pour extraire la propriété par son nom.

public class logic 
{
    public object getNew() 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;
    }
}

De toute évidence, vous risquez une erreur d'exécution avec cette méthode. C’est ce que C# essaie de vous empêcher de faire.

J'ai lu quelque part qu'une future version de C# vous permettra de transmettre des objets sous forme d'interface dont ils n'hériteront pas mais qui correspondent. Ce qui résoudrait aussi votre problème.

(Je vais essayer de déterrer l'article)

Une autre méthode, même si je ne suis pas sûr que cela vous épargne du code, consisterait à sous-classer A et B et à hériter d'une interface avec IntProperty.

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

public class A2 : A, IIntProp {}

public class B2 : B, IIntProp {}
2
ajouté
La réflexion est synonyme de problèmes d'optimisation et (bien plus important encore) de difficultés à résoudre les erreurs d'exécution. Héritage et une interface commune signifient déclarer une sous-classe à l'avance pour chacune de ces classes (impossible de la créer anonymement sur place) et ne fonctionne pas si elles n'utilisent pas le même nom de propriété à chaque fois.
ajouté l'auteur Jack, source
@Ewan Une réflexion sûre peut être puissante pour supprimer la duplication de code, mais il est inutile de la requérir ici. Certaines solutions ont un passe-partout minimal sans réflexion et permettent à l'utilisateur d'activer la réflexion pour éliminer le passe-passe.
ajouté l'auteur Jack, source
vous prenez sûrement le même risque avec votre solution c ++?
ajouté l'auteur Ewan, source
@ Jack, il y a des inconvénients, mais considérez que la réflexion est largement utilisée dans les frameworks Mappers, Serializers, Injection de dépendance, etc. et que l'objectif est de le faire avec le moins de duplication de code possible.
ajouté l'auteur Ewan, source
Bien, je couvre la méthode de l’adaptateur, mais elle a toujours un passe-partout minimal. la question est spécifiquement sur la façon d'éviter cela
ajouté l'auteur Ewan, source
@Ewan no, c ++ recherche le membre au moment de la compilation
ajouté l'auteur Caleth, source
La possibilité d'erreur d'exécution et de problèmes de performances sont les raisons pour lesquelles je ne voulais pas y aller avec réflexion. Je suis cependant très intéressé par la lecture de l'article que vous avez mentionné dans votre réponse. En attente de le lire.
ajouté l'auteur Vladimir Stokic, source

Je voulais simplement utiliser les conversions de l'opérateur implicite avec l'approche délégué/lambda de la réponse de Jack. A et B sont comme supposés:

// A and B are mutable reference types

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

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

Ensuite, il est facile d’obtenir une belle syntaxe avec des conversions implicites définies par l’utilisateur (aucune méthode d’extension ou similaire n’est nécessaire):

// 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 getter;
  readonly Action setter;

  Adapter(Func getter, Action 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(), };
}

Illustration d'utilisation:

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

La méthode Initialize montre comment utiliser Adapter sans se soucier de savoir s'il s'agit d'un A ou d'un B ou autre chose. Les invocations de la méthode Initialize montrent que nous n’avons besoin ni de cast (visible) ni de .AsProxy() ou similaire pour traiter concrètement A ou B en tant que adaptateur .

Envisagez si vous souhaitez insérer une ArgumentNullException dans les conversions définies par l'utilisateur si l'argument transmis est une référence null ou non.

0
ajouté