Search code examples
c#enumsbit-fieldsbitwise-or

Why does declaring a combination of bit fields inside an enum produce a different result than declaring it outside of the enum?


Here, I have a list of subjects indicated by bit fields with an "Optional" field containing the optional subjects at the bottom.

[Flags]
enum Subjects 
{
    Art         = 0b_0000_0001,
    Agriculture = 0b_0000_0010,
    English     = 0b_0000_0100,
    Geography   = 0b_0000_1000,
    Maths       = 0b_0001_0000,
    Science     = 0b_0010_0000,
    Optional    = Art | Agriculture,
}

When I print the optional subjects to the console, I am given an unexpected result:

Console.WriteLine(Subjects.Optional); // returns "Optional", I expected "Art, Agriculture"

Now, if I were to declare the same Optional field outside of the enum and log it:

// NOTE: I had to comment out the "Optional" field, otherwise it would return Optional once again

var optional = Subjects.Art | Subjects.Agriculture;
Console.WriteLine(optional); // returns "Art, Agriculture" not "Optional"

It works as expected.

So my question is, why do I receive a different output when I place the combined bit fields in the enum vs putting it outside the enum?


Solution

  • You're not distinguishing between enum values and variables, but these are very different.


    Enum abuse

    As an aside, I think you're abusing the purpose of an enum by trying to sneak some extra metadata about these enumvalues (i.e. whether they are optional or not) into the composed Optional field.

    I suspect the best solution for you is to move away from using the enum entirely, since enum values shouldn't have more metada surrounding them.

    I have still answered the question as my suspicion of enum abuse is solely based on a name and my interpretation of its meaning to you. It's up to you to decide whether you're trying to sneak some metadata in the enum or whether I misunderstood your intention.


    Enum values

    [Flags]
    enum Subjects 
    {
        Art         = 0b_0000_0001,
        Agriculture = 0b_0000_0010,
        Optional    = Art | Agriculture,
    }
    

    When you include the composed value in the enum, you define it as a valid enum value. You are literally telling the compiler that Subjects.Optional is a valid (and thus meaningful) value of the enum, implying that this can and should be used.

    That leads the compiler to use the Subjects.Optional value (and its string representation, i.e. "Optional") because you told the compiler that it's meaningful to you.

    Variables

    [Flags]
    enum Subjects 
    {
        Art         = 0b_0000_0001,
        Agriculture = 0b_0000_0010
    }
    
    var optional = Subjects.Art | Subjects.Agriculture;
    

    It's important to realize there that optional is a variable and not an enum value. There are only two enum values here, Art and Agriculture.

    In this case, you did not define Optional to be an enum value, and therefore the compiler cannot use or refer to an enum value that doesn't exist.

    Therefore, it falls back on figuring out which combination of enum values would result in the (combined) optional value, and it realizes that by combining Subject.Art and Subject.Agriculture, you get the value described by optional, which is why it returns a comma-separated string Art, Agriculture.


    If you want to get the comma-separated string while also retaining the composed values in the enum itself, you're going to have to generate the comma-separated string yourself. For example:

    public string AsCommaSeparatedString(Subjects myEnum)
    {
        var possibleSubjects = new List<Subjects>() { Subjects.Art, Subjects.Agriculture };
    
        var subjects = possibleSubjects.Where(possibleSubject => myEnum.HasFlag(possibleSubject));
    
        return String.Join(",", names);
    }
    

    You'll have to list all the enums values which you want to include (so others like Optional will be ignored), but that's unavoidable when you specifically want to exclude some values (like Optional) from being mentioned.