Błędny kod C#: 10 najczęstszych błędów w programowaniu C#

Opublikowany: 2022-03-11

O C Sharp

C# jest jednym z kilku języków przeznaczonych dla środowiska uruchomieniowego języka wspólnego firmy Microsoft (CLR). Języki przeznaczone dla środowiska CLR korzystają z funkcji, takich jak integracja między językami i obsługa wyjątków, ulepszone zabezpieczenia, uproszczony model interakcji ze składnikami oraz usługi debugowania i profilowania. Spośród współczesnych języków CLR C# jest najczęściej używany w złożonych, profesjonalnych projektach programistycznych, które są przeznaczone dla środowisk desktopowych, mobilnych lub serwerowych z systemem Windows.

C# to zorientowany obiektowo, silnie typizowany język. Ścisłe sprawdzanie typu w C#, zarówno w czasie kompilacji, jak i wykonywania, powoduje, że większość typowych błędów programowania C# jest zgłaszanych tak wcześnie, jak to możliwe, a ich lokalizacje są dość dokładnie określone. Może to zaoszczędzić dużo czasu w programowaniu w C Sharp, w porównaniu do śledzenia przyczyny zagadkowych błędów, które mogą wystąpić długo po tym, jak obraźliwa operacja ma miejsce w językach, które są bardziej liberalne pod względem egzekwowania bezpieczeństwa typów. Jednak wielu programistów języka C# nieświadomie (lub nieostrożnie) odrzuca korzyści płynące z tego wykrywania, co prowadzi do niektórych problemów omówionych w tym samouczku C#.

O tym samouczku programowania C Sharp

Ten samouczek opisuje 10 najczęściej popełnianych błędów programowania C# lub problemów, których należy unikać, przez programistów C# i zapewnia im pomoc.

Chociaż większość błędów omówionych w tym artykule dotyczy języka C#, niektóre dotyczą również innych języków, które są przeznaczone dla środowiska CLR lub korzystają z biblioteki klas Framework (FCL).

Typowy błąd programowania w języku C# nr 1: Używanie odwołania, takiego jak wartość lub na odwrót

Programiści C++ i wielu innych języków są przyzwyczajeni do kontrolowania tego, czy wartości, które przypisują zmiennym, są po prostu wartościami, czy też odniesieniami do istniejących obiektów. Jednak w programowaniu w C Sharp decyzja ta jest podejmowana przez programistę, który napisał obiekt, a nie przez programistę, który tworzy instancję obiektu i przypisuje go do zmiennej. Jest to częsta „uwaga” dla tych, którzy próbują nauczyć się programowania w C#.

Jeśli nie wiesz, czy obiekt, którego używasz, jest typem wartości, czy typem referencyjnym, możesz napotkać kilka niespodzianek. Na przykład:

 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

Jak widać, oba obiekty Point i Pen zostały utworzone dokładnie w ten sam sposób, ale wartość point1 pozostała niezmieniona, gdy nowa wartość współrzędnej X została przypisana do point2 , podczas gdy wartość pen1 została zmodyfikowana, gdy nowy kolor został przypisany do pen2 . Możemy zatem wywnioskować , że point1 i point2 zawierają własną kopię obiektu Point , podczas gdy pen1 i pen2 zawierają odniesienia do tego samego obiektu Pen . Ale skąd możemy to wiedzieć bez przeprowadzenia tego eksperymentu?

Odpowiedzią jest przyjrzenie się definicjom typów obiektów (co można łatwo zrobić w Visual Studio, umieszczając kursor nad nazwą typu obiektu i naciskając klawisz F12):

 public struct Point { ... } // defines a “value” type public class Pen { ... } // defines a “reference” type

Jak pokazano powyżej, w programowaniu w języku C# słowo kluczowe struct służy do definiowania typu wartości, a słowo kluczowe class służy do definiowania typu referencyjnego. Dla osób z doświadczeniem w C++, których ukołysało fałszywe poczucie bezpieczeństwa przez wiele podobieństw między słowami kluczowymi C++ i C#, to zachowanie prawdopodobnie jest niespodzianką, która może sprawić, że poprosisz o pomoc z samouczka C#.

Jeśli zamierzasz polegać na zachowaniu, które różni się w zależności od typu wartości i typu referencyjnego — na przykład możliwości przekazania obiektu jako parametru metody i zmiany przez tę metodę stanu obiektu — upewnij się, że masz do czynienia z poprawny typ obiektu, aby uniknąć problemów z programowaniem C#.

