Home > Back-end >  Constant/readonly array of objects indexed by enum
Constant/readonly array of objects indexed by enum

Time:09-21

In Delphi, one can write the following code:

type
  TMyEnum = (meFirstValue, meSecondValue);

  TInfo = record
    Name: string;
    Description: string;
  end;

const
  MyEnumDetails: array [TMyEnum] of TInfo =
    (
      (Name: 'First Value'; Description: 'This is the first value'),
      (Name: 'Second Value'; Description: 'This is the second value')
    );

This has the very nice advantage that should a new enum member be added to TMyEnum, then the compiler will complain on the MyEnumDetails constant definition, forcing the developer to resolve the situation before the application reaches the build machine.

As I'm now writing .Net 6 C# code within Visual Studio 2022, I'm trying to replicate the above, and in particular the fact that the compiler will stop with an error if an enum member has been left out.

I can use a Dictionary as mentioned here but this does not emit a warning/error if I forget a member of the enum in the initialization. I know this is not strictly a constant, nor a readonly element, but in my case I'm more interested in the compiler erroring out than the constness of the dictionary.

I also tried enumerating the enum members and use a switch statement to create the instances as needed:

foreach (var myEnum in (MyEnum[])Enum.GetValues(typeof(MyEnum)))
{
    switch (myEnum)
    {
        case MyEnum.FirstValue:
            enumDictionary.Add(myEnum, new Info("First Value", "This is the first value"));
            break;
        }
    }
}

But then again, this does not make the compiler emit any message. I was kind of expecting to get CS8509 or IDE0072/IDE0010 but nothing comes up.

So I tried moving the switch a secondary method that only does this:

private static Info GetInfo(MyEnum myEnum)
{
    switch (kind)
    {
        case MyEnum.FirstValue:
            return new Info("First Value", "This is the first value");
    }
}

And this time I did receive an error, namely the CS0161 one because GetInfo does not return a value on all code paths. But that error won't go if I provide a case for all enum members, only does it disappear if I provide a default case or a return outside the switch. And then, there are no longer any messages if an enum member is not used in the switch statement, just like with the previous attempt.

I checked in the csproj file and could not find anything that would specifically disable the CS8509 or IDE0072/IDE0010 warnings. Looking at the options page, I did not see anything either.

What have I missed here? Is there a place unknown to me that disables the above warning?

Alternatively, do you have any other suggestion as to how I can achieve the goal of having the compiler stopping on a missing enum member?

CodePudding user response:

The following program will give you a CS8509 compile error:

using System;
using System.Collections.Generic;

namespace Demo;

public static class Program
{
    public static void Main()
    {
        initDict();    
    }

    static void initDict()
    {
        foreach (var myEnum in (EnumTest[])Enum.GetValues(typeof(EnumTest)))
        {
            string value = myEnum switch
            {
                EnumTest.Zero => "Case: Zero",
                EnumTest.One  => "Case: One",
                EnumTest.Two  => "Case: Two"
            };

            _enumDictionary.Add(myEnum, value);
        }
    }

    static readonly Dictionary<EnumTest, string> _enumDictionary = new();
}

public enum EnumTest
{
    Zero,
    One,
    Two,
    Three
}

If you comment-out the Three item in enum EnumTest it will compile OK, but with the following warning instead:

Warning CS8524 The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value. For example, the pattern '(Demo.EnumTest)3' is not covered.

If you don't want that warning, you can suppress it by surrounding the affected code with:

#pragma warning disable CS8524

string value = myEnum switch
{
    EnumTest.Zero => "Case: Zero",
    EnumTest.One  => "Case: One",
    EnumTest.Two  => "Case: Two"
};

#pragma warning restore CS8524

If you do that, you'll still get the error if you add a new item to enum EnumTest without adding it to the switch.

Obviously you have to enable Code Analysis for this to work!

Is that the sort of thing you're looking for?

CodePudding user response:

We have solved this using a helper class in an unit test assembly.

public static class EnumTestHelper
    {
        /// <summary>
        /// Encapsulates enum value checking and fails when unexpected number of enum values.
        /// Should be called on each test which handles enum values to ensure that no place in the codebase is forgotten when a new enum member is introduced.
        /// </summary>
        public static void FailOnNewEnumValue(Type enumType, int expectedValueCount)
        {
            FailOnNewEnumValue(enumType, expectedValueCount, string.Format(CultureInfo.InvariantCulture, "Add unit test for new enum value of type {0}", enumType.FullName));
        }

        /// <summary>
        /// Encapsulates enum value checking and fails when unexpected number of enum values.
        /// Should be called on each test which handles enum values to ensure that no place in the codebase is forgotten when a new enum member is introduced.
        /// </summary>
        public static void FailOnNewEnumValue(Type enumType, int expectedValueCount, string failureMessage)
        {
            Assert.AreEqual(expectedValueCount, Enum.GetValues(enumType).Length, failureMessage);
        }
    }

Then in some unit test that uses the enum, we do:

[Test]
public void TestNoNewEnumValues()
{
     EnumTestHelper.FailOnNewEnumValue(typeof(MyEnum), 5);
}

This does not cause a build time error, but still prevents pushing such an error to prod, because we cannot merge changes that fail an unit test.

  • Related