2014-10-14 11 views
22

Jestem zaskoczony przez kompilator C# zachowanie w poniższym przykładzie:Dodanie int i uint

int i = 1024; 
uint x = 2048; 
x = x+i;  // A error CS0266: Cannot implicitly convert type 'long' to 'uint' ... 

Wydaje się OK jako int + uint może przepełnić. Jednakże, jeśli uint zmienia się na int, następnie błąd zniknie, jak int + int nie może dać overflow:

int i = 1024; 
int x = 2048; 
x = x+i;  // OK, int 

Ponadto uint + uint = uint:

uint i = 1024; 
uint x = 2048; 
x = x+i;  // OK, uint 

wydaje się całkowicie niejasne.

Dlaczego int + int = int i uint + uint = uint, ale int + uint = long?

Jaka jest motywacja do podjęcia tej decyzji?

+0

Sprawdź poniżej odpowiedź Eric Lipperta. Był w rzeczywistości zaangażowany w projektowanie C#, więc jego jest jedyną odpowiedzią na "autorytatywność tej decyzji", która jest autorytatywna. –

+0

Tak, to naprawdę fajnie mieć odpowiedź od Erica Lipperta. –

Odpowiedz

11

Krótka odpowiedź brzmi „Ponieważ standard mówi, że musi być ono tak”, który zobaczyć informacyjny § 14.2.5.2 ISO 23270. normatywna § 13.1.2.(Niejawne konwersje numeryczne) mówi:

Te konwersje niejawne liczbowe są:

...

  • Od int do long, float, double lub decimal.
  • Od uint do long, ulong, float, double lub decimal.

...

Konwersje z int, uint, long lub ulong do float iz long lub ulong do double może spowodować utratę precyzji, ale nigdy nie spowoduje utraty wielkości. Inne niejawne konwersje numeryczne nigdy nie tracą żadnych informacji. (. emph kopalnia)

Sieć [lekko] Dłuższa odpowiedź jest taka, że ​​dodawane są dwa różne typy: 32-bitowa liczba całkowita i 32-bitowa liczba całkowita bez znaku:

  • z domena podpisanej 32-bitowej liczby całkowitej to -2,147,483,648 (0x80000000) — +2,147,483,647 (0x7FFFFFFF).
  • Domeną niepodpisanej 32-bitowej liczby całkowitej jest 0 (0x00000000) — +4,294,967,295 (0xFFFFFFFF).

Więc typy nie są kompatybilne, Ponieważ int nie może zawierać dowolną uintuint i nie może zawierać dowolną int. Są one niejawnie konwertowane (konwersja poszerzająca na wymaganie § 13.1.2, że żadna informacja nie zostanie utracona) do następnego największego typu, który może zawierać zarówno: long w tym przypadku, podpisaną 64-bitową liczbę całkowitą, która ma domena -2923372, 036 854 775 808 (0x8000000000000000) — +9,223,372,036,854,775,807 (0x7FFFFFFFFFFFFFFF).

edytowane Uwaga: Tak na marginesie, Wykonanie tego kodu:

var x = 1024 + 2048u ; 
Console.WriteLine("'x' is an instance of `{0}`" , x.GetType().FullName) ; 

nie otrzymując long jako oryginalnego plakatu za przykład. Zamiast tego, co jest produkowany jest:

'x' is an instance of `System.UInt32` 

Wynika to z stały składany.Pierwszy element ekspresji 1024 ma sufiks i jako taki jest int i drugi element ekspresji 2048u jest uint, zgodnie z regułami:

  • Jeżeli dosłownym ma sufiksu go ma pierwszy z tych typów, w którym jego wartość może być reprezentowana: int, uint, long, ulong.
  • Jeśli literał ma przyrostek U lub u, ma on pierwszy z tych typów, w którym jego wartość może być reprezentowana: uint, ulong.

