Czy C # daje ci „mniej sznurka do powieszenia się” niż C ++? [Zamknięte]

14

Joel Spolsky scharakteryzował C ++ jako „wystarczającą ilość liny do powieszenia się” . W rzeczywistości podsumował „Effective C ++” Scotta Meyersa:

Jest to książka, która w zasadzie mówi, że C ++ jest wystarczającą liną do zawieszenia się, a następnie kilka dodatkowych mil liny, a następnie kilka tabletek samobójczych, które są przebrane za M & Ms ...

Nie mam kopii książki, ale istnieją oznaki, że znaczna część książki dotyczy pułapek zarządzania pamięcią, które wydają się być wykonane w C #, ponieważ środowisko wykonawcze zarządza tymi problemami.

Oto moje pytania:

  1. Czy C # pozwala uniknąć pułapek, których można uniknąć w C ++ tylko poprzez staranne programowanie? Jeśli tak, to w jakim stopniu i jak można ich uniknąć?
  2. Czy są nowe, różne pułapki w C #, o których powinien wiedzieć nowy programista C #? Jeśli tak, dlaczego nie można ich uniknąć dzięki projektowi C #?
alx9r
źródło
10
Z FAQ : Your questions should be reasonably scoped. If you can imagine an entire book that answers your question, you’re asking too much.. Wierzę, że to kwalifikuje się jako takie pytanie ...
Oded
@Oded Czy odnosisz się do nagłówkowego pytania ograniczonego do postaci? A może moje 3+ bardziej precyzyjne pytania w treści mojego postu?
alx9r
3
Szczerze mówiąc - zarówno tytuł, jak i każde „bardziej precyzyjne pytanie”.
Oded,
3
Rozpocząłem dyskusję Meta na temat tego pytania.
Oded
1
Jeśli chodzi o twoje usunięte trzecie pytanie, seria Effective C # Billa Wagnera (teraz 3 książki) nauczyła mnie więcej o programowaniu C #, niż cokolwiek innego, co czytam na ten temat. Recenzja EC # Martins ma rację, ponieważ nigdy nie może być bezpośrednim zamiennikiem Effective C ++, ale myli się, sądząc, że tak powinno być. Kiedy już nie będziesz musiał się martwić łatwymi błędami, musisz przejść do trudniejszych błędów.
Mark Booth,

Odpowiedzi:

33

Podstawowa różnica między C ++ i C # wynika z nieokreślonego zachowania .

Nie ma to nic wspólnego z ręcznym zarządzaniem pamięcią. W obu przypadkach jest to rozwiązany problem.

C / C ++:

W C ++, gdy popełnisz błąd, wynik jest niezdefiniowany.
Lub, jeśli spróbujesz poczynić pewne założenia dotyczące systemu (np. Przepełnienie liczby całkowitej ze znakiem), istnieje prawdopodobieństwo, że Twój program nie zostanie zdefiniowany.

Może przeczytaj tę 3-częściową serię o nieokreślonym zachowaniu.

To sprawia, że ​​C ++ jest tak szybki - kompilator nie musi się martwić o to, co się stanie, gdy coś pójdzie nie tak, więc może uniknąć sprawdzania poprawności.

C #, Java itp.

W języku C # masz gwarancję, że wiele błędów pojawi się na twojej twarzy jako wyjątki, i masz gwarancję znacznie więcej na temat podstawowego systemu.
Jest to podstawowa bariera w tworzeniu C # tak szybko, jak C ++, ale jest to także podstawowa bariera w zapewnianiu bezpieczeństwa C ++, a także ułatwia pracę z C # i debugowanie.

Cała reszta to po prostu sos.

