Dlaczego struktury i klasy są osobnymi pojęciami w języku C #?

44

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 structza „niezmienne” (ustawienie ich pól na readonly), aby zapobiec możliwym błędom, ograniczając structuż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 classi structna 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 refi outsłowa kluczowe wydaje się miłą rzeczą.

Pytanie jest, dlaczego classi structstają się odrębne koncepcje w C # i CLR zamiast jednego rodzaju kruszywa z dwóch opcji przydziału?

Mennice97
źródło
34
„Ogólnie przyjęte rozwiązanie problemu wydaje się być deklarując wszystkie konstrukcjom jak«niezmienne»... ograniczające przydatność elemencie” Wielu ludzi jest zdania, że czyni coś niezmiennego ogólnie sprawia, że bardziej przydatny, gdy nie jest to powodem do obniżenia wydajności. Ponadto, structnie zawsze są przechowywane na stosie; rozważ obiekt z structpolem. Poza tym, jak wspomniał Mason Wheeler, problem krojenia jest prawdopodobnie największym powodem.
Doval
7
Nie jest prawdą, że C # został zainspirowany przez C ++; raczej C # został zainspirowany wszystkimi (dobrze skomentowanymi i dobrze brzmiącymi wówczas błędami) w projektowaniu zarówno C ++, jak i Java.
Pieter Geerkens
18
Uwaga: stos i stos są szczegółami implementacji. Nic nie mówi o tym, że instancje struct muszą być przydzielone na stosie, a instancje klasowe na stosie. I w rzeczywistości nie jest to nawet prawdą. Na przykład bardzo możliwe jest, że kompilator może określić za pomocą analizy ucieczki, że zmienna nie może uciec z zasięgu lokalnego, a tym samym przydzielić go do stosu, nawet jeśli jest instancją klasy. Nie mówi nawet, że w ogóle musi istnieć stos. Możesz przydzielić ramki środowiska jako listę połączoną na stercie, a nawet nie mieć stosu.
Jörg W Mittag
3
"ale potem natkniesz się na metodę przyjmującą parametr typu agregującego i aby dowiedzieć się, czy faktycznie jest on typem wartości, czy też typem referencyjnym, musisz znaleźć deklarację jego typu" Umm, dlaczego to ma znaczenie, dokładnie ? Metoda prosi o podanie wartości. Podajesz wartość. W którym momencie musisz się martwić, czy jest to typ odniesienia czy typ wartości?
Luaan

Odpowiedzi:

58

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: structtyp 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 typu Deriveddo argumentu metody typu Base, 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ą.

Mason Wheeler
źródło
29
To tylko kolejna bezcelowa subiektywna reguła dla C ++. Nie mogę przegłosować, ale zrobiłbym, gdybym mógł.
Bartek Banachewicz
17
@MasonWheeler: „ jest straszny bałagan ” brzmi dość subiektywnie. Zostało to już omówione w długim wątku komentarza do innej twojej odpowiedzi ; wątek został zniszczony (niestety, ponieważ zawierał użyteczne komentarze, chociaż w sosie wojennym). Nie sądzę, że warto powtórzyć całą sprawę tutaj, ale „C # dobrze to zrobiło, a C ++ źle to zrobiło” (co wydaje się być komunikatem, który próbujesz przekazać) jest rzeczywiście subiektywnym stwierdzeniem.
Andy Prowl
15
@MasonWheeler: Zrobiłem to w wątku, który został nukowany, podobnie jak kilka innych osób - dlatego myślę, że to niefortunne, że został usunięty. Nie sądzę, że dobrym pomysłem jest replikacja tego wątku tutaj, ale krótka wersja jest taka: w C ++ użytkownik typu, a nie jego projektant , decyduje, z jaką semantyką należy użyć typu (semantyka odniesienia lub wartość semantyka). Ma to wady i zalety: wściekasz się na wady, nie biorąc pod uwagę (lub nie wiedząc?) Zalet. Dlatego analiza jest subiektywna.
Andy Prowl
7
westchnienie Wierzę, że dyskusja na temat naruszenia LSP już się odbyła. I wydaje mi się, że większość ludzi zgodziła się, że wzmianka o LSP jest dość dziwna i niezwiązana, ale nie może tego sprawdzić, ponieważ mod podsunął wątek komentarza .
Bartek Banachewicz
9
Jeśli przeniesiesz ostatni akapit na górę i usuniesz bieżący pierwszy akapit, myślę, że masz idealny argument. Ale obecny pierwszy akapit jest po prostu subiektywny.
Martin York,
19

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 classi structjak 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.

Jerry Coffin
źródło
2
Jako fan C ++ myślę, że jest to doskonałe podsumowanie różnic między C # a Swiss Army Chainsaw.
David Thornley
1
@DavidThornley: Próbowałem przynajmniej napisać coś, co według mnie byłoby nieco zrównoważonym porównaniem. Nie wskazując palcami, ale niektóre z tego, co zobaczyłem, kiedy to napisałem, wydały mi się ... trochę niedokładne (by to ująć ładnie).
Jerry Coffin
7

Pochodzi z „C #: Dlaczego potrzebujemy innego języka?” - Gunnerson, Eric:

Prostota była ważnym celem projektowym dla C #.

Możliwe jest przesadzenie z prostotą i czystością języka, ale czystość ze względu na czystość jest mało przydatna dla profesjonalnego programisty. Dlatego staraliśmy się zrównoważyć nasze pragnienie posiadania prostego i zwięzłego języka z rozwiązywaniem rzeczywistych problemów, z którymi borykają się programiści.

[...]

Typy wartości , przeciążenie operatora i konwersje zdefiniowane przez użytkownika zwiększają złożoność języka , ale pozwalają znacznie uprościć ważny scenariusz dla użytkownika.

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?

manlio
źródło
1
Nie wiem, może nie używam tych brudnych, brzydkich i złych obiektów z semantyką odniesienia?
Bartek Banachewicz
Może ... Jestem zgubioną przyczyną.
manlio
2
IMHO, jedną z największych wad w Javie jest brak jakichkolwiek środków do zadeklarowania, czy zmienna jest używana do hermetyzacji tożsamości lub własności , a jedną z największych wad w C # jest brak możliwości rozróżnienia operacji na zmienna z operacji na obiekcie, do którego zmienna przechowuje odwołanie. Nawet jeśli środowisko wykonawcze nie dbało o takie rozróżnienia, możliwość określenia w języku, czy zmienna typu 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.
supercat
4

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:

// Defined structure
struct Point : IEquatable<Point>
{
  public int X,Y;
  public Point(int x, int y) { X=x; Y=y; }
  public bool Equals(Point other) { return X==other.X && y==other.Y; }
  public bool Equals(Object other)
  { return other != null && other.GetType()==typeof(this) && Equals(Point(other)); }
  public bool ToString() { return String.Format("[{0},{1}", x, y); }
  public bool GetHashCode() { return unchecked(x+y*65531); }
}        
// Auto-generated class
class boxed_Point: IEquatable<Point>
{
  public Point value; // Fake name; C++/CLI, though not C#, allow full access
  public boxed_Point(Point v) { value=v; }
  // Members chain to each member of the original
  public bool Equals(Point other) { return value.Equals(other); }
  public bool Equals(Object other) { return value.Equals(other); }
  public String ToString() { return value.ToString(); }
  public Int32 GetHashCode() { return value.GetHashCode(); }
}

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 structma w sercu nic innego jak agregację pól publicznych i prywatnych połączonych taśmą klejącą.

supercat
źródło