Comment migrer ma pensée de C ++ vers C #

12

Je suis un développeur C ++ expérimenté, je connais le langage dans les moindres détails et j'ai utilisé intensivement certaines de ses fonctionnalités spécifiques. De plus, je connais les principes d'OOD et les modèles de conception. J'apprends maintenant le C # mais je ne peux pas arrêter le sentiment de ne pas pouvoir me débarrasser de l'état d'esprit C ++. Je me suis tellement attaché aux forces du C ++ que je ne peux pas vivre sans certaines fonctionnalités. Et je ne trouve pas de bonnes solutions de contournement ou de remplacement pour eux en C #.

Quelles bonnes pratiques , modèles de conception , idiomes différents en C # du point de vue C ++ pouvez-vous suggérer? Comment obtenir un design C ++ parfait qui ne semble pas stupide en C #?

Plus précisément, je ne trouve pas de bonne façon de traiter en C # (exemples récents):

  • Contrôle de la durée de vie des ressources qui nécessitent un nettoyage déterministe (comme les fichiers). C'est facile à avoir usingen main mais comment l'utiliser correctement lorsque la propriété de la ressource est transférée [... entre les threads]? En C ++, j'utilisais simplement des pointeurs partagés et je le laissais prendre en charge la «collecte des ordures» juste au bon moment.
  • Lutte constante avec des fonctions prioritaires pour des génériques spécifiques (j'aime des choses comme la spécialisation partielle de modèles en C ++). Dois-je simplement abandonner toute tentative de faire une programmation générique en C #? Peut-être que les génériques sont limités à dessein et que ce n'est pas C -ish pour les utiliser sauf pour un domaine spécifique de problèmes?
  • Fonctionnalité de type macro. Bien qu'il s'agisse généralement d'une mauvaise idée, pour certains domaines de problèmes, il n'y a pas d'autre solution de contournement (par exemple, l'évaluation conditionnelle d'une instruction, comme avec les journaux qui ne devraient aller qu'aux versions de débogage). Ne pas les avoir signifie que je dois mettre plus de passe- if (condition) {...}partout et ce n'est toujours pas égal en termes de déclenchement d'effets secondaires.
Andrew
la source
# 1 est difficile pour moi, j'aimerais connaître la réponse moi-même. # 2 - Pourriez-vous donner un exemple de code C ++ simple et expliquer pourquoi vous en avez besoin? Pour # 3 - utilisez C#pour générer un autre code. Vous pouvez lire un CSVou un XMLou ce que vous avez en entrée et générer un C#ou un SQLfichier. Cela peut être plus puissant que l'utilisation de macros fonctionnelles.
Job
En ce qui concerne les fonctionnalités de type macro, quelles choses spécifiques essayez-vous de faire? Ce que j'ai trouvé, c'est qu'il y a généralement une construction de langage C # pour gérer ce que j'aurais fait avec une macro en C ++. Consultez également les directives du préprocesseur C #: msdn.microsoft.com/en-us/library/4y6tbswk.aspx
ravibhagw
Beaucoup de choses faites avec des macros en C ++ peuvent être faites avec la réflexion en C #.
Sebastian Redl
C'est une de mes bêtes noires quand quelqu'un dit "Le bon outil pour le bon travail - choisissez une langue en fonction de ce dont vous aurez besoin." Pour les grands programmes dans des langages à usage général, il s'agit d'une déclaration inutile et inutile. Cela dit, le C ++ est meilleur que presque n'importe quel autre langage, c'est la gestion déterministe des ressources. La gestion déterministe des ressources est-elle une caractéristique principale de votre programme ou quelque chose auquel vous êtes habitué? Si ce n'est pas une fonctionnalité utilisateur importante, vous pouvez être trop concentré sur cela.
Ben

Réponses:

16

D'après mon expérience, il n'y a pas de livre pour le faire. Les forums aident avec les idiomes et les meilleures pratiques, mais à la fin, vous devrez mettre du temps. Entraînez-vous en résolvant un certain nombre de problèmes différents en C #. Regardez ce qui était difficile / maladroit et essayez de les rendre moins difficiles et moins maladroits la prochaine fois. Pour moi, ce processus a pris environ un mois avant que je ne le comprenne.

La plus grande chose sur laquelle se concentrer est le manque de gestion de la mémoire. En C ++, cela domine chaque décision de conception que vous prenez, mais en C #, c'est contre-productif. En C #, vous voulez souvent ignorer la mort ou d'autres transitions d'état de vos objets. Ce type d'association ajoute un couplage inutile.

Surtout, concentrez-vous sur l'utilisation des nouveaux outils le plus efficacement possible, ne vous battez pas contre un attachement aux anciens.

