Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to compare System.Enum to enum (implementation) without boxing?

Tags:

c#

enums

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!

like image 656
Hatchling Avatar asked Feb 16 '16 20:02

Hatchling


4 Answers

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.

like image 124
Lukazoid Avatar answered Nov 07 '22 10:11

Lukazoid


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.

like image 3
Niall Connaughton Avatar answered Nov 07 '22 12:11

Niall Connaughton


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.

like image 2
Matthew Whited Avatar answered Nov 07 '22 10:11

Matthew Whited


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.

like image 2
Jeppe Stig Nielsen Avatar answered Nov 07 '22 11:11

Jeppe Stig Nielsen