Codice C# buggy: i 10 errori più comuni nella programmazione C#
Pubblicato: 2022-03-11A proposito di C Sharp
C# è uno dei numerosi linguaggi destinati a Microsoft Common Language Runtime (CLR). I linguaggi destinati al CLR beneficiano di funzionalità come l'integrazione tra più lingue e la gestione delle eccezioni, una maggiore sicurezza, un modello semplificato per l'interazione dei componenti e servizi di debug e profilazione. Tra i linguaggi CLR odierni, C# è il più utilizzato per progetti di sviluppo professionali complessi destinati agli ambienti desktop, mobili o server Windows.
C# è un linguaggio fortemente tipizzato orientato agli oggetti. Il controllo rigoroso del tipo in C#, sia in fase di compilazione che in fase di esecuzione, fa sì che la maggior parte degli errori di programmazione tipici di C# vengano segnalati il prima possibile e le relative posizioni siano individuate in modo abbastanza accurato. Ciò può far risparmiare molto tempo nella programmazione C Sharp, rispetto al rintracciare la causa di errori enigmatici che possono verificarsi molto tempo dopo che l'operazione incriminata ha avuto luogo in linguaggi che sono più liberali con la loro applicazione della sicurezza dei tipi. Tuttavia, molti programmatori C# eliminano inconsapevolmente (o incautamente) i vantaggi di questo rilevamento, il che porta ad alcuni dei problemi discussi in questa esercitazione su C#.
Informazioni su questo tutorial di programmazione C Sharp
Questa esercitazione descrive 10 degli errori di programmazione C# più comuni commessi o problemi da evitare dai programmatori C# e fornisce loro assistenza.
Sebbene la maggior parte degli errori discussi in questo articolo siano specifici di C#, alcuni sono rilevanti anche per altri linguaggi destinati a CLR o che utilizzano la libreria di classi Framework (FCL).
Errore comune di programmazione C# n. 1: utilizzo di un riferimento come un valore o viceversa
I programmatori di C++, e molti altri linguaggi, sono abituati a controllare se i valori che assegnano alle variabili sono semplicemente valori o sono riferimenti a oggetti esistenti. Nella programmazione C Sharp, tuttavia, tale decisione viene presa dal programmatore che ha scritto l'oggetto, non dal programmatore che istanzia l'oggetto e lo assegna a una variabile. Questo è un "gotcha" comune per coloro che cercano di imparare la programmazione C#.
Se non sai se l'oggetto che stai utilizzando è un tipo di valore o un tipo di riferimento, potresti incappare in alcune sorprese. Per esempio:
Point point1 = new Point(20, 30); Point point2 = point1; point2.X = 50; Console.WriteLine(point1.X); // 20 (does this surprise you?) Console.WriteLine(point2.X); // 50 Pen pen1 = new Pen(Color.Black); Pen pen2 = pen1; pen2.Color = Color.Blue; Console.WriteLine(pen1.Color); // Blue (or does this surprise you?) Console.WriteLine(pen2.Color); // Blue
Come puoi vedere, sia gli oggetti Point
che Pen
sono stati creati esattamente allo stesso modo, ma il valore di point1
è rimasto invariato quando è stato assegnato un nuovo valore di coordinata X
a point2
, mentre il valore di pen1
è stato modificato quando è stato assegnato un nuovo colore a pen2
. Possiamo quindi dedurre che point1
e point2
contengono ciascuno la propria copia di un oggetto Point
, mentre pen1
e pen2
contengono riferimenti allo stesso oggetto Pen
. Ma come possiamo saperlo senza fare questo esperimento?
La risposta è esaminare le definizioni dei tipi di oggetto (cosa che puoi fare facilmente in Visual Studio posizionando il cursore sul nome del tipo di oggetto e premendo F12):
public struct Point { ... } // defines a “value” type public class Pen { ... } // defines a “reference” type
Come illustrato in precedenza, nella programmazione C#, la parola chiave struct
viene utilizzata per definire un tipo di valore, mentre la parola chiave class
viene utilizzata per definire un tipo di riferimento. Per coloro con un background in C++, che sono stati cullati in un falso senso di sicurezza dalle molte somiglianze tra le parole chiave C++ e C#, questo comportamento probabilmente sorprende che potresti chiedere aiuto a un tutorial C#.
Se hai intenzione di dipendere da alcuni comportamenti che differiscono tra valori e tipi di riferimento, come la possibilità di passare un oggetto come parametro di metodo e fare in modo che quel metodo modifichi lo stato dell'oggetto, assicurati di avere a che fare con il corretto tipo di oggetto per evitare problemi di programmazione C#.
Errore comune di programmazione C# n. 2: incomprensione dei valori predefiniti per le variabili non inizializzate
In C#, i tipi di valore non possono essere null. Per definizione, i tipi di valore hanno un valore e anche le variabili non inizializzate di tipi di valore devono avere un valore. Questo è chiamato il valore predefinito per quel tipo. Ciò porta al seguente risultato, solitamente imprevisto, quando si verifica se una variabile non è inizializzata:
class Program { static Point point1; static Pen pen1; static void Main(string[] args) { Console.WriteLine(pen1 == null); // True Console.WriteLine(point1 == null); // False (huh?) } }
Perché point1
non è nullo? La risposta è che Point
è un tipo di valore e il valore predefinito per un Point
è (0,0), non null. Il mancato riconoscimento di questo è un errore molto facile (e comune) da commettere in C#.
Molti (ma non tutti) tipi di valore hanno una proprietà IsEmpty
che puoi controllare per vedere se è uguale al suo valore predefinito:
Console.WriteLine(point1.IsEmpty); // True
Quando controlli per vedere se una variabile è stata inizializzata o meno, assicurati di sapere quale valore avrà una variabile non inizializzata di quel tipo per impostazione predefinita e non fare affidamento sul fatto che sia nulla..
Errore comune di programmazione C# n. 3: utilizzo di metodi di confronto di stringhe impropri o non specificati
Esistono molti modi diversi per confrontare le stringhe in C#.
Sebbene molti programmatori utilizzino l'operatore ==
per il confronto di stringhe, in realtà è uno dei metodi meno desiderabili da impiegare, principalmente perché non specifica esplicitamente nel codice quale tipo di confronto si desidera.
Piuttosto, il modo preferito per verificare l'uguaglianza delle stringhe nella programmazione C# è con il metodo Equals
:
public bool Equals(string value); public bool Equals(string value, StringComparison comparisonType);
La prima firma del metodo (cioè, senza il parametro comparisonType
), è in realtà la stessa dell'utilizzo dell'operatore ==
, ma ha il vantaggio di essere applicata esplicitamente alle stringhe. Esegue un confronto ordinale delle stringhe, che è fondamentalmente un confronto byte per byte. In molti casi questo è esattamente il tipo di confronto che si desidera, specialmente quando si confrontano stringhe i cui valori sono impostati a livello di codice, come nomi di file, variabili di ambiente, attributi, ecc. In questi casi, purché un confronto ordinale sia effettivamente di tipo corretto di confronto per quella situazione, l'unico aspetto negativo dell'utilizzo del metodo Equals
senza un comparisonType
è che qualcuno che legge il codice potrebbe non sapere quale tipo di confronto stai facendo.
L'utilizzo della firma del metodo Equals
che include un comparisonType
Type ogni volta che si confrontano le stringhe, tuttavia, non solo renderà il codice più chiaro, ma vi farà pensare esplicitamente a quale tipo di confronto è necessario effettuare. Questa è una cosa utile da fare, perché anche se l'inglese potrebbe non fornire molte differenze tra i confronti ordinali e sensibili alla cultura, altre lingue forniscono molte e ignorare la possibilità di altre lingue ti apre a molte potenzialità per errori lungo la strada. Per esempio:
string s = "strasse"; // outputs False: Console.WriteLine(s == "straße"); Console.WriteLine(s.Equals("straße")); Console.WriteLine(s.Equals("straße", StringComparison.Ordinal)); Console.WriteLine(s.Equals("Straße", StringComparison.CurrentCulture)); Console.WriteLine(s.Equals("straße", StringComparison.OrdinalIgnoreCase)); // outputs True: Console.WriteLine(s.Equals("straße", StringComparison.CurrentCulture)); Console.WriteLine(s.Equals("Straße", StringComparison.CurrentCultureIgnoreCase));
La pratica più sicura consiste nel fornire sempre un parametro comparisonType
al metodo Equals
. Ecco alcune linee guida di base:
- Quando si confrontano le stringhe che sono state immesse dall'utente o che devono essere visualizzate all'utente, utilizzare un confronto sensibile alle impostazioni cultura (
CurrentCulture
oCurrentCultureIgnoreCase
). - Quando si confrontano stringhe a livello di codice, utilizzare il confronto ordinale (
Ordinal
oOrdinalIgnoreCase
). -
InvariantCulture
eInvariantCultureIgnoreCase
non devono essere generalmente utilizzati se non in circostanze molto limitate, poiché i confronti ordinali sono più efficienti. Se è necessario un confronto sensibile alla cultura, di solito dovrebbe essere eseguito rispetto alla cultura corrente o a un'altra cultura specifica.
Oltre al metodo Equals
, le stringhe forniscono anche il metodo Compare
, che fornisce informazioni sull'ordine relativo delle stringhe anziché solo un test di uguaglianza. Questo metodo è preferibile agli operatori <
, <=
, >
e >=
, per gli stessi motivi discussi in precedenza, per evitare problemi con C#.
Errore comune di programmazione C# n. 4: utilizzo di istruzioni iterative (anziché dichiarative) per manipolare le raccolte
In C# 3.0, l'aggiunta di Language-Integrated Query (LINQ) al linguaggio ha cambiato per sempre il modo in cui le raccolte vengono interrogate e manipolate. Da allora, se stai usando istruzioni iterative per manipolare le raccolte, non hai usato LINQ quando probabilmente avresti dovuto.
Alcuni programmatori C# non sanno nemmeno dell'esistenza di LINQ, ma fortunatamente quel numero sta diventando sempre più piccolo. Molti pensano ancora, tuttavia, che a causa della somiglianza tra le parole chiave LINQ e le istruzioni SQL, il suo unico utilizzo sia nel codice che interroga i database.
Sebbene le query del database siano un uso molto prevalente delle istruzioni LINQ, in realtà funzionano su qualsiasi raccolta enumerabile (ovvero qualsiasi oggetto che implementa l'interfaccia IEnumerable). Quindi, ad esempio, se avevi una matrice di Account, invece di scrivere un elenco C# per ciascuno:
decimal total = 0; foreach (Account account in myAccounts) { if (account.Status == "active") { total += account.Balance; } }
potresti semplicemente scrivere:
decimal total = (from account in myAccounts where account.Status == "active" select account.Balance).Sum();
Sebbene questo sia un esempio piuttosto semplice di come evitare questo problema di programmazione C# comune, ci sono casi in cui una singola istruzione LINQ può facilmente sostituire dozzine di istruzioni in un ciclo iterativo (o cicli nidificati) nel codice. E meno codice generale significa meno opportunità per l'introduzione di bug. Tieni presente, tuttavia, che potrebbe esserci un compromesso in termini di prestazioni. Negli scenari critici per le prestazioni, in particolare quando il codice iterativo è in grado di formulare ipotesi sulla tua raccolta che LINQ non può, assicurati di eseguire un confronto delle prestazioni tra i due metodi.
Errore comune di programmazione C# n. 5: mancata considerazione degli oggetti sottostanti in un'istruzione LINQ
LINQ è ottimo per astrarre il compito di manipolare raccolte, siano esse oggetti in memoria, tabelle di database o documenti XML. In un mondo perfetto, non avresti bisogno di sapere quali sono gli oggetti sottostanti. Ma l'errore qui è presumere che viviamo in un mondo perfetto. In effetti, istruzioni LINQ identiche possono restituire risultati diversi se eseguite sugli stessi identici dati, se tali dati sono in un formato diverso.
Si consideri ad esempio la seguente affermazione:
decimal total = (from account in myAccounts where account.Status == "active" select account.Balance).Sum();
Cosa succede se uno degli account.Status
dell'oggetto è uguale ad “Attivo” (notare la A maiuscola)? Bene, se myAccounts
fosse un oggetto DbSet
(che è stato impostato con la configurazione predefinita senza distinzione tra maiuscole e minuscole), l'espressione where
corrisponderebbe comunque a quell'elemento. Tuttavia, se myAccounts
si trovava in un array in memoria, non corrisponderebbe e quindi produrrebbe un risultato diverso per total.
Ma aspetta un minuto. Quando abbiamo parlato del confronto di stringhe in precedenza, abbiamo visto che l'operatore ==
eseguiva un confronto ordinale di stringhe. Allora perché in questo caso l'operatore ==
esegue un confronto senza distinzione tra maiuscole e minuscole?
La risposta è che quando gli oggetti sottostanti in un'istruzione LINQ sono riferimenti ai dati della tabella SQL (come nel caso dell'oggetto DbSet di Entity Framework in questo esempio), l'istruzione viene convertita in un'istruzione T-SQL. Gli operatori seguono quindi le regole di programmazione T-SQL, non le regole di programmazione C#, quindi il confronto nel caso precedente finisce per non fare distinzione tra maiuscole e minuscole.
In generale, anche se LINQ è un modo utile e coerente per interrogare raccolte di oggetti, in realtà devi comunque sapere se la tua affermazione verrà tradotta o meno in qualcosa di diverso da C# sotto il cofano per assicurarti che il comportamento del tuo codice sarà essere come previsto in fase di esecuzione.
Errore di programmazione C# comune n. 6: essere confuso o simulato dai metodi di estensione
Come accennato in precedenza, le istruzioni LINQ funzionano su qualsiasi oggetto che implementa IEnumerable. Ad esempio, la seguente semplice funzione sommerà i saldi su qualsiasi raccolta di conti:
public decimal SumAccounts(IEnumerable<Account> myAccounts) { return myAccounts.Sum(a => a.Balance); }
Nel codice precedente, il tipo del parametro myAccounts è dichiarato come IEnumerable<Account>
. Poiché myAccounts
fa riferimento a un metodo Sum
(C# usa la familiare "notazione punto" per fare riferimento a un metodo su una classe o un'interfaccia), ci aspetteremmo di vedere un metodo chiamato Sum()
nella definizione dell'interfaccia IEnumerable<T>
. Tuttavia, la definizione di IEnumerable<T>
, non fa riferimento a nessun metodo Sum
e si presenta semplicemente così:
public interface IEnumerable<out T> : IEnumerable { IEnumerator<T> GetEnumerator(); }
Allora, dov'è definito il metodo Sum()
? C# è fortemente tipizzato, quindi se il riferimento al metodo Sum
non fosse valido, il compilatore C# lo contrassegnerebbe sicuramente come errore. Sappiamo quindi che deve esistere, ma dove? Inoltre, dove sono le definizioni di tutti gli altri metodi forniti da LINQ per interrogare o aggregare queste raccolte?
La risposta è che Sum()
non è un metodo definito sull'interfaccia IEnumerable
. Piuttosto, è un metodo statico (chiamato "metodo di estensione") definito sulla classe System.Linq.Enumerable
:
namespace System.Linq { public static class Enumerable { ... // the reference here to “this IEnumerable<TSource> source” is // the magic sauce that provides access to the extension method Sum public static decimal Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, decimal> selector); ... } }
Quindi cosa rende un metodo di estensione diverso da qualsiasi altro metodo statico e cosa ci consente di accedervi in altre classi?
La caratteristica distintiva di un metodo di estensione è il modificatore this
sul suo primo parametro. Questa è la “magia” che lo identifica al compilatore come metodo di estensione. Il tipo del parametro che modifica (in questo caso IEnumerable<TSource>
) denota la classe o l'interfaccia che sembrerà quindi implementare questo metodo.
(Come punto a margine, non c'è nulla di magico nella somiglianza tra il nome dell'interfaccia IEnumerable
e il nome della classe Enumerable
su cui è definito il metodo di estensione. Questa somiglianza è solo una scelta stilistica arbitraria.)
Con questa comprensione, possiamo anche vedere che la funzione sumAccounts
che abbiamo introdotto sopra potrebbe invece essere stata implementata come segue:

public decimal SumAccounts(IEnumerable<Account> myAccounts) { return Enumerable.Sum(myAccounts, a => a.Balance); }
Il fatto che avremmo potuto implementarlo in questo modo solleva invece la domanda: perché avere metodi di estensione? I metodi di estensione sono essenzialmente una comodità del linguaggio di programmazione C# che consente di "aggiungere" metodi ai tipi esistenti senza creare un nuovo tipo derivato, ricompilare o modificare in altro modo il tipo originale.
I metodi di estensione vengono introdotti nell'ambito includendo un using [namespace];
dichiarazione nella parte superiore del file. Devi sapere quale spazio dei nomi C# include i metodi di estensione che stai cercando, ma è abbastanza facile da determinare una volta che sai cosa stai cercando.
Quando il compilatore C# rileva una chiamata al metodo su un'istanza di un oggetto e non trova quel metodo definito nella classe dell'oggetto di riferimento, esamina tutti i metodi di estensione che rientrano nell'ambito per cercare di trovarne uno che corrisponda al metodo richiesto firma e classe. Se ne trova uno, passerà il riferimento all'istanza come primo argomento a quel metodo di estensione, quindi il resto degli argomenti, se presenti, verrà passato come argomenti successivi al metodo di estensione. (Se il compilatore C# non trova alcun metodo di estensione corrispondente nell'ambito, genererà un errore.)
I metodi di estensione sono un esempio di "zucchero sintattico" da parte del compilatore C#, che ci permette di scrivere codice che è (di solito) più chiaro e più gestibile. Più chiaro, cioè, se sei a conoscenza del loro utilizzo. Altrimenti, può creare un po' di confusione, soprattutto all'inizio.
Sebbene ci siano certamente vantaggi nell'uso dei metodi di estensione, possono causare problemi e chiedere aiuto alla programmazione C# per quegli sviluppatori che non ne sono a conoscenza o non li comprendono correttamente. Ciò è particolarmente vero quando si guardano esempi di codice online o qualsiasi altro codice pre-scritto. Quando tale codice produce errori del compilatore (perché richiama metodi che chiaramente non sono definiti nelle classi su cui vengono invocati), la tendenza è pensare che il codice si applichi a una versione diversa della libreria oa una libreria completamente diversa. È possibile dedicare molto tempo alla ricerca di una nuova versione o di una "libreria mancante" fantasma che non esiste.
Anche gli sviluppatori che hanno familiarità con i metodi di estensione vengono ancora catturati occasionalmente, quando c'è un metodo con lo stesso nome sull'oggetto, ma la sua firma del metodo differisce in modo sottile da quella del metodo di estensione. Si può perdere molto tempo alla ricerca di un errore di battitura o di un errore che semplicemente non c'è.
L'uso dei metodi di estensione nelle librerie C# sta diventando sempre più diffuso. Oltre a LINQ, Unity Application Block e il framework dell'API Web sono esempi di due moderne librerie molto utilizzate da Microsoft che utilizzano anche metodi di estensione e ce ne sono molti altri. Più moderno è il framework, più è probabile che incorpori metodi di estensione.
Naturalmente, puoi anche scrivere i tuoi metodi di estensione. Renditi conto, tuttavia, che mentre i metodi di estensione sembrano essere invocati proprio come i normali metodi di istanza, questa è in realtà solo un'illusione. In particolare, i metodi di estensione non possono fare riferimento a membri privati o protetti della classe che stanno estendendo e pertanto non possono fungere da sostituto completo per l'ereditarietà di classi più tradizionale.
Errore comune di programmazione C# n. 7: utilizzo del tipo errato di raccolta per l'attività in corso
C# fornisce un'ampia varietà di oggetti di raccolta, di cui quanto segue è solo un elenco parziale:
Array
, ArrayList
, BitArray
, BitVector32
, Dictionary<K,V>
, HashTable
, HybridDictionary
, List<T>
, NameValueCollection
, OrderedDictionary
, Queue, Queue<T>
, SortedList
, Stack, Stack<T>
, StringCollection
, StringDictionary
.
Mentre ci possono essere casi in cui troppe scelte sono cattive quanto non abbastanza scelte, questo non è il caso degli oggetti di raccolta. Il numero di opzioni disponibili può sicuramente funzionare a tuo vantaggio. Dedica un po' di tempo in più in anticipo alla ricerca e scegli il tipo di raccolta ottimale per il tuo scopo. Probabilmente si tradurrà in prestazioni migliori e meno spazio per errori.
Se c'è un tipo di raccolta specificamente mirato al tipo di elemento che hai (come stringa o bit), inclini a usarlo prima. L'implementazione è generalmente più efficiente quando è mirata a un tipo specifico di elemento.
Per sfruttare la sicurezza dei tipi di C#, di solito dovresti preferire un'interfaccia generica a una non generica. Gli elementi di un'interfaccia generica sono del tipo che specifichi quando dichiari il tuo oggetto, mentre gli elementi di interfacce non generiche sono di tipo oggetto. Quando si usa un'interfaccia non generica, il compilatore C# non può eseguire il controllo del tipo del codice. Inoltre, quando si tratta di raccolte di tipi di valore primitivi, l'utilizzo di una raccolta non generica comporterà ripetuti inscatolamenti/unboxing di tali tipi, il che può comportare un impatto negativo significativo sulle prestazioni rispetto a una raccolta generica del tipo appropriato.
Un altro problema comune in C# è scrivere il proprio oggetto raccolta. Questo non vuol dire che non sia mai appropriato, ma con una selezione così completa come quella offerta da .NET, puoi probabilmente risparmiare molto tempo usando o estendendone uno già esistente, piuttosto che reinventare la ruota. In particolare, la C5 Generic Collection Library per C# e CLI offre un'ampia gamma di raccolte aggiuntive "pronte all'uso", come strutture di dati ad albero persistenti, code di priorità basate su heap, elenchi di array indicizzati hash, elenchi collegati e molto altro.
Errore di programmazione C# comune n. 8: trascurare di liberare risorse
L'ambiente CLR utilizza un Garbage Collector, quindi non è necessario liberare esplicitamente la memoria creata per alcun oggetto. In effetti, non puoi. Non esiste un equivalente dell'operatore di delete
C++ o della funzione free()
in C . Ma ciò non significa che puoi semplicemente dimenticare tutti gli oggetti dopo aver finito di usarli. Molti tipi di oggetti incapsulano qualche altro tipo di risorsa di sistema (ad esempio, un file del disco, una connessione al database, un socket di rete, ecc.). Lasciare queste risorse aperte può esaurire rapidamente il numero totale di risorse di sistema, degradando le prestazioni e, in definitiva, causando errori di programma.
Mentre un metodo distruttore può essere definito su qualsiasi classe C#, il problema con i distruttori (chiamati anche finalizzatori in C#) è che non puoi sapere con certezza quando verranno chiamati. Vengono chiamati dal Garbage Collector (su un thread separato, che può causare ulteriori complicazioni) in un momento futuro indeterminato. Cercare di aggirare queste limitazioni forzando la Garbage Collection con GC.Collect()
non è una procedura consigliata per C#, poiché ciò bloccherà il thread per un periodo di tempo sconosciuto mentre raccoglie tutti gli oggetti idonei per la raccolta.
Questo non vuol dire che non ci siano buoni usi per i finalizzatori, ma liberare risorse in modo deterministico non è uno di questi. Piuttosto, quando operi su una connessione a file, rete o database, vuoi liberare esplicitamente la risorsa sottostante non appena hai finito con essa.
Le perdite di risorse sono un problema in quasi tutti gli ambienti. Tuttavia, C# fornisce un meccanismo robusto e semplice da usare che, se utilizzato, può rendere le perdite molto più rare. Il framework .NET definisce l'interfaccia IDisposable
, che consiste esclusivamente nel metodo Dispose()
. Qualsiasi oggetto che implementa IDisposable
prevede di avere quel metodo chiamato ogni volta che il consumatore dell'oggetto ha finito di manipolarlo. Ciò si traduce in una liberazione esplicita e deterministica delle risorse.
Se stai creando ed eliminando un oggetto nel contesto di un singolo blocco di codice, è fondamentalmente imperdonabile dimenticare di chiamare Dispose()
, perché C# fornisce un'istruzione using
che assicurerà che Dispose()
venga chiamato indipendentemente da come il blocco di codice è terminato (che si tratti di un'eccezione, di un'istruzione di ritorno o semplicemente della chiusura del blocco). E sì, è la stessa istruzione using
menzionata in precedenza che viene utilizzata per includere gli spazi dei nomi C# nella parte superiore del file. Ha un secondo scopo, completamente non correlato, di cui molti sviluppatori C# non sono a conoscenza; vale a dire, per garantire che Dispose()
venga chiamato su un oggetto quando si esce dal blocco di codice:
using (FileStream myFile = File.OpenRead("foo.txt")) { myFile.Read(buffer, 0, 100); }
Creando un blocco using
nell'esempio sopra, sai per certo che myFile.Dispose()
verrà chiamato non appena avrai finito con il file, indipendentemente dal fatto che Read()
generi un'eccezione.
Errore comune di programmazione C# n. 9: evitare le eccezioni
C# continua l'imposizione dell'indipendenza dai tipi in runtime. Ciò consente di individuare molti tipi di errori in C# molto più rapidamente rispetto a linguaggi come C++, dove conversioni di tipo errate possono comportare l'assegnazione di valori arbitrari ai campi di un oggetto. Tuttavia, ancora una volta, i programmatori possono sprecare questa fantastica funzionalità, causando problemi con C#. Cadono in questa trappola perché C# fornisce due modi diversi di fare le cose, uno che può generare un'eccezione e uno che non lo farà. Alcuni eviteranno la via dell'eccezione, immaginando che non dover scrivere un blocco try/catch risparmi loro un po' di codice.
Ad esempio, ecco due modi diversi per eseguire un cast di tipo esplicito in C#:
// METHOD 1: // Throws an exception if account can't be cast to SavingsAccount SavingsAccount savingsAccount = (SavingsAccount)account; // METHOD 2: // Does NOT throw an exception if account can't be cast to // SavingsAccount; will just set savingsAccount to null instead SavingsAccount savingsAccount = account as SavingsAccount;
L'errore più ovvio che potrebbe verificarsi con l'uso del Metodo 2 sarebbe il mancato controllo del valore restituito. Ciò comporterebbe probabilmente un'eventuale NullReferenceException, che potrebbe eventualmente emergere in un momento molto successivo, rendendo molto più difficile rintracciare l'origine del problema. Al contrario, il Metodo 1 avrebbe immediatamente generato InvalidCastException
rendendo l'origine del problema molto più immediatamente evidente.
Inoltre, anche se ti ricordi di controllare il valore restituito nel Metodo 2, cosa farai se lo trovi nullo? Il metodo che stai scrivendo è un luogo appropriato per segnalare un errore? C'è qualcos'altro che puoi provare se il cast fallisce? In caso contrario, lanciare un'eccezione è la cosa corretta da fare, quindi potresti anche lasciare che accada il più vicino possibile alla fonte del problema.
Ecco un paio di esempi di altre coppie di metodi comuni in cui uno genera un'eccezione e l'altro no:
int.Parse(); // throws exception if argument can't be parsed int.TryParse(); // returns a bool to denote whether parse succeeded IEnumerable.First(); // throws exception if sequence is empty IEnumerable.FirstOrDefault(); // returns null/default value if sequence is empty
Alcuni sviluppatori C# sono così "contrari all'eccezione" da presumere automaticamente che il metodo che non genera un'eccezione sia superiore. Sebbene ci siano alcuni casi selezionati in cui ciò può essere vero, non è affatto corretto come generalizzazione.
A titolo di esempio specifico, nel caso in cui si dispone di un'azione alternativa legittima (ad es. predefinita) da intraprendere se fosse stata generata un'eccezione, l'approccio senza eccezioni potrebbe essere una scelta legittima. In tal caso, potrebbe davvero essere meglio scrivere qualcosa del genere:
if (int.TryParse(myString, out myInt)) { // use myInt } else { // use default value }
invece di:
try { myInt = int.Parse(myString); // use myInt } catch (FormatException) { // use default value }
Tuttavia, non è corretto presumere che TryParse
sia quindi necessariamente il metodo "migliore". A volte è così, a volte no. Ecco perché ci sono due modi per farlo. Usa quello corretto per il contesto in cui ti trovi, ricordando che le eccezioni possono sicuramente essere tue amiche come sviluppatore.
Errore comune di programmazione C# n. 10: consentire l'accumulo di avvisi del compilatore
Sebbene questo problema non sia sicuramente specifico di C#, è particolarmente eclatante nella programmazione C# poiché abbandona i vantaggi del rigoroso controllo dei tipi offerto dal compilatore C#.
Gli avvisi vengono generati per un motivo. Mentre tutti gli errori del compilatore C# indicano un difetto nel codice, lo fanno anche molti avvisi. Ciò che differenzia i due è che, nel caso di un avviso, il compilatore non ha problemi a emettere le istruzioni rappresentate dal codice. Anche così, trova il tuo codice un po' incerto e c'è una ragionevole probabilità che il tuo codice non rifletta accuratamente le tue intenzioni.
Un semplice esempio comune per il bene di questo tutorial di programmazione C# è quando modifichi l'algoritmo per eliminare l'uso di una variabile che stavi utilizzando, ma ti dimentichi di rimuovere la dichiarazione della variabile. Il programma funzionerà perfettamente, ma il compilatore contrassegnerà la dichiarazione di variabile inutile. Il fatto che il programma funzioni perfettamente fa sì che i programmatori trascurino di correggere la causa dell'avviso. Inoltre, i programmatori sfruttano una funzionalità di Visual Studio che consente loro di nascondere facilmente gli avvisi nella finestra "Elenco errori" in modo che possano concentrarsi solo sugli errori. Non ci vuole molto prima che ci siano dozzine di avvisi, tutti beatamente ignorati (o peggio ancora nascosti).
Ma se ignori questo tipo di avviso, prima o poi, qualcosa del genere potrebbe benissimo farsi strada nel tuo codice:
class Account { int myId; int Id; // compiler warned you about this, but you didn't listen! // Constructor Account(int id) { this.myId = Id; // OOPS! } }
E alla velocità che Intellisense ci consente di scrivere codice, questo errore non è così improbabile come sembra.
Ora hai un grave errore nel tuo programma (sebbene il compilatore lo abbia contrassegnato solo come avviso, per i motivi già spiegati) e, a seconda della complessità del tuo programma, potresti perdere molto tempo a rintracciarlo. Se avessi prestato attenzione a questo avviso in primo luogo, avresti evitato questo problema con una semplice correzione di cinque secondi.
Ricorda, il compilatore C Sharp ti fornisce molte informazioni utili sulla robustezza del tuo codice... se stai ascoltando. Non ignorare gli avvisi. Di solito ci vogliono solo pochi secondi per risolverli e aggiustarne di nuovi quando si verificano può farti risparmiare ore. Allenati ad aspettarti che la finestra "Elenco errori" di Visual Studio visualizzi "0 errori, 0 avvisi", in modo che qualsiasi avviso ti metta a disagio abbastanza da risolverli immediatamente.
Naturalmente, ci sono eccezioni a ogni regola. Di conseguenza, ci possono essere momenti in cui il tuo codice sembrerà un po' incerto al compilatore, anche se è esattamente come volevi che fosse. In questi casi molto rari, utilizzare #pragma warning disable [warning id]
solo attorno al codice che attiva l'avviso e solo per l'ID avviso che viene attivato. Questo eliminerà quell'avviso, e solo quell'avviso, in modo che tu possa ancora stare all'erta per i nuovi.
Incartare
C# è un linguaggio potente e flessibile con molti meccanismi e paradigmi che possono migliorare notevolmente la produttività. Come con qualsiasi strumento o linguaggio software, tuttavia, avere una comprensione o un apprezzamento limitati delle sue capacità a volte può essere più un impedimento che un vantaggio, lasciando uno nel proverbiale stato di "sapere abbastanza per essere pericoloso".
L'uso di un tutorial in C Sharp come questo per familiarizzare con le sfumature chiave di C#, come (ma non solo) i problemi sollevati in questo articolo, aiuterà nell'ottimizzazione di C# evitando alcune delle sue insidie più comuni del linguaggio.
Ulteriori letture sul blog di Toptal Engineering:
- Domande essenziali per l'intervista in C#
- C# vs. C++: cosa c'è al centro?