Czy przechowywanie niektórych wartości jako ciągów znaków jest złą praktyką?

19

To bardzo niejasny tytuł, ale nie mogłem wymyślić lepszego sposobu na sformułowanie go. Ale, jako przykład, zastanów się, w jakim kierunku porusza się postać w grze. Po prostu używanie sznurka, a następnie robienie czegoś takiego, wydaje się trochę niewłaściwe if(character.direction == "left"). Wydaje mi się, że nie pozostawia zbyt wiele miejsca na błędy głupie, jak przypadkowo użyciu Leftlub lczy cokolwiek zamiast left. Czy moje podejrzenia są prawidłowe? Jeśli tak, jaki jest preferowany sposób osiągnięcia czegoś takiego?

Cal7
źródło
10
masz na myśli, oprócz wyliczeń, czy twój język je obsługuje?
hoffmale
12
Masz na myśli Stringly Typed ?
RubberDuck
Niektóre języki nie mogą przechowywać wartości innych niż ciągi znaków, np bash.
mouviciel
nie wiem dlaczego, ale przypomniałem sobie „Zdefiniuj” w C. Możesz robić rzeczy takie jak: # zdefiniować fałsz prawda
linuxunil

Odpowiedzi:

28

Jeśli używany język obsługuje użycie wyliczeń, użyłbym ich. Pozwala ograniczyć liczbę opcji dostępnych dla danego typu. np. w Javie:

public enum Direction {
    LEFT,
    RIGHT,
    UP,
    DOWN
}
Może czynnik
źródło
2
Ta odpowiedź nie przyczynia się do zrozumienia problemu PO; Nie widzę tu żadnego uzasadnienia, jedynie opinia ograniczona w zakresie języków z klasami enumtakimi jak java. Wiele języków (np. VBA) ma enum, ale może to nie być najlepsza opcja, ponieważ brakuje tego samego zabezpieczenia na przyszłość. Zobacz moją odpowiedź na dyskusję, dlaczego enumsam jest niekompletnym, często kruchym i specyficznym dla języka rozwiązaniem.
sqykly
12

Straszną praktyką jest używanie w kodzie literałów (lub liczb magicznych).

Wyliczenia są dobre lub przynajmniej używają stałych (wyliczenia są oczywiście tylko typem otoki dla jednej lub więcej powiązanych ze sobą stałych).

const string LEFT = "left"
if (someThing == LEFT) { doStuff(); }

Ten prosty przełącznik ma tak wiele zalet, że nawet nie wiedziałbym, od czego zacząć je wyjaśniać (przynajmniej nie bez kawy). Czytając magiczne liczby, jest to ta sama dokładna koncepcja, stosowana tylko do wartości liczbowej zamiast wartości ciągu.

Oto jeden szybki przegląd, jestem pewien, że jest jeszcze setki: /programming/47882/what-is-a-magic-number-and-why-is-it-bad

Jleach
źródło
1
Problem, jaki mam z tym podejściem, polega na tym, że masz błędy w literówkach, takie jak --const string LEFT = "letf" - podczas gdy jeśli pracujesz z wyliczeniami, które zostaną złapane w czasie kompilacji.
Pieter B
@PieterB - Myślałem, że pytanie OP jest wystarczająco szerokie, a przykład „pozostawił” to tylko arbitralny przykład - w którym wszystkie przypadki nie pasowałyby do wyliczenia. Zgadzam się, że dla zestawu wskazówek konkretnie, enum jest najlepszym wyborem, ale moja odpowiedź miała być bardziej ogólna w tym, że „jeśli umieścisz go w literale, przynajmniej uczyń go stałym” (wyliczenia, oczywiście, będący rodzajem stałej)
jleach
3
Miałem przyjemność pracować z aplikacją, która przypisała dni weekendu jako te dni, których nazwa zaczyna się od „s”. Bardzo zaskoczeni byli z powodu „faktu”, że kiedy umiędzynarodowili aplikację, Francja miała tylko jednodniowy weekend.
Pieter B
4
Nazwij to mikrooptymalizacją, ale niektóre języki mogą interpretować to jako porównanie wartości i tracić dużo czasu na sprawdzanie każdego znaku w ciągu. Jeszcze bardziej prawdopodobne, jeśli zrobisz to, co zrobił pytający i porównasz z zakodowanym ciągiem „left”. Jeśli nie ma potrzeby, aby stała wartość była łańcuchem (to znaczy, nie drukujesz jej), lepiej byłoby zamiast tego użyć const int.
Devsman
4

Krótka odpowiedź: Tak, ciągi nie są idealne do zadań innych niż przechowywanie i uzyskiwanie dostępu do sekwencji znaków tekstowych, a nawet jeśli leżącymi u podstaw bitami abstrakcji są ciągi znaków, odniesienie ich jako zmiennych lub stałych jest korzystne .

Długa odpowiedź: większość języków oferuje typy, które są bliżej dziedziny twojego problemu, a nawet jeśli nie, prawdopodobnie mają jakąś metodę, dzięki której możesz zdefiniować leftjako byt inny niż right(itp. Itp.). Przekształcenie go w ciąg za pomocą "left"jest po prostu przepadkiem języka i pomocnej funkcji IDE, takiej jak sprawdzanie błędów. Nawet jeśli musisz użyć