Comment obtenir un design C ++ parfait qui ne semble pas stupide en C #?

En réalisant que différents idiomes poussent vers différentes approches de conception. Vous ne pouvez pas simplement porter quelque chose de 1: 1 entre (la plupart) des langues et vous attendre à quelque chose de beau et d'idiomatique à l'autre bout.

Mais en réponse à des scénarios spécifiques:

Ressources non gérées partagées - C'était une mauvaise idée en C ++, et c'est tout autant une mauvaise idée en C #. Si vous avez un fichier, ouvrez-le, utilisez-le et fermez-le. Le partager entre des threads demande simplement des problèmes, et si un seul thread l'utilise vraiment, alors ouvrez-le / possédez / fermez-le. Si vous avez vraiment un fichier à longue durée de vie pour une raison quelconque, créez une classe pour le représenter et laissez l'application gérer cela selon les besoins (fermer avant de quitter, lier près au type d'événements de fermeture d'application, etc.)

Spécialisation partielle de modèle - Vous n'avez pas de chance ici, bien que plus j'utilise C #, plus je trouve que l'utilisation de modèle que j'ai faite en C ++ tombe dans YAGNI. Pourquoi faire quelque chose de générique quand T est toujours un entier? D'autres scénarios sont mieux résolus en C # par une application libérale des interfaces. Vous n'avez pas besoin de génériques lorsqu'un type d'interface ou de délégué peut fortement taper ce que vous voulez faire varier.

Conditionnelles du préprocesseur - C # has #if, go noix. Mieux encore, il possède des attributs conditionnels qui supprimeront simplement cette fonction du code si un symbole de compilateur n'est pas défini.

Telastyn
la source
En ce qui concerne la gestion des ressources, en C #, il est assez courant d'avoir des classes de gestionnaire (qui peuvent être statiques ou dynamiques).
rwong
Concernant les ressources partagées: celles qui sont gérées sont faciles en C # (si les conditions de race ne sont pas pertinentes), celles qui ne sont pas gérées sont beaucoup plus méchantes.
Déduplicateur
@rwong - commun, mais les classes de gestionnaire sont toujours un anti-modèle à éviter.
Telastyn
En ce qui concerne la gestion de la mémoire, j'ai trouvé que le passage au C # rendait cela plus difficile, surtout au début. Je pense que vous devez être plus conscient qu'en C ++. En C ++, vous savez exactement quand et où chaque objet est alloué, désalloué et référencé. En C #, tout cela vous est caché. Plusieurs fois en C #, vous ne réalisez même pas qu'une référence à un objet a été obtenue et que la mémoire commence à fuir. Amusez-vous à retrouver les fuites de mémoire en C #, qui sont généralement triviales à comprendre en C ++. Bien sûr, avec l'expérience, vous apprenez ces nuances de C #, donc elles ont tendance à devenir moins problématiques.
Dunk
4

Pour autant que je puisse voir, la seule chose permettant une gestion déterministe de la durée de vie est IDisposable, qui tombe en panne (comme dans: pas de support de système de langue ou de type) quand, comme vous l'avez remarqué, vous devez transférer la propriété d'une telle ressource.

Être dans le même bateau que vous - développeur C ++ faisant C # atm. - le manque de support pour modéliser le transfert de propriété (fichiers, connexions db, ...) dans les types / signatures C # a été très ennuyeux pour moi, mais je dois admettre que cela fonctionne assez bien pour "juste" s'appuyer sur les documents convention et API pour bien faire les choses.

  • Utilisez IDisposablepour effectuer un nettoyage déterministe si nécessaire.
  • Lorsque vous devez transférer la propriété d'un IDisposable, assurez-vous simplement que l'API impliquée est claire à ce sujet et ne l'utilisez pas usingdans ce cas, car elle usingne peut pas être "annulée" pour ainsi dire.

Oui, ce n'est pas aussi élégant que ce que vous pouvez exprimer dans le système de type avec C ++ moderne (déplacer la sémantique, etc.), mais c'est le travail, et (étonnamment pour moi) la communauté C # dans son ensemble ne semble pas soins trop nombreux, AFAICT.

Martin Ba
la source
2

Vous avez mentionné deux fonctionnalités C ++ spécifiques. Je vais discuter de RAII:

Il n'est généralement pas applicable, car C # est un ramasse-miettes. La construction C # la plus proche est probablement l' IDisposableinterface, utilisée comme suit:

using (FileStream fs = File.OpenRead(@"c:\path\filename.txt"))
{
   //Do stuff with the file.
} //File will be closed here.

