Dwie struktury z tymi samymi członkami, ale o różnych nazwach, czy to dobry pomysł?

49

Piszę program, który wymaga pracy ze współrzędnymi biegunowymi i kartezjańskimi.

Czy ma sens tworzenie dwóch różnych struktur dla każdego rodzaju punktów, jeden z Xi Yczłonkowie, a drugi z Ri Thetaczłonkowie.

Lub jest go zbyt dużo i lepiej jest mieć tylko jedną z struct firsti secondjako członków.

To, co piszę, jest proste i niewiele się zmieni. Ale jestem ciekaw, co jest lepsze z punktu widzenia projektowania.

Myślę, że pierwsza opcja jest lepsza. Wydaje się bardziej czytelny i skorzystam z kontroli typu.

Wszechmocny wielbłąd Moha
źródło
11
Zawsze tworzę nową strukturę / klasę dla każdego celu. Potrzebujesz wektora 3d, utwórz struct 3d_vector z trzema pływakami. Potrzebujesz reprezentacji uvw, utwórz struct texture_coords z trzema zmiennoprzecinkowymi. Potrzebujesz pozycji w środowisku 3d, utwórz pozycję struktury z trzema zmiennoprzecinkowymi. Dostajesz punkt. Pozwala to na znacznie lepszą czytelność niż używanie tego samego. Jeśli przeszkadza ci definiowanie tego samego wiele razy, użyj podstawowej struktury 3-float i zdefiniuj wiele nazw, aby były tą samą strukturą.
Kevin,
Jeśli mają jakieś wspólne metody, to może jedna. Czy kiedykolwiek musiałbyś porównać ich równość?
paparazzo
8
@ Sidney Chyba że absolutnie potrzebujesz funkcjonalności, której bym nie potrzebował. Musisz wykonać operację sin / arcsin, aby przekonwertować obie reprezentacje. Wprowadzi to trochę bitrotu w najmniej znaczących bitach za każdym razem, gdy wykonasz konwersję. Jestem prawie pewien, że skończyłbyś z podobnym bólem, przez co przeszedłem próbując poradzić sobie z klasą, która zapewniała zarówno częstotliwość zdarzeń, jak i czas pomiędzy reprezentacją czegoś (xi 1 / x). Śledzenie, która reprezentacja jest kanoniczna w klasie i radzenie sobie z wszystkimi zaokrąglającymi bólami głowy, nie jest czymś, co chciałbym zrobić ponownie.
Dan Neely,
3
Dobrym przykładem typu danych, który może reprezentować wiele rzeczy, jest ciąg znaków, ale „stringy typed” jest antypatrakiem. Napisz swój przykład. Spróbuj zaimplementować iloczyn skalarny dla typu, który obsługuje oba układy współrzędnych.
Nathan Cooper
1
Powinieneś starać się używać różnych typów, gdy ma to zastosowanie, aby uzyskać korzyści z bezpieczeństwa typu - zapobiegnie to wysłaniu niewłaściwego typu do / z funkcji (przy użyciu sprawdzania poprawności czasu kompilacji, w zależności od języka programowania). Ułatwi to konserwację, ponieważ można znaleźć wszystkie prawdziwe zastosowania pewnego typu (bez uzyskiwania błędnych zastosowań z powodu skonfliktowania typów).
Erik Eidt,

Odpowiedzi:

17

Widziałem oba rozwiązania, więc zdecydowanie zależy to od kontekstu.

Aby zwiększyć czytelność, posiadanie wielu struktur, jak sugerujesz, jest bardzo skuteczne. Jednak w niektórych środowiskach chcesz wykonywać typowe operacje na tych strukturach i znajdujesz powielanie kodu, na przykład operacje wektorowe na macierzach *. Może to być frustrujące, gdy dana operacja nie jest dostępna w twoim smaku wektora, ponieważ nikt jej tam nie przenosił.

Skrajnym rozwiązaniem (które ostatecznie daliśmy) jest posiadanie klasy bazowej opartej na szablonie CRTP z funkcjami get <0> () get <1> () i get <2> (), aby uzyskać elementy w sposób ogólny. Funkcje te są następnie zdefiniowane w strukturze kartezjańskiej lub polarnej, która wywodzi się z tej klasy bazowej. Rozwiązuje wszystkie problemy, ale ma dość głupią cenę: konieczność uczenia się metaprogramowania szablonów. Jeśli jednak metaprogramowanie szablonów jest już uczciwą grą dla twojego projektu, może być dobre.

