Come migrare il mio pensiero da C ++ a C #


12

Sono uno sviluppatore C ++ esperto, conosco il linguaggio in modo molto dettagliato e ho usato intensamente alcune delle sue caratteristiche specifiche. Inoltre, conosco i principi di OOD e modelli di progettazione. Ora sto imparando C # ma non riesco a smettere di non riuscire a liberarmi della mentalità C ++. Mi sono legato così duramente ai punti di forza del C ++ che non posso vivere senza alcune delle funzionalità. E non riesco a trovare soluzioni alternative valide o sostituzioni per loro in C #.

Quali buone pratiche , modelli di progettazione , modi di dire che sono diverse in C # dal punto di vista C ++ si può suggerire? Come ottenere un design C ++ perfetto non sembrare stupido in C #?

In particolare, non riesco a trovare un buon modo C # -ish per affrontare (esempi recenti):

  • Controllo della durata delle risorse che richiedono una pulizia deterministica (come i file). È facile avere usinga portata di mano ma come usarlo correttamente quando viene trasferita la proprietà della risorsa [... tra i thread]? In C ++ userei semplicemente i puntatori condivisi e lascerei che si occupasse della "garbage collection" al momento giusto.
  • Difficoltà costante con funzioni prioritarie per generici specifici (amo le cose come la specializzazione parziale dei template in C ++). Dovrei semplicemente abbandonare qualsiasi tentativo di fare una programmazione generica in C #? Forse i generici sono limitati di proposito e non è C # -ish usarli se non per uno specifico dominio di problemi?
  • Funzionalità macro-simile. Sebbene generalmente sia una cattiva idea, per alcuni domini di problemi non c'è altra soluzione alternativa (ad esempio la valutazione condizionale di un'istruzione, come con i log che dovrebbero andare solo alle versioni di debug). Non averli significa che ho bisogno di mettere più if (condition) {...}boilerplate e non è ancora uguale in termini di innesco di effetti collaterali.

# 1 è difficile per me, vorrei conoscere la risposta da solo. # 2 - potresti dare un semplice esempio di codice C ++ e spiegare perché ne hai bisogno? Per # 3: utilizzare C#per generare altro codice. Puoi leggere un CSVo un XMLo cosa hai come file di input e generare un C#o un SQLfile. Questo può essere più potente dell'utilizzo di macro funzionali.
Giobbe

Sul tema della funzionalità macro-simile, quali cose specifiche stai cercando di fare? Quello che ho scoperto è che di solito esiste un costrutto in linguaggio C # per gestire ciò che avrei fatto con una macro in C ++. Verificate anche le direttive del preprocessore C #: msdn.microsoft.com/en-us/library/4y6tbswk.aspx
ravibhagw

Molte cose fatte con le macro in C ++ possono essere fatte con la riflessione in C #.
Sebastian Redl,

È uno dei miei animali domestici quando qualcuno dice "Lo strumento giusto per il lavoro giusto: scegli una lingua in base a ciò di cui avrai bisogno". Per programmi di grandi dimensioni in lingue generiche, questa è una dichiarazione inutile e inutile. Detto questo, la cosa in cui C ++ è migliore di quasi qualsiasi altro linguaggio è la gestione delle risorse deterministica. La gestione delle risorse deterministiche è una caratteristica principale del tuo programma o qualcosa a cui sei abituato? Se non è una funzionalità utente importante, potresti essere troppo concentrato su quello.
Ben

Risposte:


16

Nella mia esperienza, non esiste un libro per farlo. I forum aiutano con i modi di dire e le migliori pratiche, ma alla fine dovrai impegnarti nel tempo. Esercitati risolvendo una serie di problemi diversi in C #. Guarda cosa è stato difficile / imbarazzante e cerca di renderli meno difficili e meno imbarazzanti la prossima volta. Per me, questo processo è durato circa un mese prima di "averlo".

La cosa più importante su cui concentrarsi è la mancanza di gestione della memoria. In C ++ questo domina ogni singola decisione di progettazione che prendi, ma in C # è controproducente. In C #, spesso vuoi ignorare la morte o altre transizioni di stato dei tuoi oggetti. Questo tipo di associazione aggiunge un accoppiamento non necessario.

Principalmente, concentrati sull'uso dei nuovi strumenti nel modo più efficace, senza battere te stesso su un attaccamento a quelli vecchi.