użytkownik541686
źródło
Wszystkie niezdefiniowane rzeczy są naprawdę zdefiniowane przez implementację, więc jeśli przepełnisz liczbę całkowitą bez znaku w Visual Studio, otrzymasz wyjątek, jeśli włączysz odpowiednie flagi kompilatora. Teraz wiem, że o tym mówisz, ale nie jest to zachowanie nieokreślone , po prostu ludzie tego nie sprawdzają. (to samo z naprawdę nieokreślonym zachowaniem, jak operator ++, jest dobrze zdefiniowane przez każdy kompilator). Można powiedzieć to samo z C #, tylko że jest tylko jedna implementacja - mnóstwo „niezdefiniowanych zachowań”, jeśli działasz w Mono - np. bugzilla.xamarin.com/show_bug.cgi?id=310
gbjbaanb
1
Czy to jest naprawdę zdefiniowane, czy tylko zdefiniowane przez cokolwiek, co robi obecna implementacja .net w obecnej wersji systemu Windows? Nawet niezdefiniowane zachowanie c ++ jest w pełni zdefiniowane, jeśli zdefiniujesz je tak, jak robi to g ++.
Martin Beckett,
6
Przepełnienie liczb całkowitych bez znaku wcale nie jest UB. To przepełnione podpisane liczb całkowitych to UB.
DeadMG
6
@gbjbaanb: Jak powiedział DeadMG - przepełnienie liczb całkowitych ze znakiem jest niezdefiniowane. To nie realizacja zdefiniowane. Te frazy mają określone znaczenie w standardzie C ++ i nie są tym samym. Nie popełnij tego błędu.
user541686,
1
@CharlesSalvia: Uh, jak dokładnie „C ++ ułatwia korzystanie z pamięci podręcznej procesora” niż C #? I jaką kontrolę daje Ci C ++ nad pamięcią, której nie możesz mieć w C #?
user541686,
12

Czy C # pozwala uniknąć pułapek, których można uniknąć w C ++ tylko poprzez staranne programowanie? Jeśli tak, to w jakim stopniu i jak można ich uniknąć?

Większość to robi, a niektóre nie. I oczywiście tworzy kilka nowych.

  1. Niezdefiniowane zachowanie - Największą pułapką w C ++ jest to, że jest wiele niezdefiniowanych języków. Kompilator może dosłownie wysadzić wszechświat, kiedy robisz te rzeczy, i będzie dobrze. Naturalnie, jest to niczym niezwykłym, ale to jest dość powszechne dla programu działać na jednej maszynie i naprawdę nie ma dobrego powodu nie działać na innym. Lub gorzej, subtelnie zachowuj się inaczej. C # ma kilka przypadków nieokreślonego zachowania w specyfikacji, ale są one rzadkie i w obszarach języka, które są rzadko podróżowane. C ++ ma możliwość napotkania niezdefiniowanego zachowania za każdym razem, gdy wypowiadasz instrukcję.

  2. Wycieki pamięci - jest to mniejszy problem dla współczesnego C ++, ale dla początkujących i przez około połowę swojego życia, C ++ sprawiło, że bardzo łatwo wyciek pamięci. Skuteczne C ++ pojawiło się tuż przy ewolucji praktyk w celu wyeliminowania tego problemu. To powiedziawszy, C # może nadal wyciekać pamięć. Najczęstszym przypadkiem, na który wpadają ludzie, jest przechwytywanie zdarzeń. Jeśli masz obiekt i umieściłeś jedną z jego metod jako procedurę obsługi zdarzenia, właściciel tego zdarzenia musi zostać GC, aby obiekt umarł. Większość początkujących nie zdaje sobie sprawy, że moduł obsługi zdarzeń liczy się jako odniesienie. Występują również problemy z brakiem rozporządzalnych zasobów, które mogą przeciekać pamięć, ale nie są one tak powszechne, jak wskaźniki we wstępnym C ++.

  3. Kompilacja - C ++ ma opóźniony model kompilacji. Prowadzi to do wielu sztuczek, z którymi można się dobrze bawić i skrócić czas kompilacji.

  4. Ciągi - Modern C ++ sprawia, że ​​jest to trochę lepsze, ale char*odpowiada za ~ 95% wszystkich naruszeń bezpieczeństwa przed rokiem 2000. Dla doświadczonych programistów skupią się std::string, ale wciąż jest coś, czego należy unikać i problem w starszych / gorszych bibliotekach . I to modli się, abyś nie potrzebował obsługi Unicode.

I tak naprawdę to wierzchołek góry lodowej. Głównym problemem jest to, że C ++ jest bardzo słabym językiem dla początkujących. Jest to dość niespójne, a wiele starych, naprawdę złych pułapek rozwiązano poprzez zmianę idiomów. Problem polega na tym, że początkujący muszą nauczyć się idiomów z czegoś takiego jak Effective C ++. C # całkowicie eliminuje wiele z tych problemów i sprawia, że ​​reszta jest mniej ważna, dopóki nie przejdziesz dalej ścieżką uczenia się.

