So migrieren Sie mein Denken von C ++ nach C #

12

Ich bin ein erfahrener C ++ - Entwickler, kenne die Sprache sehr genau und habe einige ihrer Besonderheiten intensiv genutzt. Außerdem kenne ich Prinzipien von OOD und Entwurfsmustern. Ich lerne jetzt C #, aber ich kann das Gefühl nicht aufhalten, die C ++ - Denkweise nicht loswerden zu können. Ich habe mich so sehr an die Stärken von C ++ gebunden, dass ich ohne einige der Features nicht leben kann. Und ich kann keine guten Workarounds oder Ersetzungen für sie in C # finden.

Welche bewährten Methoden , Entwurfsmuster und Redewendungen , die sich in C # von der C ++ - Perspektive unterscheiden, können Sie vorschlagen? Wie erhält man ein perfektes C ++ - Design, das in C # nicht dumm aussieht?

Insbesondere kann ich keinen guten c # -ischen Weg finden, um damit umzugehen (aktuelle Beispiele):

  • Steuern der Lebensdauer von Ressourcen, die eine deterministische Bereinigung erfordern (z. B. Dateien). Dies ist einfach usingin der Hand zu haben, aber wie man es richtig benutzt, wenn das Eigentum an der Ressource übertragen wird [... zwischen Threads]? In C ++ verwende ich einfach gemeinsame Zeiger und lasse es zur richtigen Zeit für die 'Garbage Collection' sorgen.
  • Ständiges Kämpfen mit übergeordneten Funktionen für bestimmte Generika (ich mag Dinge wie teilweise Template-Spezialisierung in C ++). Sollte ich irgendwelche Versuche, eine generische Programmierung in C # durchzuführen, einfach abbrechen? Vielleicht sind Generika absichtlich eingeschränkt und es ist nicht C # -isch, sie zu verwenden, außer für einen bestimmten Bereich von Problemen?
  • Makrofunktion. Während dies im Allgemeinen eine schlechte Idee ist, gibt es für einige Problembereiche keine andere Problemumgehung (z. B. bedingte Auswertung einer Anweisung, wie bei Protokollen, die nur für Debug-Releases verwendet werden sollen). Wenn ich sie nicht habe, muss ich mehr if (condition) {...}Heizplatte einsetzen, und es ist immer noch nicht gleich, was die Auslösung von Nebenwirkungen angeht.
Andrew
quelle
# 1 ist eine schwierige Frage für mich, würde die Antwort gerne selbst erfahren. # 2 - Können Sie ein einfaches C ++ - Codebeispiel geben und erklären, warum Sie das brauchen? Für # 3 - C#zum Generieren von anderem Code verwenden. Sie können eine CSVoder eine XMLoder welche Datei als Eingabe lesen und eine C#oder eine SQLDatei generieren . Dies kann leistungsfähiger sein als die Verwendung von Funktionsmakros.
Job
Welche konkreten Dinge versuchen Sie beim Thema makrofunktionale Funktionen zu tun? Was ich gefunden habe, ist, dass es normalerweise ein C # -Sprachenkonstrukt gibt, um das zu handhaben, was ich mit einem Makro in C ++ gemacht hätte. Lesen
ravibhagw
Viele Dinge, die mit Makros in C ++ gemacht werden, können mit Reflektion in C # gemacht werden.
Sebastian Redl
Es ist eine meiner Lieblingsbeschwerden, wenn jemand sagt: "Das richtige Werkzeug für den richtigen Job - wählen Sie eine Sprache, die auf Ihren Bedürfnissen basiert." Für große Programme in Mehrzwecksprachen ist dies eine nutzlose, nicht hilfreiche Aussage. Allerdings ist das, was C ++ besser kann als fast jede andere Sprache, deterministisches Ressourcenmanagement. Ist deterministisches Ressourcenmanagement ein Hauptmerkmal Ihres Programms oder etwas, an das Sie gewöhnt sind? Wenn es sich nicht um eine wichtige Benutzerfunktion handelt, sind Sie möglicherweise zu sehr darauf fokussiert.
Ben

Antworten:

16