Come ottenere un design C ++ perfetto non sembrare stupido in C #?

Rendendosi conto che diversi modi di dire spingono verso approcci progettuali diversi. Non puoi semplicemente portare qualcosa di 1: 1 tra (la maggior parte) lingue e aspettarti qualcosa di bello e idiomatico dall'altra parte.

Ma in risposta a scenari specifici:

Risorse non gestite condivise - Questa è stata una cattiva idea in C ++ ed è altrettanto una cattiva idea in C #. Se hai un file, aprilo, usalo e chiudilo. La condivisione tra i thread richiede solo problemi e se solo un thread lo sta davvero utilizzando, è necessario aprirlo / possederlo / chiuderlo. Se hai davvero un file di lunga durata per qualche motivo, crea una classe per rappresentarlo e lascia che l'applicazione lo gestisca come necessario (chiudi prima dell'uscita, lega vicino all'applicazione Chiusura di eventi, ecc.)

Specializzazione parziale del modello - Sei sfortunato qui, anche se più ho usato C # più trovo che l'uso del modello che ho fatto in C ++ rientri in YAGNI. Perché creare qualcosa di generico quando T è sempre un int? Altri scenari sono meglio risolti in C # dall'applicazione liberale di interfacce. Non hai bisogno di generici quando un'interfaccia o un tipo delegato possono digitare fortemente ciò che vuoi variare.

Condizionatori del preprocessore - C # ha #if, impazzisci. Meglio ancora, ha attributi condizionali che elimineranno semplicemente quella funzione dal codice se non viene definito un simbolo del compilatore.


Per quanto riguarda la gestione delle risorse, in C # è abbastanza comune avere classi manager (che possono essere statiche o dinamiche).
rwong,

Per quanto riguarda le risorse condivise: quelle gestite sono facili in C # (se le condizioni di gara sono irrilevanti), quelle non gestite sono molto più cattive.
Deduplicatore,

@rwong - comune, ma le classi manager sono ancora un anti-pattern da evitare.
Telastyn,

Per quanto riguarda la gestione della memoria, ho scoperto che il passaggio a C # ha reso questo più difficile, soprattutto all'inizio. Penso che devi essere più consapevole che in C ++. In C ++ sai esattamente quando e dove ogni oggetto è allocato, deallocato e referenziato. In C #, questo è tutto nascosto da te. Molte volte in C # non ti rendi nemmeno conto che è stato ottenuto un riferimento a un oggetto e che la memoria inizia a perdere. Divertiti a rintracciare le perdite di memoria in C #, che di solito sono banali da capire in C ++. Naturalmente, con l'esperienza apprendi queste sfumature di C # in modo che tendano a diventare meno un problema.
Dunk,

4

Per quanto posso vedere, l'unica cosa che consente la gestione della vita deterministica è IDisposableche si interrompe (come in: nessuna lingua o supporto del sistema di tipo) quando, come hai notato, è necessario trasferire la proprietà di tale risorsa.

Essere nella stessa barca come te - sviluppatore C ++ che fa C # atm. - la mancanza di supporto per modellare il trasferimento della proprietà (file, connessioni db, ...) nei tipi / firme C # mi è stata molto fastidiosa, ma devo ammettere che funziona abbastanza bene "affidarsi" sulla convenzione e sui documenti API per farlo bene.

  • Utilizzare IDisposableper eseguire la pulizia deterministica se necessario.
  • Quando devi trasferire la proprietà di un IDisposable, assicurati solo che l'API in questione sia chiara su questo e non utilizzarla usingin questo caso, perché usingper così dire non può essere "cancellata".

Sì, non è così elegante come quello che puoi esprimere nel sistema di tipi con il moderno C ++ (sposta la semantica, ecc.), Ma fa il suo lavoro e (sorprendentemente per me) la comunità C # nel suo insieme non sembra cura troppo, AFAICT.


2

Hai menzionato due funzionalità C ++ specifiche. Discuterò RAII:

Di solito non è applicabile, poiché C # viene raccolto. La costruzione C # più vicina è probabilmente l' IDisposableinterfaccia, utilizzata come segue:

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

