Jak przenieść moje myślenie z C ++ do C #

12

Jestem doświadczonym programistą C ++, znam język bardzo szczegółowo i intensywnie korzystałem z niektórych jego specyficznych funkcji. Znam też zasady OOD i wzorce projektowe. Uczę się teraz C #, ale nie mogę przestać czuć, że nie mogę pozbyć się sposobu myślenia w C ++. Tak mocno związałem się z mocnymi stronami C ++, że nie mogę żyć bez niektórych funkcji. I nie mogę znaleźć żadnych dobrych obejść lub zamienników dla nich w języku C #.

Jakie dobre praktyki , wzorce projektowe , idiomy , które różnią się w C # od perspektywy C ++, możesz zasugerować? Jak uzyskać idealny projekt w C ++, który nie wygląda głupio w C #?

W szczególności nie mogę znaleźć dobrego sposobu radzenia sobie w języku C # (ostatnie przykłady):

  • Kontrolowanie czasu życia zasobów, które wymagają deterministycznego czyszczenia (np. Plików). Łatwo jest to mieć usingpod ręką, ale jak właściwie z niego korzystać, gdy własność zasobu jest przenoszona [... między wątkami]? W C ++ po prostu używałbym wspólnych wskaźników i pozwalał zająć się „odśmiecaniem” w odpowiednim czasie.
  • Ciągłe zmaganie się z nadpisywaniem funkcji dla określonych generycznych (uwielbiam takie rzeczy, jak częściowa specjalizacja szablonów w C ++). Czy powinienem po prostu zrezygnować z jakichkolwiek prób programowania ogólnego w języku C #? Może generyczne są celowo ograniczone i używanie ich nie jest C # poza konkretną domeną problemów?
  • Funkcjonalność przypominająca makro. Chociaż ogólnie jest to zły pomysł, w przypadku niektórych domen problemów nie ma innego obejścia (np. Warunkowa ocena instrukcji, podobnie jak w przypadku dzienników, które powinny przejść tylko do wersji debugowania). Brak ich oznacza, że ​​muszę umieścić więcej if (condition) {...}płyt kotłowych i nadal nie jest to równe pod względem wywoływania efektów ubocznych.
Andrzej
źródło
# 1 jest dla mnie trudny, sam chciałbym poznać odpowiedź. # 2 - czy możesz podać prosty przykład kodu C ++ i wyjaśnić, dlaczego go potrzebujesz? Dla # 3 - użyj C#do wygenerowania innego kodu. Możesz odczytać plik CSVlub XMLplik, który zapisałeś jako dane wejściowe i wygenerować plik C#lub SQLplik. Może to być bardziej wydajne niż używanie makr funkcjonalnych.
Job
Jakie konkretne rzeczy próbujesz zrobić na temat funkcji podobnych do makr? Odkryłem, że zwykle istnieje konstrukcja języka C # do obsługi tego, co zrobiłbym z makrem w C ++. Zobacz także dyrektywy preprocesora C #: msdn.microsoft.com/en-us/library/4y6tbswk.aspx
ravibhagw
Wiele rzeczy zrobionych z makrami w C ++ można zrobić z odbiciem w C #.
Sebastian Redl,
To jeden z moich pomysłów, gdy ktoś mówi: „Właściwe narzędzie do właściwej pracy - wybierz język na podstawie tego, czego będziesz potrzebować”. W przypadku dużych programów w językach ogólnego zastosowania jest to bezużyteczne, nieprzydatne stwierdzenie. To powiedziawszy, rzeczą, w której C ++ jest lepszy niż prawie jakikolwiek inny język, jest deterministyczne zarządzanie zasobami. Czy deterministyczne zarządzanie zasobami jest podstawową funkcją Twojego programu, czy czymś, do czego jesteś przyzwyczajony? Jeśli nie jest to ważna funkcja użytkownika, możesz być nadmiernie skoncentrowany na tym.
Ben

Odpowiedzi:

16

Z mojego doświadczenia wynika, że ​​nie ma na to książki. Fora pomagają w posługiwaniu się idiomami i najlepszymi praktykami, ale w końcu będziesz musiał poświęcić czas. Ćwicz, rozwiązując wiele różnych problemów w języku C #. Spójrz na to, co było trudne / niezręczne i spróbuj sprawić, by następnym razem były mniej trudne i mniej niezręczne. Dla mnie ten proces trwał około miesiąca, zanim go „dostałem”.

Najważniejszą rzeczą, na której należy się skupić, jest brak zarządzania pamięcią. W C ++ włada każdą decyzją dotyczącą projektu, ale w C # przynosi efekty odwrotne do zamierzonych. W języku C # często chcesz zignorować śmierć lub inne zmiany stanu swoich obiektów. Ten rodzaj powiązania dodaje niepotrzebne połączenie.

Przeważnie skup się na jak najskuteczniejszym korzystaniu z nowych narzędzi, a nie na walce o przywiązanie do starych.

Jak uzyskać idealny projekt w C ++, który nie wygląda głupio w C #?

Uświadamiając sobie, że różne idiomy dążą do różnych podejść projektowych. Nie możesz po prostu przenieść czegoś 1: 1 między (większością) języków i oczekiwać czegoś pięknego i idiomatycznego na drugim końcu.

Ale w odpowiedzi na konkretne scenariusze:

Współużytkowane niezarządzane zasoby - To był zły pomysł w C ++, a tak samo zły pomysł w C #. Jeśli masz plik, otwórz go, użyj go i zamknij. Udostępnianie go między wątkami wymaga tylko kłopotów, a jeśli tylko jeden wątek naprawdę go używa, niech ten wątek otworzy / będzie miał / zamknie go. Jeśli z jakiegoś powodu naprawdę masz plik o długim czasie życia, stwórz klasę, która będzie go reprezentować i pozwól aplikacji zarządzać nim w razie potrzeby (zamknij przed wyjściem, powiązaj zdarzenia związane z zamykaniem aplikacji itp.)

Częściowa specjalizacja szablonów - Nie masz tu szczęścia, chociaż im więcej użyłem C #, tym bardziej znajduję użycie szablonu, które zrobiłem w C ++, by wpaść w YAGNI. Po co tworzyć coś ogólnego, gdy T jest zawsze int? Inne scenariusze najlepiej rozwiązywać w języku C # przez swobodne stosowanie interfejsów. Nie potrzebujesz ogólnych, gdy typ interfejsu lub delegata może zdecydowanie wpisać to, co chcesz zmienić.

Warunki wstępne preprocesora - C # ma #if, zwariuj. Co więcej, ma atrybuty warunkowe , które po prostu usuwają tę funkcję z kodu, jeśli nie jest zdefiniowany symbol kompilatora.

Telastyn
źródło
Jeśli chodzi o zarządzanie zasobami, w języku C # dość powszechne jest posiadanie klas menedżerów (które mogą być statyczne lub dynamiczne).
rwong
Jeśli chodzi o wspólne zasoby: Zarządzane są łatwe w C # (jeśli warunki wyścigowe są nieistotne), niezarządzane są znacznie bardziej nieprzyjemne.
Deduplicator
@rwong - powszechne, ale klasy menedżerów są nadal anty-wzorcem, którego należy unikać.
Telastyn
Jeśli chodzi o zarządzanie pamięcią, zauważyłem, że przejście na C # utrudniło to, szczególnie na początku. Myślę, że musisz być bardziej świadomy niż w C ++. W C ++ dokładnie wiesz, kiedy i gdzie każdy obiekt jest przydzielany, zwalniany i odwoływany. W C # to wszystko jest przed tobą ukryte. Wiele razy w języku C # nawet nie zdajesz sobie sprawy, że uzyskano odniesienie do obiektu i pamięć zaczyna wyciekać. Baw się dobrze, śledząc wycieki pamięci w C #, które zwykle są trywialne w C ++. Oczywiście z doświadczeniem uczysz się tych niuansów języka C #, więc stają się one mniejszym problemem.
Dunk
4