A ponieważ optymalizator wie, jakie wartości są suma jest precomputed i oceniane jako uint.

Spójność jest hobgoblinem małych umysłów.

+0

Dobra odpowiedź, choć zauważam, że oryginalny cytat brzmi: "Głupia konsystencja to hobgoblin małych umysłów". To "głupie" uważam za istotne; istnieje wiele konsekwencji, które są ważne, a nie głupie, a zajmowanie się nimi nie jest małostkowe. –

+0

Chociaż Eric Lippert wyraził swoją opinię, myślę, że ten jest lepszy i bardziej przekonujący. Chociaż +1 dla obu wyjaśnień. –

+0

@AntonK ... cóż, geez. Dzięki. [rumienienie] –

12

Jest przejawem rozdzielczości przeciążenia dla typów liczbowych

promocja numeryczna składa się z automatycznego wykonywania pewnych niejawne konwersje z operandów z predefiniowanych jednoargumentowych i binarnych operatorów numerycznych. Promocja liczbowa nie jest odrębnym mechanizmem, ale raczej efektem zastosowania rozwiązania przeciwprzeciążeniowego dla predefiniowanych operatorów. Promocja numeryczna w szczególności nie wpływa na ocenę operatorów zdefiniowanych przez użytkownika, chociaż operatory zdefiniowane przez użytkownika mogą być zaimplementowane, aby wykazywać podobne efekty.

http://msdn.microsoft.com/en-us/library/aa691328(v=vs.71).aspx

Jeśli spojrzeć na

long operator *(long x, long y); 
uint operator *(uint x, uint y); 

z tego linku, widzisz te dwa możliwe przeciążenia (przykład dotyczy operator *, ale to samo dotyczy operator +) .

Urządzenie uint jest niejawnie konwertowane na long w celu zmniejszenia obciążenia, podobnie jak int.

Od uint do długich, ulong, float, double lub decimal.

Od int do long, zmiennoprzecinkowe, podwójne lub dziesiętne.

http://msdn.microsoft.com/en-us/library/aa691282(v=vs.71).aspx

Jaka jest motywacja do tej decyzji?

Najprawdopodobniej członek zespołu projektowego odpowie na ten aspekt. Eric Lippert, gdzie jesteś? :-) Zauważ, że poniższe rozumowanie @ Nicolasa jest bardzo prawdopodobne, że oba operandy są konwertowane do typu "najmniejszego", który może zawierać pełny zakres wartości dla każdego operandu.

+3

Jestem tutaj. Dodałem odpowiedź. –

+0

@EricLippert: Cieszę się, że mogłeś to zrobić :-) –

2
i  int i = 1024; 
uint x = 2048; 
// Technique #1 
    x = x + Convert.ToUInt32(i);  
// Technique #2 
    x = x + checked((uint)i); 
// Technique #3 
    x = x + unchecked((uint) i); 
// Technique #4 
    x = x + (uint)i; 
+0

Jaka jest motywacja? – Mark

+0

Liczby można manipulować, ale normalizacja nie jest, tutaj właśnie zilustrowałem wyjście, w przenośni, banan (int) to owoc (długi), jabłko (uint) to owoc (długi), banan to nie jabłko –

+1

Możesz chcesz dodać swoją motywację do odpowiedzi (zamiast w komentarzu). – Mark

5

Myślę, że zachowanie kompilatora jest dość logiczne i oczekiwane.

W poniższym kodzie:

int i; 
int j; 
var k = i + j; 

Jest dokładny przeciążenie dla tej operacji, więc k jest int. Ta sama logika ma zastosowanie przy dodawaniu dwóch uint, dwóch byte lub co masz. Praca kompilatora jest łatwa, jest szczęśliwa, ponieważ rozdzielczość przeciążenia znajduje dokładne dopasowanie. Istnieje spora szansa, że ​​osoba pisząca ten kod oczekuje, że k będzie numerem int i jest świadoma, że ​​operacja może zostać przepełniona w pewnych okolicznościach.