Powszechny błąd programowania w języku C# nr 2: Niezrozumienie wartości domyślnych dla niezainicjowanych zmiennych

W języku C# typy wartości nie mogą mieć wartości null. Z definicji typy wartości mają wartość, a nawet niezainicjowane zmienne typów wartości muszą mieć wartość. Nazywa się to wartością domyślną dla tego typu. Prowadzi to do następującego, zwykle nieoczekiwanego wyniku podczas sprawdzania, czy zmienna jest niezainicjowana:

 class Program { static Point point1; static Pen pen1; static void Main(string[] args) { Console.WriteLine(pen1 == null); // True Console.WriteLine(point1 == null); // False (huh?) } }

Dlaczego point1 nie ma wartości null? Odpowiedź brzmi, że Point jest typem wartości, a domyślna wartość Point to (0,0), a nie null. Nierozpoznanie tego jest bardzo łatwym (i powszechnym) błędem do popełnienia w C#.

Wiele (ale nie wszystkie) typów wartości ma właściwość IsEmpty , którą można sprawdzić, czy jest ona równa jej wartości domyślnej:

 Console.WriteLine(point1.IsEmpty); // True

Kiedy sprawdzasz, czy zmienna została zainicjowana, czy nie, upewnij się, że wiesz, jaką wartość będzie miała domyślnie niezainicjowana zmienna tego typu i nie polegaj na jej wartości null.

Częsty błąd programowania w języku C# nr 3: Używanie niewłaściwych lub nieokreślonych metod porównywania ciągów

Istnieje wiele różnych sposobów porównywania ciągów w C#.

Chociaż wielu programistów używa operatora == do porównywania ciągów, w rzeczywistości jest to jedna z najmniej pożądanych metod do zastosowania, głównie dlatego, że nie określa wyraźnie w kodzie, jaki typ porównania jest pożądany.

Zamiast tego preferowanym sposobem testowania równości ciągów w programowaniu C# jest metoda Equals :

 public bool Equals(string value); public bool Equals(string value, StringComparison comparisonType);

Pierwsza sygnatura metody (tj. bez parametru comparisonType ) jest w rzeczywistości taka sama, jak użycie operatora == , ale ma tę zaletę, że jest jawnie stosowana do łańcuchów. Wykonuje porządkowe porównanie ciągów, które jest w zasadzie porównaniem bajt po bajcie. W wielu przypadkach jest to dokładnie taki typ porównania, jaki chcesz, zwłaszcza podczas porównywania ciągów, których wartości są ustawione programowo, takie jak nazwy plików, zmienne środowiskowe, atrybuty itp. W takich przypadkach, o ile porównanie porządkowe jest rzeczywiście właściwym typem porównania dla tej sytuacji, jedyną wadą korzystania z metody Equals bez comparisonType jest to, że ktoś czytający kod może nie wiedzieć, jakiego typu porównania dokonujesz.

Użycie sygnatury metody Equals , która zawiera typ ComparisonType za każdym razem, gdy comparisonType ciągi, nie tylko sprawi, że kod stanie się wyraźniejszy, ale także sprawi, że wyraźnie zastanowisz się, jaki typ porównania musisz wykonać. Warto to zrobić, ponieważ nawet jeśli angielski może nie zapewniać wielu różnic między porównaniami porządkowymi i wrażliwymi na kulturę, inne języki zapewniają ich wiele, a ignorowanie możliwości innych języków otwiera się na duży potencjał dla błędy w drodze. Na przykład:

 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));

Najbezpieczniejszą praktyką jest zawsze dostarczanie parametru comparisonType do metody Equals . Oto kilka podstawowych wskazówek:

  • Podczas porównywania ciągów, które zostały wprowadzone przez użytkownika lub mają być wyświetlane użytkownikowi, użyj porównania uwzględniającego kulturę ( CurrentCulture lub CurrentCultureIgnoreCase ).
  • Podczas porównywania ciągów programistycznych użyj porównania porządkowego ( Ordinal lub OrdinalIgnoreCase ).
  • InvariantCulture i InvariantCultureIgnoreCase generalnie nie mogą być używane z wyjątkiem bardzo ograniczonych okoliczności, ponieważ porównania porządkowe są bardziej wydajne. Jeśli konieczne jest porównanie uwzględniające kulturę, zwykle należy je przeprowadzić względem bieżącej kultury lub innej określonej kultury.