O ile widzę, jedyną rzeczą, która pozwala na deterministyczne zarządzanie czasem życia, jest IDisposableawaria (jak w: brak obsługi języka lub systemu typów), kiedy, jak zauważyłeś, musisz przenieść własność takiego zasobu.

Będąc w tej samej łodzi co Ty - programista C ++ robi bankomat C #. - brak wsparcia dla modelu przenoszenia własności (plików, połączeń db ...) w typach / podpisach C # był dla mnie bardzo denerwujący, ale muszę przyznać, że „OK” działa całkiem dobrze w sprawie konwencji i dokumentów API, aby to zrobić poprawnie.

  • Służy IDisposabledo wykonywania deterministycznego czyszczenia, jeśli jest to konieczne.
  • Gdy musisz przenieść własność IDisposable, po prostu upewnij się, że interfejs API jest jasny i nie używaj usingw tym przypadku, ponieważ usingnie można tak powiedzieć „anulować”.

Tak, nie jest tak elegancki, jak to, co można wyrazić w systemie czcionek za pomocą nowoczesnego C ++ (przenosić sematykę itp.), Ale wykonuje to zadanie i (o dziwo) społeczność C # jako całość nie wydaje się nadmierna opieka, AFAICT.

Martin Ba
źródło
2

Wspomniałeś o dwóch konkretnych funkcjach C ++. Omówię RAII:

Zwykle nie ma zastosowania, ponieważ C # jest śmieciami zbierane. Najbliższą konstrukcją C # jest prawdopodobnie IDisposableinterfejs, używany w następujący sposób:

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

Nie należy spodziewać się IDisposableczęstego wdrażania (i jest to nieco zaawansowane), ponieważ nie jest to konieczne w przypadku zasobów zarządzanych. Jednak powinieneś użyć usingkonstruktora (lub zadzwonić Dispose, jeśli usingjest to niemożliwe) do wszystkiego, co implementuje IDisposable. Nawet jeśli to zepsujesz, dobrze zaprojektowane klasy C # spróbują sobie z tym poradzić (używając finalizatorów). Jednak w języku zbierającym śmieci wzorzec RAII nie działa w pełni (ponieważ gromadzenie nie jest deterministyczne), więc czyszczenie zasobów będzie opóźnione (czasem katastroficzne).

Brian
źródło
RAII jest moim największym problemem. Powiedzmy, że mam zasób (powiedzmy plik), który jest tworzony przez jeden wątek i przekazywany do innego. Jak mogę się upewnić, że zostanie usunięty w pewnym momencie, gdy wątek już go nie potrzebuje? Oczywiście mógłbym wywołać Dispose, ale wtedy wygląda to o wiele gorzej niż wspólne wskaźniki w C ++. Nie ma w tym nic z RAII.
Andrew,
@Andrew To, co opisujesz, wydaje się sugerować przeniesienie własności, więc teraz drugi wątek jest odpowiedzialny za Disposing()plik i prawdopodobnie powinien go użyć using.
svick,
@Andrew - Zasadniczo nie powinieneś tego robić. Zamiast tego powinieneś powiedzieć wątkowi, jak uzyskać plik i pozwolić mu mieć własność do końca życia.
Telastyn
1
@Telastyn Czy to oznacza, że ​​oddanie własności nad zarządzanym zasobem jest większym problemem w C # niż w C ++? Całkiem zaskakujące.
Andrew,
6
@Andrew: Pozwól, że podsumuję: w C ++ otrzymujesz jednolite, półautomatyczne zarządzanie wszystkimi zasobami. W języku C # masz całkowicie automatyczne zarządzanie pamięcią - i całkowicie ręczne zarządzanie wszystkim innym. Nie ma prawdziwej zmiany, więc twoim jedynym prawdziwym pytaniem nie jest „jak to naprawić?”, Tylko „jak się do tego przyzwyczaić?”
Jerry Coffin
2