Rozważmy teraz przypadek Pytasz o:

uint i; 
int j; 
var k = i + j; 

Co robi kompilator zobaczyć? Cóż, widzi operację, która nie ma odpowiedniego przeciążenia; nie ma przeciążenia operatora, które pobiera i uint jako jego dwa argumenty. Dlatego algorytm rozwiązywania przeciążenia idzie naprzód i próbuje znaleźć przeciążenie operatora, które może być ważne. Oznacza to, że musi znaleźć przeciążenie, w którym typy mogą "trzymać" oryginalne operandy; to znaczy, że zarówno i, jak i j muszą być niejawnie przekształcalne w wymieniony typ (typy).

Kompilator nie może domyślnie przekonwertować uint na int, ponieważ taka konwersja się nie powiodła. Nie można niejawnie przekonwertować int na uint, ponieważ ta konwersja również nie istnieje (obie mogą spowodować zmianę wielkości). Tak więc jedynym wyborem, jaki naprawdę ma, jest wybór pierwszego szerszego typu, który może "pomieścić" oba typy argumentów, co w tym przypadku to long. Kiedy oba argumenty są niejawnie przekształcony w longk będący long jest oczywisty.

Motywacją tego zachowania jest wybór przez IMO najbezpieczniejszej dostępnej opcji, a nie sekundowe odgadywanie intencji wątpliwego programisty. Kompilator nie może się domyślić, co osoba pisząca ten kod spodziewa się uzyskać. An int? Dlaczego nie miałby to być uint? Obie opcje wydają się równie złe. Kompilator wybiera jedyną ścieżkę logiczną; bezpieczny: long. Jeśli programista chce, aby k był albo int lub unit, musi tylko jawnie rzutować jeden z operandów.

I wreszcie, algorytm przeciążenia kompilatora C# nie bierze pod uwagę typu powrotu przy podejmowaniu decyzji o najlepszym przeciążeniu. Zatem fakt, że przechowujesz operację, powoduje, że uint jest całkowicie nieistotny dla kompilatora i nie ma żadnego wpływu na proces rozwiązywania przeciążenia.

To wszystko spekulacje z mojej strony, i mogę się całkowicie mylić. Ale robi wydaje się logiczne rozumowanie.

+0

Odpowiedź jest dobra, ale chcę wyjaśnić, że konwersja z int na uint nie jest zła, ponieważ traci * informacje *. Nie traci informacji; 100% bitów zostaje zachowanych po konwersji. Możesz w obie strony od int do uint iz powrotem, bez utraty informacji. Problem polega na zmianie * magnitudo *. Wartość int -1 staje się wartością uint cztery miliardy i coś, ogromna zmiana. –

+0

@EricLippert whoops, naprawił to. Dziękuję Ci! – InBetween

2

Numeryczne reguły promocji dla C# są luźno oparte na regułach Java i C, które działają poprzez identyfikację typu, do którego oba operandy mogą być konwertowane, a następnie sprawiają, że wynik jest tego samego typu. Wydaje mi się, że takie podejście było rozsądne w latach 80., ale nowsze języki powinny je odłożyć na korzyść tego, który patrzy na to, w jaki sposób używane są wartości (np. Gdybym projektował język, wtedy pod warunkiem Int32 i1,i2,i3; Int64 l;, kompilator przetworzyłby i4=i1+i2+i3; używając 32-bitowego matematyka [wyrzucanie wyjątku w przypadku przepełnienia] przetwarzałby l=i1+i2+i3; z matematyką 64-bitową.), ale reguły C# są tym, czym są i nie wydają się zmieniać.

