Search Unity

Switching enum : int?

Discussion in 'Scripting' started by ArachnidAnimal, Apr 25, 2015.

  1. ArachnidAnimal

    ArachnidAnimal

    Joined:
    Mar 3, 2015
    Posts:
    1,815
    For the life of me I cannot understand why the C# compiler complains about the following code:

    public enum ROOM : int
    {
    ROOM_900 =0,
    ROOM_901 =1
    }

    int i = 0;
    switch (i) {
    case ROOM.ROOM_900:
    .....
    }
    The compiler error is regarding the case ROOM.ROOM_900 line of code:
    Cannot implicitly convert type `ROOM' to `int'. An explicit conversion exists (are you missing a cast?)

    I don't understand this because I've declared that the ROOM enum are ints. So why does a cast need to be performed?

    Can someone explain this to me? What is the point of declaring the enum as ints if the compiler is going to complain about the above code?

    Thanks
     
    chaueur likes this.
  2. makeshiftwings

    makeshiftwings

    Joined:
    May 28, 2011
    Posts:
    3,350
    It's because when you do "enum something : int" it's setting the underlying storage to an int but the type itself is still "enum" and the compiler only knows it's "enum"; it doesn't know that it's an enum<string> or enum<int> or anything like that. Enum was in C# before generics and type parameters were introduced. So you have to cast it to an int, as in "case (int) ROOM.ROOM_900:".
     
    Shikamounty and ArachnidAnimal like this.
  3. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    Because it's easy to get wrong. In most cases this automatic casting is not what you want to do. It also makes it difficult to change the enum later.

    Implicit casts are for casts that just make sense. No information is lost. Nothing can go wrong. The compiler says "No big deal, I got this".

    Explicit casts are for ones that carry some risk, or have the potential to go wrong. It's the developer saying "I know exactly what I want here, just do it and let me deal with the consequences".
     
  4. ArachnidAnimal

    ArachnidAnimal

    Joined:
    Mar 3, 2015
    Posts:
    1,815
    I guess I don't understand why an enum type would ever change.
    I understand that you may need to do something like this:
    byte b = (byte) ROOM.ROOM_900;

    But To me, seems the compiler should be "smart enough" to treat ROOM.ROOM_900 as an int value in a switch statement.

    having to do "(int)ROOM.ROOM_900" is like having to do:
    int x = 0;
    int y = (int) x; //Not necessary to cast
     
    Mashimaro7, Taloose and rakkarage like this.
  5. ArachnidAnimal

    ArachnidAnimal

    Joined:
    Mar 3, 2015
    Posts:
    1,815
    also, this code doesn't compile:

    ROOM.ROOM_900=(byte) ROOM.ROOM_900;

    so how can the enum type ever change?
     
  6. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    I guess you need some real world examples of the cast failing.

    Code (CSharp):
    1. enum EpicFail {untested = 0, fail = 1}
    2. EpicFail epicFail;
    3.  
    4. epicFail = (EpicFail) 3; // This will fail
    5. int myInt = (int)EpicFail.pass + (int)EpicFail.fail; // This has no meaning in the context of the enum
    6. switch ((int)epicFail){...} // This fails as soon as the enum is changed
    Given that casting can fail in so many ways, it makes sense that the casting is explicit. The whole point of enums is to avoid magic numbers that can fail. You aren't supposed to cast them to their underlying data type in most situations.
     
    ArachnidAnimal likes this.
  7. ArachnidAnimal

    ArachnidAnimal

    Joined:
    Mar 3, 2015
    Posts:
    1,815
    you have this code:

    enum ROOMS: int {
    x=0,
    y =1
    }

    void test()
    {
    ROOMS myRoom;
    }

    how can you ever change myRoom's x and y at this point?
    it is my understanding that you cant.
    The compiler even tells me I cant alter myRoom:
    error: Static member `ROOMS.x' cannot be accessed with an instance reference

    if so, then why is the compiler telling me that I need this code:
    int z = (int) myRoom.x;
    instead of just
    int z = myRoom.x

    what im not understanding is that if myRoom.x and myRoom.y are always ints, why do I need a cast?
    If they are not allways ints, then I understand why you need a cast.
    Are they not always ints?
     
  8. makeshiftwings

    makeshiftwings

    Joined:
    May 28, 2011
    Posts:
    3,350
    They're not always ints. You can use longs for example:

    enum ROOMS : long
    {
    x = 999999999998L,
    y = 999999999999L
    }

    and then this would cause an overflow if you could cast back to an int automatically. You can manually cast a long to an int, but if the value is too big, it chops off half the bits and you could end up with two different enum values equating to the same ints, which would break things.
     
    ArachnidAnimal likes this.
  9. ArachnidAnimal

    ArachnidAnimal

    Joined:
    Mar 3, 2015
    Posts:
    1,815
    OK, you've stated that they are longs when you declared the enum.
    The compiler knows they are longs.

    To change my question to conform to your example: how can you change ROOMS's x and y to be of type int?
    (I dont think you can, because they are always longs)

    So using your example, why do I need:
    long f = (long) ROOMS.x;
    instead of just
    long f = ROOMS.x

    maybe my understanding of what an ENUM is not right.
    I only use the enums to simulate a collection of "const" variables.
     
  10. makeshiftwings

    makeshiftwings

    Joined:
    May 28, 2011
    Posts:
    3,350
    Ah I see what you mean. So, you're right that you can't actually ever change the values of an enum to a different type, but the problem is that all enums are of type "Enum", and the compiler doesn't know what the underlying type is. Like, consider this:

    Code (csharp):
    1. enum ROOMS : long
    2. {
    3.     One = 999999999L,
    4.     etc....
    5. }
    6.  
    7. void SomeMethod()
    8. {
    9.     var room = ROOMS.One;  // <--here, the compiler could know that room is an enum with underlying type long but...
    10.     AnotherMethod(room);
    11. }
    12.  
    13. void AnotherMethod(Enum someEnum)
    14. {
    15.     int x = someEnum;  // <-- here, the compiler doesn't know what the underlying type of someEnum is, since the function takes any sort of enum
    16. }
    Now, Microsoft could have made it easier by making enums like generics, so you could do "Enum<long> ROOMS" and then if you wanted a function that would only accept enums that were longs, you could do "AnotherMethod(Enum<long> someEnum)". And if they were just adding enum now they might do it that way. But they added enum way back in C# 1.0 before generics were around and before there were type parameters like that, so it's like the older non-generic List, in that the compiler doesn't know what type of object is inside it.
     
    ArachnidAnimal likes this.
  11. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    Enums are replacements for discrete data. They normally represent type. Something like

    Code (CSharp):
    1. enum DamageType {slash, pierce, blunt}
    In general there is no need to cast them back to the underlying data type.

    Code (CSharp):
    1. switch (damageType) {
    2.     case DamageType.slash :
    3.         ...
    4. }
    As indicated there are issues with casting, so it requires an explicit cast. It's only a single operation to add if you really want to do it. Not sure what the deal is.
     
    ArachnidAnimal likes this.
  12. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    Yes you can. It's called code maintenance. If the need ever comes up to change the enum values, you'll be grateful you didn't hard code in any actual numbers.
     
  13. Galf

    Galf

    Joined:
    Feb 24, 2013
    Posts:
    27
    Well, I'll play devil's advocate here. For code like the following:

    Code (CSharp):
    1. const short SOME_VALUE = 5;
    2. int valueToCheck;
    3. ...
    4. switch (valueToCheck)
    5. {
    6.   case SOME_VALUE:
    7.     ...
    8.     break;
    9. }
    the compiler would not complain because short is a subset of int. As the compiler knows that ROOMS must be some subset of int as well, it's a bit inconsistent. Code maintenance is mostly irrelevant, because there's nothing stopping us from changing the above code to check against a long instead of a short. In one case the compiler is permissive, in another, the compiler is overprotective.

    Sure, it's not a big deal to explicitly convert the enum to an int, but it doesn't seem like it should be necessary
     
    ArachnidAnimal likes this.
  14. makeshiftwings

    makeshiftwings

    Joined:
    May 28, 2011
    Posts:
    3,350
    That's not what I meant. :p Obviously you can change the type of any object if you literally go into the code and change the type. TTTTa was talking about changing the type of a value after it is assigned.
     
    ArachnidAnimal likes this.
  15. makeshiftwings

    makeshiftwings

    Joined:
    May 28, 2011
    Posts:
    3,350
    You can implicitly convert a smaller type to a larger type (short to int) but you can't implicitly convert a longer type to a shorter type (int to short, or long to int) or switch unsigned/signed (uint to int). If you change the first line to "const long SOME_VALUE = 5;" it will no longer compile.
     
    ArachnidAnimal likes this.
  16. ArachnidAnimal

    ArachnidAnimal

    Joined:
    Mar 3, 2015
    Posts:
    1,815
    yes, that is exactly what my issue was with it. thanks
     
  17. Galf

    Galf

    Joined:
    Feb 24, 2013
    Posts:
    27
    Yeah, and I explicitly mentioned that when I said nothing is stopping us from changing short to long and getting compiler errors. But long is NOT a subset of int -- short and ROOMs are, yet short compiles and ROOMs does not -- do you see the inconsistency?

    You could make another argument and say, "Well, but ROOMs is a non-numeric type that just happens to be stored in an int, whereas short is fundamentally numeric". But if we instead change SOME_VALUE to a char with value 'x', once again the code compiles -- and char is not numeric (byte is)
     
  18. kdubnz

    kdubnz

    Joined:
    Apr 19, 2014
    Posts:
    177
    Another sample that may help resolve the issue .. ( or not )
    This is meant to be run as a console App in a C# IDE.

    Code (CSharp):
    1. namespace Enums03
    2. {
    3.     using System;
    4.  
    5.     public enum Room
    6.     {
    7.         None = -1,
    8.         ROOM_900 = 0, // value 0
    9.         ROOM_901 = 1,
    10.         ROOM_909 = 15,
    11.         ROOM_912 = 20
    12.     }
    13.  
    14.     internal class Program
    15.     {
    16.         private static void Main(string[] args)
    17.         {
    18.             int[] myIntegerLevels = {
    19.                                         0,
    20.                                         1,
    21.                                         2,
    22.                                         3,
    23.                                         15,
    24.                                         20,
    25.                                         21
    26.                                     };
    27.             Room currentRoom;
    28.             //
    29.             foreach (int myIntegerLevel in myIntegerLevels) {
    30.                 currentRoom = Enum.IsDefined(typeof (Room), myIntegerLevel)
    31.                                   ? (Room) myIntegerLevel
    32.                                   : Room.None;
    33.  
    34.                 switch (currentRoom) {
    35.                     case Room.None:
    36.                         Console.WriteLine("Level: {0} -- Doing stuff for No Room",
    37.                                           myIntegerLevel);
    38.                         break;
    39.                     case Room.ROOM_900:
    40.                         Console.WriteLine("Level: {0} -- Doing stuff for Room 900",
    41.                                           myIntegerLevel);
    42.                         break;
    43.                     case Room.ROOM_901:
    44.                         Console.WriteLine("Level: {0} -- Doing stuff for Room 901",
    45.                                           myIntegerLevel);
    46.                         break;
    47.                     case Room.ROOM_909:
    48.                         Console.WriteLine("Level: {0} -- Doing stuff for Room 909",
    49.                                           myIntegerLevel);
    50.                         break;
    51.                     case Room.ROOM_912:
    52.                         Console.WriteLine("Level: {0} -- Doing stuff for Room 912",
    53.                                           myIntegerLevel);
    54.                         break;
    55.                 }
    56.             }
    57.             Console.ReadKey();
    58.         }
    59.     }
    60. }
     
    ArachnidAnimal likes this.
  19. ArachnidAnimal

    ArachnidAnimal

    Joined:
    Mar 3, 2015
    Posts:
    1,815
    How does this compile for you? because the compiler in Unity would at least complain about: "case Room.ROOM_900".
     
  20. makeshiftwings

    makeshiftwings

    Joined:
    May 28, 2011
    Posts:
    3,350
    But ROOMS could be a long or a uint, so it's not necessarily a subset of int. See the example I posted above where you pass the enum to a different method.

    What you're saying would be true if an enum tracked its type at compile time like a generic, like i was saying. If instead of enum : long { blah... } it was Enum<long> { blah... } then the type would be "Enum<long>" and you'd know that it was an enum with underlying type long. But it doesn't work that way, due to enums being added before generics, and instead the compiler can only knows that the type of all enums is "Enum", it can't tell what the underlying type is when you pass it to a function.
     
    ArachnidAnimal likes this.
  21. makeshiftwings

    makeshiftwings

    Joined:
    May 28, 2011
    Posts:
    3,350
    It works because the switch statement is taking a ROOM rather than an int. "Room currentRoom; switch(currentRoom)"
     
    ArachnidAnimal likes this.
  22. kdubnz

    kdubnz

    Joined:
    Apr 19, 2014
    Posts:
    177
    'Would' or 'Does' complain ??
     
  23. Galf

    Galf

    Joined:
    Feb 24, 2013
    Posts:
    27
    Can you point me to Mono or MSDN documentation that talks about this inability? Regardless of whether or not generics were implemented at the time, it seems like a waste to allow an explicit backing type if the compiler won't know about that backing type. My understanding is that the explicit backing type is meant as an optimization, and if the compiler doesn't know about it when passing the enum to methods, then it's going to have to use the largest type every time, which defeats the whole purpose.
     
  24. ArachnidAnimal

    ArachnidAnimal

    Joined:
    Mar 3, 2015
    Posts:
    1,815
    never mind, i see now that you have "switch (currentRoom)"
     
  25. makeshiftwings

    makeshiftwings

    Joined:
    May 28, 2011
    Posts:
    3,350
    Wellllll.... yeah you have a point. The compiler actually must track the underlying type sometimes. But you'd still need some kind of syntax to express which type of enum you wanted. For example, how would you declare a method that only accepts Enums with underlying longs? Nowadays you could do something like "void SomeMethod(Enum<long> stuff)" but there was no way to declare something like before generics. I guess Microsoft could have done it all differently, but I don't know what goes on in their heads; but with the way it is now where "Enum" is the only type and there's no specific "Enum32" and "Enum64", it makes sense that you have to cast, even if under the hood there actually are hidden "Enum32" and "Enum64" types.
     
    ArachnidAnimal likes this.
  26. makeshiftwings

    makeshiftwings

    Joined:
    May 28, 2011
    Posts:
    3,350
    Actually, now that I think about it, in the original example the compiler definitely does know the type, because in that case it converts "ROOM.ROOM_900" to a const int and ignores the enum data anyway. So.... who knows. I guess the real answer is "Microsoft likes it this way". To me, it still feels correct to have to cast to and from an enum because an enum is usually supposed to represent a stricter set of values than a regular numeric type. But maybe they could have made it implicit if they wanted to.
     
  27. Galf

    Galf

    Joined:
    Feb 24, 2013
    Posts:
    27
    Yeah, that's essentially what I was getting at -- it's a semantic choice by Microsoft, which is fine. The correct way (and the way that avoids breaking encapsulation) is to handle enums with their actual type, not the backing type. Idealism aside, though, that approach can be annoying, particularly when dealing with deserialization.
     
  28. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    I'm still arguing that implicit casting would be a bad idea. It would mean operators like +, -, *, and / would work with enums. In most cases this is a bad idea. You could pass in an enum as a argument to any function that takes an int. I can't be the only one to see that backfiring.

    The most common use case for casting of enums is serialisation. But serialisation is always an edge case.

    @kdub. Your code example is a case of stretching enums far further then they were intended to go. Holding the room data in a struct or class is a far better approach then trying to force an enum to do the job.

    In general ignoring maintainability and extendabilty is suicide for programmers. This whole thread seems to be about making an enum more then it was ever intended to be.
     
    Chris-Trueman likes this.
  29. kdubnz

    kdubnz

    Joined:
    Apr 19, 2014
    Posts:
    177

    If that's the case, I probably should stop providing code. I thought the code simply dealt with handling enums in switch statements.
     
  30. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    I could just be being pedantic. Its been a long day. But I would have wrapped each room and the text into a data class.
     
    ArachnidAnimal likes this.
  31. kdubnz

    kdubnz

    Joined:
    Apr 19, 2014
    Posts:
    177
    I'm sure there are several reasonable ways to treat whatever data the user was dealing with.
    The OP wanted to know about handling enums in a switch statement ... end of story.
     
    Kiwasi likes this.
  32. ArachnidAnimal

    ArachnidAnimal

    Joined:
    Mar 3, 2015
    Posts:
    1,815
    Kiwasi likes this.