9

I've come across a recurring issue in a few of my recent projects in which I find myself using enums to represent state, or type, or something else, and I need to check against a few conditions. Some of these conditions span multiple enums, so I end up with a load of logic in switch and if blocks, which I don't like. This is a particular problem when having to cast things to and from the enum, like when the enum is being used to check an int that you're getting from a web request or a calculation.

Is there something in C++ or C# that can be used as a nested enum? Something like this:

enum Animal
{
    Mammal
    {
        Cat,
        Dog,
        Bear
    },
    Bird
    {
        Pigeon,
        Hawk,
        Ostrich
    },
    Fish
    {
        Goldfish,
        Trout,
        Shark
    },
    Platypus    //srsly what is that thing
};

Obviously, it may or may not be declared like that, but you get the idea. The point is that in code you could use it like Animal thisAnimal = Animal.Mammal.Cat and then later check if (thisAnimal.IsMember(Animal.Mammal)) or something like that.

I've seen Java's EnumSet, and found them pretty useful, but I don't think they're an exact match for the functionality I'm after. For this example, you'd have to declare an enum with all the animals at one level, and then add them all to the relevant sets. That would mean that when using the original enum, higher-level things like Mammal or Invertebrate would appear on the same "level" as something very specific like African Swallow, which would imply that they were (to some degree) interchangeable, which isn't true. In theory, a nested structure as above might allow you to specify the level of "specificness" needed, so you could get this:

enum Animal::Kingdom.Order.Family.Genus.Species
{ /*stuff*/ }

Organism::Kingdom.Phylum.Class.Order.Family thisThing;

thisThing = Animalia.Cordata;                               //compiler error
thisThing = Animalia.Chordata.Aves.Passeri.Hirundinidae;    //compiles OK

Does a structure like this exist anywhere? If not, how might I build one for C++ and/or C# and have it remain as generic and re-usable as possible?

13
  • 4
    This is why OOP exists. Enums are supposed to be simple listings. If any complex hiearchy is required, you should create your own structures.
    – Euphoric
    Commented Feb 18, 2013 at 10:33
  • @Euphoric Generally I'd agree, but when all you want to do is list simple data like "is this a thing" and "what sort of thing is it" it seems like overkill to build a whole set of classes. The situations where I'd use this are cases where an enum is almost good enough, but too flat. I used the example of animals - if the software was doing something with animals, I'd use something more complex, but for, say, landlord's management software, all you need is what type of pet the tenant has so you can check if (Tenant.Pet.IsMember(Animal::Fish)) because they're only allowed a fish, or something. Commented Feb 18, 2013 at 10:46
  • 3
    @anaximander: The problem is that enums call for switch statements to handle them. And switch statements are a nightmare when you need to extend the enum. If your enum is large enough that it needs subcategories, it is certainly large enough to expand regularly. Here is the advantage of OOP: You can define all necessary operations (such as isMovement(), isInteraction(), isCombat()) and your application scales completely differently when you add new actions.
    – thiton
    Commented Feb 18, 2013 at 12:51
  • 1
    @anaximander How is a dictionary of enums and a factory method to create the enum from a string better than actual class hierarchy and factory method to get instance of the right class using a dictionary?
    – svick
    Commented Feb 18, 2013 at 13:38
  • 3
    A platypus is a monotreme, which is a kind of egg-laying mammal. It's in a small group of other animals along with echidnas. The more you know
    – KChaloux
    Commented Feb 18, 2013 at 13:47

4 Answers 4

10

I agree with others that this seems overengineered. Usually, you want either a simple enum or a complex hierarchy of classes, it's not a good idea to combine the two.