Czy są nowe, różne pułapki w C #, o których powinien wiedzieć nowy programista C #? Jeśli tak, to dlaczego nie można uniknąć projektu C #?

Wspomniałem o problemie z „wyciekiem pamięci”. To nie jest problem językowy tak bardzo, jak programista oczekujący czegoś, czego język nie może zrobić.

Innym jest to, że finalizator dla obiektu C # nie jest technicznie gwarantowany do uruchomienia przez środowisko wykonawcze. To zwykle nie ma znaczenia, ale powoduje, że niektóre rzeczy są projektowane inaczej, niż można się spodziewać.

Innym półpułapkiem, na który wpadli programiści, jest semantyka przechwytywania anonimowych funkcji. Kiedy przechwytujesz zmienną, przechwytujesz zmienną . Przykład:

List<Action> actions = new List<Action>();
for(int x = 0; x < 10; ++x ){
    actions.Add(() => Console.WriteLine(x));
}

foreach(var action in actions){
    action();
}

Nie robi tego, co naiwnie się myśli. Drukuje się 1010 razy.

Jestem pewien, że zapominam o wielu innych, ale głównym problemem jest to, że są mniej rozpowszechnione.

Telastyn
źródło
4
Wycieki pamięci należą już do przeszłości i tak też jest char*. Nie wspominając o tym, że nadal możesz przeciekać pamięć w C # w porządku.
DeadMG,
2
Nazywanie szablonów „wklejaniem uwielbianego łańcucha” to trochę za dużo. Szablony są naprawdę jedną z najlepszych funkcji C ++.
Charles Salvia,
2
@CharlesSalvia Jasne, są naprawdę wyróżnikiem C ++. I tak, być może jest to nadmierne uproszczenie wpływu kompilacji. Ale mają nieproporcjonalnie duży wpływ na czasy kompilacji i rozmiar wyjściowy, szczególnie jeśli nie jesteś ostrożny.
Telastyn
2
@deadMG na pewno, choć argumentowałbym, że wiele sztuczek związanych z meta-programowaniem używanych / potrzebnych w C ++ jest ... lepiej zaimplementowanych za pomocą innego mechanizmu.
Telastyn
2
@Telastyn cały czas type_traits polega na uzyskiwaniu informacji o typie w czasie kompilacji, aby można było wykorzystać te informacje do specjalnych zadań, takich jak specjalizowanie się w szablonach lub przeciążanie funkcjienable_if
Charles Salvia
10

Moim zdaniem niebezpieczeństwa związane z C ++ są nieco przesadzone.

Istotne niebezpieczeństwo jest następujące: podczas gdy C # pozwala wykonywać „niebezpieczne” operacje na wskaźnikach przy użyciu unsafesłowa kluczowego, C ++ (będący przeważnie nadzbiorem C) pozwala używać wskaźników, kiedy tylko masz na to ochotę. Oprócz zwykłych zagrożeń związanych ze stosowaniem wskaźników (które są takie same w przypadku C), takich jak wycieki pamięci, przepełnienie bufora, zwisające wskaźniki itp., C ++ wprowadza nowe sposoby poważnego zepsucia.

Ta „dodatkowa lina”, że tak powiem, o której mówił Joel Spolsky , w zasadzie sprowadza się do jednej rzeczy: pisania zajęć, które wewnętrznie zarządzają własną pamięcią, znaną również jako „ Reguła 3 ” (którą można teraz nazwać Regułą 4 lub Reguła 5 w C ++ 11). Oznacza to, że jeśli kiedykolwiek chcesz napisać klasę, która wewnętrznie zarządza przydziałami pamięci, musisz wiedzieć, co robisz, w przeciwnym razie program prawdopodobnie się zawiesi. Musisz ostrożnie utworzyć konstruktor, skopiować konstruktor, destruktor i operator przypisania, co jest zaskakująco łatwe do popełnienia błędu, często powodując dziwne awarie w czasie wykonywania.

JEDNAK , w codziennym programowaniu w C ++, bardzo rzadko jest pisanie klasy, która zarządza własną pamięcią, więc mylące jest twierdzenie, że programiści C ++ zawsze muszą być „ostrożni”, aby uniknąć tych pułapek. Zwykle będziesz robić coś więcej:

