14

How can I compare a System.Enum to an enum without boxing? For example, how can I make the following code work without boxing the enum?

enum Color
{
    Red,
    Green,
    Blue
}

...

System.Enum myEnum = GetEnum(); // Returns a System.Enum. 
                                // May be a Color, may be some other enum type.

...

if (myEnum == Color.Red) // ERROR!
{
    DoSomething();
}

To be specific, the intent here is not to compare the underlying values. In this case, the underlying values are not meant to matter. Instead, if two Enums have the same underlying value, they should not be considered equal if they are two different kinds of enums:

enum Fruit
{
    Apple = 0,
    Banana = 1,
    Orange = 2
}

enum Vegetable
{
    Tomato = 0,
    Carrot = 1,
    Celery = 2
}

myEnum = Vegetable.Tomato;
if (myEnum != Fruit.Apple) // ERROR!
{
    // Code should reach this point 
    // even though they're the same underlying int values

    Log("Works!");
}

This is basically the same functionality as Enum.Equals(Object). Unfortunately Equals() requires boxing the enum, which in our case would be a naughty thing to do.

Is there a nice way to compare two arbitrary enums without boxing or otherwise creating a bunch of overhead?

Thanks for any help!

Hatchling
  • 191
  • 1
  • 7
  • 3
    As far as I know you can't. What's the problem with boxing them? – Alexander Derck Feb 16 '16 at 20:38
  • So you want to compare the integer-values for different enums and still be able to differ if the value is either a `Vegetable` or a `Fruit`? I doubt this is possible and I don´t see any problem on boxing the values before comparing them. – MakePeaceGreatAgain Feb 16 '16 at 20:38
  • You can get the type name through reflection, and use this in a custom comparer. But if your 'no-boxing' argument is due to performance; this is significant slower. – Stefan Feb 16 '16 at 20:43
  • 2
    Is it acceptable, in your case, to simply check to see if myEnum is a Vegetable with the `is` operator? – Khale_Kitha Feb 16 '16 at 20:43
  • 1
    We're trying to avoid creating garbage as much as we can. We're using Unity C# and garbage can be a real sticky issue. A GC.Collect() call can take a huge hit on the frame rate. The less garbage we make, the better. – Hatchling Feb 16 '16 at 20:45
  • 2
    An other option would be to check the problem at the source: why are you using such a generic `GetEnum` function? – Stefan Feb 16 '16 at 20:45
  • @Khale_Kitha That might work - we could create a generic comparison method that attempts to cast it to TEnum. If successful, it could then compare the two TEnums directly. I'll try it – Hatchling Feb 16 '16 at 20:50
  • 1
    The optimal solution that would be the best performing would be make GetEnum() return the correct type instead of System.Enum. can you show how GetEnum works so that it shows the need of System.Enum? There may be a way to fix the problem at the source. – Scott Chamberlain Feb 16 '16 at 21:12
  • http://stackoverflow.com/questions/1085144/what-is-the-difference-between-boxing-unboxing-and-type-casting – Matthew Whited Feb 16 '16 at 21:21
  • http://stackoverflow.com/questions/12294655/net-boxing-unboxing-vs-casting-performance – Matthew Whited Feb 16 '16 at 21:25
  • @ScottChamberlain Currently I'm using a state machine to define what actions are available to the player given his current state. One option that came to mind was to use strings to identify states - that is, if CurrentState.Name == "Attack", do X. Strings are kind of a pain in that there is no way to be sure that you haven't made a typo. Another thought came to mind - use an enum. We could have one enum that defines every state the player might be in. That could work, but unfortunately it'd be coupling a lot of functionality together in that enum... – Hatchling Feb 16 '16 at 21:34
  • @ScottChamberlain The third option was to have the states store a System.Enum - that is, an object which holds the type of enum, and the underlying value. Then you could have the flexibility of strings without coupling features together. (For example, you could have an `MeleeState.StrongSwing`, and a `GunState.Firing`, without having to couple `Melee` and `Gun` together.) – Hatchling Feb 16 '16 at 21:38
  • 1
    As soon as you have `myEnum` as a `System.Enum`, that `myEnum` is __already__ boxed (for `System.Enum` is a reference type (`class` type)). So that box will just be re-used if you use it with `Equals`. So where does the value you want to compare it to, come from? Is it too a box from the outset? Then `Equals` will compare the types from the "headers" of the boxes first, and if they match will compare the underlying numerical values. That seems to be what you want? – Jeppe Stig Nielsen Feb 16 '16 at 21:56
  • @JeppeStigNielsen It is acceptable that the former value is stored as a `System.Enum` (which is indeed a box for a value type as you pointed out) because it is only created once at initialization and stored as a field. In this case, `myEnum.Equals(someEnum)` would work perfectly fine *if* it didn't box `someEnum`, in that `someEnum` is likely to be provided by the user as a constant. He could, I suppose, cast each `enum` he might want to use to a `System.Enum` and statically cache them somewhere for future use, but that'd be really ugly and inconvenient. – Hatchling Feb 16 '16 at 22:07

