Cod C# Buggy: Cele mai frecvente 10 greșeli în programarea C#
Publicat: 2022-03-11Despre C Sharp
C# este una dintre mai multe limbi care vizează Microsoft Common Language Runtime (CLR). Limbile care vizează CLR beneficiază de caracteristici precum integrarea în mai multe limbi și gestionarea excepțiilor, securitate îmbunătățită, un model simplificat pentru interacțiunea componentelor și servicii de depanare și profilare. Dintre limbajele CLR de astăzi, C# este cel mai utilizat pentru proiecte complexe de dezvoltare profesională care vizează mediile Windows desktop, mobile sau server.
C# este un limbaj puternic tipizat, orientat pe obiecte. Verificarea strictă a tipului în C#, atât la compilare, cât și la timpul de execuție, are ca rezultat raportarea cât mai devreme a majorității erorilor tipice de programare C# și locațiile lor identificate destul de precis. Acest lucru poate economisi mult timp în programarea C Sharp, în comparație cu urmărirea cauzei erorilor derutante care pot apărea mult timp după ce operațiunea ofensătoare are loc în limbi care sunt mai liberale cu aplicarea siguranței de tip. Cu toate acestea, o mulțime de programatori C# aruncă fără să vrea (sau neglijent) beneficiile acestei detectări, ceea ce duce la unele dintre problemele discutate în acest tutorial C#.
Despre acest tutorial de programare C Sharp
Acest tutorial descrie 10 dintre cele mai frecvente greșeli de programare C# făcute sau probleme care trebuie evitate de către programatorii C# și le oferă ajutor.
În timp ce majoritatea greșelilor discutate în acest articol sunt specifice C#, unele sunt relevante și pentru alte limbaje care vizează CLR sau folosesc Framework Class Library (FCL).
Greșeală comună de programare C# #1: Utilizarea unei referințe ca o valoare sau invers
Programatorii C++ și multe alte limbaje sunt obișnuiți să controleze dacă valorile pe care le atribuie variabilelor sunt pur și simplu valori sau sunt referințe la obiecte existente. În programarea C Sharp, totuși, acea decizie este luată de programatorul care a scris obiectul, nu de programatorul care instanțiază obiectul și îl atribuie unei variabile. Aceasta este o „înțelegere” obișnuită pentru cei care încearcă să învețe programarea C#.
Dacă nu știți dacă obiectul pe care îl utilizați este un tip de valoare sau un tip de referință, ați putea avea câteva surprize. De exemplu:
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
După cum puteți vedea, ambele obiecte Point
și Pen
au fost create exact în același mod, dar valoarea point1
a rămas neschimbată atunci când o nouă valoare a coordonatei X
i-a fost atribuită pen1
point2
fost modificată atunci când i-a fost atribuită o nouă culoare. pen2
. Prin urmare, putem deduce că point1
și point2
conțin fiecare propria copie a unui obiect Point
, în timp ce pen1
și pen2
conțin referințe la același obiect Pen
. Dar cum putem ști asta fără să facem acest experiment?
Răspunsul este să te uiți la definițiile tipurilor de obiect (pe care le poți face cu ușurință în Visual Studio, plasând cursorul peste numele tipului de obiect și apăsând F12):
public struct Point { ... } // defines a “value” type public class Pen { ... } // defines a “reference” type
După cum se arată mai sus, în programarea C#, cuvântul cheie struct
este folosit pentru a defini un tip de valoare, în timp ce cuvântul cheie class
este folosit pentru a defini un tip de referință. Pentru cei cu un fundal C++, care au fost adormiți într-un fals sentiment de securitate de multele asemănări dintre cuvintele cheie C++ și C#, acest comportament este probabil o surpriză care vă poate face să cereți ajutor de la un tutorial C#.
Dacă veți depinde de un comportament care diferă între tipurile de valoare și de referință - cum ar fi capacitatea de a trece un obiect ca parametru de metodă și de a face ca metoda să schimbe starea obiectului - asigurați-vă că aveți de-a face cu tipul corect de obiect pentru a evita problemele de programare C#.
Greșeală comună de programare C# #2: înțelegerea greșită a valorilor implicite pentru variabilele neinițializate
În C#, tipurile de valori nu pot fi nule. Prin definiție, tipurile de valoare au o valoare și chiar și variabilele neinițializate ale tipurilor de valoare trebuie să aibă o valoare. Aceasta se numește valoarea implicită pentru acel tip. Acest lucru duce la următorul rezultat, de obicei neașteptat, atunci când se verifică dacă o variabilă este neinițializată:
class Program { static Point point1; static Pen pen1; static void Main(string[] args) { Console.WriteLine(pen1 == null); // True Console.WriteLine(point1 == null); // False (huh?) } }
De ce nu este point1
nul? Răspunsul este că Point
este un tip de valoare, iar valoarea implicită pentru un Point
este (0,0), nu nulă. Eșecul de a recunoaște acest lucru este o greșeală foarte ușor (și comună) de făcut în C#.
Multe (dar nu toate) tipurile de valori au o proprietate IsEmpty
pe care o puteți verifica pentru a vedea dacă este egală cu valoarea implicită:
Console.WriteLine(point1.IsEmpty); // True
Când verificați pentru a vedea dacă o variabilă a fost inițializată sau nu, asigurați-vă că știți ce valoare va avea implicit o variabilă neinițializată de acest tip și nu vă bazați că este nulă.
Greșeală comună de programare C# #3: Folosirea metodelor de comparare a șirurilor necorespunzătoare sau nespecificate
Există multe moduri diferite de a compara șirurile în C#.
Deși mulți programatori folosesc operatorul ==
pentru compararea șirurilor, este de fapt una dintre cele mai puțin dorite metode de folosit, în primul rând pentru că nu specifică în mod explicit în cod ce tip de comparație este dorit.
Mai degrabă, modalitatea preferată de a testa egalitatea șirurilor în programarea C# este metoda Equals
:
public bool Equals(string value); public bool Equals(string value, StringComparison comparisonType);
Prima semnătură a metodei (adică, fără parametrul comparisonType
), este de fapt aceeași cu utilizarea operatorului ==
, dar are avantajul de a fi aplicată în mod explicit șirurilor de caractere. Ea efectuează o comparație ordinală a șirurilor, care este practic o comparație octet cu octet. În multe cazuri, acesta este exact tipul de comparație pe care îl doriți, mai ales când comparați șiruri de caractere ale căror valori sunt setate programatic, cum ar fi numele fișierelor, variabilele de mediu, atributele etc. În aceste cazuri, atâta timp cât o comparație ordinală este într-adevăr tipul corect de comparație pentru acea situație, singurul dezavantaj al utilizării metodei Equals
fără un tip de comparisonType
este că cineva care citește codul poate să nu știe ce tip de comparație faci.
Folosirea semnăturii metodei Equals
care include un comparisonType
de fiecare dată când comparați șiruri, totuși, nu numai că vă va face codul mai clar, ci vă va face să vă gândiți în mod explicit la ce tip de comparație trebuie să faceți. Acesta este un lucru util de făcut, deoarece, chiar dacă engleza poate să nu ofere o mulțime de diferențe între comparațiile ordinale și cele sensibile la cultură, alte limbi oferă o mulțime, iar ignorarea posibilității altor limbi vă deschide un potențial foarte mare pentru erori pe drum. De exemplu:
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));
Cea mai sigură practică este de a furniza întotdeauna un parametru comparisonType
la metoda Equals
. Iată câteva îndrumări de bază:
- Când comparați șirurile care au fost introduse de utilizator sau care urmează să fie afișate utilizatorului, utilizați o comparație sensibilă la cultură (
CurrentCulture
sauCurrentCultureIgnoreCase
). - Când comparați șirurile programatice, utilizați comparația ordinală (
Ordinal
sauOrdinalIgnoreCase
). - În general,
InvariantCulture
șiInvariantCultureIgnoreCase
nu trebuie utilizate decât în circumstanțe foarte limitate, deoarece comparațiile ordinale sunt mai eficiente. Dacă este necesară o comparație conștientă de cultură, aceasta ar trebui efectuată de obicei față de cultura actuală sau de altă cultură specifică.
Pe lângă metoda Equals
, șirurile oferă și metoda Compare
, care vă oferă informații despre ordinea relativă a șirurilor în loc de doar un test de egalitate. Această metodă este de preferat operatorilor <
, <=
, >
și >=
, din aceleași motive ca cele discutate mai sus – pentru a evita problemele C#.
Greșeala comună de programare C# #4: Utilizarea instrucțiunilor iterative (în loc de declarative) pentru a manipula colecțiile
În C# 3.0, adăugarea interogării integrate în limbaj (LINQ) la limbaj a schimbat pentru totdeauna modul în care colecțiile sunt interogate și manipulate. De atunci, dacă utilizați instrucțiuni iterative pentru a manipula colecțiile, nu ați folosit LINQ atunci când probabil ar fi trebuit.
Unii programatori C# nici măcar nu știu de existența lui LINQ, dar, din fericire, acest număr devine din ce în ce mai mic. Mulți încă mai cred că, din cauza asemănării dintre cuvintele cheie LINQ și instrucțiunile SQL, singura sa utilizare este în codul care interogează bazele de date.
În timp ce interogarea bazei de date este o utilizare foarte răspândită a instrucțiunilor LINQ, acestea funcționează de fapt peste orice colecție enumerabilă (adică, orice obiect care implementează interfața IEnumerable). Deci, de exemplu, dacă ați avut o serie de conturi, în loc să scrieți o listă C# pentru fiecare:
decimal total = 0; foreach (Account account in myAccounts) { if (account.Status == "active") { total += account.Balance; } }
ai putea doar sa scrii:
decimal total = (from account in myAccounts where account.Status == "active" select account.Balance).Sum();
Deși acesta este un exemplu destul de simplu despre cum să evitați această problemă comună de programare C#, există cazuri în care o singură instrucțiune LINQ poate înlocui cu ușurință zeci de instrucțiuni într-o buclă iterativă (sau bucle imbricate) în codul dvs. Și mai puțin cod general înseamnă mai puține oportunități de introducere a erorilor. Rețineți, totuși, că poate exista un compromis în ceea ce privește performanța. În scenariile critice pentru performanță, mai ales în cazul în care codul dvs. iterativ este capabil să facă presupuneri despre colecția dvs. pe care LINQ nu le poate face, asigurați-vă că faceți o comparație de performanță între cele două metode.
Greșeală comună de programare C# #5: Eșecul de a lua în considerare obiectele de bază într-o instrucțiune LINQ
LINQ este excelent pentru abstractizarea sarcinii de manipulare a colecțiilor, fie că sunt obiecte în memorie, tabele de baze de date sau documente XML. Într-o lume perfectă, nu ar trebui să știi care sunt obiectele de la bază. Dar eroarea aici este să presupunem că trăim într-o lume perfectă. De fapt, instrucțiunile LINQ identice pot returna rezultate diferite atunci când sunt executate pe exact aceleași date, dacă datele respective se întâmplă să fie într-un format diferit.
De exemplu, luați în considerare următoarea afirmație:
decimal total = (from account in myAccounts where account.Status == "active" select account.Balance).Sum();
Ce se întâmplă dacă unul dintre conturile obiectului. account.Status
este egală cu „Activ” (notați A majusculă)? Ei bine, dacă myAccounts
a fost un obiect DbSet
(care a fost configurat cu configurația implicită care nu ține seama de majuscule), expresia where
s-ar potrivi în continuare cu acel element. Cu toate acestea, dacă myAccounts
a fost într-o matrice în memorie, nu s-ar potrivi și, prin urmare, ar produce un rezultat diferit pentru total.
Dar stai un minut. Când am vorbit mai devreme despre compararea șirurilor, am văzut că operatorul ==
a efectuat o comparație ordinală a șirurilor. Deci, de ce în acest caz operatorul ==
efectuează o comparație care nu ține seama de majuscule și minuscule?
Răspunsul este că, atunci când obiectele de bază dintr-o instrucțiune LINQ sunt referințe la date de tabel SQL (cum este cazul obiectului Entity Framework DbSet din acest exemplu), instrucțiunea este convertită într-o instrucțiune T-SQL. Operatorii urmează apoi regulile de programare T-SQL, nu regulile de programare C#, astfel încât comparația în cazul de mai sus ajunge să nu țină seama de majuscule și minuscule.
În general, chiar dacă LINQ este o modalitate utilă și consecventă de a interoga colecții de obiecte, în realitate trebuie totuși să știți dacă declarația dvs. va fi sau nu tradusă în altceva decât C# sub capotă pentru a vă asigura că comportamentul codului dvs. va să fie așa cum era de așteptat în timpul execuției.
Greșeală comună de programare C# # 6: A fi confuz sau falsificat prin metodele de extensie
După cum am menționat mai devreme, instrucțiunile LINQ funcționează pe orice obiect care implementează IEnumerable. De exemplu, următoarea funcție simplă va aduna soldurile oricărei colecții de conturi:
public decimal SumAccounts(IEnumerable<Account> myAccounts) { return myAccounts.Sum(a => a.Balance); }
În codul de mai sus, tipul parametrului myAccounts este declarat ca IEnumerable<Account>
. Deoarece myAccounts
face referire la o metodă Sum
(C# folosește „notația de puncte” familiară pentru a face referire la o metodă pe o clasă sau interfață), ne-am aștepta să vedem o metodă numită Sum()
în definiția interfeței IEnumerable<T>
. Cu toate acestea, definiția lui IEnumerable<T>
nu face referire la nicio metodă Sum
și arată pur și simplu astfel:
public interface IEnumerable<out T> : IEnumerable { IEnumerator<T> GetEnumerator(); }
Deci, unde este definită metoda Sum()
? C# este tastat puternic, deci dacă referința la metoda Sum
ar fi nevalidă, compilatorul C# ar semnala cu siguranță o eroare. Știm așadar că trebuie să existe, dar unde? Mai mult, unde sunt definițiile tuturor celorlalte metode pe care LINQ le oferă pentru interogarea sau agregarea acestor colecții?
Răspunsul este că Sum()
nu este o metodă definită pe interfața IEnumerable
. Mai degrabă, este o metodă statică (numită „metodă de extensie”) care este definită în clasa 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); ... } }
Deci, ce face o metodă de extensie diferită de orice altă metodă statică și ce ne permite să o accesăm în alte clase?
Caracteristica distinctivă a unei metode de extensie este modificatorul this
pe primul său parametru. Aceasta este „magia” care o identifică la compilator ca o metodă de extensie. Tipul parametrului pe care îl modifică (în acest caz IEnumerable<TSource>
) denotă clasa sau interfața care va apărea apoi pentru a implementa această metodă.
(Ca punct secundar, nu este nimic magic în legătură cu asemănarea dintre numele interfeței IEnumerable
și numele clasei Enumerable
pe care este definită metoda de extensie. Această asemănare este doar o alegere stilistică arbitrară.)
Cu această înțelegere, putem vedea, de asemenea, că funcția sumAccounts
pe care am introdus-o mai sus ar fi putut fi implementată după cum urmează:

public decimal SumAccounts(IEnumerable<Account> myAccounts) { return Enumerable.Sum(myAccounts, a => a.Balance); }
Faptul că l-am fi putut implementa în acest fel ridică întrebarea de ce să avem metode de extensie? Metodele de extensie sunt, în esență, o comoditate a limbajului de programare C# care vă permite să „adăugați” metode la tipurile existente fără a crea un nou tip derivat, a recompila sau a modifica în alt mod tipul original.
Metodele de extensie sunt aduse în domeniu prin includerea unei using [namespace];
declarație în partea de sus a fișierului. Trebuie să știți ce spațiu de nume C# include metodele de extensie pe care le căutați, dar acest lucru este destul de ușor de determinat odată ce știți ce căutați.
Când compilatorul C# întâlnește un apel de metodă pe o instanță a unui obiect și nu găsește acea metodă definită în clasa de obiecte la care se face referire, apoi se uită la toate metodele de extensie care sunt în domeniu pentru a încerca să găsească una care se potrivește cu metoda necesară semnătură și clasă. Dacă găsește unul, va trece referința instanței ca prim argument la acea metodă de extensie, apoi restul argumentelor, dacă există, vor fi transmise ca argumente ulterioare metodei de extensie. (Dacă compilatorul C# nu găsește nicio metodă de extensie corespunzătoare în domeniul de aplicare, va arunca o eroare.)
Metodele de extensie sunt un exemplu de „zahăr sintactic” din partea compilatorului C#, care ne permite să scriem cod care este (de obicei) mai clar și mai ușor de întreținut. Mai clar, adică dacă ești conștient de utilizarea lor. În caz contrar, poate fi puțin confuz, mai ales la început.
Deși cu siguranță există avantaje în utilizarea metodelor de extensie, acestea pot cauza probleme și un strigăt pentru ajutor de programare C# pentru acei dezvoltatori care nu le cunosc sau nu le înțeleg în mod corespunzător. Acest lucru este valabil mai ales când se uită la mostre de cod online sau la orice alt cod pre-scris. Când un astfel de cod produce erori de compilator (deoarece invocă metode care în mod clar nu sunt definite pe clasele pe care sunt invocate), tendința este de a crede că codul se aplică unei versiuni diferite a bibliotecii sau unei biblioteci cu totul diferite. Se poate petrece mult timp căutând o nouă versiune sau o „biblioteca lipsă” fantomă care nu există.
Chiar și dezvoltatorii care sunt familiarizați cu metodele de extensie sunt încă prinși ocazional, când există o metodă cu același nume pe obiect, dar semnătura metodei acesteia diferă într-un mod subtil de cea a metodei de extensie. Se poate pierde mult timp căutând o greșeală de tipar sau o eroare care pur și simplu nu există.
Utilizarea metodelor de extensie în bibliotecile C# devine din ce în ce mai răspândită. Pe lângă LINQ, Unity Application Block și cadrul Web API sunt exemple de două biblioteci moderne foarte utilizate de Microsoft, care folosesc și metode de extensie și există multe altele. Cu cât cadrul este mai modern, cu atât este mai probabil să încorporeze metode de extensie.
Desigur, puteți scrie și propriile metode de extensie. Realizați, totuși, că, în timp ce metodele de extensie par să fie invocate la fel ca metodele de instanță obișnuite, aceasta este într-adevăr doar o iluzie. În special, metodele dvs. de extensie nu pot face referire la membrii privați sau protejați ai clasei pe care o extind și, prin urmare, nu pot servi ca înlocuitor complet pentru moștenirea de clasă mai tradițională.
Greșeală comună de programare C# #7: Folosirea unui tip greșit de colecție pentru sarcina în cauză
C# oferă o mare varietate de obiecte de colecție, următoarele fiind doar o listă parțială:
Array
, ArrayList
, BitArray
, BitVector32
, Dictionary<K,V>
, HashTable
, HybridDictionary
, List<T>
, NameValueCollection
, OrderedDictionary
, Queue, Queue<T>
, SortedList
, Stack, Stack<T>
, StringCollection
, StringDictionary
, .
Deși pot exista cazuri în care prea multe alegeri sunt la fel de proaste ca și nu sunt suficiente, nu este cazul obiectelor de colecție. Numărul de opțiuni disponibile poate funcționa cu siguranță în avantajul tău. Acordați-vă puțin timp în plus pentru cercetare și alegeți tipul de colecție optim pentru scopul dvs. Probabil va avea ca rezultat performanțe mai bune și mai puțin spațiu pentru erori.
Dacă există un tip de colecție care vizează în mod special tipul de element pe care îl aveți (cum ar fi șir sau bit), înclinați-vă spre utilizarea acestuia mai întâi. Implementarea este în general mai eficientă atunci când este direcționată către un anumit tip de element.
Pentru a profita de siguranța de tip C#, de obicei ar trebui să preferați o interfață generică față de una negenerică. Elementele unei interfețe generice sunt de tipul pe care îl specificați atunci când vă declarați obiectul, în timp ce elementele interfețelor negenerice sunt de tip obiect. Când utilizați o interfață non-generică, compilatorul C# nu vă poate verifica codul. De asemenea, atunci când aveți de-a face cu colecții de tipuri de valori primitive, utilizarea unei colecții negenerice va avea ca rezultat introducerea/unboxingul repetat a acestor tipuri, ceea ce poate avea ca rezultat un impact negativ semnificativ asupra performanței în comparație cu o colecție generică de tipul adecvat.
O altă problemă comună C# este să scrieți propriul obiect de colecție. Asta nu înseamnă că nu este niciodată potrivit, dar cu o selecție la fel de cuprinzătoare precum cea oferită de .NET, probabil că poți economisi mult timp utilizând sau extinzând una care există deja, mai degrabă decât reinventând roata. În special, C5 Generic Collection Library pentru C# și CLI oferă o gamă largă de colecții suplimentare „din cutie”, cum ar fi structuri de date arbore persistente, cozi de prioritate bazate pe heap, liste de matrice indexate hash, liste legate și multe altele.
Greșeala comună de programare C# #8: neglijarea resurselor gratuite
Mediul CLR folosește un colector de gunoi, așa că nu trebuie să eliberați în mod explicit memoria creată pentru orice obiect. De fapt, nu poți. Nu există un echivalent al operatorului de delete
C++ sau al funcției free()
în C . Dar asta nu înseamnă că poți uita de toate obiectele după ce le-ai terminat de folosit. Multe tipuri de obiecte încapsulează un alt tip de resursă de sistem (de exemplu, un fișier de disc, conexiune la bază de date, soclu de rețea etc.). Lăsarea acestor resurse deschise poate epuiza rapid numărul total de resurse de sistem, degradând performanța și ducând în cele din urmă la erori de program.
În timp ce o metodă de distrugere poate fi definită pe orice clasă C#, problema cu destructorii (numiți și finalizatori în C#) este că nu puteți ști cu siguranță când vor fi apelați. Ele sunt apelate de colectorul de gunoi (pe un fir separat, care poate provoca complicații suplimentare) la un moment nedeterminat în viitor. Încercarea de a ocoli aceste limitări forțând colectarea gunoiului cu GC.Collect()
nu este o bună practică C#, deoarece aceasta va bloca firul de execuție pentru o perioadă necunoscută de timp în timp ce colectează toate obiectele eligibile pentru colectare.
Acest lucru nu înseamnă că nu există utilizări bune pentru finalizatoare, dar eliberarea resurselor într-un mod determinist nu este una dintre ele. Mai degrabă, atunci când operați pe o conexiune de fișier, rețea sau bază de date, doriți să eliberați în mod explicit resursa de bază imediat ce ați terminat cu ea.
Scurgerile de resurse sunt o preocupare în aproape orice mediu. Cu toate acestea, C# oferă un mecanism robust și simplu de utilizat care, dacă este utilizat, poate face scurgerile o apariție mult mai rară. Cadrul .NET definește interfața IDisposable
, care constă exclusiv din metoda Dispose()
. Orice obiect care implementează IDisposable
se așteaptă să aibă acea metodă apelată ori de câte ori consumatorul obiectului termină de manipulat. Acest lucru are ca rezultat o eliberare explicită, deterministă a resurselor.
Dacă creați și eliminați un obiect în contextul unui singur bloc de cod, este practic inexcusabil să uitați să apelați Dispose()
, deoarece C# oferă o instrucțiune using
care va asigura că Dispose()
este apelat indiferent de modul în care blocul de cod. este ieșit (fie că este o excepție, o instrucțiune de returnare sau pur și simplu închiderea blocului). Și da, este aceeași declarație de using
menționată anterior, care este folosită pentru a include spații de nume C# în partea de sus a fișierului. Are un al doilea scop, complet fără legătură, de care mulți dezvoltatori C# nu sunt conștienți; și anume, pentru a ne asigura că Dispose()
este apelat pe un obiect atunci când blocul de cod este ieșit:
using (FileStream myFile = File.OpenRead("foo.txt")) { myFile.Read(buffer, 0, 100); }
Prin crearea unui bloc de using
în exemplul de mai sus, știți sigur că myFile.Dispose()
va fi apelat de îndată ce ați terminat cu fișierul, indiferent dacă Read()
aruncă sau nu o excepție.
Greșeala obișnuită de programare în C# # 9: Evitați excepții
C# își continuă aplicarea siguranței de tip în timpul de execuție. Acest lucru vă permite să identificați multe tipuri de erori în C# mult mai rapid decât în limbaje precum C++, unde conversiile de tip defecte pot duce la alocarea de valori arbitrare câmpurilor unui obiect. Cu toate acestea, din nou, programatorii pot risipi această caracteristică grozavă, ceea ce duce la probleme C#. Ei cad în această capcană deoarece C# oferă două moduri diferite de a face lucrurile, unul care poate arunca o excepție și unul care nu. Unii se vor sfii de ruta de excepție, imaginându-și că nu trebuie să scrie un bloc try/catch îi salvează ceva de codare.
De exemplu, iată două moduri diferite de a efectua o distribuție de tip explicit în 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;
Cea mai evidentă eroare care ar putea apărea cu utilizarea Metodei 2 ar fi eșecul verificării valorii returnate. Acest lucru ar duce probabil la o eventuală NullReferenceException, care ar putea apărea într-un moment mult mai târziu, făcând mult mai dificilă depistarea sursei problemei. În schimb, metoda 1 ar fi aruncat imediat o InvalidCastException
, făcând sursa problemei mult mai evidentă imediat.
Mai mult, chiar dacă vă amintiți să verificați valoarea returnată în Metoda 2, ce veți face dacă găsiți că este nulă? Metoda pe care o scrieți este un loc potrivit pentru a raporta o eroare? Mai poți încerca ceva dacă distribuția nu reușește? Dacă nu, atunci aruncarea unei excepții este lucrul corect de făcut, așa că la fel de bine ați lăsa să se întâmple cât mai aproape de sursa problemei.
Iată câteva exemple de alte perechi comune de metode în care una aruncă o excepție, iar cealaltă nu:
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
Unii dezvoltatori C# sunt atât de „defavorizați la excepție” încât presupun automat că metoda care nu aruncă o excepție este superioară. Deși există anumite cazuri selecte în care acest lucru poate fi adevărat, nu este deloc corect ca o generalizare.
Ca exemplu specific, într-un caz în care aveți o acțiune alternativă legitimă (de exemplu, implicită) de luat în cazul în care ar fi fost generată o excepție, atunci abordarea fără excepție ar putea fi o alegere legitimă. Într-un astfel de caz, poate fi într-adevăr mai bine să scrieți ceva de genul acesta:
if (int.TryParse(myString, out myInt)) { // use myInt } else { // use default value }
în loc de:
try { myInt = int.Parse(myString); // use myInt } catch (FormatException) { // use default value }
Cu toate acestea, este incorect să presupunem că TryParse
este, prin urmare, în mod necesar metoda „mai bună”. Uneori așa este, alteori nu. De aceea există două moduri de a face acest lucru. Folosește-l pe cel corect pentru contextul în care te afli, amintindu-ți că excepțiile pot fi cu siguranță prietenul tău ca dezvoltator.
Greșeală comună de programare C# #10: Permiterea avertizărilor compilatorului să se acumuleze
Deși această problemă cu siguranță nu este specifică C#, este deosebit de flagrantă în programarea C#, deoarece renunță la beneficiile verificării stricte de tip oferite de compilatorul C#.
Avertismentele sunt generate pentru un motiv. În timp ce toate erorile compilatorului C# semnifică un defect în codul dvs., multe avertismente fac și ele. Ceea ce le diferențiază pe cele două este că, în cazul unui avertisment, compilatorul nu are nicio problemă să emită instrucțiunile pe care le reprezintă codul tău. Chiar și așa, codul dvs. găsește puțin neplăcut și există o probabilitate rezonabilă ca codul dvs. să nu reflecte cu acuratețe intenția dvs.
Un exemplu simplu comun de dragul acestui tutorial de programare C# este atunci când vă modificați algoritmul pentru a elimina utilizarea unei variabile pe care o utilizați, dar uitați să eliminați declarația variabilei. Programul va rula perfect, dar compilatorul va semnaliza declarația de variabilă inutilă. Faptul că programul rulează perfect îi face pe programatori să neglijeze să remedieze cauza avertismentului. În plus, codificatorii profită de o caracteristică Visual Studio care le facilitează ascunderea avertismentelor în fereastra „Lista de erori”, astfel încât să se poată concentra numai asupra erorilor. Nu durează mult până când apar zeci de avertismente, toate ignorate cu fericire (sau și mai rău, ascunse).
Dar dacă ignori acest tip de avertisment, mai devreme sau mai târziu, ceva de genul acesta s-ar putea foarte bine să-și găsească drumul în codul tău:
class Account { int myId; int Id; // compiler warned you about this, but you didn't listen! // Constructor Account(int id) { this.myId = Id; // OOPS! } }
Și la viteza pe care Intellisense ne permite să scriem cod, această eroare nu este atât de improbabilă pe cât pare.
Acum aveți o eroare gravă în programul dvs. (deși compilatorul a semnalat-o doar ca un avertisment, din motivele deja explicate) și, în funcție de cât de complex este programul dvs., puteți pierde mult timp urmărindu-l pe acesta. Dacă ați fi acordat atenție acestui avertisment în primul rând, ați fi evitat această problemă cu o simplă remediere de cinci secunde.
Amintiți-vă, compilatorul C Sharp vă oferă o mulțime de informații utile despre robustețea codului dvs.... dacă ascultați. Nu ignora avertismentele. De obicei, durează doar câteva secunde pentru a remedia, iar repararea altora noi atunci când se întâmplă vă poate economisi ore. Antrenați-vă să vă așteptați ca fereastra „Lista de erori” din Visual Studio să afișeze „0 Erori, 0 Avertismente”, astfel încât orice avertismente să vă facă suficient de inconfortabil pentru a le aborda imediat.
Desigur, există excepții de la fiecare regulă. În consecință, pot exista momente în care codul tău va arăta puțin neplăcut pentru compilator, chiar dacă este exact așa cum ai vrut să fie. În acele cazuri foarte rare, utilizați #pragma warning disable [warning id]
numai în jurul codului care declanșează avertismentul și numai pentru ID-ul avertismentului pe care îl declanșează. Acest lucru va suprima acel avertisment și numai acel avertisment, astfel încât să puteți rămâne în continuare alert pentru altele noi.
Învelire
C# este un limbaj puternic și flexibil, cu multe mecanisme și paradigme care pot îmbunătăți considerabil productivitatea. Ca și în cazul oricărui instrument sau limbaj software, totuși, a avea o înțelegere sau o apreciere limitată a capacităților sale poate fi uneori mai mult un impediment decât un beneficiu, lăsându-l în starea proverbială de „a ști suficient pentru a fi periculos”.
Utilizarea unui tutorial C Sharp ca acesta pentru a vă familiariza cu nuanțele cheie ale C#, cum ar fi (dar nu limitat la) problemele ridicate în acest articol, va ajuta la optimizarea C# evitând în același timp unele dintre capcanele sale mai frecvente ale limba.
Citiți suplimentare pe blogul Toptal Engineering:
- Întrebări esențiale pentru interviu C#
- C# vs. C++: Ce este la bază?