Cort Ammon
źródło
1
Twoja odpowiedź jest bardzo interesująca. czy można podać przykład?
Moha Wszechmogący wielbłąd
1
Mogę zbadać przykład tego, co zrobiłem: FIR filtrujący bieguny, kartezjan i ich wektory. Matematyka była dość podobna, bez zawijania (odwijania), w kodzie i tak niektóre części zostały zduplikowane ze względów wydajnościowych i zastosowaliśmy szablony tam, gdzie było to samo. Użyłem różnych nazw dla wszystkich rzeczy. „Ekstremalne rozwiązanie” Corta mogło zaoszczędzić kilka duplikatów, ale nie prawie wszystkie z nich.
Eugene Ryabtsev,
1
Moją pierwszą reakcją było to, że taką sytuację lepiej rozwiązać za pomocą castingu, ale okazuje się to nieco ryzykowne .
200_sukces
114

Tak, to ma sens.

Wartość struktury nie polega tylko na tym, że hermetyzuje ona dane pod przydatną nazwą. Wartość polega na tym, że kodyfikuje twoje intencje, dzięki czemu kompilator może pomóc ci sprawdzić, czy pewnego dnia ich nie naruszysz (np. Pomylisz zestaw współrzędnych biegunowych z zestawem kartezjańskich).

Ludzie źle pamiętają takie dręczące szczegóły, ale potrafią tworzyć odważne, pomysłowe plany. Komputery są dobre w myleniu szczegółów, a złe w kreatywnych planach. Dlatego zawsze dobrym pomysłem jest przeniesienie tak drobnych szczegółów do komputera, abyś mógł swobodnie pracować nad wielkim planem.

Kilian Foth
źródło
6
+1 Idealny opis używania komputera do robienia tego, co komputery potrafią dobrze, i pozwalania mózgowi skupić się na własnej pracy.
BrianH
8
„Programy powinny być pisane z myślą o ludziach do czytania, a tylko przypadkowo o uruchamianiu maszyn”. - z „Struktury i interpretacji programów komputerowych” Abelsona i Sussmana.
hlovdal
18

Tak, chociaż zarówno kartezjański, jak i biegunowy są (na ich miejscu) niezwykle sensownymi schematami reprezentacji współrzędnych, najlepiej, aby nigdy nie były mieszane (jeśli masz punkt kartezjański {1,1}, jest to zupełnie inny punkt niż biegun {1,1 }).

W zależności od potrzeb, może on również być warta realizacji współrzędnych interfejs, za pomocą metod, takich jak X(), Y(), Displacement()i Angle()(ewentualnie Radius()i Theta(), w zależności od).

Vatine
źródło
Jeszcze ważniejsze jest, jeśli OP tworzy klasy z tych struktur, ponieważ operacje na współrzędnych kartezjańskich i biegunowych są różne.
Mindwin,
1
+1 za ostatni akapit, to idealne rozwiązanie. Punkt to przestrzeń to obiekt; wewnętrzna reprezentacja tego punktu nie powinna mieć znaczenia. Oczywiście rzeczywiste problemy (wydajność, błędy zaokrąglania) mogą mieć znaczenie. Wszystko zależy od tego, do czego jest wykorzystywany.
BlueRaja - Danny Pflughoeft
Również w tym przykładzie jest mało prawdopodobne, aby się to zmieniło, ale jeśli były to 2 inne klasy, nic nie mówi, że mogą się różnić w niektórych momentach.
dyesdyes,
8

Ostatecznie celem programowania jest przełączanie bitów tranzystora w celu wykonania użytecznej pracy. Ale myślenie na tak niskim poziomie doprowadziłoby do niemożliwego do opanowania szaleństwa, dlatego istnieją języki programowania wyższego poziomu, które pomogą ci ukryć złożoność.

Jeśli utworzysz tylko jedną strukturę z nazwanymi członkami firsti second, wówczas nazwy te nic nie znaczą; traktowałbyś je zasadniczo jako adresy pamięci. To przeczy celowi języka programowania wysokiego poziomu.

Co więcej, tylko dlatego, że wszystkie są reprezentowalne jako doublenie oznacza, że ​​można ich używać zamiennie. Na przykład θ jest kątem bezwymiarowym, podczas gdy y ma jednostki długości. Ponieważ typy nie są logicznie zastępowalne, powinny być dwiema niekompatybilnymi strukturami.