Vous ne devriez pas vous attendre à l'implémenter IDisposablesouvent (et cela est légèrement avancé), car ce n'est pas nécessaire pour les ressources gérées. Cependant, vous devez utiliser la usingconstruction (ou Disposevous appeler , si cela usingest impossible) pour tout ce qui est implémenté IDisposable. Même si vous vous trompez, des classes C # bien conçues essaieront de gérer cela pour vous (en utilisant les finaliseurs). Cependant, dans un langage récupéré, le modèle RAII ne fonctionne pas complètement (car la collecte n'est pas déterministe), donc le nettoyage de vos ressources sera retardé (parfois de manière catastrophique).

Brian
la source
RAII est en effet mon plus gros problème. Disons que j'ai une ressource (par exemple un fichier) qui est créée par un thread et distribuée à un autre. Comment puis-je m'assurer qu'il est éliminé à un certain moment où ce thread n'en a plus besoin? Bien sûr, je pourrais appeler Dispose mais cela semble beaucoup plus laid que les pointeurs partagés en C ++. Il n'y a rien de RAII dedans.
Andrew
@Andrew Ce que vous décrivez semble impliquer un transfert de propriété, donc maintenant le deuxième thread est responsable Disposing()du fichier et devrait probablement être utilisé using.
svick
@Andrew - En général, vous ne devriez pas faire ça. Vous devez plutôt indiquer au thread comment obtenir le fichier et lui laisser la propriété de la durée de vie.
Telastyn
1
@Telastyn Cela signifie-t-il que céder la propriété d'une ressource gérée est un problème plus important en C # qu'en C ++? Assez surprenant.
Andrew
6
@Andrew: Permettez-moi de résumer: en C ++, vous obtenez une gestion uniforme et semi-automatique de toutes les ressources. En C #, vous obtenez une gestion complètement automatique de la mémoire - et une gestion complètement manuelle de tout le reste. Il n'y a pas vraiment de changement, donc votre seule vraie question n'est pas "comment résoudre ce problème?", Mais seulement "comment m'y habituer?"
Jerry Coffin
2

C'est une très bonne question, car j'ai vu un certain nombre de développeurs C ++ écrire un terrible code C #.

Il vaut mieux ne pas penser à C # comme "C ++ avec une syntaxe plus agréable". Ce sont des langues différentes qui nécessitent des approches différentes dans votre réflexion. C ++ vous oblige à penser constamment à ce que le CPU et la mémoire feront. C # n'est pas comme ça. C # est spécifiquement conçu pour que vous ne pensiez pas au processeur et à la mémoire, mais que vous pensiez plutôt au domaine commercial pour lequel vous écrivez .

Un exemple de cela que j'ai vu est que beaucoup de développeurs C ++ aimeront utiliser pour les boucles car ils sont plus rapides que foreach. C'est généralement une mauvaise idée en C # car cela restreint les types possibles de la collection sur laquelle itérer (et donc la réutilisation et la flexibilité du code).

Je pense que la meilleure façon de réajuster du C ++ au C # est d'essayer d'aborder le codage sous un angle différent. Au début, cela sera difficile, car au fil des ans, vous serez complètement habitué à utiliser le fil "que font le CPU et la mémoire" dans votre cerveau pour filtrer le code que vous écrivez. Mais en C #, vous devriez plutôt penser aux relations entre les objets dans le domaine métier. "Qu'est-ce que je veux faire" par opposition à "que fait l'ordinateur".

Si vous voulez faire quelque chose pour tout dans une liste d'objets, au lieu d'écrire une boucle for sur la liste, créez une méthode qui prend an IEnumerable<MyObject>et utilisez une foreachboucle.

Vos exemples spécifiques:

Contrôle de la durée de vie des ressources qui nécessitent un nettoyage déterministe (comme les fichiers). C'est facile à avoir en main mais comment l'utiliser correctement lorsque la propriété de la ressource est transférée [... entre les threads]? En C ++, j'utiliserais simplement des pointeurs partagés et le laisserais prendre en charge la «collecte des ordures» juste au bon moment.

Vous ne devriez jamais faire cela en C # (en particulier entre les threads). Si vous devez faire quelque chose dans un fichier, faites-le au même endroit à la fois. Il est correct (et même une bonne pratique) d'écrire une classe wrapper qui gère la ressource non managée qui est transmise entre vos classes, mais n'essayez pas de passer un fichier entre les threads et demandez à des classes distinctes d'écrire / lire / fermer / ouvrir. Ne partagez pas la propriété des ressources non gérées. Utilisez le Disposemodèle pour gérer le nettoyage.