Oprócz metody Equals ciągi udostępniają również metodę Compare , która zapewnia informacje o względnej kolejności ciągów zamiast tylko testu równości. Ta metoda jest lepsza niż operatory < , <= , > i >= z tych samych powodów, co omówiono powyżej — aby uniknąć problemów języka C#.

Powiązane: 12 podstawowych pytań do rozmowy kwalifikacyjnej .NET

Typowy błąd programowania w języku C# nr 4: Używanie instrukcji iteracyjnych (zamiast deklaratywnych) do manipulowania kolekcjami

W języku C# 3,0 dodanie zapytania zintegrowanego z językiem (LINQ) do języka zmieniło na zawsze sposób, w jaki kolekcje są odpytywane i manipulowane. Od tego czasu, jeśli używasz instrukcji iteracyjnych do manipulowania kolekcjami, nie używałeś LINQ, kiedy prawdopodobnie powinieneś.

Niektórzy programiści C# nawet nie wiedzą o istnieniu LINQ, ale na szczęście liczba ta staje się coraz mniejsza. Wielu nadal uważa jednak, że ze względu na podobieństwo między słowami kluczowymi LINQ i instrukcjami SQL, jego jedynym zastosowaniem jest kod, który wysyła zapytania do baz danych.

Podczas gdy zapytania do bazy danych są bardzo powszechnym użyciem instrukcji LINQ, w rzeczywistości działają one nad każdą wyliczalną kolekcją (tj. dowolnym obiektem, który implementuje interfejs IEnumerable). Na przykład, jeśli masz tablicę kont, zamiast pisać listę C# foreach:

 decimal total = 0; foreach (Account account in myAccounts) { if (account.Status == "active") { total += account.Balance; } }

możesz po prostu napisać:

 decimal total = (from account in myAccounts where account.Status == "active" select account.Balance).Sum();

Chociaż jest to dość prosty przykład, jak uniknąć tego typowego problemu z programowaniem w języku C#, istnieją przypadki, w których pojedyncza instrukcja LINQ może łatwo zastąpić dziesiątki instrukcji w pętli iteracyjnej (lub zagnieżdżonych pętli) w kodzie. A mniej ogólnego kodu oznacza mniej możliwości wprowadzenia błędów. Pamiętaj jednak, że może wystąpić kompromis pod względem wydajności. W scenariuszach krytycznych dla wydajności, zwłaszcza gdy kod iteracyjny może przyjmować założenia dotyczące kolekcji, których LINQ nie może, pamiętaj o przeprowadzeniu porównania wydajności między dwiema metodami.

Typowy błąd programowania w języku C# nr 5: brak uwzględnienia obiektów bazowych w instrukcji LINQ

LINQ doskonale nadaje się do abstrahowania zadania manipulowania kolekcjami, niezależnie od tego, czy są to obiekty w pamięci, tabele bazy danych czy dokumenty XML. W idealnym świecie nie musiałbyś wiedzieć, jakie są leżące pod nim obiekty. Ale tutaj błąd polega na założeniu, że żyjemy w idealnym świecie. W rzeczywistości identyczne instrukcje LINQ mogą zwracać różne wyniki, gdy są wykonywane na dokładnie tych samych danych, jeśli te dane są w innym formacie.

Rozważmy na przykład następujące stwierdzenie:

 decimal total = (from account in myAccounts where account.Status == "active" select account.Balance).Sum();

Co się stanie, jeśli jedno z account.Status obiektu.Status jest równy „Aktywny” (zwróć uwagę na duże A)? Cóż, jeśli myAccounts było obiektem DbSet (który został skonfigurowany z domyślną konfiguracją nieuwzględniającą wielkości liter), wyrażenie where nadal będzie pasować do tego elementu. Jeśli jednak myAccounts znajdowało się w tablicy w pamięci, nie pasowałby, a zatem dałby inny wynik dla sumy.

Ale poczekaj chwilę. Kiedy mówiliśmy wcześniej o porównaniu ciągów, widzieliśmy, że operator == wykonuje porządkowe porównanie ciągów. Dlaczego więc w tym przypadku operator == wykonuje porównanie bez uwzględniania wielkości liter?