To bardzo dobre pytanie, ponieważ widziałem wielu programistów C ++ piszących okropny kod C #.

Najlepiej nie myśleć o C # jako „C ++ z ładniejszą składnią”. Są to różne języki, które wymagają odmiennego podejścia do myślenia. C ++ zmusza cię do ciągłego myślenia o tym, co zrobi procesor i pamięć. C # nie jest taki. C # jest specjalnie zaprojektowany, abyś nie myślał o procesorze i pamięci, a zamiast tego myślisz o domenie biznesowej, dla której piszesz .

Jednym z przykładów tego, co widziałem, jest to, że wielu programistów C ++ lubi używać pętli, ponieważ są szybsze niż foreach. Jest to zwykle zły pomysł w języku C #, ponieważ ogranicza możliwe typy iteracji kolekcji (a tym samym możliwość ponownego użycia i elastyczność kodu).

Myślę, że najlepszym sposobem na dostosowanie od C ++ do C # jest próba podejścia do kodowania z innej perspektywy. Na początku będzie to trudne, ponieważ z biegiem lat będziesz przyzwyczajony do używania w mózgu wątku „co robi procesor i pamięć” do filtrowania pisanego kodu. Ale w języku C # powinieneś zamiast tego myśleć o relacjach między obiektami w domenie biznesowej. „Co chcę robić”, a nie „co robi komputer”.

Jeśli chcesz coś zrobić na liście obiektów, zamiast pisać na liście pętlę for, utwórz metodę, która pobiera pętlę IEnumerable<MyObject>i używa foreach.

Twoje konkretne przykłady:

Kontrolowanie czasu życia zasobów, które wymagają deterministycznego czyszczenia (np. Plików). Łatwo jest to wykorzystać, ale jak właściwie z niego korzystać, gdy własność zasobu jest przenoszona [... między wątkami]? W C ++ po prostu używałbym wspólnych wskaźników i pozwalał zająć się „odśmiecaniem” w odpowiednim czasie.

Nigdy nie powinieneś tego robić w języku C # (szczególnie między wątkami). Jeśli chcesz coś zrobić z plikiem, zrób to w jednym miejscu na raz. Jest ok (i rzeczywiście dobra praktyka), aby napisać klasę opakowania, która zarządza niezarządzanym zasobem, który jest przekazywany między twoimi klasami, ale nie próbuj przekazywać pliku między wątkami i mieć osobne klasy, które zapisują / czytają / zamykają / otwierają. Nie dziel się własnością niezarządzanych zasobów. Użyj Disposewzoru, aby poradzić sobie z porządkiem.

Ciągłe zmaganie się z nadpisywaniem funkcji dla określonych generycznych (uwielbiam takie rzeczy, jak częściowa specjalizacja szablonów w C ++). Czy powinienem po prostu zrezygnować z jakichkolwiek prób programowania ogólnego w języku C #? Może generyczne są celowo ograniczone i używanie ich nie jest C # poza konkretną domeną problemów?

Ogólne w C # są zaprojektowane, aby być ogólne. Specjalizacje generyczne powinny być obsługiwane przez klasy pochodne. Dlaczego warto List<T>zachowywać się inaczej, jeśli jest to List<int>a List<string>? Wszystkie operacje na List<T>są ogólne, aby można je było zastosować do dowolnej List<T> . Jeśli chcesz zmienić zachowanie .Addmetody na List<string>, a następnie utwórz klasę pochodną MySpecializedStringCollection : List<string>lub klasę złożoną, MySpecializedStringCollection : IList<string>która korzysta z tego, co ogólne, ale robi różne rzeczy. Pomoże to uniknąć naruszenia zasady substytucji Liskowa i po prostu rujnować innych, którzy korzystają z twojej klasy.