Meiner Erfahrung nach gibt es dafür kein Buch. Foren helfen mit Redewendungen und Best Practices, aber am Ende müssen Sie die Zeit investieren. Üben Sie, indem Sie eine Reihe verschiedener Probleme in C # lösen. Schauen Sie sich an, was schwierig / umständlich war, und versuchen Sie, sie beim nächsten Mal weniger schwierig und weniger umständlich zu machen. Für mich hat dieser Prozess ungefähr einen Monat gedauert, bis ich ihn "verstanden" habe.

Das Wichtigste, worauf Sie sich konzentrieren sollten, ist der Mangel an Speicherverwaltung. In C ++ entscheidet dieser Lord über jede einzelne Entwurfsentscheidung, in C # ist dies jedoch kontraproduktiv. In C # möchten Sie häufig den Tod oder andere Statusübergänge Ihrer Objekte ignorieren . Diese Art von Assoziation fügt eine unnötige Kopplung hinzu.

Konzentrieren Sie sich in der Regel darauf, die neuen Tools am effektivsten zu nutzen, ohne sich über die Anhaftung an die alten Gedanken zu machen.

Wie erhält man ein perfektes C ++ - Design, das in C # nicht dumm aussieht?

Indem wir erkennen, dass verschiedene Redewendungen zu unterschiedlichen Designansätzen führen. Man kann nicht einfach 1: 1 zwischen (den meisten) Sprachen portieren und am anderen Ende etwas Schönes und Idiomatisches erwarten.

Aber als Reaktion auf bestimmte Szenarien:

Freigegebene nicht verwaltete Ressourcen - Dies war in C ++ eine schlechte Idee und in C # ebenfalls eine schlechte Idee. Wenn Sie eine Datei haben, öffnen Sie sie, verwenden Sie sie und schließen Sie sie. Das Teilen zwischen Threads ist nur ein Problem, und wenn nur ein Thread es wirklich verwendet, muss dieser Thread es öffnen / besitzen / schließen. Wenn Sie aus irgendeinem Grund wirklich eine langlebige Datei haben, erstellen Sie eine Klasse, die diese repräsentiert, und lassen Sie die Anwendung diese nach Bedarf verwalten (schließen Sie sie vor dem Beenden, schließen Sie sie mit Ereignissen zum Schließen der Anwendung usw.).

Teilweise Template-Spezialisierung - Sie haben hier Pech, aber je mehr ich C # verwendet habe, desto mehr finde ich die Template-Verwendung, die ich in C ++ gemacht habe, um in YAGNI zu fallen. Warum etwas generisches machen, wenn T immer ein int ist? Andere Szenarien lassen sich in C # am besten durch eine liberale Anwendung von Schnittstellen lösen. Sie benötigen keine Generika, wenn ein Schnittstellen- oder Delegattyp genau das eingeben kann, was Sie variieren möchten.

Preprozessor-Bedingungen - C # ist #ifverrückt geworden. Besser noch, es hat bedingte Attribute , die diese Funktion einfach aus dem Code entfernen, wenn kein Compiler-Symbol definiert ist.

Telastyn
quelle
In Bezug auf die Ressourcenverwaltung sind in C # häufig Manager-Klassen vorhanden (die statisch oder dynamisch sein können).
Rwong
In Bezug auf gemeinsam genutzte Ressourcen: Die verwalteten sind in C # einfach (wenn die Rennbedingungen irrelevant sind), die nicht verwalteten sind weitaus unangenehmer.
Deduplizierer
@rwong - gebräuchlich, aber Manager-Klassen sind immer noch ein zu vermeidendes Anti-Pattern.
Telastyn
In Bezug auf die Speicherverwaltung stellte ich fest, dass der Wechsel zu C # dies insbesondere zu Beginn erschwerte. Ich denke, man muss sich mehr bewusst sein als in C ++. In C ++ wissen Sie genau, wann und wo jedes Objekt zugewiesen, freigegeben und referenziert wird. In C # ist dies alles vor Ihnen verborgen. Oft merkt man in C # nicht einmal, dass ein Verweis auf ein Objekt vorhanden ist und der Speicher beginnt zu lecken. Viel Spaß beim Aufspüren von Speicherverlusten in C #, die in C ++ in der Regel trivial sind. Natürlich lernt man mit der Erfahrung diese Nuancen von C #, so dass sie weniger ein Problem darstellen.
Dunk
4