7 Answers7

7

This can be done with a generic implementation like so:

private static bool Equals<TEnum>(Enum first, TEnum second)
    where TEnum : struct
{
    var asEnumType = first as TEnum?;
    return asEnumType != null && EqualityComparer<TEnum>.Default.Equals(asEnumType.Value, second);
}

The only heap allocated memory will be the lazy instantiation for each EqualityComparer<TEnum>.Default value, however this will only happen once for each type of enum.

Lukazoid
  • 19,016
  • 3
  • 62
  • 85
3

I'd be careful about worrying about GCs to this level unless you have a really specific need to ensure low latency. This doesn't mean "my web sever gets a lot of hits", it means things like sound/video playback, games, or low latency trading applications. If it's just "my app has to not be slow" then avoiding the GC completely is overkill. If you're avoiding boxing to avoid one allocation, you need to be avoiding every other allocation as well, which will include logging, creating closures, any string concatenation and of course any use of the new keyword for a reference type.

However, if you are genuinely in a scenario where you must avoid the GC and this is the last or worst of your allocation problems, then it may be worth looking into. Be sure to have a read through the documentation on GC Latency Modes - if you only need low latency for specific periods, then you can change the behaviour of the GC to suit you without changing all your code to avoid allocating memory.

So looking at the enum boxing issue. As others have already said, the value is already boxed inside the System.Enum object. You said in a comment that this is fine because this value is defined once for the application lifetime. In that case, I would consider defining it not as an enum, but as static class values. See this question for more details on the general approach.

If you take this approach, then you have a single object created for each possible enum value for the entire application lifetime. You're then free to use reference comparison or any other comparison you want and you won't have to deal with boxing.

So for your example, you could have something like

class Color
{
    public static Color Red { get; } = new Color();
    public static Color Green { get; } = new Color();

    private Color()
    {
    }
}

Then any comparisons between enum values are by definition between two objects, so no boxing/unboxing occurs at comparison time. And now you definitely have a clear distinction between different enum types - Fruit.Apple and Vegetable.Tomato are never going to compare equal.

Community
  • 1
  • 1
Niall Connaughton
  • 15,518
  • 10
  • 52
  • 48
2

You need to cast which has almost not impact on performance. (even if you were boxing... which you wouldn't be... it would have very little impact.)

If you are worried then use a generic method.

TEnum GetEnum<TEnum>() where TEnum : struct

Then you would get back the back type you are expecting and no casting or boxing would take place.

Matthew Whited
  • 22,160
  • 4
  • 52
  • 69
2

So you have:

System.Enum myEnum = ...;

and you know myEnum is already some boxed enum value. You want to compare that to Color.Red without creating another box with Color.Red in it. You could do that with:

if (myEnum is Color && (Color)myEnum == Color.Red)
{
  ...
}

The is keyword will re-use the existing box. The conversion from System.Enum to Color is an unboxing conversion (not sure if the type check will not be performed twice but we are really really micro-optimizing here). The == here will do a simple integer comparison on the two numerical values (for example two 32-bit integers, depending on the underlying integral type of Color) residing on the call stack.

Jeppe Stig Nielsen
  • 60,409
  • 11
  • 110
  • 181
  • This solution fits the problem perfectly. Thanks! :) However, I neglected to mention that, ideally, we'd want to enclose this comparison within a single method call to improve on readability and maintainability. I presume this involves using generics - perhaps something along the lines of `bool Equals(System.Enum first, TEnum second)`. I've tried using generics but a `System.Enum` cannot be cast to a `TEnum` no matter what constraints you put on the `TEnum`. Work arounds I've looked into so far introduce garbage. :( If only C# allowed macros, as with C++ ;) – Hatchling Feb 16 '16 at 22:37