Non dovresti aspettarti di implementarlo IDisposablespesso (e farlo è leggermente avanzato), poiché non è necessario per le risorse gestite. Tuttavia, dovresti usare il usingcostrutto (o chiamarti Dispose, se usingè impossibile) per qualsiasi implementazione IDisposable. Anche se sbagli, le classi C # ben progettate proveranno a gestirlo per te (usando i finalizzatori). Tuttavia, in un linguaggio raccolto dalla spazzatura il modello RAII non funziona completamente (poiché la raccolta non è deterministica), quindi la pulizia delle risorse verrà ritardata (a volte catastroficamente).


RAII è davvero il mio più grande problema. Supponiamo di avere una risorsa (ad esempio un file) creata da un thread e assegnata a un altro. Come posso assicurarmi che venga eliminato in un determinato momento quando quel thread non ne ha più bisogno? Certo, potrei chiamare Dispose ma poi sembra molto più brutto dei puntatori condivisi in C ++. Non c'è nulla di RAII in esso.
Andrew

@Andrew Quello che descrivi sembra implicare il trasferimento della proprietà, quindi ora il secondo thread è responsabile Disposing()del file e probabilmente dovrebbe essere usato using.
svick

@Andrew - In generale, non dovresti farlo. Dovresti invece dire al thread come ottenere il file e lasciarlo avere la proprietà della vita.
Telastyn,

1
@Telastyn Significa che cedere la proprietà di una risorsa gestita è un problema maggiore in C # che in C ++? Abbastanza sorprendente.
Andrew,

6
@Andrew: Permettetemi di riassumere: in C ++, si ottiene una gestione semi-automatica uniforme di tutte le risorse. In C #, ottieni la gestione completamente automatica della memoria e la gestione completamente manuale di tutto il resto. Non è possibile cambiarlo, quindi la tua unica vera domanda non è "come posso risolvere questo?", Solo "come mi abituo a questo?"
Jerry Coffin,

2

Questa è un'ottima domanda, dato che ho visto diversi sviluppatori C ++ scrivere codice C # terribile.

È meglio non pensare a C # come "C ++ con una sintassi migliore". Sono lingue diverse che richiedono approcci diversi nel tuo modo di pensare. Il C ++ ti obbliga a pensare costantemente a cosa faranno la CPU e la memoria. C # non è così. C # è progettato specificamente per non pensare alla CPU e alla memoria e invece al dominio aziendale per cui stai scrivendo .

Un esempio di ciò che ho visto è che molti sviluppatori C ++ adoreranno usare i loop perché sono più veloci di foreach. Di solito questa è una cattiva idea in C # perché limita i possibili tipi della raccolta su cui si esegue l'iterazione (e quindi la riusabilità e la flessibilità del codice).

Penso che il modo migliore per riadattare da C ++ a C # sia cercare di approcciare la programmazione da una prospettiva diversa. All'inizio questo sarà difficile, perché nel corso degli anni sarai completamente abituato a usare il thread "cosa stanno facendo la CPU e la memoria" nel tuo cervello per filtrare il codice che scrivi. Ma in C #, dovresti invece pensare alle relazioni tra gli oggetti nel dominio aziendale. "Cosa voglio fare" anziché "cosa sta facendo il computer".

Se vuoi fare qualcosa per tutto in un elenco di oggetti, invece di scrivere un ciclo for nell'elenco, crea un metodo che accetta un IEnumerable<MyObject>e usa un foreachciclo.

I tuoi esempi specifici:

Controllo della durata delle risorse che richiedono una pulizia deterministica (come i file). È facile averlo a portata di mano, ma come usarlo correttamente quando la proprietà della risorsa viene trasferita [... tra i thread]? In C ++ userei semplicemente i puntatori condivisi e lascerei che si occupasse della "garbage collection" al momento giusto.

Non dovresti mai farlo in C # (in particolare tra i thread). Se devi fare qualcosa su un file, fallo in un posto alla volta. Va bene (e in effetti è una buona pratica) scrivere una classe wrapper che gestisca la risorsa non gestita che viene passata tra le classi, ma non provare a passare un file tra i thread e avere classi separate scrivere / leggere / chiudere / aprirlo. Non condividere la proprietà di risorse non gestite. Usa il Disposemodello per gestire la pulizia.

Difficoltà costante con funzioni prioritarie per generici specifici (amo le cose come la specializzazione parziale dei template in C ++). Dovrei semplicemente abbandonare qualsiasi tentativo di fare una programmazione generica in C #? Forse i generici sono limitati di proposito e non è C # -ish usarli se non per uno specifico dominio di problemi?