class Foo
{
    public:

    Foo(const std::string& s) 
        : m_first_name(s)
    { }

    private:

    std::string m_first_name;
};

Ta klasa wygląda dość blisko tego, co robisz w Javie lub C # - nie wymaga jawnego zarządzania pamięcią (ponieważ klasa biblioteki std::stringzajmuje się tym wszystkim automatycznie) i nie jest wymagana żadna funkcja „Reguły 3”, ponieważ domyślna Konstruktor kopiowania i operator przypisania są w porządku.

Tylko wtedy, gdy próbujesz zrobić coś takiego:

class Foo
{
    public:

    Foo(const char* s)
    { 
        std::size_t len = std::strlen(s);
        m_name = new char[len + 1];
        std::strcpy(m_name, s);
    }

    Foo(const Foo& f); // must implement proper copy constructor

    Foo& operator = (const Foo& f); // must implement proper assignment operator

    ~Foo(); // must free resource in destructor

    private:

    char* m_name;
};

W takim przypadku nowicjuszom może być trudne uzyskanie poprawnego przypisania, destruktora i konstruktora kopiowania. Ale w większości przypadków nie ma powodu, aby to robić. C ++ sprawia, że ​​bardzo łatwo jest uniknąć ręcznego zarządzania pamięcią przez 99% czasu, używając klas bibliotek takich jak std::stringi std::vector.

Innym powiązanym problemem jest ręczne zarządzanie pamięcią w sposób, który nie bierze pod uwagę możliwości zgłoszenia wyjątku. Lubić:

char* s = new char[100];
some_function_which_may_throw();
/* ... */
delete[] s;

Jeśli some_function_which_may_throw()faktycznie nie wyjątek, jesteś w lewo z wyciekiem pamięci, ponieważ pamięć przeznaczona na snigdy nie zostaną odzyskane. Ale znowu, w praktyce nie jest to już problemem z tego samego powodu, dla którego „Reguła 3” nie jest już tak naprawdę dużym problemem. Bardzo rzadko (i zwykle nie jest to konieczne) zarządzanie własną pamięcią za pomocą surowych wskaźników. Aby uniknąć powyższego problemu, wystarczy użyć std::stringlub std::vector, a destruktor zostanie automatycznie wywołany podczas rozwijania stosu po zgłoszeniu wyjątku.

Tak więc ogólnym tematem jest to, że wiele funkcji C ++, które nie zostały odziedziczone po C, takich jak automatyczna inicjalizacja / niszczenie, konstruktory kopiowania i wyjątki, zmusza programistę do zachowania szczególnej ostrożności podczas ręcznego zarządzania pamięcią w C ++. Ale znowu jest to problem tylko wtedy, gdy zamierzasz ręcznie zarządzać pamięcią, co nie jest już prawie konieczne, gdy masz standardowe pojemniki i inteligentne wskaźniki.

Tak więc, moim zdaniem, podczas gdy C ++ daje ci dużo dodatkowej liny, prawie nigdy nie trzeba jej używać do powieszenia się, a pułapek, o których mówił Joel, są banalnie łatwe do uniknięcia we współczesnym C ++.

Charles Salvia
źródło
To była reguła trzech w C ++ 03, a teraz jest reguła czterech w C ++ 11.
DeadMG,
1
Możesz to nazwać „Regułą 5” dla konstruktora kopiowania, konstruktora przenoszenia, przypisania kopii, przypisania przeniesienia i destruktora. Ale semantyka przenoszenia nie zawsze jest konieczna tylko do właściwego zarządzania zasobami.
Charles Salvia,
Nie potrzebujesz osobnego zadania przeniesienia i skopiowania. Idiom kopiowania i zamiany może wykonywać oba operatory w jednym.
DeadMG,
2
Odpowiada na pytanie Does C# avoid pitfalls that are avoided in C++ only by careful programming?. Odpowiedź brzmi: „nie bardzo, bo banalnie łatwo jest uniknąć pułapek, o których mówił Joel we współczesnym C ++”
Charles Salvia
1
IMO, podczas gdy języki wysokiego poziomu, takie jak C # lub Java, zapewniają zarządzanie pamięcią i inne rzeczy, które powinny ci pomóc , nie zawsze działają tak, jak powinny. Nadal zajmujesz się projektowaniem kodu, więc nie pozostawiasz wycieków pamięci (co nie jest dokładnie tym, co nazwałbyś w C ++). Z mojego doświadczenia wynika, że ​​NAWET łatwiej jest zarządzać pamięcią w C ++, ponieważ wiesz, że wywoływane będą destruktory, a w większości przypadków zajmują się czyszczeniem. W końcu C ++ ma inteligentne wskaźniki w przypadkach, gdy projektowanie nie pozwala na wydajne zarządzanie pamięcią. C ++ jest świetny, ale nie dla manekinów.
Pijusn
3