left = "left";

lub coś równoważnego, ma tę zaletę, że używa łańcucha, gdy odwołujesz się do niego w całym kodzie. Na przykład,

if (player.direction == Left)

zostanie wykryty jako błąd czasu kompilacji (najlepszy przypadek, java itp.) lub oznaczony przez dowolne IDE warte swoich bitów jako zły (najgorszy przypadek, podstawowy itp.).

Ponadto posiadanie pojęcia bycia leftodniesieniem pomoże ci dostosować się do dowolnego kierunku, w którym zdecydujesz się pójść z typem kierunku. Używając "left", kierunek jest zobowiązany do bycia String. Jeśli znajdziesz lub utworzysz lepszą abstrakcję dla tego typu, musisz przejść do przeszukiwania całego kodu, zmieniając każde wystąpienie "left". Stała lub zmienna nie będzie wymagać żadnych zmian, jeśli typ zostanie opracowany.

Typ, który naprawdę chcesz, jest specyficzny dla Twojej domeny. Co planuje zrobić z twoim kodem left? Być może będziesz chciał wykonać kopię lustrzaną osi x tekstury lub duszka skierowanego w lewo lub poruszającego się do przodu; jeśli tak, możesz chcieć utworzyć leftobiekt za pomocą właściwości lub metody, która odzwierciedla to zachowanie, a twój rightobiekt ma przeciwny wynik bez dublowania. Możesz wykonać tę logikę w obiekcie reprezentującym duszki lub tekstury, w którym to przypadku ponownie będziesz cierpieć z powodu używania "left"zamiast z leftpowodów wymienionych powyżej.

W Javie (i prawdopodobnie w innych językach, których jeszcze nie znam) enumjest dobrą alternatywą, ponieważ w Javie enumjest równie przydatna, jak final classze skończonym zestawem instancji. Możesz zdefiniować zachowania, jeśli ich potrzebujesz, i możesz przejść do otwartej klasy bez żadnych kłopotów poza plikiem, który deklaruje enum, ale wiele języków postrzega enumjako prymitywny typ lub wymaga specjalnej składni. To przecieka enumkod do kodu, który nie ma z tym nic wspólnego i może wymagać późniejszej korekty. enumPrzed rozważeniem tej opcji upewnij się, że rozumiesz koncepcję swojego języka .

sqykly
źródło
2

Nie określiłeś swojego języka, ale aby dać ogólną odpowiedź, powinieneś używać ciągów, gdy naprawdę potrzebujesz tekstu, a nie reprezentować coś. Podany przykład, na przykład, byłby po prostu nie do zaakceptowania w oprogramowaniu produkcyjnym.

Każdy język ma różne podstawowe typy danych i powinieneś użyć tego, który jest najbardziej odpowiedni dla zadania. Na przykład możesz użyć stałych numerycznych do reprezentowania różnych stanów. Tak więc lewy mógłby mieć wartość zero, a prawy byłby jeden. Poza wymienionym powodem wartości liczbowe zajmują mniej miejsca (pamięci) i zawsze jest szybsze porównywanie liczb niż porównywanie ciągów znaków.

Jak sugerowano, użyj wyliczeń, jeśli Twój język na to pozwala. W twoim konkretnym przykładzie jest to zdecydowanie lepszy wybór.

Nagev
źródło
Szybkość i pamięć raczej nie mają znaczenia. To powiedziawszy, wolą wyliczenia lub stałe. Ciągi mają większy sens, jeśli dane pochodzą z formatu tekstowego, np. XML, ale nawet wtedy muszą być spójne ze wszystkimi wielkimi literami lub wszystkimi niedomiarami. Lub zawsze konwertuj , np. if (uppercase(foo) == "LEFT")
user949300
2
@ user949300 Konwersja ciągów na wszystkie wielkie lub wszystkie małe litery również nie jest wiarygodna. Istnieje możliwość, że ktoś zdecyduje się odwiedzić inny lokal (lub rozwinąć tam działalność), taki jak Turcja , i przerwać naiwne porównania łańcuchów górnych (lub dolnych).
8bittree
Dbanie o szybkość i pamięć jest zawsze dobrą praktyką, nie są nieskończonymi zasobami. Chociaż można świadomie poświęcić to ze względu na czytelność lub inne kompromisy, co nie ma miejsca w tym przypadku. Ale nie dbanie o nie może łatwo prowadzić do powolnych lub marnotrawnych aplikacji.
Nagev,
Porównywanie ciągów będzie prawdopodobnie 50 razy wolniejsze niż porównywanie wyliczeń. W większości przypadków nie ma to znaczenia. Ale jeśli twój kod jest już trochę powolny, ta różnica może sprawić, że będzie niedopuszczalny. Ważniejsze jest oczywiście to, że kompilator nie może pomóc ci w błędach ortograficznych, jeśli używasz łańcuchów.
gnasher729
1

Wystarczy użyć typu danych, który ma sens w danej sytuacji.