I generici in C # sono progettati per essere generici. Le specializzazioni dei generici dovrebbero essere gestite da classi derivate. Perché un List<T>comportamento dovrebbe essere diverso se si tratta di a List<int>o a List<string>? Tutte le operazioni su List<T>sono generiche in modo da applicarsi a qualsiasi List<T> . Se si desidera modificare il comportamento del .Addmetodo su a List<string>, quindi creare una classe derivata MySpecializedStringCollection : List<string>o una classe composta MySpecializedStringCollection : IList<string>che utilizza il generico internamente ma fa le cose in modo diverso. Questo ti aiuta a evitare di violare il principio di sostituzione di Liskov e di fregare in modo regale gli altri che usano la tua classe.

Funzionalità macro-simile. Sebbene generalmente sia una cattiva idea, per alcuni domini di problemi non c'è altra soluzione alternativa (ad esempio la valutazione condizionale di un'istruzione, come con i log che dovrebbero andare solo alle versioni di debug). Non averli significa che devo aggiungere altro se (condition) {...} boilerplate e non è ancora uguale in termini di innesco di effetti collaterali.

Come altri hanno già detto, è possibile utilizzare i comandi del preprocessore per farlo. Gli attributi sono ancora migliori. In generale, gli attributi sono il modo migliore per gestire cose che non sono funzionalità "core" per una classe.

In breve, quando scrivi C #, tieni presente che dovresti pensare interamente al dominio aziendale e non alla CPU e alla memoria. È possibile ottimizzare in un secondo momento, se necessario , ma il codice dovrebbe riflettere le relazioni tra i principi aziendali che si sta tentando di mappare.


3
WRT. trasferimento di risorse: "non si dovrebbe mai fare questo" non taglia IMHO. Spesso, un ottimo design suggerisce il trasferimento di una IDisposable( non la risorsa non elaborata) da una classe all'altra: basta vedere l' API StreamWriter dove questo ha perfettamente senso Dispose()sul flusso passato. E c'è il buco nel linguaggio C # - non puoi esprimerlo nel sistema dei tipi.
Martin Ba,

2
"Un esempio di ciò che ho visto è che molti sviluppatori C ++ adoreranno usare i loop perché sono più veloci di foreach." Questa è una cattiva idea anche in C ++. L'astrazione a costo zero è la grande potenza del C ++ e nella maggior parte dei casi i foreach o altri algoritmi non dovrebbero essere più lenti di un ciclo scritto a mano per il loop.
Sebastian Redl,

1

La cosa più diversa per me era la tendenza a rinnovare tutto . Se vuoi un oggetto, dimentica di provare a passarlo a una routine e condividi i dati sottostanti, sembra che il modello C # sia solo per creare un nuovo oggetto. Questo, in generale, sembra essere molto più del modo C #. All'inizio questo schema sembrava molto inefficiente, ma immagino che la creazione della maggior parte degli oggetti in C # sia un'operazione rapida.

Tuttavia, ci sono anche le classi statiche che mi infastidiscono perché ogni tanto provi a crearne una solo per scoprire che devi semplicemente usarlo. Trovo che non sia così facile sapere quale usare.

Sono abbastanza felice in un programma C # di ignorarlo quando non ne ho più bisogno, ma ricorda solo cosa contiene quell'oggetto - in genere devi solo pensare se ha alcuni dati "insoliti", gli oggetti che consistono solo in memoria vai via da solo, gli altri dovranno essere eliminati o dovrai guardare come distruggerli - manualmente o con un blocco usando se no.

lo prenderai molto rapidamente, però, C # non è affatto diverso da VB, quindi puoi trattarlo come lo stesso strumento RAD che è (se aiuta a pensare a C # come VB.NET, allora fallo, la sintassi è solo leggermente diverso e i miei vecchi tempi di utilizzo di VB mi hanno portato nella giusta mentalità per lo sviluppo di RAD). L'unico bit difficile è determinare quale delle enormi librerie .net dovresti usare. Google è sicuramente tuo amico qui!


1
Passare un oggetto a una routine per condividere i dati sottostanti è perfettamente corretto in C #, almeno dal punto di vista della sintassi.
Brian,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.