Naprawdę nie zgodziłbym się. Może mniej pułapek niż C ++, jakie istniały w 1985 roku.

Czy C # pozwala uniknąć pułapek, których można uniknąć w C ++ tylko poprzez staranne programowanie? Jeśli tak, to w jakim stopniu i jak można ich uniknąć?

Nie całkiem. Reguły takie jak Reguła Trzech straciły ogromne znaczenie w C ++ 11 dzięki unique_ptri shared_ptrsą znormalizowane. Używanie klas standardowych w dość rozsądny sposób nie jest „ostrożnym kodowaniem”, lecz „podstawowym kodowaniem”. Ponadto odsetek populacji C ++, którzy są nadal wystarczająco głupi, niedoinformowani lub oboje, aby wykonywać takie czynności, jak ręczne zarządzanie pamięcią, jest znacznie niższy niż wcześniej. Rzeczywistość jest taka, że ​​wykładowcy, którzy chcą wykazać takie zasady, muszą spędzić tygodnie, próbując znaleźć przykłady, w których nadal mają zastosowanie, ponieważ zajęcia standardowe obejmują praktycznie każdy możliwy do wyobrażenia przypadek użycia. Wiele skutecznych technik C ++ poszło tą samą drogą - drogą dodo. Wiele innych nie jest tak specyficznych dla C ++. Daj mi zobaczyć. Pomijając pierwszy element, kolejne dziesięć to:

  1. Nie koduj C ++ tak, jak to jest C. To naprawdę zdrowy rozsądek.
  2. Ogranicz swoje interfejsy i używaj enkapsulacji. OOP.
  3. Twórcy dwufazowych kodów inicjujących powinni zostać spaleni na stosie. OOP.
  4. Dowiedz się, co to jest semantyka wartości. Czy to naprawdę jest specyficzne dla C ++?
  5. Ponownie ogranicz interfejsy, tym razem w nieco inny sposób. OOP.
  6. Wirtualne niszczyciele. Tak. Ten jest prawdopodobnie nadal ważny - w pewnym stopniu. finali overridepomogliśmy zmienić tę konkretną grę na lepsze. Zrób swój destruktor, overridea zagwarantujesz niezły błąd kompilatora, jeśli odziedziczysz po kimś, kto nie zrobił jego destruktora virtual. Uczyń swoją klasę, aby finalżaden słaby peeling nie mógł przyjść i odziedziczyć po nim przypadkowo bez wirtualnego destruktora.
  7. Zdarzają się złe rzeczy, jeśli funkcje czyszczenia zawodzą. Nie jest to tak naprawdę specyficzne dla C ++ - możesz zobaczyć tę samą radę zarówno dla Javy, jak i C # - i, cóż, prawie każdego języka. Posiadanie funkcji czyszczenia, które mogą zawieść, jest po prostu złe i nie ma nic C ++ ani nawet OOP w tym elemencie.
  8. Należy pamiętać, w jaki sposób kolejność konstruktorów wpływa na funkcje wirtualne. Zabawne, że w Javie (bieżącej lub przeszłej) po prostu niepoprawnie wywołałby funkcję klasy pochodnej, co jest nawet gorsze niż zachowanie C ++. Niezależnie od tego, ten problem nie dotyczy C ++.
  9. Przeciążenia operatora powinny zachowywać się tak, jak ludzie tego oczekują. Nie bardzo konkretny. Do diabła, nie jest to nawet specyficzne dla operatora przeciążenie, to samo można zastosować do dowolnej funkcji - nie nadawaj jej jednej nazwy, a następnie zrób coś zupełnie nieintuicyjnego.
  10. Jest to obecnie uważane za złą praktykę. Wszyscy operatorzy przypisania, którzy są wyjątkowo bezpieczni w wyjątkach, radzą sobie z samodzielnym przypisywaniem w porządku, a samodzielne przypisywanie jest w rzeczywistości logicznym błędem programu, a sprawdzanie samodzielnego przypisania po prostu nie jest warte kosztów wydajności.

