Mam switch
strukturę, która ma kilka spraw do załatwienia. switch
Pracuje przy enum
co stwarza problem powielonych kodu poprzez wspólne wartości:
// All possible combinations of One - Eight.
public enum ExampleEnum {
One,
Two, TwoOne,
Three, ThreeOne, ThreeTwo, ThreeOneTwo,
Four, FourOne, FourTwo, FourThree, FourOneTwo, FourOneThree,
FourTwoThree, FourOneTwoThree
// ETC.
}
Obecnie switch
struktura obsługuje każdą wartość osobno:
// All possible combinations of One - Eight.
switch (enumValue) {
case One: DrawOne; break;
case Two: DrawTwo; break;
case TwoOne:
DrawOne;
DrawTwo;
break;
case Three: DrawThree; break;
...
}
Masz pomysł. Obecnie mam tę if
strukturę w stosie , aby zamiast tego obsługiwać kombinacje z pojedynczą linią:
// All possible combinations of One - Eight.
if (One || TwoOne || ThreeOne || ThreeOneTwo)
DrawOne;
if (Two || TwoOne || ThreeTwo || ThreeOneTwo)
DrawTwo;
if (Three || ThreeOne || ThreeTwo || ThreeOneTwo)
DrawThree;
Stwarza to problem niewiarygodnie długich ocen logicznych, które są mylące w czytaniu i trudne do utrzymania. Po przeredagowaniu tego zacząłem myśleć o alternatywach i pomyślałem o idei switch
struktury z przewrotem między przypadkami.
Muszę użyć goto
w tym przypadku, ponieważ C#
nie pozwala na awarię. Jednak zapobiega niewiarygodnie długim łańcuchom logicznym, mimo że przeskakuje w switch
strukturze i wciąż powoduje duplikację kodu.
switch (enumVal) {
case ThreeOneTwo: DrawThree; goto case TwoOne;
case ThreeTwo: DrawThree; goto case Two;
case ThreeOne: DrawThree; goto default;
case TwoOne: DrawTwo; goto default;
case Two: DrawTwo; break;
default: DrawOne; break;
}
To wciąż nie jest wystarczająco czyste rozwiązanie, a ze goto
słowem kluczowym wiąże się piętno , którego chciałbym uniknąć. Jestem pewien, że musi być lepszy sposób na oczyszczenie tego.
Moje pytanie
Czy istnieje lepszy sposób obsługi tego konkretnego przypadku bez wpływu na czytelność i łatwość konserwacji?
źródło
goto
gdy struktura wysokiego poziomu nie istnieje w twoim języku. Czasami żałuję, że nie byłofallthru
; słowo kluczowe, aby pozbyć się tego konkretnego zastosowania,goto
ale no cóż.goto
języka wysokiego poziomu, takiego jak C #, prawdopodobnie przeoczyłeś wiele innych (i lepszych) alternatyw projektowania i / lub implementacji. Bardzo odradzam.Odpowiedzi:
Uważam, że kod jest trudny do odczytania za pomocą
goto
instrukcji. Poleciłbym ustrukturyzować twojeenum
inaczej. Na przykład, jeśli twojeenum
pole bitowe reprezentowało jedną z opcji, może wyglądać następująco:Atrybut Flagi informuje kompilator, że konfigurujesz wartości, które się nie nakładają. Kod wywołujący ten kod może ustawić odpowiedni bit. Następnie możesz zrobić coś takiego, aby wyjaśnić, co się dzieje:
Wymaga to kodu, który konfiguruje się,
myEnum
aby poprawnie ustawić pola bitowe i oznaczone atrybutem Flagi. Ale możesz to zrobić, zmieniając wartości wyliczeń w przykładzie na:Kiedy piszesz liczbę w formularzu
0bxxxx
, podajesz ją w postaci binarnej. Widać więc, że ustawiamy bit 1, 2 lub 3 (cóż, technicznie 0, 1 lub 2, ale masz pomysł). Możesz także nazwać kombinacje za pomocą bitowego LUB, jeśli kombinacje mogą być często zestawiane razem.źródło
public enum ExampleEnum { One = 1 << 0, Two = 1 << 1, Three = 1 << 2, OneAndTwo = One | Two, OneAndThree = One | Three, TwoAndThree = Two | Three };
. Nie trzeba nalegać na C # 7 +.[Flags]
Atrybut nie nic kompilatora sygnał. Dlatego wciąż musisz jawnie deklarować wartości wyliczeniowe jako potęgi 2.HasFlag
jest opisowym i wyraźnym terminem na wykonywaną operację i abstrahuje implementację od funkcjonalności. Używanie,&
ponieważ jest powszechne w innych językach, nie ma większego sensu niż używaniebyte
typu zamiast deklarowania anenum
.IMO leży u podstaw problemu, ponieważ ten fragment kodu nawet nie powinien istnieć.
Najwyraźniej masz trzy niezależne warunki i trzy niezależne działania, które należy podjąć, jeśli te warunki są prawdziwe. Dlaczego więc to wszystko jest umieszczone w jednym kawałku kodu, który potrzebuje trzech flag boolowskich, aby powiedzieć mu, co ma robić (niezależnie od tego, czy zamieniasz je w wyliczenie), a potem robi jakąś kombinację trzech niezależnych rzeczy? Wydaje się, że zasada jednej odpowiedzialności ma tutaj dzień wolny od pracy.
Umieść wywołania w trzech funkcjach, do których należą (tzn. Gdy odkryjesz potrzebę wykonywania działań) i wyślij kod z tych przykładów do kosza.
Gdyby było dziesięć flag i akcji, a nie trzy, czy rozszerzyłbyś ten rodzaj kodu na 1024 różne kombinacje? Mam nadzieję, że nie! Jeśli 1024 to za dużo, 8 również jest za dużo z tego samego powodu.
źródło
Nigdy nie używaj gotos to jedna z koncepcji informatyki „okłamuje dzieci” . To właściwa rada w 99% przypadków, a czasy, kiedy tak nie jest, są tak rzadkie i wyspecjalizowane, że dla wszystkich jest znacznie lepiej, jeśli wyjaśniono to nowym programistom jako „nie używaj ich”.
Kiedy więc powinny być używane? Jest kilka scenariuszy , ale podstawowy wydaje się, że uderzasz: kiedy kodujesz maszynę stanu . Jeśli nie ma lepiej zorganizowanego i ustrukturyzowanego wyrażenia twojego algorytmu niż maszyna stanu, wówczas jego naturalne wyrażenie w kodzie obejmuje nieustrukturyzowane gałęzie i nie można wiele z tym zrobić, co prawdopodobnie nie tworzy struktury sama maszyna stanowa jest bardziej niejasna niż mniejsza.
Autorzy kompilatorów wiedzą o tym, dlatego kod źródłowy większości kompilatorów implementujących parsery LALR * zawiera gotos. Jednak bardzo niewiele osób faktycznie koduje własne analizatory leksykalne i analizatory składni.
* - IIRC, możliwe jest zaimplementowanie gramatyki LALL w sposób całkowicie rekurencyjny, bez uciekania się do skokowych tabel lub innych nieustrukturyzowanych instrukcji sterujących, więc jeśli jesteś naprawdę przeciwny goto, jest to jedno wyjście.
Teraz następne pytanie brzmi: „Czy ten przykład jest jednym z tych przypadków?”
Patrząc na to, widzę, że masz trzy różne możliwe kolejne stany, w zależności od przetwarzania bieżącego stanu. Ponieważ jeden z nich („domyślny”) jest po prostu wierszem kodu, technicznie można się go pozbyć, przypinając ten wiersz kodu na końcu stanów, których dotyczy. To doprowadziłoby cię do 2 możliwych następnych stanów.
Jeden z pozostałych („Trzy”) jest rozgałęziony tylko z jednego miejsca, które widzę. Abyś mógł się go pozbyć w ten sam sposób. Otrzymasz kod, który wygląda następująco:
Jednak znowu był to podany przez ciebie przykład zabawki. W przypadkach, w których „default” zawiera nietrywialną ilość kodu, „trzy” jest przenoszone z wielu stanów lub (co najważniejsze) dalsze utrzymanie prawdopodobnie doda lub skomplikuje te stany , na pewno byłoby lepiej używając gotos (a być może nawet pozbywając się struktury enum-case, która ukrywa naturę automatu stanu, chyba że istnieje jakiś dobry powód, dla którego musi zostać).
źródło
goto
.Najlepszą odpowiedzią jest użycie polimorfizmu .
Kolejna odpowiedź, która, według IMO, sprawia, że jeśli wszystko jest bardziej przejrzyste i prawdopodobnie krótsze :
goto
to chyba mój 58 wybór tutaj ...źródło
Dlaczego nie to:
OK, zgadzam się, to jest hackish (nie jestem deweloperem C # btw, więc przepraszam za kod), ale z punktu widzenia wydajności jest to konieczne? Używanie wyliczeń jako indeksu tablicy jest poprawnym C #.
źródło
int[ExempleEnum.length] COUNTS = { 1, 3, 4, 2, 5, 3 };
:?Jeśli nie możesz lub nie chcesz używać flag, użyj funkcji rekurencyjnej. W trybie 64-bitowym kompilator wygeneruje kod, który jest bardzo podobny do twojej
goto
instrukcji. Po prostu nie musisz sobie z tym poradzić.źródło
Przyjęte rozwiązanie jest w porządku i jest konkretnym rozwiązaniem twojego problemu. Chciałbym jednak postawić na alternatywne, bardziej abstrakcyjne rozwiązanie.
Z mojego doświadczenia wynika, że użycie wyliczeń do zdefiniowania przepływu logiki jest zapachem kodu, ponieważ często jest oznaką złego projektu klasy.
Natrafiłem na prawdziwy przykład tego, co dzieje się w kodzie, nad którym pracowałem w zeszłym roku. Pierwotny programista stworzył jedną klasę, która logowała zarówno import, jak i eksport, i przełączała się między nimi w oparciu o wyliczenie. Teraz kod był podobny i miał trochę zduplikowanego kodu, ale był na tyle inny, że spowodowało to, że kod był znacznie trudniejszy do odczytania i praktycznie niemożliwy do przetestowania. Skończyłem przefakturowanie tego na dwie osobne klasy, co uprościło obie i faktycznie pozwoliło mi wykryć i wyeliminować wiele niezgłoszonych błędów.
Jeszcze raz muszę stwierdzić, że używanie wyliczeń do kontrolowania przepływu logiki jest często problemem projektowym. W ogólnym przypadku Enums powinny być stosowane głównie w celu zapewnienia bezpiecznych dla danego typu, przyjaznych konsumentom wartości, w których możliwe wartości są jasno określone. Są one lepiej wykorzystywane jako właściwość (na przykład jako identyfikator kolumny w tabeli) niż jako mechanizm kontroli logiki.
Rozważmy problem przedstawiony w pytaniu. Naprawdę nie znam tutaj kontekstu ani tego, co reprezentuje ten enum. Czy to losowanie kart? Rysowanie obrazów? Czerpiesz krew? Czy zamówienie jest ważne? Nie wiem też, jak ważna jest wydajność. Jeśli wydajność lub pamięć mają kluczowe znaczenie, to rozwiązanie prawdopodobnie nie będzie tym, czego potrzebujesz.
W każdym razie rozważmy wyliczenie:
Mamy tutaj szereg różnych wartości wyliczeniowych, które reprezentują różne koncepcje biznesowe.
Zamiast tego moglibyśmy zastosować abstrakcje w celu uproszczenia.
Rozważmy następujący interfejs:
Następnie możemy zaimplementować to jako klasę abstrakcyjną:
Możemy mieć konkretną klasę reprezentującą rysunek pierwszy, drugi i trzeci (które ze względu na argument mają inną logikę). Mogą potencjalnie korzystać z klasy bazowej zdefiniowanej powyżej, ale zakładam, że koncepcja DrawOne różni się od koncepcji reprezentowanej przez wyliczenie:
A teraz mamy trzy oddzielne klasy, które można skomponować, aby zapewnić logikę dla innych klas.
To podejście jest bardziej szczegółowe. Ale ma to zalety.
Rozważ następującą klasę, która zawiera błąd:
O ile prostsze jest wykrycie brakującego wywołania drawThree.Draw ()? A jeśli kolejność jest ważna, kolejność losowań jest również bardzo łatwa do zobaczenia i do naśladowania.
Wady tego podejścia:
Zalety tego podejścia:
Rozważ to (lub podobne) podejście, ilekroć poczujesz potrzebę zapisania złożonego kodu kontroli logicznej w instrukcjach przypadków. W przyszłości będziesz szczęśliwy, że to zrobiłeś.
źródło
Jeśli zamierzasz użyć przełącznika tutaj, Twój kod będzie faktycznie szybszy, jeśli będziesz rozpatrywać każdą sprawę osobno
w każdym przypadku wykonywana jest tylko jedna operacja arytmetyczna
także, jak powiedzieli inni, jeśli rozważasz użycie goto, prawdopodobnie powinieneś przemyśleć swój algorytm (chociaż przyznaję, że brak literatury C # może być powodem do użycia goto). Zobacz słynny artykuł Edgara Dijkstry „Idź do oświadczenia uznanego za szkodliwy”
źródło
W twoim konkretnym przykładzie, ponieważ wszystko, czego tak naprawdę chciałeś od wyliczenia, to wskaźnik do / do-not dla każdego z kroków, rozwiązanie, które przepisuje twoje trzy
if
instrukcje, jest lepsze niż aswitch
, i dobrze, że masz zaakceptowaną odpowiedź .Ale jeśli miałeś bardziej złożoną logikę, która nie zadziałała tak czysto, to nadal uważam, że
goto
s toswitch
zdanie jest mylące. Wolałbym zobaczyć coś takiego:To nie jest idealne, ale myślę, że tak jest lepiej niż z
goto
s. Jeśli sekwencje zdarzeń są tak długie i powielają się tak bardzo, że tak naprawdę nie ma sensu przeliterować pełnej sekwencji dla każdego przypadku, wolę podprogram, niżgoto
w celu ograniczenia powielania kodu.źródło
Powodem do nienawiści tego
goto
słowa kluczowego jest kodUps! To najwyraźniej nie zadziała.
message
Zmienna nie jest zdefiniowana w tej ścieżce kodu. Więc C # nie przejdzie tego. Ale może to być dorozumiane. RozważaćI załóżmy, że
display
to zawieraWriteLine
stwierdzenie.Tego rodzaju błąd może być trudny do znalezienia, ponieważ
goto
zasłania ścieżkę kodu.To jest uproszczony przykład. Załóżmy, że prawdziwy przykład nie byłby tak oczywisty. Może być pięćdziesiąt linii kodu pomiędzy
label:
i użyciemmessage
.Język może pomóc rozwiązać ten problem, ograniczając sposób
goto
korzystania z niego, tylko zstępując z bloków. Ale C #goto
nie jest tak ograniczony. Może przeskakiwać kod. Ponadto, jeśli zamierzasz ograniczyćgoto
, równie dobrze jest zmienić nazwę. Inne języki używająbreak
do schodzenia z bloków, z liczbą (bloków do wyjścia) lub etykietą (na jednym z bloków).Pojęcie
goto
to jest instrukcją języka maszynowego niskiego poziomu. Ale jedynym powodem, dla którego mamy języki wyższego poziomu, jest to, że jesteśmy ograniczeni do abstrakcji wyższego poziomu, np. Zakresu zmiennego.To powiedziawszy, jeśli użyjesz C #
goto
wewnątrzswitch
instrukcji do przeskakiwania od przypadku do przypadku, jest to w zasadzie nieszkodliwe. Każda sprawa jest już punktem wejścia. Nadal uważam, że nazywanie tegogoto
jest głupie w tej sytuacji, ponieważ łączy to nieszkodliwe użyciegoto
z bardziej niebezpiecznymi formami. Wolałbym, żeby używalicontinue
do tego czegoś takiego . Ale z jakiegoś powodu zapomnieli mnie zapytać, zanim napisali ten język.źródło
C#
języku nie można przeskakiwać deklaracji zmiennych. Na dowód uruchom aplikację konsoli i wprowadź kod podany w poście. Na etykiecie pojawi się błąd kompilatora informujący, że błędem jest użycie nieprzypisanej zmiennej lokalnej „message” . W językach, w których jest to dozwolone, jest to uzasadniona obawa, ale nie w języku C #.Kiedy masz tak wiele możliwości (a nawet więcej, jak mówisz), to może nie jest to kod, ale dane.
Utwórz słownik odwzorowujący wartości wyliczeniowe na akcje, wyrażone albo jako funkcje, albo jako prostszy typ wyliczenia reprezentujący akcje. Następnie kod można sprowadzić do prostego wyszukiwania w słowniku, a następnie albo wywołując wartość funkcji, albo przełączając uproszczone opcje.
źródło
Użyj pętli i różnicy między przerwaniem a kontynuowaniem.
źródło