Dlaczego mogę przypisać 0,0 do wartości wyliczenia, ale nie 1,0


90

Tak z ciekawości: dlaczego mogę przypisać 0,0 do zmiennej, która jest typu wyliczeniowego, ale nie 1,0? Spójrz na następujący kod:

public enum Foo
{
    Bar,
    Baz
}

class Program
{
    static void Main()
    {
        Foo value1 = 0.0;
        Foo value2 = 1.0;   // This line does not compile
        Foo value3 = 4.2;   // This line does not compile
    }
}

Myślałem, że konwersje między typami liczbowymi i wartościami wyliczenia są dozwolone tylko za pośrednictwem rzutów? Oznacza to, że mógłbym napisać Foo value2 = (Foo) 1.0;, aby wiersz 2 Mainmógł się kompilować. Dlaczego istnieje wyjątek dla wartości 0.0w C #?


17
Dla mnie to dziwne, że do niestandardowego wyliczenia można przypisać podwójny literał 0,0. Nie oznacza to, że nie można przypisać 1.0dosłownego do niestandardowego wyliczenia.
Ilya Ivanov

2
Podejrzewam, że kompilator traktuje to jako 0zamiast tego. Miałem podobne pytanie raz Rawling napisali wielką odpowiedzi tutaj .
Polubowne

2
IdeOne go nie kompiluje.
Johnny Mopp

Odpowiedzi:


98

To błąd, którego możesz użyć 0.0. Kompilator niejawnie traktuje wszystkie wyrażenia stałe o wartości zero jako po prostu 0.

Teraz jest poprawna dla kompilatora, aby umożliwić niejawna konwersja od stałej intekspresji od 0 do wyliczenia zgodnie z sekcją 6.1.3 C # 5 specyfikacji:

Niejawna konwersja wyliczenia umożliwia konwersję dziesiętnej liczby całkowitej literału 0 na dowolny typ wyliczeniowy i na dowolny typ dopuszczający wartość null, którego typem bazowym jest typ wyliczeniowy. W tym drugim przypadku konwersja jest oceniana przez konwersję na podstawowy typ wyliczeniowy i zawijanie wyniku (§4.1.10).

Rozmawiałem o tym wcześniej z zespołem C #: chcieliby usunąć przypadkową konwersję z wartości 0,0 (a nawet 0,0 mi 0,0f) na wartości wyliczeniowe, ale niestety, jak rozumiem, zepsuło to zbyt dużo kodu - mimo że Przede wszystkim nigdy nie powinno było na to pozwolić.

Mono mcskompilator zabrania wszystkich tych pływających konwersji punktów, choć nie pozwalają:

const int Zero = 0;
...

SomeEnum x = Zero;

pomimo faktu, że Zerojest to wyrażenie stałe, ale nie literał dziesiętny.

Nie zdziwiłbym się, gdyby w przyszłości zmiana specyfikacji C # pozwoliła na dowolne wyrażenie będące liczbą całkowitą o wartości 0 (tj. Naśladowanie mcs), ale nie spodziewałbym się, że konwersje zmiennoprzecinkowe kiedykolwiek będą oficjalnie poprawne. (Myliłem się wcześniej, przewidując przyszłość języka C #, oczywiście ...)


3
Zgodnie ze specyfikacją ma to być tylko dosłowne zero, więc powinno odrzucić 1-1- stałe intwyrażenie o wartości 0. Ale jak zauważyłeś, kompilator jest niezgodny ze specyfikacją tutaj.
Damien_The_Unbeliever

4
it broke too much code- naprawdę trudno sobie wyobrazić powody, dla których warto pisać taki kod.
Ilya Ivanov

1
@ObsidianPhoenix: Nie jestem pewien, co masz na myśli. To jest dokładnie to, co odpowiada: SomeEnum x = (SomeEnum) 0;. Tak jest, niezależnie od tego, czy istnieje nazwana wartość zero, czy nie.
Jon Skeet

