Czy powinienem unikać używania unsigned int w C #?

23

Ostatnio pomyślałem o użyciu liczb całkowitych bez znaku w języku C # (i chyba podobny argument można powiedzieć o innych „językach wysokiego poziomu”)

Gdy w potrzebie liczby całkowitej zwykle nie stoję przed dylematem wielkości liczby całkowitej, przykładem może być właściwość age klasy Person (ale pytanie nie ogranicza się do właściwości). Mając to na uwadze, o ile widzę, istnieje tylko jedna zaleta używania niepodpisanej liczby całkowitej („uint”) nad liczbą całkowitą ze znakiem („int”) - czytelność. Jeśli chcę wyrazić pogląd, że wiek może być tylko dodatni, mogę to osiągnąć, ustawiając typ wieku na uint.

Z drugiej strony obliczenia liczb całkowitych bez znaku mogą prowadzić do różnego rodzaju błędów i utrudniają wykonywanie operacji, takich jak odejmowanie dwóch grup wiekowych. (Przeczytałem, że jest to jeden z powodów, dla których Java pominęła liczby całkowite bez znaku)

W przypadku C # mogę również pomyśleć, że klauzula ochronna na seterze byłaby rozwiązaniem, które daje to, co najlepsze z dwóch światów, ale nie miałoby to zastosowania, gdy na przykład wiek byłby przejściem do jakiejś metody. Obejściem tego problemu byłoby zdefiniowanie klasy o nazwie Wiek, a jedyną rzeczą byłaby wiek właściwości, ale ten wzorzec spowodowałby, że utworzyłem wiele klas i byłby źródłem zamieszania (inni programiści nie wiedzieliby, kiedy obiekt jest tylko opakowaniem a kiedy jest to coś bardziej sofistycznego).

Jakie są najlepsze ogólne praktyki dotyczące tego problemu? Jak mam poradzić sobie z tego typu scenariuszem?

Belgi
źródło
1
Dodatkowo, unsigned int nie jest zgodny z CLS, co oznacza, że ​​nie można wywoływać interfejsów API, które ich używają, z innych języków .NET.
Nathan Cooper
2
@NathanCooper: ... „nie można wywołać interfejsów API, które używają ich od niektórych innych językach”. Metadane dla nich są znormalizowane, więc wszystkie języki .NET, które obsługują typy niepodpisane, będą dobrze współpracować.
Ben Voigt
5
Aby odnieść się do twojego konkretnego przykładu, nie miałbym w ogóle właściwości o nazwie Wiek. Miałbym właściwość o nazwie Urodziny lub CreationTime lub cokolwiek innego i obliczyłbym z tego wiek.
Eric Lippert,
2
„... ale ten wzorzec sprawiłby, że stworzyłem wiele klas i byłby źródłem zamieszania”, tak właściwie to jest właściwe. Po prostu znajdź niesławny wzór antystresowy Primitive Obsession .
Songo,

Odpowiedzi:

24

Projektanci platformy .NET Framework wybrali 32-bitową liczbę całkowitą ze znakiem jako swój „numer ogólnego przeznaczenia” z kilku powodów:

  1. Może obsłużyć liczby ujemne, szczególnie -1 (których Framework używa do wskazania warunku błędu; dlatego wszędzie tam, gdzie wymagane jest indeksowanie, używana jest podpisana int, mimo że liczby ujemne nie mają znaczenia w kontekście indeksowania).
  2. Jest wystarczająco duży, aby spełniać większość celów, a jednocześnie wystarczająco mały, aby można go było używać ekonomicznie niemal wszędzie.

Powodem używania bez znaku int jest brak czytelności; ma możliwość uzyskania matematyki zapewnianej tylko przez int bez znaku.

Klauzule ochronne, walidacja i warunki wstępne umowy są całkowicie akceptowalnymi sposobami na zapewnienie prawidłowych zakresów liczbowych. Rzadko rzeczywisty zakres liczbowy odpowiada dokładnie liczbie od zera do 2 32 -1 (lub cokolwiek natywny zakres liczbowy jest typem liczbowym, który wybrałeś), więc użycie a uintdo ograniczenia umowy interfejsu do liczb dodatnich jest rodzajem bez związku.

