Supposons que j'ai deux classes qui 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 ou hériter de la même classe qui contiendrait alors IntProperty.
Je veux appliquer une logique sur la IntProperty
propriété des deux classes, et en C ++, je pourrais utiliser une classe de modèle pour le faire assez facilement:
template <class T>
class LogicToBeApplied
{
public:
void T CreateElement();
};
template <class T>
T LogicToBeApplied<T>::CreateElement()
{
T retVal;
retVal.IntProperty = 50;
return retVal;
}
Et puis je pourrais faire quelque chose comme ça:
LogicToBeApplied<ClassA> classALogic;
LogicToBeApplied<ClassB> classBLogic;
ClassA classAElement = classALogic.CreateElement();
ClassB classBElement = classBLogic.CreateElement();
De cette façon, 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 where
clauses différentes même si le code de la logique est exactement le même:
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;
}
}
Je sais que si je veux avoir différentes classes dans la where
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 que j'ai décrit ci-dessus. C'est juste qu'il est très ennuyeux d'avoir deux méthodes complètement identiques. Je ne veux pas non plus utiliser la réflexion à cause des problèmes de performances.
Quelqu'un peut-il suggérer une approche où cela peut être écrit d'une manière plus élégante?
Réponses:
Ajoutez une interface proxy (parfois appelée adaptateur , parfois avec des différences subtiles), implémentez-la
LogicToBeApplied
en termes de proxy, puis ajoutez un moyen de construire une instance de ce proxy à partir de deux lambdas: une pour la propriété get et une pour l'ensemble.Maintenant, chaque fois que vous devez passer un IProxy mais avoir une instance des classes tierces, vous pouvez simplement passer quelques lambdas:
De plus, vous pouvez écrire des aides simples pour construire des instances LamdaProxy à partir d'instances A ou B. Il peut même s'agir de méthodes d'extension pour vous donner un style "fluide":
Et maintenant, la construction de procurations ressemble à ceci:
Quant à votre usine, je verrais si vous pouvez la refactoriser en une méthode d'usine "principale" qui accepte un IProxy et exécute toute la logique dessus et d'autres méthodes qui passent simplement
new A().Proxied()
ounew B().Proxied()
:Il n'y a aucun moyen de faire l'équivalent de votre code C ++ en C # car les modèles C ++ reposent sur un typage structurel . Tant que deux classes ont le même nom et la même signature de méthode, en C ++, vous pouvez appeler cette méthode de manière générique sur les deux. C # a un typage nominal - le nom d'une classe ou d'une interface fait partie de son type. Par conséquent, les classes
A
etB
ne peuvent pas être traitées de la même manière à quelque titre que ce soit, sauf si une relation explicite "est une" est définie par héritage ou implémentation d'interface.Si la mise en œuvre de ces boilerplate méthodes par classe est trop, vous pouvez écrire une fonction qui prend un objet et pensivement construit un
LambdaProxy
en recherchant un nom de propriété spécifique:Cela échoue lamentablement quand on donne des objets de type incorrect; la réflexion introduit par nature la possibilité de défaillances que le système de type C # ne peut pas empêcher. Heureusement, vous pouvez éviter la réflexion jusqu'à ce que la charge de maintenance des assistants devienne trop importante, car vous n'êtes pas obligé de modifier l'interface IProxy ou la mise en œuvre de LambdaProxy pour ajouter le sucre réfléchissant.
Une partie de la raison pour laquelle cela fonctionne est qu'il
LambdaProxy
est "au maximum générique"; il peut adapter n'importe quelle valeur qui implémente "l'esprit" du contrat IProxy car l'implémentation de LambdaProxy est complètement définie par les fonctions getter et setter données. Cela fonctionne même si les classes ont des noms différents pour la propriété, ou différents types qui sont sensiblement et en toute sécurité représentables en tant queint
s, ou s'il existe un moyen de mapper le concept quiProperty
est censé représenter à d'autres fonctionnalités de la classe. L'indirection fournie par les fonctions vous offre une flexibilité maximale.la source
ReflectiveProxier
, pourriez-vous créer un proxy en utilisant ledynamic
mot-clé? 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 maintenabilité seraient beaucoup plus simples.Voici un aperçu de l'utilisation des adaptateurs sans hériter de A et / ou B, avec la possibilité de les utiliser pour les objets A et B existants:
Je préfère généralement ce type d'adaptateur d'objet aux proxys 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.
la source
new int Property
? vous n'observez rien.Vous pourriez vous adapter
ClassA
etClassB
via une interface commune. De cette façon, votre codeLogicAToBeApplied
reste le même. Pas très différent de ce que vous avez cependant.la source
A
,B
types à une interface commune. Le gros avantage est que nous n'avons pas à reproduire la logique commune. L'inconvénient est que la logique instancie désormais le wrapper / proxy au lieu du type réel.LogicToBeApplied
a une certaine complexité et ne doit en aucun cas être répété à deux endroits dans la base de code. Ensuite, le code passe-partout supplémentaire est souvent négligeable.La version C ++ ne fonctionne que parce que ses modèles utilisent le «typage statique du canard» - 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 modèle d'adaptateur peut être utilisé pour garder votre algorithme général et continuer à fonctionner sur des types arbitraires… avec quelques restrictions. En particulier, 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 cible dans une interface distincte. Ici, ces interactions sont la construction et l'attribution de propriété:
Dans une interprétation de POO, ce serait un exemple du modèle de stratégie , bien que mélangé avec des génériques.
Nous pouvons ensuite réécrire votre logique pour utiliser ces interactions:
Les définitions d'interaction devraient ressembler à:
Le gros inconvénient de cette approche est que le programmeur doit écrire et transmettre une instance d'interaction lors de l'appel de 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 le plus proche que vous pouvez obtenir 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 langues, le compilateur intervient et sélectionne implicitement l'instance d'interaction correcte afin que vous ne voyiez pas réellement l'argument supplémentaire. Ceci est également similaire aux méthodes d'extension de C #, mais n'est pas limité aux méthodes statiques.
la source
Si vous voulez vraiment faire attention au vent, vous pouvez utiliser "dynamique" pour que le compilateur s'occupe de toutes les méchancetés de réflexion pour vous. Cela entraînera une erreur d'exécution si vous passez un objet à SetSomeProperty qui n'a pas de propriété nommée SomeProperty.
la source
Les autres réponses identifient correctement le problème et fournissent des solutions viables. C # ne prend pas (généralement) en charge le "typage du canard" ("S'il marche comme un canard ..."), il n'y a donc aucun moyen de forcer votre
ClassA
etClassB
d'être interchangeable s'il n'était pas conçu de cette façon.Cependant, si vous êtes déjà prêt à accepter le risque d'une erreur d'exécution, il y a une réponse plus simple que d'utiliser Reflection.
C # a le
dynamic
mot - clé qui est parfait pour des situations comme celle-ci. Il indique au compilateur "Je ne saurai pas de quel type il s'agit avant l'exécution (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 souhaitez:
Notez également l'utilisation du
static
mot - clé. Cela vous permet de l'utiliser comme:L'
dynamic
utilisation de la réflexion n'a aucune incidence sur les performances globales , la façon dont il y a le coup unique (et la complexité supplémentaire) de l'utilisation de la réflexion. La première fois que votre code frappera l'appel dynamique avec un type spécifique, il y aura une petite surcharge , mais les appels répétés seront tout aussi rapides que le code standard. Cependant, vous aurez obtenir unRuntimeBinderException
si vous essayez de passer quelque chose qui n'a pas cette propriété, et il n'y a pas de bonne façon de vérifier que l' avance. Vous voudrez peut-être traiter spécifiquement cette erreur de manière utile.la source
Vous pouvez utiliser la réflexion pour extraire la propriété par son nom.
Évidemment, 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 passer des objets en tant qu'interface qu'ils n'héritent pas mais correspondent. Ce qui résoudrait également votre problème.
(Je vais essayer de déterrer l'article)
Une autre méthode, bien que je ne sois pas sûr qu'elle vous enregistre aucun code, serait de sous-classer A et B et d'hériter également d'une interface avec IntProperty.
la source
Je voulais juste utiliser des
implicit operator
conversions avec l'approche déléguée / lambda de la réponse de Jack.A
etB
sont tels que supposés:Ensuite, il est facile d'obtenir une syntaxe agréable avec des conversions implicites définies par l'utilisateur (aucune méthode d'extension ou similaire n'est nécessaire):
Illustration d'utilisation:
La
Initialize
méthode montre comment vous pouvez travailler avecAdapter
sans se soucier de savoir si c'est unA
ou unB
ou quelque chose d'autre. Les invocations de laInitialize
méthode montrent que nous n'avons pas besoin de fonte (visible).AsProxy()
ou similaire pour traiter le bétonA
ouB
comme unAdapter
.Demandez-vous si vous souhaitez lancer un
ArgumentNullException
dans les conversions définies par l'utilisateur si l'argument passé est une référence nulle ou non.la source