2
@ObsidianPhoenix: No nie, ponieważ wartość Test.Footo 1, a nie 0 ... znowu, to jest dokładnie to samo, co gdybyś napisał Test v1 = (Test) 0;- i to zachowanie zachowuje się dla każdej wartości, która nie jest nazwaną wartością w wyliczeniu.
Jon Skeet

2
@JonSkeet czy to zostanie naprawione w Roslyn?
Max

98

Odpowiedź Jona jest poprawna. Dodałbym do tego następujące punkty.

  • Spowodowałem ten głupi i zawstydzający błąd. Wiele przeprosin.

  • Błąd został spowodowany przez mnie niezrozumienie semantyki predykatu „wyrażenie ma wartość zero” w kompilatorze; Sądziłem, że sprawdza tylko pod kątem równości zerowej liczby całkowitej, podczas gdy w rzeczywistości sprawdzał więcej w linijkach "czy to jest domyślna wartość tego typu?" W rzeczywistości we wcześniejszej wersji błędu można było przypisać wartość domyślną dowolnego typu do wyliczenia! Teraz są to tylko domyślne wartości liczb. (Lekcja: uważnie określaj predykaty pomocnika).

  • Zachowanie, które próbowałem zaimplementować, a które zepsułem, było w rzeczywistości obejściem dla nieco innego błędu. Możesz przeczytać całą straszną historię tutaj: https://docs.microsoft.com/en-us/archive/blogs/ericlippert/the-root-of-all-evil-part-one i https://docs.microsoft .com / en-us / archive / blogs / ericlippert / the-root-of-all-evil-part-two (lekcja: Bardzo łatwo jest wprowadzić nowe gorsze błędy podczas naprawiania starych.)

  • Zespół C # zdecydował się zachować to błędne zachowanie, zamiast je naprawiać, ponieważ ryzyko złamania istniejącego kodu bez istotnych korzyści było zbyt wysokie. (Lekcja: zrób to dobrze za pierwszym razem!)

  • Kod, który napisałem w Roslyn, aby zachować to zachowanie, można znaleźć w metodzie IsConstantNumericZerow https://github.com/dotnet/roslyn/blob/master/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/ConversionsBase.cs - zobacz to, aby uzyskać więcej informacji o tym, czym dokładnie jest zachowanie Roslyn. Napisałem prawie cały kod w katalogu Conversions; Zachęcam do przeczytania tego wszystkiego, ponieważ w komentarzach znajduje się wiele interesujących faktów na temat tego, jak C # odbiega od specyfikacji. Ozdobiłem każdą etykietą SPEC VIOLATION, aby były łatwe do znalezienia.

Jeszcze jeden interesujący punkt: C # umożliwia również użycie dowolnej wartości wyliczenia w inicjatorze wyliczenia, niezależnie od jej zerowej wartości:

enum E { A = 1 }
enum F { B = E.A }  // ???

Specyfikacja jest nieco niejasna, czy powinno to być legalne, czy nie, ale znowu, ponieważ było to w kompilatorze przez długi czas, nowe kompilatory prawdopodobnie utrzymają to zachowanie.


10
To jest naprawdę fajne, w końcu mogę zobaczyć kod, który napisałeś. To niesamowite, że kod źródłowy Roslyn jest open source. Teraz w pełni rozumiem, że istnieją uzasadnione powody (techniczne / prawne), aby nie podawać historii zmian, ale byłoby wspaniale zobaczyć historię zmian, aby zobaczyć, jak ewoluował kod.
Rozwiązanie

The C# team decided to enshrine this buggy behaviour rather than fixing it because the risk of breaking existing code for no compelling benefit was too high.Nie sądzę, by było wielu ludzi, którzy polegaliby na tym zachowaniu i jest to jedna z tych dziwactw, które być może lepiej byłoby po prostu naprawić. Jednak tak naprawdę nie wyrządza wiele szkód (z wyjątkiem projektów wdrażających specyfikację).
Aidiakapi