But if you really want to do this (in C#), I think it's useful to recap what exactly do you want:

  • Separate types for the hierarchy Kingdom, Phylum, etc., which do not form inheritance hierarchy (otherwise, Phylum could be assigned to Kingdom). Though they could inherit from a common base class.
  • Each expression like Animalia.Chordata.Aves has to be assignable to a variable, which means we have to work with instances, not nested static types. This is especially problematic for the root type, because there are no global variables in C#. You could solve that by using a singleton. Also, I think there should be only one root, so the code above would become something like Organisms.Instance.Animalia.Chordata.Aves.
  • Each member has to be a different type, so that Animalia.Chordata compiled, but Plantae.Chordata didn't.
  • Each member needs to somehow know all its children, for the IsMember() method to work.

The way I would implement these requirements is to start with a class like EnumSet<TChild> (though the name could be better), where TChild is the type of the children of this level in hierarchy. This class would also contain a collection of all its children (see later about filling it). We also need another type to represent leaf level of the hierarchy: non-generic EnumSet:

abstract class EnumSet
{}

abstract class EnumSet<TChild> : EnumSet where TChild : EnumSet
{
    protected IEnumerable<TChild> Children { get; private set; }

    public bool Contains(TChild child)
    {
        return Children.Contains(child);
    }
}

Now we need to create a class for each level in the hierarchy:

abstract class Root : EnumSet<Kingdom>
{}

abstract class Kingdom : EnumSet<Phylum>
{}

abstract class Phylum : EnumSet
{}

And finally some concrete classes:

class Organisms : Root
{
    public static readonly Organisms Instance = new Organisms();

    private Organisms()
    {}

    public readonly Animalia Animalia = new Animalia();
    public readonly Plantae Plantae = new Plantae();
}

class Plantae : Kingdom
{
    public readonly Anthophyta Anthophyta = new Anthophyta();
}

class Anthophyta : Phylum
{}

class Animalia : Kingdom
{
    public readonly Chordata Chordata = new Chordata();
}

class Chordata : Phylum
{}

Notice that children are always fields of the parent class. What this means is that to fill the Children collection, we can use reflection:

public EnumSet()
{
    Children = GetType().GetFields(BindingFlags.Instance | BindingFlags.Public)
                        .Select(f => f.GetValue(this))
                        .Cast<TChild>()
                        .ToArray();
}

One problem with this approach is that Contains() always works only one level down. So, you can do Organisms.Instance.Contains(animalia), but not .Contains(chordata). You can do that by adding overloads of Contains() to the specific hierarchy classes, e.g.:

abstract class Root : EnumSet<Kingdom>
{
    public bool Contains(Phylum phylum)
    {
        return Children.Any(c => c.Contains(phylum));
    }
}

But this would be a lot of work for deep hierarchies.


After all of this, you end up with quite a lot of repetitive code. One way to fix that would be to have a text file that describes the hierarchy and use a T4 template to generate all the classes based on that.

3
  • 1
    Yeah... that's a lot of code. And it's still not generic; it's tied to classification of organisms - you couldn't reuse it for, say, state, or player actions in a game, or days of the week, or something. Not criticising; just looks like it can't be done. Commented Feb 18, 2013 at 14:07
  • 1
    Well, it would be generic if you did it using code generation. Otherwise, I don't think you can make something like this generic.
    – svick
    Commented Feb 18, 2013 at 15:46
  • 1
    Code generation isn't really something I fancy getting into, for all sorts of reasons. Commented Feb 18, 2013 at 15:51
2

I think this is the best way you can implement it: https://stackoverflow.com/a/24969525/538387

But as you see, it adds more complexity to the code, which is not a good practice in general.

I suggest to keep every enum values flat, just like a normal enum definition, and add a few extension or helper methods. like:

public enum Animal
{
    // Mammals
    Cat,
    Dog,
    Bear

    // Birds
    Pigeon,
    Hawk,
    Ostrich

    // Fishes
    Goldfish,
    Trout,
    Shark
};


public static class AnimalExtension
{
    public static bool IsMammal(this Animal animal)
    {
        return animal == Animal.Cat
            || animal == Animal.Dog
            || animal == Animal.Bear;
    }

    public static bool IsBird(this Animal animal)
    {
        ...
    }

    public static bool IsFish(this Animal animal)
    {
        ...
    }
}

So you can easily write in your code:

Animal animal = GetValueFromASource();

if (animal.IsMammal())
{
    Console.Write("This is a mammal.");
}
1

My solution would be to use bit flags. If you are not familiar see here. The general idea is simple yet still falls in line with your need to use enums.

Each grouping of, for lack of a better work, classes, is just a span of bits. So for mammals you just delegate a stretch of bits. In your case you would have your None(0x0) organism, perhaps the Platypus. Then 0x1 is Cat, 0x2 is dog, 0x4 is bear. To check for inclusion in the Mammals category just AND it with a 7 and be sure the result is the value being checked.

To check for inclusion in the Animal kingdom you would just AND it with the all bit flagged stretch of values you have.

This is not exactly the best technique for scalability but it may be the functionality you are looking for.

1
  • Broadly agree, although I'd use a bit field for animal or other things at this level, then a bit field which decided between bird/mammal/fish, then a bit field choosing between fish, or choosing between mammals, etc. And reserve a few values for categories you haven't thought of...
    – MZB
    Commented Apr 20, 2020 at 20:28
0

Enums are integral types and that is not going to change so whatever solution we come up with we can stop calling it an enum.

All speculation about bad design decisions aside, I suggest you use a string with a reserved separator character. The purpose is still identification, you just do not like the flatness.

So how about Animal_Mammal_Cat for an id?

You can easily split that into its subparts and process it using a nested switch statement. That will give you the opportunity to invoke custom handling based on the "class" of identifier you are dealing with.

It is not that uncommon in business administration to have identifiers that have contextual information in them. Like order number 202004201 as the first order on April 20th of 2020. Guaranteed to be unique, can be used a technical id (key) and provides a way to filter in time.

3
  • Eh, no. However common you think it is doesn't make it any less wrong. Parsing strings for data is a primitive attempt at data management. If you need multiple fields from <something> then you should just create a class with those fields explicitly defined and separate from each other. There's no (reasonable) justification not to. Commented Mar 11, 2021 at 19:17
  • @searchengine27 It is not wrong, it is addressing a need. Like offering an efficient way to enter data, create a new id or conveying information instantly to the user reading the field. If you want to manipulate or process data automatically you can still have that class that you talk about but that would address a different purpose. Commented Mar 11, 2021 at 20:29
  • No, what I'm saying is there are much better approaches to the two things you just suggested as reasons to do this. Entering data is complicated by the need to concatenate data unnecessarily. IDs should NEVER be concatenated data, and should be made of composite keys. Manipulating data is thereby easier as well. Whether or not you will modify is irrelevant - the data is always accessed separately. Why complicate all that just because? There's no strong reason to ever concatenate data into a single string. In fact, there's usually plenty of evidence to the contrary where you should never do it. Commented Mar 11, 2021 at 20:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.