Odpowiedź brzmi, że gdy obiekty bazowe w instrukcji LINQ są odwołaniami do danych tabeli SQL (tak jak w przypadku obiektu Entity Framework DbSet w tym przykładzie), instrukcja jest konwertowana na instrukcję T-SQL. Operatory następnie postępują zgodnie z regułami programowania T-SQL, a nie regułami programowania C#, więc porównanie w powyższym przypadku nie uwzględnia wielkości liter.

Ogólnie rzecz biorąc, mimo że LINQ jest pomocnym i spójnym sposobem wykonywania zapytań o kolekcje obiektów, w rzeczywistości nadal musisz wiedzieć, czy Twoja instrukcja zostanie przetłumaczona na coś innego niż C# pod maską, aby upewnić się, że zachowanie Twojego kodu będzie być zgodnie z oczekiwaniami w czasie wykonywania.

Częsty błąd programowania w języku C# nr 6: Pomylenie lub sfałszowanie przez metody rozszerzające

Jak wspomniano wcześniej, instrukcje LINQ działają na dowolnym obiekcie, który implementuje IEnumerable. Na przykład następująca prosta funkcja zsumuje salda na dowolnym zestawie rachunków:

 public decimal SumAccounts(IEnumerable<Account> myAccounts) { return myAccounts.Sum(a => a.Balance); }

W powyższym kodzie typ parametru myAccounts jest zadeklarowany jako IEnumerable<Account> . Ponieważ myAccounts odwołuje się do metody Sum (C# używa znanej „notacji kropkowej” do odwoływania się do metody w klasie lub interfejsie), spodziewalibyśmy się zobaczyć metodę o nazwie Sum() w definicji interfejsu IEnumerable<T> . Jednak definicja IEnumerable<T> , nie odwołuje się do żadnej metody Sum i wygląda po prostu tak:

 public interface IEnumerable<out T> : IEnumerable { IEnumerator<T> GetEnumerator(); }

Więc gdzie jest zdefiniowana metoda Sum() ? C# jest silnie wpisany, więc jeśli odwołanie do metody Sum było nieprawidłowe, kompilator C# z pewnością oznaczyłby to jako błąd. Wiemy więc, że musi istnieć, ale gdzie? Co więcej, gdzie są definicje wszystkich innych metod, które LINQ zapewnia do wykonywania zapytań lub agregowania tych kolekcji?

Odpowiedź brzmi, że Sum() nie jest metodą zdefiniowaną w interfejsie IEnumerable . Jest to raczej metoda statyczna (zwana „metodą rozszerzającą”), która jest zdefiniowana w klasie 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); ... } }

Czym więc metoda rozszerzająca różni się od innych metod statycznych i co umożliwia nam dostęp do niej w innych klasach?

Cechą wyróżniającą metodę rozszerzającą jest this modyfikator pierwszego parametru. Jest to „magia”, która identyfikuje ją w kompilatorze jako metodę rozszerzającą. Typ parametru, który modyfikuje (w tym przypadku IEnumerable<TSource> ) wskazuje klasę lub interfejs, który następnie pojawi się w celu zaimplementowania tej metody.

(Na marginesie nie ma nic magicznego w podobieństwie między nazwą interfejsu IEnumerable a nazwą klasy Enumerable , w której zdefiniowana jest metoda rozszerzająca. To podobieństwo jest tylko arbitralnym wyborem stylistycznym.)

Mając to na uwadze, możemy również zobaczyć, że wprowadzona powyżej funkcja sumAccounts mogła zamiast tego zostać zaimplementowana w następujący sposób:

 public decimal SumAccounts(IEnumerable<Account> myAccounts) { return Enumerable.Sum(myAccounts, a => a.Balance); }

Fakt, że moglibyśmy zaimplementować to w ten sposób, rodzi pytanie, dlaczego w ogóle mamy metody rozszerzające? Metody rozszerzające są zasadniczo wygodą języka programowania C#, która umożliwia "dodawanie" metod do istniejących typów bez tworzenia nowego typu pochodnego, ponownej kompilacji lub innego modyfikowania oryginalnego typu.

Metody rozszerzające są wprowadzane do zakresu poprzez uwzględnienie using [namespace]; oświadczenie u góry pliku. Musisz wiedzieć, która przestrzeń nazw C# zawiera metody rozszerzenia, których szukasz, ale jest to dość łatwe do ustalenia, gdy już wiesz, czego szukasz.