Funkcjonalność przypominająca makro. Chociaż ogólnie jest to zły pomysł, w przypadku niektórych domen problemów nie ma innego obejścia (np. Warunkowa ocena instrukcji, podobnie jak w przypadku dzienników, które powinny przejść tylko do wersji debugowania). Brak ich oznacza, że ​​muszę postawić więcej, jeśli (warunek) {...} płyta kotłowa i nadal nie jest równa pod względem wywoływania efektów ubocznych.

Jak powiedzieli inni, możesz to zrobić za pomocą poleceń preprocesora. Atrybuty są jeszcze lepsze. Ogólnie atrybuty są najlepszym sposobem obsługi rzeczy, które nie są funkcjami „rdzenia” dla klasy.

Krótko mówiąc, pisząc C #, pamiętaj, że powinieneś myśleć wyłącznie o domenie biznesowej, a nie o procesorze i pamięci. W razie potrzeby możesz zoptymalizować później , ale kod powinien odzwierciedlać relacje między zasadami biznesowymi, które próbujesz zmapować.

Stephen
źródło
3
WRT. transfer zasobów: „nigdy nie powinieneś tego robić” nie ogranicza IMHO. Często bardzo dobry projekt sugeruje przeniesienie IDisposable( nie surowego zasobu) z jednej klasy do drugiej: wystarczy zobaczyć API StreamWriter, gdzie ma to sens dla Dispose()przekazywanego strumienia. I jest dziura w języku C # - nie można tego wyrazić w systemie pisma.
Martin Ba
2
„Jednym z przykładów tego, co widziałem, jest to, że wielu programistów C ++ lubi używać pętli, ponieważ są szybsze niż foreach.” To także zły pomysł w C ++. Abstrakcja o zerowym koszcie jest wielką mocą C ++, a foreach lub inne algorytmy nie powinny być wolniejsze niż odręcznie napisane dla pętli w większości przypadków.
Sebastian Redl,
1

Najbardziej odmienna była dla mnie tendencja do wszystkiego nowego . Jeśli chcesz obiekt, zapomnij o próbie przekazania go do rutyny i współdzielenia podstawowych danych, wygląda na to, że wzorzec C # służy tylko do stworzenia nowego obiektu. Zasadniczo wydaje się, że jest to znacznie więcej w języku C #. Na początku ten wzorzec wydawał się bardzo nieefektywny, ale myślę, że tworzenie większości obiektów w C # to szybka operacja.

Jednak są też klasy statyczne, które naprawdę mnie denerwują, ponieważ od czasu do czasu próbujesz stworzyć jedną tylko po to, by odkryć, że musisz jej po prostu użyć. Nie jest tak łatwo wiedzieć, którego użyć.

Jestem całkiem szczęśliwy w programie C #, że po prostu go ignoruję, gdy już go nie potrzebuję, ale pamiętaj tylko, co zawiera ten obiekt - generalnie musisz tylko pomyśleć, jeśli ma jakieś „niezwykłe” dane, obiekty składające się tylko z pamięci po prostu odejdźcie sami, inni będą musieli się pozbyć lub będziesz musiał spojrzeć na to, jak je zniszczyć - ręcznie lub za pomocą bloku użycia, jeśli nie.

bardzo szybko go podniesiesz, C # prawie nie różni się od VB, więc możesz traktować to jako to samo narzędzie RAD (jeśli pomaga myśleć o C # jak VB.NET, zrób to, składnia jest tylko nieznacznie inne, a moje dawne czasy używania VB doprowadziły mnie do właściwego sposobu myślenia o rozwoju RAD). Jedynym trudnym bitem jest określenie, którego z wielkich bibliotek .net należy użyć. Google jest tutaj zdecydowanie Twoim przyjacielem!

gbjbaanb
źródło
1
Przekazywanie obiektu do procedury w celu współużytkowania bazowych danych jest całkowicie poprawne w języku C #, przynajmniej z perspektywy składni.
Brian