Jeśli naprawdę chcesz zagrać w sztuczki związane z ponownym użyciem pamięci - a prawie na pewno nie powinieneś - możesz użyć uniontypu C, aby wyrazić swoją intencję.

200_sukces
źródło
Najlepsza odpowiedź, IMHO
Dean Radcliffe,
2

Po pierwsze, obaj wyraźnie, zgodnie z całkowicie dźwiękową odpowiedzią @ Kilian-foth.

Chciałbym jednak dodać:

Zapytaj: Czy naprawdę masz operacje, które są typowe dla obu, gdy są traktowane jako pary double? Zauważ, że to nie to samo, co powiedzenie, że masz operacje, które dotyczą obu na ich własnych warunkach. Na przykład „fabuła (Coord)” dba o to, czy Coordjest to biegun, czy kartezjański. Z drugiej strony utrwalenie pliku po prostu traktuje dane takie, jakie są. Jeśli naprawdę nie mają operacje generycznych, należy rozważyć albo definiując klasę bazową lub definiowania konwerter do std::pair<double, double>Or tuplelub cokolwiek masz w swoim języku.

Ponadto, jednym podejściem może być traktowanie jednego typu współrzędnych jako bardziej fundamentalnego, a drugiego jedynie jako obsługi interakcji użytkownika lub zewnętrznej.

Więc może zapewnić wszystkie podstawowe operacje są kodowane Cartesian, a następnie zapewnić wsparcie dla konwersji Polardo Cartesian. Pozwala to uniknąć wdrażania różnych wersji wielu operacji.

Keith
źródło
1

Możliwym rozwiązaniem, w zależności od języka i jeśli wiesz, że obie klasy będą miały podobne metody i operacje, byłoby jednokrotne zdefiniowanie klasy i użycie aliasów typów w celu jawnego nazwania typów inaczej.

Ma to również tę zaletę, że dopóki klasy są dokładnie takie same, możesz zachować tylko jedną, ale gdy tylko zajdzie potrzeba ich zmiany, nie trzeba modyfikować kodu za ich pomocą, ponieważ typy były już używane oczywiście.

Inną opcją, zależną ponownie od użycia klas (jeśli potrzebujesz polimorfizmu itp.), Jest użycie dziedziczenia publicznego dla obu nowych typów, aby miały one ten sam interfejs publiczny co typ wspólny, który reprezentują. Pozwala to również na oddzielną ewolucję typów.

Svalorzen
źródło
Nazwiska członków obu klas nie są takie same. w rzeczywistości nazwy są jedyną różnicą między dwiema klasami
Moha Wszechmogący wielbłąd
@ Mhd.Tahawi Możesz zaimplementować metody pobierające i ustawiające o odpowiednich nazwach, upewniając się, że klasa, której używasz, jest taka sama, ale podając odpowiednie nazwy dla operacji, których chcesz użyć. Staje się trochę bardziej szczegółowy, ale będziesz musiał powielić mniej kodu.
Svalorzen,
0

Uważam, że posiadanie takich samych nazw członków jest w tym przypadku złym pomysłem, ponieważ sprawia, że ​​kod jest bardziej podatny na błędy.

Wyobraź sobie scenariusz: masz kilka kartezjańskich punktów: pntA i pntB. Następnie z jakiegoś powodu decydujesz, że powinny być lepiej reprezentowane we współrzędnych biegunowych, i zmieniasz deklarację i konstruktor.

Teraz, jeśli wszystkie twoje operacje były tylko wywołaniami metod, takimi jak:

double distance = pntA.distanceFrom(pntB);

więc wszystko w porządku. Ale co, jeśli użyjesz członków wyraźnie? Porównać

double leftMargin = abs(pntA.x - pntB.x);
double leftMargin = abs(pntA.first - pntB.first);

W pierwszym przypadku kod nie zostanie skompilowany. Natychmiast zobaczysz błąd i będziesz mógł go naprawić. Ale jeśli masz takie same nazwy członków, błąd będzie tylko na poziomie logicznym, znacznie trudniejszy do wykrycia.

Jeśli piszesz w języku nie zorientowanym obiektowo, jeszcze łatwiej jest przekazać niewłaściwą strukturę do funkcji. Co powstrzyma Cię przed napisaniem następującego kodu?

double distance = calculate_distance_polar(cartesianPointA, polarPointB);

Z drugiej strony różne typy danych pozwalają znaleźć błąd podczas kompilacji.

IMil
źródło