1

Couldn't you just check

if (myEnum.GetType () != Fruit.Apple.GetType ()) ...

Konstantine
  • 71
  • 1
  • 9
  • In this case there wont be any distinction between Fruit.Apple.GetType() and Fruit.Orange.GetType() - both have the same type value - Fruit. – mojorisinify Feb 16 '16 at 20:58
  • Yes, but if they are of the same Enum type you can just use Fruit.Apple == Fruit.Orange to compare them. – Konstantine Feb 16 '16 at 21:04
  • I guess that is the primary problem from the question. @Kostas Giann – mojorisinify Feb 16 '16 at 21:09
  • 1
    The primary problem is to compare different enum types, since the obvious solution which is Enum.Equals(object) boxes the second enum to an object, which is what he is trying to avoid. He was pretty clear : To be specific, the intent here is not to compare the underlying values. In this case, the underlying values are not meant to matter. Instead, if two Enums have the same underlying value, they should not be considered equal if they are two different kinds of enum. @mojorisinify – Konstantine Feb 16 '16 at 21:12
1

You can check to see the type of the Enum in the if condition :

if (myEnum is Fruit && myEnum.Equals(Fruit.Apple))
{
    // Code should reach this point 
    // even though they're the same underlying int values

    Log("Works!");
}
mojorisinify
  • 377
  • 5
  • 22
  • 2
    I'm afraid this would result in the same error as before, where `System.Enum` cannot be compared to a `Fruit` (or other `enum` implementation) using the `==` operator. – Hatchling Feb 16 '16 at 21:03
  • Doesn't `Enum.Equals` box `Fruit.Apple` inside an `object`, though? – Hatchling Feb 16 '16 at 21:39
1

You could have GetEnum return a type object instead of type Enum and cast down to an int.

enum Fruit
{
    Apple = 0,
    Banana = 1,
    Orange = 2
}

enum Vegetable
{
    Tomato = 0,
    Carrot = 1,
    Celery = 2
}

object GetEnum()
{
    // for example
    return Fruit.Banana;
}

...

var myEnum = GetEnum();
Vegetable veg = Vegetable.Carrot;

if ((int)myEnum == (int)veg)
{
    Console.Write("SAME");
}

Not sure how much overhead this entails however.

mike
  • 455
  • 2
  • 6
  • But that's boxing, isn't it? That's what the OP was trying to avoid. The GetEnum method must box the value type to return it as object. – Chris Dunaway Feb 16 '16 at 21:30
  • Casting to `int` doesn't box `veg` i don't think. `myEnum` is essentially a box already - in this case it boxes the value `Fruit.Banana` - and I don't think casting from a boxed value to a value type would allocate more heap memory. This could work, assuming you also compared the types afterwards - however, I'd like to avoid assuming the underlying type is an `int`. – Hatchling Feb 16 '16 at 21:46
  • 1
    Unfortunately, casting to an `(int)` doesn't work for the `System.Enum` but `Convert.ToInt32()` does the trick. This would work, except it is a pain for the end user to have to write this comparison manually each time. Generics would solve this problem, but unfortunately you cannot constrain a generic to an `enum`, and `struct`s cannot be cast to `(int)`s. `Convert.ToInt32()` unfortunately uses boxing, bringing us back to the original problem. – Hatchling Feb 16 '16 at 22:13
  • myEnum is boxed already. So converting it to an int is _unboxing_ it. – Niall Connaughton Feb 16 '16 at 22:25
  • Regarding the first line of this answer: Using `object` boxes in exactly the same way as using `System.Enum` does, so it really makes no difference. – Jeppe Stig Nielsen Feb 16 '16 at 22:30