Robert Harvey
źródło
2
Niezła odpowiedź! Mogą się również zdarzyć przypadki, w których niepodpisana int może faktycznie przypadkowo wygenerować więcej błędów (choć prawdopodobnie natychmiast zauważone, ale nieco mylące) - wyobraź sobie zapętlanie w odwrotnej kolejności z licznikiem int bez znaku, ponieważ pewien rozmiar jest liczbą całkowitą: for (uint j=some_size-1; j >= 0; --j)- ups ( nie jestem pewien, czy jest to problem w języku C #)! Znalazłem ten problem w kodzie, przed którym próbowałem jak najwięcej używać niepodpisanej int po stronie C - i ostatecznie zmieniliśmy go tak, aby sprzyjał intpóźniej, a nasze życie było znacznie łatwiejsze, z mniejszą liczbą ostrzeżeń kompilatora.
14
„Rzadko zakres liczbowy w świecie rzeczywistym odpowiada liczbie od zera do 2 ^ 32-1”. Z mojego doświadczenia wynika, że ​​jeśli będziesz potrzebować liczby większej niż 2 ^ 31, najprawdopodobniej będziesz również potrzebować liczb większych niż 2 ^ 32, więc równie dobrze możesz po prostu przejść do (podpisanego) int64 na ten punkt.
Mason Wheeler
3
@Panzercrisis: To trochę poważne. Prawdopodobnie bardziej trafne byłoby powiedzenie „Używaj przez intwiększość czasu, ponieważ jest to ustalona konwencja i tego większość ludzi spodziewa się, że będzie używana rutynowo. Używaj, uintgdy potrzebujesz specjalnych możliwości a uint.” Pamiętaj, że projektanci Frameworków zdecydowali się na przestrzeganie tej konwencji, więc nie możesz nawet używać jej uintw wielu kontekstach (nie jest kompatybilna z typem).
Robert Harvey
2
@Panzercrisis To może być zbyt silne sformułowanie; ale nie jestem pewien, czy kiedykolwiek użyłem niepodpisanych typów w C #, z wyjątkiem przypadków, gdy dzwoniłem do win32 apis (gdzie konwencja jest taka, że ​​stałe / flagi / etc są niepodpisane).
Dan Neely,
4
To jest rzeczywiście dość rzadkie. Jedyny raz, kiedy używam niepodpisanych ints, jest w scenariuszach kręcących bity.
Robert Harvey
8

Zasadniczo należy zawsze używać najbardziej konkretnego typu danych dla swoich danych.

Jeśli na przykład używasz Entity Framework do pobierania danych z bazy danych, EF automatycznie użyje typu danych najbliższego typowi użytemu w bazie danych.

Istnieją dwa problemy z tym w języku C #.
Po pierwsze, większość programistów C # używa tylko intdo reprezentowania liczb całkowitych (chyba że istnieje powód do użycia long). Oznacza to, że inni programiści nie będą myśleć o sprawdzeniu typu danych, więc otrzymają błędy przepełnienia wspomniane powyżej. Druga, bardziej poważny problem, jest / było to, że .NET za oryginalne operatory arytmetyczne obsługiwane tylko int, uint, long, ulong, float, dwu-, i decimal*. Tak jest do dzisiaj (patrz sekcja 7.8.4 w specyfikacji języka C # 5.0 ). Możesz to przetestować samodzielnie, używając następującego kodu:

byte a, b;
a = 1;
b = 2;
var c = a - b;      //In visual studio, hover over "var" and the tip will indicate the data type, or you can get the value from cName below.
string cName = c.GetType().Namespace + '.' + c.GetType().Name;

Wynikiem naszego byte- bytejest int( System.Int32).

Te dwa problemy doprowadziły do ​​tak częstej praktyki „tylko int do liczb całkowitych”.

Aby odpowiedzieć na twoje pytanie, w języku C # zwykle dobrym pomysłem jest trzymanie się, intchyba że:

  • Zautomatyzowany generator kodu używał innej wartości (jak Entity Framework).
  • Wszyscy inni programiści w projekcie są świadomi, że korzystasz z mniej popularnych typów danych (dołącz komentarz wskazujący, że użyłeś tego typu danych i dlaczego).
  • Mniej popularne typy danych są już powszechnie używane w projekcie.
  • Program wymaga korzyści z mniej powszechnego typu danych (masz 100 milionów tych, które musisz przechowywać w pamięci RAM, więc różnica między a bytei an intlub a inti longjest krytyczna, lub różnice arytmetyczne wcześniej wspomnianych niepodpisanych).

Jeśli potrzebujesz wykonać matematykę na danych, trzymaj się typowych typów.
Pamiętaj, że możesz rzucać z jednego rodzaju na inny. Może to być mniej wydajne z punktu widzenia procesora, więc prawdopodobnie lepiej jest z jednym z 7 popularnych typów, ale jest to opcja w razie potrzeby.

Enumerations ( enum) jest jednym z moich osobistych wyjątków od powyższych wytycznych. Jeśli mam tylko kilka opcji, określę wyliczenie jako bajt lub krótki. Jeśli potrzebuję tego ostatniego bitu w oznaczonym wyliczeniu, określę typ, aby uintmóc użyć wartości szesnastkowej do ustawienia wartości flagi.

Jeśli używasz właściwości z kodem ograniczającym wartość, wyjaśnij w tagu podsumowania, jakie są ograniczenia i dlaczego.

* Aliasy C # są używane zamiast nazw .NET takich jak, System.Int32ponieważ jest to pytanie C #.

Uwaga: istniał blog lub artykuł od twórców platformy .NET (którego nie mogę znaleźć), w którym wskazano na ograniczoną liczbę funkcji arytmetycznych i kilka powodów, dla których nie martwili się o to. Jak pamiętam, wskazali, że nie mieli planów dodania obsługi innych typów danych.

Uwaga: Java nie obsługuje niepodpisanych typów danych i wcześniej nie obsługiwała 8 lub 16 bitowych liczb całkowitych. Ponieważ wielu programistów C # pochodziło z języka Java lub musiało pracować w obu językach, ograniczenia jednego języka były czasem sztucznie narzucane w drugim.

Trisped
źródło
Moja ogólna zasada brzmi: „używaj int, chyba że nie możesz”.
PerryC
@PerryC Uważam, że jest to najczęstsza konwencja. Celem mojej odpowiedzi było przedstawienie bardziej kompletnej konwencji, która pozwala korzystać z funkcji językowych.
Trisped
6

Musisz przede wszystkim pamiętać o dwóch rzeczach: danych, które reprezentujesz, oraz o wszelkich pośrednich krokach w swoich obliczeniach.

Z pewnością warto mieć wiek unsigned int, ponieważ zwykle nie bierzemy pod uwagę wieku ujemnego. Ale potem wspominasz o odejmowaniu jednego wieku od drugiego. Jeśli po prostu ślepo odejmiemy jedną liczbę całkowitą od drugiej, to z pewnością możliwe jest uzyskanie liczby ujemnej, nawet jeśli wcześniej ustaliliśmy, że ujemne wieku nie mają sensu. Tak więc w tym przypadku chciałbyś, aby twoje obliczenia zostały wykonane przy użyciu liczby całkowitej ze znakiem.

W odniesieniu do tego, czy niepodpisane wartości są złe, czy nie, powiedziałbym, że ogromnym uogólnieniem jest twierdzenie, że niepodpisane wartości są złe. Java, jak wspomniałeś, nie ma niepodpisanych wartości i ciągle mnie denerwuje. A bytemoże mieć wartość od 0-255 lub 0x00-0xFF. Ale jeśli chcesz utworzyć bajt większy niż 127 (0x7F), musisz zapisać go jako liczbę ujemną lub rzucić liczbę całkowitą na bajt. Otrzymujesz kod, który wygląda następująco:

byte a = 0x80; // Won't compile!
byte b = (byte) 0x80;
byte c = -128; // Equal to b

Powyższe denerwuje mnie bez końca. Nie wolno mi mieć bajtu o wartości 197, nawet jeśli jest to całkowicie poprawna wartość dla większości rozsądnych ludzi zajmujących się bajtami. Mogę rzucić liczbę całkowitą lub znaleźć wartość ujemną (197 == -59 w tym przypadku). Weź również pod uwagę to:

byte a = 70;
byte b = 80;
byte c = a + b; // c == -106

Jak widać, dodanie dwóch bajtów z prawidłowymi wartościami i zakończenie bajtu z prawidłową wartością powoduje zmianę znaku. Nie tylko to, ale nie jest od razu oczywiste, że 70 + 80 == -106. Technicznie jest to przepełnienie, ale moim zdaniem (jako istota ludzka) bajt nie powinien przepełniać wartości poniżej 0xFF. Kiedy wykonuję arytmetykę na papierze, nie uważam, że ósmy bit jest bitem znaku.

Pracuję z wieloma liczbami całkowitymi na poziomie bitów, a posiadanie wszystkiego, co jest podpisane, zwykle sprawia, że ​​wszystko jest mniej intuicyjne i trudniejsze w obsłudze, ponieważ musisz pamiętać, że przesunięcie w prawo liczby ujemnej daje nowe 1s liczby. Podczas gdy przesunięcie w prawo niepodpisanej liczby całkowitej nigdy tego nie robi. Na przykład:

signed byte b = 0b10000000;
b = b >> 1; // b == 0b1100 0000
b = b & 0x7F;// b == 0b0100 0000

unsigned byte b = 0b10000000;
b = b >> 1; // b == 0b0100 0000;

To tylko dodatkowe kroki, które moim zdaniem nie powinny być konieczne.

Chociaż użyłem bytepowyżej, to samo dotyczy 32-bitowych i 64-bitowych liczb całkowitych. Brak posiadania unsignedjest paraliżujący i szokuje mnie, że istnieją języki wysokiego poziomu, takie jak Java, które w ogóle na to nie pozwalają. Ale dla większości ludzi nie stanowi to problemu, ponieważ wielu programistów nie zajmuje się arytmetyką na poziomie bitów.

Na koniec warto używać liczb całkowitych bez znaku, jeśli myślisz o nich jak o bitach, i warto używać liczb całkowitych ze znakiem, gdy myślisz o nich jako o liczbach.

Shaz
źródło
7
Podzielam twoją frustrację związaną z językami bez niepodpisanych typów całkowitych (szczególnie bajtów), ale obawiam się, że nie jest to bezpośrednia odpowiedź na zadane tutaj pytanie. Być może można dodać do wniosku, który jak sądzę, może być: „używane są liczby całkowite, jeśli myślisz o ich wartości bitów i podpisane liczb całkowitych, jeśli myślimy o nich jako numery.”
5gon12eder
1
tak powiedziałem w komentarzu powyżej. Cieszę się, że ktoś inny myśli w ten sam sposób.
Robert Bristol-Johnnson