Oczywiście nie będę przechodził przez każdy element Effective C ++, ale większość z nich po prostu stosuje podstawowe pojęcia do C ++. Znajdziesz tę samą radę w dowolnym zorientowanym obiektowo języku operatora przeciążalnego operatora. Wirtualne niszczyciele są prawie jedyną pułapką w C ++ i nadal są ważne - chociaż, prawdopodobnie, z finalklasą C ++ 11, nie jest tak ważne, jak było. Pamiętaj, że Effective C ++ został napisany, kiedy pomysł zastosowania OOP i specyficzne cechy C ++ były wciąż bardzo nowe. Te elementy nie dotyczą pułapek w C ++, a więcej o tym, jak radzić sobie ze zmianą z C i jak poprawnie używać OOP.

Edycja: Pułapki w C ++ nie obejmują takich pułapek malloc. Po pierwsze, każdą pułapkę, którą można znaleźć w kodzie C, można również znaleźć w niebezpiecznym kodzie C #, więc nie jest to szczególnie istotne, a po drugie, tylko dlatego, że Standard definiuje go dla współdziałania, nie oznacza, że ​​użycie go jest uważane za C ++ kod. Norma również określa goto, ale jeśli miałbyś napisać z niego gigantyczną kupkę spaghetti, uznałbym, że to twój problem, a nie język. Istnieje duża różnica między „ostrożnym kodowaniem” a „przestrzeganiem podstawowych idiomów języka”.

Czy są nowe, różne pułapki w C #, o których powinien wiedzieć nowy programista C #? Jeśli tak, to dlaczego nie można uniknąć projektu C #?

usingdo bani. To naprawdę działa. I nie mam pojęcia, dlaczego nie zrobiono czegoś lepszego. Ponadto, Base[] = Derived[]i prawie każde użycie Object, które istnieje, ponieważ oryginalni projektanci nie zauważyli ogromnego sukcesu, jakim były szablony w C ++, i zdecydowali, że „Po prostu odziedziczmy wszystko od wszystkiego i stracimy całe nasze bezpieczeństwo typu” był mądrzejszym wyborem . Wierzę również, że można znaleźć paskudne niespodzianki w takich warunkach jak wyścig z delegatami i inne tego rodzaju zabawy. Są też inne ogólne rzeczy, takie jak przerażające ssanie leków generycznych w porównaniu do szablonów, naprawdę bardzo niepotrzebne wymuszone umieszczanie wszystkiego w classitd. I takie rzeczy.

DeadMG
źródło
5
Wykształcona baza użytkowników lub nowe konstrukcje tak naprawdę nie osłabiają liny. Są tylko obejściami, więc mniej ludzi kończy się zawieszeniem. Chociaż to wszystko jest dobry komentarz do Effective C ++ i jego kontekstu w ewolucji języka.
Telastyn
2
Nie. Chodzi o to, jak kilka elementów w Effective C ++ to pojęcia, które mogłyby równie dobrze odnosić się do dowolnego języka obiektowego o typie wartości. A uczenie bazy użytkowników kodowania rzeczywistego C ++ zamiast C zdecydowanie zmniejsza sznur, który daje C ++. Również chciałbym się spodziewać, że nowe konstrukcje językowe malejących linę. Chodzi o to, że tylko dlatego, że definicja w C ++ Standard mallocnie oznacza, że ​​powinieneś to zrobić, podobnie jak fakt, że kurwa gotojak suka oznacza, że ​​jest to lina, z którą możesz się powiesić.
DeadMG,
2
Korzystanie z części C C ++ nie różni się niczym od pisania całego kodu unsafew języku C #, co jest równie złe. Mógłbym wymienić każdą pułapkę kodowania C # jak również C, jeśli chcesz.
DeadMG,
@DeadMG: tak naprawdę pytanie powinno brzmieć: „programista C ++ ma wystarczająco dużo liny, aby zawiesić się, dopóki jest programistą C”
gbjbaanb
„Ponadto odsetek populacji C ++, którzy nadal są wystarczająco głupi, niedoinformowani lub oboje, aby wykonywać takie czynności, jak ręczne zarządzanie pamięcią, jest znacznie niższy niż wcześniej”. Wymagany cytat.
dan04,
3