Gdy kompilator C# napotka wywołanie metody na wystąpieniu obiektu i nie znajdzie metody zdefiniowanej w klasie obiektu, do którego się odwołuje, następnie sprawdza wszystkie metody rozszerzające, które znajdują się w zakresie, aby spróbować znaleźć taką, która pasuje do wymaganej metody podpis i klasa. Jeśli go znajdzie, przekaże odwołanie do wystąpienia jako pierwszy argument do tej metody rozszerzającej, a pozostałe argumenty, jeśli istnieją, zostaną przekazane jako kolejne argumenty do metody rozszerzającej. (Jeśli kompilator C# nie znajdzie żadnej odpowiedniej metody rozszerzenia w zakresie, zgłosi błąd).

Metody rozszerzające są przykładem „cukru składniowego” ze strony kompilatora C#, który pozwala nam pisać kod (zazwyczaj) jaśniejszy i łatwiejszy w utrzymaniu. Wyraźniejsze, to znaczy, jeśli jesteś świadomy ich użycia. W przeciwnym razie może to być nieco mylące, zwłaszcza na początku.

Chociaż z pewnością istnieją zalety korzystania z metod rozszerzających, mogą one powodować problemy i wołać o pomoc programistyczną w języku C# dla tych programistów, którzy nie są ich świadomi lub nie rozumieją ich właściwie. Jest to szczególnie widoczne w przypadku przeglądania próbek kodu online lub dowolnego innego wstępnie napisanego kodu. Kiedy taki kod generuje błędy kompilatora (ponieważ wywołuje metody, które wyraźnie nie są zdefiniowane w klasach, w których są wywoływane), istnieje tendencja do myślenia, że ​​kod dotyczy innej wersji biblioteki lub zupełnie innej biblioteki. Dużo czasu można poświęcić na szukanie nowej wersji lub widmowej „brakującej biblioteki”, która nie istnieje.

Nawet programiści, którzy są zaznajomieni z metodami rozszerzającymi, wciąż czasami dają się złapać, gdy istnieje metoda o tej samej nazwie w obiekcie, ale jej sygnatura metody różni się w subtelny sposób od sygnatury metody rozszerzającej. Wiele czasu można zmarnować na szukanie literówki lub błędu, którego po prostu nie ma.

Korzystanie z metod rozszerzających w bibliotekach C# staje się coraz bardziej powszechne. Oprócz LINQ, Unity Application Block i framework Web API są przykładami dwóch intensywnie używanych nowoczesnych bibliotek firmy Microsoft, które również korzystają z metod rozszerzających, i jest wiele innych. Im bardziej nowoczesny framework, tym większe prawdopodobieństwo, że będzie zawierał metody rozszerzające.

Oczywiście możesz również napisać własne metody rozszerzające. Zdaj sobie jednak sprawę, że chociaż metody rozszerzające wydają się być wywoływane tak jak zwykłe metody instancji, w rzeczywistości jest to tylko złudzenie. W szczególności metody rozszerzające nie mogą odwoływać się do prywatnych lub chronionych elementów członkowskich klasy, którą rozszerzają, i dlatego nie mogą służyć jako kompletny zamiennik bardziej tradycyjnego dziedziczenia klas.

Częsty błąd programowania w języku C# nr 7: użycie niewłaściwego typu kolekcji dla danego zadania

C# zapewnia dużą różnorodność obiektów kolekcji, a poniższe są tylko częściową listą:
Array , ArrayList , BitArray , BitVector32 , Dictionary<K,V> , HashTable , HybridDictionary , List<T> , NameValueCollection , OrderedDictionary , Queue, Queue<T> , SortedList , Stack, Stack<T> , StringCollection , StringDictionary .

Chociaż mogą wystąpić przypadki, w których zbyt wiele wyborów jest tak samo zły, jak niewystarczająca liczba wyborów, nie dotyczy to obiektów kolekcji. Liczba dostępnych opcji może z pewnością działać na Twoją korzyść. Poświęć trochę więcej czasu na wstępne badania i wybierz optymalny typ kolekcji do swoich celów. Prawdopodobnie spowoduje to lepszą wydajność i mniej miejsca na błędy.

Jeśli istnieje typ kolekcji ukierunkowany na typ elementu, który posiadasz (np. ciąg lub bit), skłaniaj się do używania tego w pierwszej kolejności. Implementacja jest ogólnie bardziej wydajna, gdy jest ukierunkowana na określony typ elementu.