Soweit ich sehen kann, ist das einzige, was ein deterministisches Lebensdauermanagement ermöglicht IDisposable, das zusammenbricht (wie in: keine Sprach- oder Typsystemunterstützung), wenn Sie, wie Sie bemerkt haben, den Besitz einer solchen Ressource übertragen müssen.

Im selben Boot wie Sie - C ++ - Entwickler, der C # atm ausführt. - Die mangelnde Unterstützung für das Modellieren der Eigentumsübertragung (Dateien, DB-Verbindungen, ...) in den C # -Typen / Signaturen war für mich sehr ärgerlich, aber ich muss zugeben, dass es in Ordnung ist, sich auf "nur" zu verlassen auf Konvention und API-Dokumente, um dies richtig zu machen.

  • Verwenden Sie IDisposablediese Option, um bei Bedarf eine deterministische Bereinigung durchzuführen .
  • Wenn Sie das Eigentum an einem übertragen müssen IDisposable, stellen Sie einfach sicher, dass die betreffende API diesbezüglich klar ist, und verwenden Sie sie usingin diesem Fall nicht, da usingsie sozusagen nicht "storniert" werden kann.

Ja, es ist nicht so elegant wie das, was Sie im Typensystem mit modernem C ++ ausdrücken können (Verschiebungssematik usw.), aber es ist die Aufgabe, und (erstaunlicherweise für mich) die C # -Community als Ganzes scheint es nicht zu sein Pflege übermäßig, AFAICT.

Martin Ba
quelle
2

Sie haben zwei spezifische C ++ - Funktionen erwähnt. Ich werde RAII diskutieren:

Es ist in der Regel nicht anwendbar, da C # Müll gesammelt wird. Die nächstgelegene C # -Konstruktion ist wahrscheinlich die IDisposableSchnittstelle, die wie folgt verwendet wird:

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

Sie sollten nicht mit einer häufigen Implementierung rechnen IDisposable(und dies ist etwas fortgeschritten), da dies für verwaltete Ressourcen nicht erforderlich ist. Sie sollten jedoch das usingKonstrukt verwenden (oder sich Disposeselbst aufrufen , wenn dies nicht möglich usingist), um etwas zu implementieren IDisposable. Selbst wenn Sie dies vermasseln, versuchen gut gestaltete C # -Klassen, dies für Sie zu handhaben (mithilfe von Finalisierern). In einer Garbage-Collected-Sprache funktioniert das RAII-Muster jedoch nicht vollständig (da die Erfassung nicht deterministisch ist), sodass Ihre Ressourcenbereinigung verzögert wird (manchmal katastrophal).

Brian
quelle
RAII ist in der Tat mein größtes Problem. Angenommen, ich habe eine Ressource (z. B. eine Datei), die von einem Thread erstellt und an einen anderen weitergegeben wird. Wie kann ich sicherstellen, dass es zu einem bestimmten Zeitpunkt entsorgt wird, wenn dieser Thread es nicht mehr benötigt? Natürlich könnte ich Dispose aufrufen, aber dann sieht es viel hässlicher aus als freigegebene Zeiger in C ++. Da ist nichts von RAII drin.
Andrew
@Andrew Was du beschreibst, scheint eine Übertragung des Eigentums zu bedeuten, also ist jetzt der zweite Thread für Disposing()die Datei verantwortlich und sollte wahrscheinlich verwenden using.
Svick
@ Andrew - Im Allgemeinen sollten Sie das nicht tun. Sie sollten dem Thread stattdessen mitteilen, wie die Datei abgerufen werden soll, und den Besitz der Lebensdauer zulassen.
Telastyn
1
@Telastyn Bedeutet dies, dass das Verschenken des Eigentums an einer verwalteten Ressource in C # ein größeres Problem darstellt als in C ++? Sehr überraschend.
Andrew
6
@ Andrew: Lassen Sie mich zusammenfassen: In C ++ erhalten Sie eine einheitliche, halbautomatische Verwaltung aller Ressourcen. In C # erhalten Sie eine vollständig automatische Speicherverwaltung - und eine vollständig manuelle Verwaltung aller anderen Elemente. Es gibt keine wirkliche Änderung, daher lautet Ihre einzige wirkliche Frage nicht "Wie behebe ich das?", Sondern nur "Wie gewöhne ich mich daran?".
Jerry Coffin
2

