4

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?

Mars Bars
  • 55
  • 5
  • 1
    "I had to comment out the "Optional" field" <-- yes. So it comes down two these two cases: `value_mapping_to_enum_field.ToString()` and `value_not_mapping_to_a_specific_enum_field_while_mapping_to_flag_combination.ToString()`. The "where" is only relevant as it changes the condition from one to the other. – user2864740 May 11 '20 at 05:37
  • See https://learn.microsoft.com/en-us/dotnet/standard/base-types/enumeration-format-strings which describes the behavior. There does not seem to be an format specified to 'always prefer to decompose as flags'. – user2864740 May 11 '20 at 05:44
  • Try following : Console.WriteLine(((uint)optional).ToString("X8")); – jdweng May 11 '20 at 08:36

2 Answers2

3

You could have written your enum declaration the following way, giving the same results:

[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    = 0b_0000_0011
}

How should the compiler know that Optional is a composed field? When a field exists, it will be chosen in the ToString() method. If you want to avoid that, you can either remove the Optional field and add an extension method:

public bool IsOptional(this Subjects subjects)
 {
 return subjects.HasFlag(Subjects.Art) && subjects.HasFlag(Subjects.Agriculture);
 }

Or you can write your own method that converts your enum to a string, maybe using the description attribute to get another value for the Optional field

SomeBody
  • 7,515
  • 2
  • 17
  • 33
  • Note that _"How should the compiler know that Optional is a composed field?"_ is maybe not the best way to tackle OP's expectations, as flag-enum values with multiple `1` bits are commonly understood to be combinations of flags enum values with a single `1` bit. While that is not necessarily true for every conceivable scenario, that is the default behavior for a flags enum. – Flater May 11 '20 at 15:16
2

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.

Flater
  • 12,908
  • 4
  • 39
  • 62