Aby skorzystać z bezpieczeństwa typów C#, zwykle należy preferować interfejs ogólny niż nieogólny. Elementy interfejsu ogólnego są typu określonego podczas deklarowania obiektu, podczas gdy elementy interfejsów nieogólnych są typu object. W przypadku korzystania z interfejsu nieogólnego kompilator języka C# nie może sprawdzić typu kodu. Ponadto w przypadku kolekcji pierwotnych typów wartości użycie kolekcji nieogólnej spowoduje wielokrotne pakowanie/rozpakowywanie tych typów, co może skutkować znacznym negatywnym wpływem na wydajność w porównaniu z ogólną kolekcją odpowiedniego typu.

Innym powszechnym problemem języka C# jest napisanie własnego obiektu kolekcji. Nie oznacza to, że nigdy nie jest to właściwe, ale dzięki tak wszechstronnemu wyborowi, jaki oferuje .NET, prawdopodobnie możesz zaoszczędzić dużo czasu, używając lub rozszerzając już istniejący, zamiast odkrywać koło na nowo. W szczególności C5 Generic Collection Library dla C# i CLI oferuje szeroką gamę dodatkowych kolekcji „po wyjęciu z pudełka”, takich jak trwałe struktury danych drzewa, kolejki priorytetów oparte na stercie, listy tablic indeksowanych haszowaniem, listy połączone i wiele innych.

Częsty błąd programowania w języku C# nr 8: Zaniedbywanie wolnych zasobów

Środowisko CLR wykorzystuje garbage collector, więc nie musisz jawnie zwalniać pamięci utworzonej dla żadnego obiektu. W rzeczywistości nie możesz. Nie ma odpowiednika operatora delete C++ ani funkcji free() w C . Ale to nie znaczy, że możesz po prostu zapomnieć o wszystkich obiektach po zakończeniu ich używania. Wiele typów obiektów zawiera inny rodzaj zasobów systemowych (np. plik dyskowy, połączenie z bazą danych, gniazdo sieciowe itp.). Pozostawienie tych zasobów otwartych może szybko wyczerpać całkowitą liczbę zasobów systemowych, obniżając wydajność i ostatecznie prowadząc do błędów programu.

Chociaż metodę destruktora można zdefiniować w dowolnej klasie C#, problem z destruktorami (zwanymi również finalizatorami w C#) polega na tym, że nie można mieć pewności, kiedy zostaną wywołane. Wywoływane są przez garbage collector (na osobnym wątku, co może powodować dodatkowe komplikacje) w nieokreślonym czasie w przyszłości. Próba obejścia tych ograniczeń przez wymuszenie wyrzucania elementów bezużytecznych za pomocą funkcji GC.Collect() nie jest najlepszym rozwiązaniem C#, ponieważ spowoduje to zablokowanie wątku na nieznany czas podczas zbierania wszystkich obiektów kwalifikujących się do pobrania.

Nie oznacza to, że finalizatory nie mają dobrych zastosowań, ale zwalnianie zasobów w deterministyczny sposób nie jest jednym z nich. Zamiast tego, gdy pracujesz na połączeniu z plikiem, siecią lub bazą danych, chcesz jawnie zwolnić zasób źródłowy, gdy tylko z nim skończysz.

Wycieki zasobów są problemem w prawie każdym środowisku. Jednak C# zapewnia mechanizm, który jest solidny i prosty w użyciu, który, jeśli zostanie wykorzystany, może sprawić, że wycieki będą znacznie rzadsze. Platforma .NET definiuje interfejs IDisposable , który składa się wyłącznie z metody Dispose() . Każdy obiekt, który implementuje IDisposable , oczekuje, że ta metoda będzie wywoływana za każdym razem, gdy konsument obiektu zakończy manipulowanie nim. Powoduje to wyraźne, deterministyczne uwolnienie zasobów.