Lutte constante avec des fonctions prioritaires pour des génériques spécifiques (j'aime des choses comme la spécialisation partielle de modèles en C ++). Dois-je simplement abandonner toute tentative de faire une programmation générique en C #? Peut-être que les génériques sont limités à dessein et que ce n'est pas C -ish pour les utiliser sauf pour un domaine spécifique de problèmes?

Les génériques en C # sont conçus pour être génériques. Les spécialisations des génériques devraient être gérées par des classes dérivées. Pourquoi un List<T>comportement différent devrait-il se produireList<int> ou d'un List<string>? Toutes les opérations sur List<T>sont génériques pour s'appliquer à tout List<T> . Si vous souhaitez modifier le comportement de la .Addméthode sur a List<string>, créez une classe dérivée MySpecializedStringCollection : List<string>ou une classe composée MySpecializedStringCollection : IList<string>qui utilise le générique en interne mais fait les choses de manière différente. Cela vous aide à éviter de violer le principe de substitution de Liskov et de visser royalement les autres qui utilisent votre classe.

Fonctionnalité de type macro. Bien qu'il s'agisse généralement d'une mauvaise idée, pour certains domaines de problèmes, il n'y a pas d'autre solution de contournement (par exemple, l'évaluation conditionnelle d'une instruction, comme avec les journaux qui ne devraient aller qu'aux versions de débogage). Ne pas les avoir signifie que je dois en mettre plus si (condition) {...} passe-partout et ce n'est toujours pas égal en termes de déclenchement d'effets secondaires.

Comme d'autres l'ont dit, vous pouvez utiliser des commandes de préprocesseur pour ce faire. Les attributs sont encore meilleurs. En général, les attributs sont le meilleur moyen de gérer des choses qui ne sont pas des fonctionnalités "essentielles" pour une classe.

En bref, lors de l'écriture de C #, gardez à l'esprit que vous devez penser entièrement au domaine de l'entreprise et non au processeur et à la mémoire. Vous pouvez optimiser plus tard si nécessaire , mais votre code doit refléter les relations entre les principes commerciaux que vous essayez de mapper.

Stephen
la source
3
WRT. transfert de ressources: "vous ne devriez jamais faire cela" ne coupe pas à mon humble avis. Souvent, une très bonne conception suggère le transfert d'une IDisposable( pas la ressource brute) d'une classe à une autre: il suffit de voir l' API StreamWriter où cela a un sens parfait pour Dispose()le flux passé. Et il y a le trou dans le langage C # - vous ne pouvez pas l'exprimer dans le système de type.
Martin Ba
2
"Un exemple de cela que j'ai vu est que beaucoup de développeurs C ++ aimeront utiliser pour les boucles car ils sont plus rapides que foreach." C'est aussi une mauvaise idée en C ++. L'abstraction à coût nul est la grande puissance du C ++, et foreach ou d'autres algorithmes ne devraient pas être plus lents qu'une boucle for écrite à la main dans la plupart des cas.
Sebastian Redl
1

La chose la plus différente pour moi était la tendance à tout renouveler . Si vous voulez un objet, oubliez d'essayer de le passer à une routine et de partager les données sous-jacentes, il semble que le modèle C # soit juste pour créer vous-même un nouvel objet. Cela, en général, semble être beaucoup plus de la manière C #. Au début, ce modèle semblait très inefficace, mais je suppose que la création de la plupart des objets en C # est une opération rapide.

Cependant, il y a aussi les classes statiques qui m'ennuient vraiment car de temps en temps vous essayez d'en créer une pour découvrir que vous n'avez qu'à l'utiliser. Je trouve que ce n'est pas si facile de savoir lequel utiliser.

Je suis assez heureux dans un programme C # de simplement l'ignorer quand je n'en ai plus besoin, mais rappelez-vous juste ce que cet objet contient - en général, vous n'avez qu'à penser s'il contient des données `` inhabituelles '', les objets qui ne sont constitués que de mémoire le feront partez tout seuls, les autres devront être éliminés ou vous devrez voir comment les détruire - manuellement ou à l'aide d'un bloc d'utilisation sinon.

vous le récupérerez très rapidement, C # n'est guère différent de VB, vous pouvez donc le traiter comme le même outil RAD qu'il est (si cela aide à penser à C # comme VB.NET, faites-le, la syntaxe n'est que légèrement différent, et mes vieux jours d'utilisation de VB m'ont mis dans la bonne mentalité pour le développement RAD). La seule difficulté est de déterminer quelle partie des énormes bibliothèques .net vous devez utiliser. Google est certainement votre ami ici!

gbjbaanb
la source
1
Passer un objet à une routine pour partager des données sous-jacentes est parfaitement bien en C #, au moins du point de vue de la syntaxe.
Brian