Należy zauważyć, że zasady promocji C# z definicji zawsze wybierają przeciążenia, które są uważane za "najbardziej odpowiednie" według specyfikacji językowej, ale to nie znaczy, że są naprawdę najbardziej odpowiednie do jakiegokolwiek użytecznego celu. Na przykład: double f=1111111100/11111111.0f; wydaje się, że powinno dać 100.0, i byłoby poprawnie obliczone, gdyby oba operandy były promowane na double, ale kompilator zamiast tego przekonwertuje liczbę całkowitą 1111111100 na float, uzyskując 1111111040,0f, a następnie wykona podział dając 99,999992370605469.

14

Dlaczego int + int = int i uint + uint = uint, ale int + uint = long? Jaka jest motywacja do tej decyzji?

Sposób pytanie jest sformułowane implikuje założenie, że zespół projektowy chciał int + uint być długie i wybrał zasady typu, aby osiągnąć ten cel. To założenie jest fałszywe.

raczej myśl Zespół projektowy:

  • Jakie operacje matematyczne są ludzie najprawdopodobniej do wykonania?
  • Jakie operacje matematyczne można wykonać bezpiecznie i efektywnie?
  • Jakie konwersje między typami numerycznymi można wykonywać bez utraty wielkości i precyzji?
  • W jaki sposób reguły dotyczące rozwiązywania problemów przez operatora mogą być proste i zgodne z regułami rozwiązywania problemów z przeciążeniem metody?

Oprócz wielu innych czynników, takich jak to, czy projekt działa dla programów debuggable, utrzymywalnych, możliwych do sprawdzenia i tak dalej. (Zauważam, że nie było mnie w pokoju na to szczególne spotkanie projektowe, ponieważ poprzedziło to mój czas w zespole projektowym, ale przeczytałem ich notatki i znam rodzaje rzeczy, które dotyczyłyby zespołu projektantów w tym okresie).

Badanie tych pytań doprowadziło do obecnego projektu: operacje arytmetyczne są zdefiniowane jako int + int -> int, uint + uint -> uint, long + long -> long, int mogą zostać zamienione na długie, uint może zostać przekonwertowany na długi i tak dalej.

konsekwencją z tych decyzji jest to, że podczas dodawania uint + int, rozdzielczość przeciążenie wybiera długo + długo jak najbliższego meczu, długo + długo trwa długo, dlatego uint + int jest długa.

Wykonywanie uint + int ma nieco inne zachowanie, które można uznać za bardziej sensowne, nie było wcale celem projektu zespołu, ponieważ mieszanie wartości podpisanych i niepodpisanych jest pierwsze, rzadko w praktyce, a po drugie prawie zawsze jest błędem. Zespół projektowy mógł dodać specjalne przypadki dla co kombinację liczb całkowitych ze znakiem i bez znaku, jedną, dwie, cztery i osiem bajtów, jak również znak, zmienną, podwójną i dziesiętną lub dowolny podzestaw z wielu setek przypadków, ale działa to przeciwko prostocie.

Krótko mówiąc, z jednej strony mamy wiele prac projektowych, aby stworzyć funkcję, której nie chcemy, aby ktokolwiek używał prostszego w użyciu kosztem bardzo skomplikowanej specyfikacji. Z drugiej strony mamy prostą specyfikację, która wywołuje niecodzienne zachowanie w rzadkim przypadku, którego nikt nie oczekuje w praktyce. Biorąc pod uwagę te wybory, które wybrałbyś? Zespół projektowy C# wybrał to drugie.

+1

Jeśli programista naprawdę wymaga jednego z "rzadkich w praktyce" zachowań (i wie, że rzeczywiste wartości nie spowodują błędu, takiego jak przepełnienie całkowite), zawsze mogą wymusić inną rozdzielczość przeciążania poprzez jawne rzucanie, prawda? –

+0

@EricJ .: Dokładnie. Aktor może być postrzegany jako programista, który wyjaśnia kompilatorowi: Nie, * naprawdę *, mam na myśli traktować tę rzecz jako liczbę całkowitą ze znakiem, mimo że tak nie jest. –