Jeśli tworzysz i usuwasz obiekt w kontekście pojedynczego bloku kodu, w zasadzie niewybaczalne jest zapomnienie o wywołaniu Dispose() , ponieważ C# zapewnia instrukcję using , która zapewni wywołanie Dispose( Dispose() bez względu na to, jak blok kodu jest zakończony (niezależnie od tego, czy jest to wyjątek, instrukcja return, czy po prostu zamknięcie bloku). I tak, jest to ta sama instrukcja using wspomniana wcześniej, która służy do dołączania przestrzeni nazw C# na górze pliku. Ma drugi, zupełnie niezwiązany cel, o którym wielu programistów C# nie zdaje sobie sprawy; mianowicie, aby upewnić się, że Dispose() zostanie wywołane na obiekcie, gdy blok kodu zostanie zakończony:

 using (FileStream myFile = File.OpenRead("foo.txt")) { myFile.Read(buffer, 0, 100); }

Tworząc blok using w powyższym przykładzie, wiesz na pewno, że myFile.Dispose() zostanie wywołane, gdy tylko skończysz z plikiem, niezależnie od tego, czy Read() zgłosi wyjątek.

Częsty błąd programowania w języku C# nr 9: Unikanie wyjątków

C# kontynuuje wymuszanie bezpieczeństwa typów w czasie wykonywania. Pozwala to znacznie szybciej wskazać wiele typów błędów w języku C# niż w językach takich jak C++, gdzie błędne konwersje typów mogą skutkować przypisaniem arbitralnych wartości do pól obiektu. Jednak po raz kolejny programiści mogą zmarnować tę wspaniałą funkcję, prowadząc do problemów z C#. Wpadają w tę pułapkę, ponieważ C# zapewnia dwa różne sposoby robienia rzeczy, jeden, który może zgłosić wyjątek, a drugi, który nie. Niektórzy unikają trasy wyjątków, dochodząc do wniosku, że brak konieczności pisania bloku try/catch oszczędza im trochę kodowania.

Na przykład, oto dwa różne sposoby wykonywania jawnego rzutowania typu w 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;

Najbardziej oczywistym błędem, jaki mógłby wystąpić przy użyciu Metody 2, jest niesprawdzenie zwracanej wartości. Spowodowałoby to prawdopodobnie wyjątek NullReferenceException, który mógłby pojawić się znacznie później, co znacznie utrudniłoby wyśledzenie źródła problemu. W przeciwieństwie do tego Metoda 1 natychmiast wyrzuciłaby InvalidCastException , dzięki czemu źródło problemu byłoby znacznie bardziej oczywiste.

Co więcej, nawet jeśli pamiętasz, aby sprawdzić wartość zwracaną w Metodzie 2, co zrobisz, jeśli uznasz, że jest ona pusta? Czy metoda, którą piszesz, jest odpowiednim miejscem do zgłoszenia błędu? Czy jest coś innego, co możesz spróbować, jeśli rzutowanie się nie powiedzie? Jeśli nie, rzucanie wyjątku jest właściwą rzeczą do zrobienia, więc równie dobrze możesz pozwolić, aby wydarzyło się tak blisko źródła problemu, jak to możliwe.

Oto kilka przykładów innych popularnych par metod, z których jedna zgłasza wyjątek, a druga nie:

 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

Niektórzy programiści języka C# są tak „niekorzystni dla wyjątków”, że automatycznie zakładają, że metoda, która nie zgłasza wyjątku, jest lepsza. Chociaż istnieją pewne wybrane przypadki, w których może to być prawdą, nie jest to wcale poprawne jako uogólnienie.

