Podczas programowania w języku C # natknąłem się na dziwną decyzję dotyczącą projektu języka, której po prostu nie rozumiem.
Zatem C # (i CLR) ma dwa zagregowane typy danych: struct
(typ wartości, przechowywane na stosie, bez dziedziczenia) i class
(typ referencyjny, przechowywane na stercie, ma dziedziczenie).
Ta konfiguracja na początku brzmi nieźle, ale potem natkniesz się na metodę przyjmującą parametr typu agregującego i aby dowiedzieć się, czy faktycznie jest to typ wartości, czy typ referencyjny, musisz znaleźć deklarację jego typu. Czasami może być naprawdę mylące.
Ogólnie przyjętym rozwiązaniem problemu wydaje się być uznanie wszystkich struct
za „niezmienne” (ustawienie ich pól na readonly
), aby zapobiec możliwym błędom, ograniczając struct
użyteczność.
Na przykład C ++ wykorzystuje o wiele bardziej użyteczny model: pozwala utworzyć instancję obiektu na stosie lub na stercie i przekazać ją według wartości lub referencji (lub wskaźnika). Ciągle słyszę, że C # został zainspirowany przez C ++ i po prostu nie rozumiem, dlaczego nie przyjął tej jednej techniki. Łączenie class
i struct
na jednej konstrukcji z dwoma różnymi opcjami środków (sterty i stosu) i przekazując je wokół jako wartości lub (wyraźnie) jako odniesienia za pośrednictwem ref
i out
słowa kluczowe wydaje się miłą rzeczą.
Pytanie jest, dlaczego class
i struct
stają się odrębne koncepcje w C # i CLR zamiast jednego rodzaju kruszywa z dwóch opcji przydziału?
źródło
struct
nie zawsze są przechowywane na stosie; rozważ obiekt zstruct
polem. Poza tym, jak wspomniał Mason Wheeler, problem krojenia jest prawdopodobnie największym powodem.Odpowiedzi:
Powodem, dla którego C # (i Java oraz zasadniczo każdy inny język OO opracowany po C ++) nie skopiował modelu C ++ w tym aspekcie, jest to, że sposób, w jaki robi C ++, jest strasznym bałaganem.
Prawidłowo zidentyfikowałeś odpowiednie punkty powyżej:
struct
typ wartości, brak dziedziczenia.class
: typ referencyjny, ma dziedziczenie. Rodzaje dziedziczenia i wartości (a ściślej mówiąc polimorfizm i przekazywanie według wartości) nie mieszają się; jeśli przekazujesz obiekt typuDerived
do argumentu metody typuBase
, a następnie wywołujesz na nim metodę wirtualną, jedynym sposobem na uzyskanie właściwego zachowania jest upewnienie się, że przekazane dane były odwołaniem.Pomiędzy tym a wszystkimi innymi mesami, na które natrafisz w C ++, mając dziedziczne obiekty jako typy wartości ( przychodzą mi na myśl konstruktory kopiowania i krojenie obiektów !), Najlepszym rozwiązaniem jest po prostu powiedz nie.
Dobry projekt języka nie tylko implementuje funkcje, ale także wie, których funkcji nie należy implementować, a jednym z najlepszych sposobów na to jest uczenie się na błędach tych, którzy przyszli przed tobą.
źródło
Analogicznie, C # jest w zasadzie zestawem narzędzi dla mechaników, w których ktoś przeczytał, że ogólnie należy unikać szczypiec i kluczy nastawnych, więc w ogóle nie zawiera kluczy nastawnych, a szczypce są zamknięte w specjalnej szufladzie oznaczonej jako „niebezpieczne” , i można z niego korzystać wyłącznie za zgodą przełożonego, po podpisaniu zrzeczenia się odpowiedzialności zwalniającej pracodawcę z odpowiedzialności za zdrowie.
Dla porównania, C ++ obejmuje nie tylko klucze nastawne i szczypce, ale także narzędzia specjalne o nieparzystej kuli, których cel nie jest od razu widoczny, a jeśli nie znasz właściwego sposobu ich trzymania, mogą z łatwością odciąć twoje kciuk (ale gdy zrozumiesz, jak z nich korzystać, możesz robić rzeczy, które są zasadniczo niemożliwe za pomocą podstawowych narzędzi w przyborniku C #). Ponadto ma tokarkę, frezarkę, szlifierkę do powierzchni, piłę taśmową do cięcia metalu itp., Aby umożliwić projektowanie i tworzenie całkowicie nowych narzędzi za każdym razem, gdy poczujesz taką potrzebę (ale tak, narzędzia tych mechaników mogą i będą powodować poważne obrażenia, jeśli nie wiesz, co z nimi robisz - lub nawet po prostu nieostrożnie).
Odzwierciedla to podstawową różnicę w filozofii: C ++ stara się zapewnić ci wszystkie narzędzia, których możesz potrzebować w zasadzie do każdego projektu, jaki chcesz. Prawie nie próbuje kontrolować sposobu korzystania z tych narzędzi, dlatego łatwo jest ich używać do tworzenia projektów, które działają dobrze tylko w rzadkich sytuacjach, a także projektów, które są prawdopodobnie kiepskim pomysłem i nikt nie wie o sytuacji, w której prawdopodobnie będą w ogóle dobrze działać. W szczególności wiele z tego odbywa się poprzez oddzielenie decyzji projektowych - nawet tych, które w praktyce są prawie zawsze powiązane. W rezultacie istnieje ogromna różnica między zwykłym pisaniem w C ++, a dobrym pisaniem w C ++. Aby dobrze pisać w C ++, musisz znać wiele idiomów i praktycznych reguł (w tym praktycznych zasad, jak poważnie przemyśleć ponownie, zanim złamiesz inne reguły). W rezultacie, C ++ jest zorientowany bardziej na łatwość obsługi (przez ekspertów) niż na łatwość uczenia się. Są również (zbyt wiele) okoliczności, w których korzystanie z nich nie jest zbyt trudne.
C # robi o wiele więcej, aby narzucić (lub przynajmniej bardzo mocno zasugerować) to, co projektanci języków uważają za dobre praktyki projektowe. Sporo rzeczy, które są oddzielone w C ++ (ale zwykle idą razem w praktyce), są bezpośrednio połączone w języku C #. Pozwala to „niebezpiecznemu” kodowi nieco przekraczać granice, ale szczerze mówiąc, nie za dużo.
Rezultat jest taki, że z jednej strony istnieje całkiem sporo projektów, które mogą być wyrażane dość bezpośrednio w C ++, które są znacznie bardziej niezdarne do wyrażenia w języku C #. Z drugiej strony, jest to cały dużo łatwiej uczyć się C #, a szanse na produkcję naprawdę straszny projekt, który nie będzie działać w danej sytuacji (lub prawdopodobnie jakiekolwiek inne) są znacznie zmniejszone. W wielu (prawdopodobnie nawet w większości) przypadkach można uzyskać solidny, wykonalny projekt, po prostu „płynąc z prądem”, że tak powiem. Lub, jak jeden z moich przyjaciół (przynajmniej lubię myśleć o nim jako o przyjacielu - nie jestem pewien, czy naprawdę się zgadza) lubi to ująć, C # ułatwia wpadnięcie w pułapkę sukcesu.
Patrząc dokładniej na pytanie, w jaki sposób
class
istruct
jak się mają w dwóch językach: obiekty utworzone w hierarchii dziedziczenia, w których można użyć obiektu klasy pochodnej pod przykrywką jego klasy podstawowej / interfejsu, jesteś w zasadzie utknąłem w fakcie, że zwykle musisz to zrobić za pomocą jakiegoś wskaźnika lub odwołania - na konkretnym poziomie dzieje się tak, że obiekt klasy pochodnej zawiera pamięć, którą można traktować jako instancję klasy podstawowej / interfejs, a obiektem pochodnym jest manipulowany przez adres tej części pamięci.W C ++ programista musi to zrobić poprawnie - gdy korzysta z dziedziczenia, do niego należy upewnienie się, że (na przykład) funkcja, która działa z klasami polimorficznymi w hierarchii, robi to za pomocą wskaźnika lub odwołania do bazy klasa.
W języku C # to, co jest zasadniczo tym samym podziałem między typami, jest znacznie bardziej wyraźne i wymuszone przez sam język. Programista nie musi podejmować żadnych kroków, aby przekazać instancję klasy przez referencję, ponieważ tak się stanie domyślnie.
źródło
Pochodzi z „C #: Dlaczego potrzebujemy innego języka?” - Gunnerson, Eric:
Referencyjna semantyka obiektów jest sposobem na uniknięcie wielu problemów (oczywiście i nie tylko wycinania obiektów), ale problemy w świecie rzeczywistym mogą czasem wymagać obiektów o wartości semantycznej (np. Spójrz na dźwięki, jakbym nigdy nie powinien używać semantyki referencyjnej, prawda? z innego punktu widzenia).
Czy może być lepsze podejście niż segregowanie brudnych, brzydkich i złych obiektów semantycznych pod znacznikiem
struct
?źródło
int[]
powinna być współdzielona, czy zmienna (tablice mogą być albo, ale ogólnie nie oba), pomogłoby sprawić, że zły kod wyglądałby źle.Zamiast myśleć o typach wartości pochodzących z nich
Object
, bardziej pomocne byłoby pomyślenie o typach lokalizacji do przechowywania istniejących w całkowicie odrębnym wszechświecie od typów instancji klas, ale dla każdego typu wartości, który ma odpowiedni typ obiektu stosu. Miejsce przechowywania typu struktury po prostu zawiera konkatenację pól publicznych i prywatnych tego typu, a typ sterty jest generowany automatycznie zgodnie ze wzorem:i dla instrukcji takiej jak: Console.WriteLine („Wartość to {0}”, somePoint);
do przetłumaczenia jako: boxed_Point box1 = nowy boxed_Point (somePoint); Console.WriteLine („Wartość to {0}”, pole 1);
W praktyce, ponieważ typy lokalizacji pamięci i typy instancji sterty istnieją w osobnych wszechświatach, nie trzeba wywoływać typów instancji sterty takich jak
boxed_Int32
; ponieważ system będzie wiedział, jakie konteksty wymagają instancji obiektu sterty, a które wymagają lokalizacji pamięci.Niektórzy uważają, że wszelkie typy wartości, które nie zachowują się jak przedmioty, należy uznać za „złe”. Przyjmuję pogląd przeciwny: ponieważ miejsca przechowywania typów wartości nie są ani obiektami, ani odniesieniami do obiektów, oczekiwanie, że powinny zachowywać się jak obiekty, należy uznać za nieprzydatne. W przypadkach, gdy struct może użytecznie zachowywać się jak obiekt, nie ma w tym nic złego, ale każde
struct
ma w sercu nic innego jak agregację pól publicznych i prywatnych połączonych taśmą klejącą.źródło