Dies ist eine sehr gute Frage, da ich eine Reihe von C ++ - Entwicklern gesehen habe, die schrecklichen C # -Code geschrieben haben.

Es ist am besten, sich C # nicht als "C ++ mit besserer Syntax" vorzustellen. Es sind verschiedene Sprachen, die unterschiedliche Denkansätze erfordern. C ++ zwingt Sie, ständig darüber nachzudenken, was die CPU und der Arbeitsspeicher tun werden. C # ist nicht so. C # ist speziell so konzipiert, dass Sie nicht an die CPU und den Arbeitsspeicher denken, sondern an die Geschäftsdomäne, für die Sie schreiben .

Ein Beispiel dafür, das ich gesehen habe, ist, dass viele C ++ - Entwickler gern for-Schleifen verwenden, weil sie schneller sind als foreach. Dies ist in C # normalerweise eine schlechte Idee, da dadurch die möglichen Typen der Auflistung eingeschränkt werden, über die iteriert wird (und daher die Wiederverwendbarkeit und Flexibilität des Codes).

Ich denke, dass der beste Weg, um von C ++ auf C # umzustellen, darin besteht, zu versuchen, das Codieren aus einer anderen Perspektive zu betrachten. Zunächst wird dies schwierig, da Sie im Laufe der Jahre völlig daran gewöhnt sein werden, den von Ihnen geschriebenen Code mithilfe des Threads "Was macht CPU und Speicher?" Im Gehirn zu filtern. In C # sollten Sie stattdessen über die Beziehungen zwischen den Objekten in der Geschäftsdomäne nachdenken. "Was möchte ich tun" im Gegensatz zu "Was macht der Computer?"

Wenn Sie mit einer Liste von Objekten etwas anfangen möchten, anstatt eine for-Schleife in die Liste zu schreiben, erstellen Sie eine Methode, die IEnumerable<MyObject>eine foreachSchleife verwendet.

Ihre konkreten Beispiele:

Steuern der Lebensdauer von Ressourcen, die eine deterministische Bereinigung erfordern (z. B. Dateien). Dies ist einfach in der Hand zu haben, aber wie man es richtig benutzt, wenn der Besitz der Ressource übertragen wird [... zwischen Threads]? In C ++ verwende ich einfach gemeinsame Zeiger und lasse es zur richtigen Zeit für die 'Garbage Collection' sorgen.

Sie sollten dies niemals in C # tun (insbesondere zwischen Threads). Wenn Sie etwas mit einer Datei tun müssen, tun Sie dies jeweils an einem Ort. Es ist in Ordnung (und in der Tat eine gute Praxis), eine Wrapper-Klasse zu schreiben, die die nicht verwalteten Ressourcen verwaltet, die zwischen Ihren Klassen übertragen werden. Versuchen Sie jedoch nicht, eine Datei zwischen Threads zu übertragen, und lassen Sie separate Klassen schreiben / lesen / schließen / öffnen. Teilen Sie nicht das Eigentum an nicht verwalteten Ressourcen. Verwenden Sie das DisposeMuster, um mit der Bereinigung umzugehen.

Ständiges Kämpfen mit übergeordneten Funktionen für bestimmte Generika (ich mag Dinge wie teilweise Template-Spezialisierung in C ++). Sollte ich irgendwelche Versuche, eine generische Programmierung in C # durchzuführen, einfach abbrechen? Vielleicht sind Generika absichtlich eingeschränkt und es ist nicht C # -isch, sie zu verwenden, außer für einen bestimmten Bereich von Problemen?

Generika in C # sind so konzipiert, dass sie generisch sind. Spezialisierungen von Generika sollten von abgeleiteten Klassen behandelt werden. Warum sollte sich ein List<T>Mensch anders verhalten, wenn es ein List<int>oder ein ist List<string>? Alle Operationen für List<T>sind generisch und gelten für alle List<T> . Wenn Sie das Verhalten der .AddMethode auf a ändern möchten List<string>, erstellen Sie eine abgeleitete Klasse MySpecializedStringCollection : List<string>oder eine zusammengesetzte Klasse, MySpecializedStringCollection : IList<string>die den generischen Wert intern verwendet, die Dinge jedoch auf andere Weise ausführt. Dies hilft Ihnen zu vermeiden, dass Sie das Liskov-Substitutionsprinzip verletzen und andere, die Ihre Klasse nutzen, auf königliche Weise verarschen.