Jako konkretny przykład, w przypadku, gdy masz alternatywne uzasadnione (np. domyślne) działanie do podjęcia w przypadku wygenerowania wyjątku, podejście bez wyjątków może być uzasadnionym wyborem. W takim przypadku może rzeczywiście lepiej napisać coś takiego:

 if (int.TryParse(myString, out myInt)) { // use myInt } else { // use default value }

zamiast:

 try { myInt = int.Parse(myString); // use myInt } catch (FormatException) { // use default value }

Jednak założenie, że TryParse jest koniecznie „lepszą” metodą, jest błędne. Czasami tak jest, czasami nie. Dlatego są na to dwa sposoby. Użyj właściwego dla kontekstu, w którym się znajdujesz, pamiętając, że wyjątki z pewnością mogą być twoim przyjacielem jako programistą.

Częsty błąd programowania w języku C# nr 10: Zezwalanie na gromadzenie się ostrzeżeń kompilatora

Chociaż ten problem z pewnością nie jest specyficzny dla C#, jest szczególnie rażący w programowaniu C#, ponieważ porzuca zalety ścisłego sprawdzania typu oferowanego przez kompilator C#.

Ostrzeżenia są generowane z jakiegoś powodu. Chociaż wszystkie błędy kompilatora C# oznaczają defekt w kodzie, wiele ostrzeżeń również. To, co je odróżnia, to fakt, że w przypadku ostrzeżenia kompilator nie ma problemu z wyemitowaniem instrukcji, które reprezentuje Twój kod. Mimo to Twój kod jest nieco podejrzany i istnieje uzasadnione prawdopodobieństwo, że Twój kod nie odzwierciedla dokładnie Twoich zamiarów.

Typowym prostym przykładem na potrzeby tego samouczka programowania w języku C# jest zmodyfikowanie algorytmu w celu wyeliminowania użycia używanej zmiennej, ale zapomniano usunąć deklarację zmiennej. Program będzie działał idealnie, ale kompilator oznaczy bezużyteczną deklarację zmiennej. Fakt, że program działa idealnie, powoduje, że programiści zaniedbują naprawienie przyczyny ostrzeżenia. Ponadto programiści korzystają z funkcji programu Visual Studio, która ułatwia im ukrywanie ostrzeżeń w oknie „Lista błędów”, dzięki czemu mogą skupić się tylko na błędach. Nie trwa długo, zanim pojawią się dziesiątki ostrzeżeń, wszystkie błogo zignorowane (lub, co gorsza, ukryte).

Ale jeśli zignorujesz tego typu ostrzeżenie, prędzej czy później, coś takiego może bardzo dobrze znaleźć się w twoim kodzie:

 class Account { int myId; int Id; // compiler warned you about this, but you didn't listen! // Constructor Account(int id) { this.myId = Id; // OOPS! } }

A przy szybkości Intellisense pozwala nam pisać kod, ten błąd nie jest tak nieprawdopodobny, jak się wydaje.

Masz teraz poważny błąd w swoim programie (chociaż kompilator oznaczył go tylko jako ostrzeżenie, z powodów już wyjaśnionych) i w zależności od tego, jak złożony jest twój program, możesz stracić dużo czasu na śledzenie tego. Gdybyś zwrócił uwagę na to ostrzeżenie w pierwszej kolejności, uniknąłbyś tego problemu dzięki prostej, pięciosekundowej naprawie.

Pamiętaj, że kompilator C Sharp daje ci wiele przydatnych informacji o solidności twojego kodu… jeśli słuchasz. Nie ignoruj ​​ostrzeżeń. Naprawienie ich zajmuje zwykle tylko kilka sekund, a naprawianie nowych, gdy się pojawią, może zaoszczędzić wiele godzin. Naucz się oczekiwać, że okno „Lista błędów” programu Visual Studio będzie wyświetlać „0 błędów, 0 ostrzeżeń”, tak aby wszelkie ostrzeżenia w ogóle powodowały dyskomfort, aby natychmiast je rozwiązać.

Oczywiście od każdej reguły są wyjątki. W związku z tym mogą się zdarzyć sytuacje, w których Twój kod będzie wyglądał nieco podejrzanie dla kompilatora, nawet jeśli jest dokładnie taki, jak zamierzałeś. W tych bardzo rzadkich przypadkach użyj #pragma warning disable [warning id] wokół tylko kodu, który wyzwala ostrzeżenie i tylko dla identyfikatora ostrzeżenia, który wyzwala. Spowoduje to wyłączenie tego ostrzeżenia i tylko tego ostrzeżenia, dzięki czemu nadal możesz zachować czujność na nowe.

Zakończyć

C# to potężny i elastyczny język z wieloma mechanizmami i paradygmatami, które mogą znacznie poprawić produktywność. Podobnie jak w przypadku każdego oprogramowania lub języka, posiadanie ograniczonego zrozumienia lub oceny jego możliwości może czasami być bardziej utrudnieniem niż korzyścią, pozostawiając przysłowiowego „wiedzy na tyle, aby być niebezpiecznym”.

Korzystanie z samouczka C Sharp, takiego jak ten, w celu zapoznania się z kluczowymi niuansami C#, takimi jak (ale w żadnym wypadku nie tylko) problemy poruszone w tym artykule, pomoże w optymalizacji C# przy jednoczesnym uniknięciu niektórych z jego częstszych pułapek język.


Dalsza lektura na blogu Toptal Engineering:

  • Niezbędne pytania do rozmowy kwalifikacyjnej w języku C#
  • C# vs. C++: co jest rdzeniem?