Użycie tego stringtypu jako podstawowej komunikacji między aplikacjami nie jest z natury problemem. W rzeczywistości czasem lepiej jest używać ciągów: jeśli masz publiczny interfejs API, może być pożądane, aby wartości wchodzące i wychodzące z interfejsu API były czytelne dla człowieka. Ułatwia osobom używającym interfejsu API naukę i debugowanie.

Prawdziwy problem z twoim kodem polega na powtarzaniu wartości.

Nie rób tego:

if (direction == 'left') {
  goLeft();
}

Jeśli popełniłeś literówkę w jednym z tych warunków lub innym użyciu „pozostawionego” gdzieś, możesz nie być w stanie skutecznie przeprowadzić wyszukiwania w całej bazie kodu, ponieważ źle ją wpisałeś!

Zamiast tego koduj na podstawie wyliczenia lub stałej - w zależności od tego, który obsługuje typ danych, którego potrzebujesz w używanym języku (językach).

Że środki LEFTpowinny być przypisane do raz i tylko raz w swojej aplikacji. Reszta kodu powinna wykorzystywać te wyliczenia lub stałe:

const string LEFT = "left";

if (direction == LEFT) {
  goLeft();
}

Umożliwia to także łatwiejszą później zmianę typu danych używanych w kierunkach, jeśli tak zdecydujesz.

svidgen
źródło
1

Czy to zła praktyka?

Prawdopodobnie tak, ale zależy to dokładnie od tego , co robisz, a także od używanego języka programowania.

W twoim przykładzie możemy wnioskować, że istnieje mały i ustalony na zawsze zestaw wartości, które może przyjąć kierunek. W tym przypadku nie ma żadnych korzyści z używania łańcucha i pewne wyraźne wady. W tym przykładzie:

  • Preferowany enumbyłby typ, jeśli Twój język programowania to obsługuje.
  • W przeciwnym razie wskazane są stałe całkowite, z jedną definicją dla stałych.

Ale odpowiedź może być inna, jeśli zbiór wartości można dynamicznie modyfikować lub z czasem musi on ewoluować. W większości języków zmiana enumtypu (np. Dodanie lub usunięcie wartości) wymaga pełnej ponownej kompilacji. (Na przykład usunięcie wartości wyliczeniowej w Javie psuje zgodność binarną.) To samo dotyczy nazwanych stałych całkowitych.

W takich scenariuszach może być wymagane inne rozwiązanie, a użycie ciągów jest jedną z opcji. (I możesz łączyć literały łańcuchowe i nazwane stałe ... jeśli scenariusz zawiera stały rdzeń z dynamicznymi rozszerzeniami.)


Jeśli wdrażasz w Javie, character.direction == "left"jest to zła praktyka z innego powodu. Nie powinieneś używać ==do porównywania ciągów, ponieważ testuje to tożsamość obiektu, a nie równość ciągów. (Działałoby, gdybyś mógł zagwarantować, że wszystkie wystąpienia "left"łańcucha zostały internowane, ale wymaga to analizy całej aplikacji).

Stephen C.
źródło
1
To, co wydaje się ustalone na zawsze, często okazuje się wcale nie tak ustalone. Cztery kierunki mogą na przykład stać się ośmioma.
Jimmy T.,
@JimmyT. - To zależy od kontekstu. Na przykład w grze zmiana liczby kierunków, które postać może przesunąć z 4 na 8, pociąga za sobą całkowity przegląd zasad gry i kodu, który implementuje reguły. Użycie Stringdo przedstawienia kierunków spowodowałoby prawie zerową różnicę w nakładzie pracy związanym z wprowadzeniem zmian. Bardziej rozsądnym podejściem jest założenie, że liczba kierunków się nie zmieni, i uznanie, że miałbyś dużo pracy do zrobienia w obie strony, gdyby zasady gry uległy tak zasadniczej zmianie.
Stephen C
@JimmyT - Całkowicie zgadzam się, że widziałem to wiele razy. Dajesz też programistom skrót do redefiniowania łańcucha, więc na przykład product = „Option” staje się product = „Option_or_Future” całkowicie podważając znaczenie kodu.
Chris Milburn,
-3

Zgodnie z sugestią powinieneś używać Enums. Jednak samo użycie Enum jako zamiennika Strignów nie jest wystarczające IMHO. W twoim przykładzie prędkość gracza powinna być w rzeczywistości wektorem za pomocą fizyki:

class Vector {
  final double x;
  final double y;
}

Ten wektor powinien następnie zostać użyty do aktualizacji pozycji gracza przy każdym tiku.

Następnie możesz połączyć to z wyliczeniem dla większej czytelności:

enum Direction {
  UP(new Vector(0, 1)),
  RIGHT(new Vector(1, 0)),
  DOWN(new Vector(0, -1)),
  LEFT(new Vector(-1, 0));

  private Vector direction;

  Direction(Vector v) {
    this.direction = v;
  }

  public Vector getVector() { return this.direction; }
}
marstato
źródło
4
-1 Za daleko posuwasz się do przykładu, który podaje.
Pieter B