Makrofunktion. Während dies im Allgemeinen eine schlechte Idee ist, gibt es für einige Problembereiche keine andere Problemumgehung (z. B. bedingte Auswertung einer Anweisung, wie bei Protokollen, die nur für Debug-Releases verwendet werden sollen). Wenn ich sie nicht habe, muss ich mehr if (condition) {...} Boilerplate einsetzen, und es ist immer noch nicht gleich, was die Auslösung von Nebenwirkungen angeht.

Wie bereits erwähnt, können Sie dazu Präprozessorbefehle verwenden. Attribute sind noch besser. Im Allgemeinen sind Attribute der beste Weg, um mit Dingen umzugehen, die keine "Kern" -Funktionalität für eine Klasse sind.

Kurz gesagt, denken Sie beim Schreiben von C # daran, dass Sie sich ausschließlich mit der Geschäftsdomäne befassen sollten und nicht mit der CPU und dem Arbeitsspeicher. Sie können später bei Bedarf optimieren , aber Ihr Code sollte die Beziehungen zwischen den Geschäftsprinzipien widerspiegeln, die Sie abbilden möchten.

Stephen
quelle
3
WRT. Ressourcentransfer: "Das solltest du nie tun" schneidet es IMHO nicht. Oft schlägt ein sehr gutes Design die Übertragung einer IDisposable( nicht der unformatierten ) Ressource von einer Klasse in eine andere vor: Sehen Sie sich einfach die StreamWriter- API an, wo dies Dispose()für den übergebenen Stream Sinn macht . Und da liegt die Lücke in der C # -Sprache - das kann man im Typensystem nicht ausdrücken.
Martin Ba
2
"Ein Beispiel dafür, das ich gesehen habe, ist, dass viele C ++ - Entwickler gern for-Schleifen verwenden, weil sie schneller sind als foreach." Dies ist auch in C ++ eine schlechte Idee. Null-Kosten-Abstraktion ist die große Stärke von C ++, und foreach oder andere Algorithmen sollten in den meisten Fällen nicht langsamer sein als eine handgeschriebene for-Schleife.
Sebastian Redl
1

Was für mich am meisten anders war, war die Neigung zu Neuem allem . Wenn Sie ein Objekt möchten, vergessen Sie, es an eine Routine weiterzugeben und die zugrunde liegenden Daten gemeinsam zu nutzen. Das C # -Muster scheint nur dazu da zu sein, sich selbst ein neues Objekt zu erstellen. Dies scheint im Allgemeinen viel mehr der C # -Methode zu entsprechen. Anfangs schien dieses Muster sehr ineffizient zu sein, aber ich denke, dass das Erstellen der meisten Objekte in C # eine schnelle Operation ist.

Es gibt jedoch auch die statischen Klassen, die mich wirklich nerven, da Sie immer wieder versuchen, eine zu erstellen, nur um festzustellen, dass Sie sie nur verwenden müssen. Ich finde es nicht so einfach zu wissen, was zu verwenden ist.

Ich bin in einem C # -Programm sehr froh, es einfach zu ignorieren, wenn ich es nicht mehr brauche, aber erinnere mich einfach daran, was dieses Objekt enthält - im Allgemeinen muss man nur denken, wenn es einige "ungewöhnliche" Daten enthält, Objekte, die nur aus Speicher bestehen Geht einfach von alleine weg, andere müssen entsorgt werden oder ihr müsst schauen, wie man sie zerstört - manuell oder durch einen benutzenden Block, wenn nicht.

Sie werden es jedoch sehr schnell verstehen, C # unterscheidet sich kaum von VB, so dass Sie es als dasselbe RAD-Tool behandeln können (wenn es Ihnen hilft, C # wie VB.NET zu verstehen, dann ist die Syntax nur geringfügig anders, und meine alten Tage mit VB haben mich in die richtige Einstellung für die RAD-Entwicklung gebracht). Das einzig schwierige ist, zu bestimmen, welches Bit der riesigen .net-Bibliotheken Sie verwenden sollten. Google ist auf jeden Fall dein Freund hier!

gbjbaanb
quelle
1
Das Übergeben eines Objekts an eine Routine zum Freigeben zugrunde liegender Daten ist in C # zumindest aus syntaktischer Sicht in Ordnung.
Brian