Czy C # pozwala uniknąć pułapek, których można uniknąć w C ++ tylko poprzez staranne programowanie? Jeśli tak, to w jakim stopniu i jak można ich uniknąć?

C # ma zalety:

  • Nie jest kompatybilny wstecz z C, co pozwala uniknąć długiej listy „złych” cech języka (np. Surowych wskaźników), które są wygodne pod względem składniowym, ale teraz uważane za zły styl.
  • Posiadanie semantyki odniesienia zamiast semantyki wartości, co sprawia, że ​​co najmniej 10 elementów Efektywnego C ++ jest dyskusyjne (ale wprowadza nowe pułapki).
  • Mniejsze zachowanie zdefiniowane w implementacji niż C ++.
    • W szczególności, w C ++ postać kodowania char, stringitp jest realizacja zdefiniowane. Rozłam między podejściem Windows do Unicode ( wchar_tdla UTF-16, chardla przestarzałych „stron kodowych”) i podejściem * nix (UTF-8) powoduje duże trudności w kodzie międzyplatformowym. C #, OTOH, gwarantuje, że a stringto UTF-16.

Czy są nowe, różne pułapki w C #, o których powinien wiedzieć nowy programista C #?

Tak: IDisposable

Czy istnieje odpowiednik książki „Effective C ++” dla C #?

Istnieje książka o nazwie Effective C #, która ma strukturę podobną do Effective C ++ .

dan04
źródło
0

Nie, C # (i Java) są mniej bezpieczne niż C ++

C ++ jest weryfikowalny lokalnie . Mogę sprawdzić pojedynczą klasę w C ++ i stwierdzić, że klasa nie przecieka pamięci lub innych zasobów, zakładając, że wszystkie klasy, do których istnieją odniesienia, są poprawne. W Javie lub C # konieczne jest sprawdzenie każdej klasy, do której istnieje odniesienie, aby ustalić, czy wymaga ona pewnego rodzaju finalizacji.

C ++:

{
   some_resource r(...);  // resource initialized
   ...
}  // resource destructor called, no leaks here

DO#:

{
   SomeResource r = new SomeResource(...); // resource initialized
   ...
} // did I need to finalize that?  May I should have used 'using' 
  // (or in Java, a grotesque try/finally construct)?  No way to tell
  // without checking the documentation for SomeResource

C ++:

{
    auto_ptr<SomeInterface> i = SomeFactory.create(...);
    i->f(...);
} // automatic finalization and memory release.  A new implementation of
  // SomeInterface can allocate and free resources with no impact
  // on existing code

DO#:

{
   SomeInterface i = SomeFactory.create(...);
   i.f(...);
   ...
} // Sure hope someone didn't create an implementation of SomeInterface
  // that requires finalization.  In C# and Java it is necessary to decide whether
  // any implementation could require finalization when the interface is defined.
  // If the initial decision is 'no finalization', then no future implementation  
  // can acquire any resource without creating potential leaks in existing code.