5
@Aidiakapi: Rzeczywiście, liczba dotkniętych nią osób powinna być niewielka; to nie jest zero. Zespół C # bardzo poważnie traktuje przełomowe zmiany. Łatwo ci powiedzieć, że lepiej to naprawić; nie musisz mieć do czynienia z zirytowanymi klientami, którzy dzwonią do twojego wiceprezesa, aby narzekać, że twoja trywialna zmiana, która nie przynosi żadnych korzyści, opóźniła integrację systemu o jeden dzień.
Eric Lippert

3
Pogarsza się. Wszystkie takie istotne zmiany zostaną (najlepiej) wymienione w przewodniku migracji firmy Microsoft do Framework. Im dłuższa jest ta lista, tym bardziej niezdecydowani użytkownicy dokonują migracji swojej aplikacji. Tak więc nawet niewielka poważna zmiana powoduje: 1. Niewielką liczbę aplikacji nie działa. 2. Niewielka liczba użytkowników odmawia aktualizacji (nawet jeśli problem ich nie dotyczy). 3. Niewielka liczba użytkowników marnuje zasoby oceniając, czy przełomowa zmiana ich dotyczy. 4. Użytkownicy z # 1, # 2 i # 3, aby narzekać na wszystkich innych.
Brian

@EricLippert Jeśli „Specyfikacja jest nieco niejasna”, czy nie ma sensu aktualizować specyfikacji? (Prawdziwe pytanie!)
James

10

Wyliczenia w C # są z definicji wartościami całkowitymi. Aby zachować spójność, język C # nie powinien akceptować żadnego z tych przypisań, ale 0.0jest dyskretnie traktowany jako integralny 0. Jest to prawdopodobnie pozostałość po C, gdzie literał 0był traktowany specjalnie i mógł zasadniczo przyjmować dowolny typ - liczbę całkowitą, liczbę zmiennoprzecinkową, wskaźnik zerowy… możesz go nazwać.


3
pytanie brzmi: dlaczego ? Jeśli pójdziesz do IL- to wpycha liczbę całkowitą na stosIL_0001: ldc.i4.0
Ilya Ivanov

@IlyaIvanov Zobacz aktualizację. Ale szczerze mówiąc, odpowiedź brzmi „bez powodu”.
Konrad Rudolph

2
Myślę, że jest to jeden z tych przypadków, w których, jeśli spojrzeć na C # specyfikacji , to nie jest legalne, ale jeśli spojrzeć na każdym kompilator C #, że MS przyniosły, to robi to.
Damien_The_Unbeliever

3

enum jest naprawdę zamierzone (we wszystkich językach, które go obsługują) jako sposób pracy ze znaczącymi i unikalnymi łańcuchami (etykietami), a nie z wartościami liczbowymi. Dlatego w twoim przykładzie powinieneś używać tylko Bar i Baz, gdy masz do czynienia z plikiem wyliczonym typem danych Foo . Nigdy nie powinieneś używać (porównywać ani przypisywać) liczby całkowitej, nawet jeśli wiele kompilatorów pozwoli ci to uciec (wyliczenia są zwykle wewnętrznie liczbami całkowitymi), aw tym przypadku kompilator beztrosko traktuje 0,0 jako 0.

Koncepcyjnie powinno być w porządku dodanie liczby całkowitej n do wyliczonej wartości, aby uzyskać n wartości w dalszej części wiersza lub wziąć val2 - val1, aby zobaczyć, jak daleko są od siebie, ale jeśli specyfikacja języka wyraźnie na to nie zezwala, ja Uniknąłbym tego. (Pomyśl o wyliczonej wartości jako o wskaźniku C, pod względem sposobów jej używania.) Nie ma powodu, dla którego nie można zaimplementować wyliczeń z liczbami zmiennoprzecinkowymi i stałym przyrostem między nimi, ale nie słyszałem o odbywa się to w dowolnym języku.


Wiem, że nie powinienem używać wyliczeń w ten sposób w C # - ale znalazłem tę łamigłówkę i chciałem wiedzieć, dlaczego działa 0.0, ale 1.0 nie. Wiedziałem, że musi to być coś z kompilatorem C #, ponieważ widać, że kod IL dla Foo v1 = 0.0;jest taki sam jak dla Foo v2 = Foo.Bar.
feO2x
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.