Kevin Cline
źródło
3
... ustalenie, czy coś dziedziczy po IDisposable, jest dość proste. Główny problem polega na tym, że musisz wiedzieć, jak używać auto_ptr(lub kilku jego krewnych). To jest przysłowiowa lina.
Telastyn
2
@Telastyn nie, chodzi o to, że zawsze używasz inteligentnego wskaźnika, chyba że naprawdę wiesz, że go nie potrzebujesz. W języku C # instrukcja using przypomina linę, o której mowa. (tzn. w C ++ musisz pamiętać o użyciu inteligentnego wskaźnika, dlaczego więc C # nie jest tak zły, chociaż musisz pamiętać, aby zawsze używać instrukcji using)
gbjbaanb
1
@gbjbaanb Ponieważ co? 5% co najwyżej klas C # jest jednorazowych? I wiesz, że musisz je zutylizować, jeśli są jednorazowe. W C ++ każdy pojedynczy obiekt jest jednorazowy. I nie wiesz, czy należy zająć się konkretną instancją. Co dzieje się w przypadku zwracanych wskaźników, które nie pochodzą z fabryki? Czy Twoim obowiązkiem jest ich oczyszczenie? Nie powinno tak być, ale czasem tak jest. I znowu, to, że zawsze powinieneś używać inteligentnego wskaźnika, nie oznacza, że ​​opcja nie przestaje istnieć. Szczególnie dla początkujących jest to poważna pułapka.
Telastyn
2
@Telastyn: Znajomość obsługi auto_ptrjest tak prosta, jak znajomość obsługi IEnumerablelub znajomość obsługi interfejsów lub nie używaj liczb zmiennoprzecinkowych dla waluty lub tym podobnych. Jest to podstawowa aplikacja DRY. Nikt, kto zna podstawy programowania, nie popełniłby tego błędu. W przeciwieństwie using. Problem usingpolega na tym, że musisz wiedzieć dla każdej klasy, czy jest ona jednorazowa (i mam nadzieję, że nigdy się nie zmieni), a jeśli nie jest jednorazowa, automatycznie banujesz wszystkie pochodne klasy, które mogą być jednorazowe.
DeadMG,
2
kevin: Uh, twoja odpowiedź nie ma sensu. To nie wina C #, że robisz to źle. Państwo ma nie zależeć finalizatorów prawidłowo napisany kod C # . Jeśli masz pole z Disposemetodą, musisz je zaimplementować IDisposable(„właściwy” sposób). Jeśli twoja klasa to robi (co jest równoważne z implementacją RAII dla twojej klasy w C ++) i używasz using(co jest jak inteligentne wskaźniki w C ++), wszystko działa idealnie. Finalizator ma przede wszystkim zapobiegać wypadkom - Disposejest odpowiedzialny za poprawność, a jeśli go nie używasz, cóż, to twoja wina, a nie C #.
user541686,
0

Tak 100% tak, ponieważ uważam, że nie można zwolnić pamięci i użyć jej w C # (zakładając, że jest zarządzana i nie przejdziesz w niebezpieczny tryb).

Ale jeśli wiesz, jak programować w C ++, czego niewiarygodna liczba ludzi nie. Nic ci nie jest. Podobnie jak lekcje Charlesa Salvii tak naprawdę nie zarządzają swoimi wspomnieniami, ponieważ wszystko odbywa się w istniejących klasach STL. Rzadko używam wskaźników. W rzeczywistości realizowałem projekty bez użycia jednego wskaźnika. (C ++ 11 ułatwia to).

Jeśli chodzi o pisanie liter, głupie pomyłki itp. ( if (i=0)Np. Klucz bc utknął po bardzo szybkim naciśnięciu ==) kompilator narzeka, co jest dobre, ponieważ poprawia jakość kodu. Innym przykładem jest zapominanie breakw instrukcjach switch i niedopuszczanie do deklarowania zmiennych statycznych w funkcji (co czasem mi się nie podoba, ale jest dobrym pomysłem imo).


źródło
4
Java i C # jeszcze pogorszyły problem =/, ==wykorzystując ==do równości referencyjnej i wprowadzając .equalsdo równości wartości. Słaby programista musi teraz śledzić, czy zmienna jest „podwójna” czy „podwójna” i koniecznie wybrać odpowiedni wariant.
kevin cline
@kevincline +1, ale w C # structmożesz zrobić, ==co działa niewiarygodnie dobrze, ponieważ przez większość czasu można było mieć tylko łańcuchy, liczby całkowite i zmiennoprzecinkowe (tj. tylko elementy struktury). W moim własnym kodzie nigdy nie mam tego problemu, chyba że chcę porównać tablice. Nie sądzę, żebym kiedykolwiek porównywał listy lub typy niestrukturalne (string, int, float, DateTime, KeyValuePair i wiele innych)
2
Python ma rację, używając ==równości wartości i isrówności odniesienia.
dan04
@ dan04 - Jak myślisz, ile rodzajów równości ma C #? Zobacz doskonały wykład błyskawicy ACCU: Niektóre przedmioty są bardziej równe niż inne
Mark Booth,