Eintrag

Java-Style Enums in C#

Damit Logik dort steht, wo sie fachlich hingehört.

Java-Style Enums in C#

Man sagt: »Wenn man programmieren kann, ist die Sprache egal. Die Konzepte sind entscheidend, nicht die Syntax.«

Grundsätzlich sehe ich das genau so. Hat man bereits ein gewisses Maß an Erfahrung in einer Sprache gesammelt, findet man sich recht flott in einer neuen zurecht. Allerdings bringen Sprachen unterschiedliche Konzepte mit und die prägen unsere Art, Probleme zu lösen. Das führt folglich dazu, dass einige Lösungen nur existieren, weil die Sprache unsere Ideen umsetzen lässt oder eben verhindert.

Wenn eine Sprache uns Wände in den Weg stellt, dann gewöhnen wir uns irgendwann daran und kommen irgendwann vielleicht gar nicht auf die Idee zu hinterfragen, ob es noch etwas dahinter gibt.

Sprachen entwickeln sich glücklicherweise weiter und bringen neue Features mit. Ein Feature, das es noch nicht in C# geschafft hat, sind Enums, die Logik enthalten dürfen. Genug geschwafelt, jetzt gibts Code!

Enums in Java

In Java dürfen Enums schon so ziemlich von Anfang an Logik enthalten:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public enum Day {
    MONDAY {
        @Override
        public String tellItLikeItIs() {
            return "Mondays are bad.";
        }
    },
    FRIDAY {
        @Override
        public String tellItLikeItIs() {
            return "Fridays are better.";
        }
    },
    SATURDAY, SUNDAY {
        @Override
        public String tellItLikeItIs() {
            return "Weekends are best.";
        }
    },
    TUESDAY, WEDNESDAY, THURSDAY {
        @Override
        public String tellItLikeItIs() {
            return "Midweek days are so-so.";
        }
    };

    public abstract String tellItLikeItIs();
}

Was ich daran schön finde: Logik kann dorthin geschrieben werden, wo sie fachlich hingehört.

Anstatt Entscheidungen in switch-Statements zu treffen, kapselt jedes Enum sein eigenes Verhalten und kann isoliert getestet werden.

Ich bin kein Java-Entwickler, habe aber gehört, dass Enums ab und an missbraucht werden, indem man ein Enum mit nur einem Wert INSTANCE definiert, um sicherzustellen, dass die (Enum)-Klasse nur einmal instanziiert werden kann…

Enums in C#

Schauen wir uns als nächstes an, wie Enums in C# aussehen:

1
2
3
4
public enum Day
{
    Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
}

Mehr geht im Grunde nicht. Sicher, man kann Attribute für einen Display Name ergänzen oder festlegen, zu welchem Integer ein Enum gebunden ist. Da hört es aber auch schon auf.

Die gleiche Logik wie aus dem Java Beispiel zu implementieren endet oft so:

1
2
3
4
5
6
7
8
9
10
11
return day switch
{ 
    Day.Monday => "Mondays are bad.",
    Day.Tuesday
        or Day.Wednesday
        or Day.Thursday => "Midweek days are so-so.",
    Day.Friday => "Fridays are better",
    Day.Saturday
        or Day.Sunday => "Weekends are best.",
    _ => throw new ArgumentOutOfRangeException(nameof(day))
};

In diesem Beispiel stellt das noch kein Problem dar. Kämen allerdings Parameter und Berechnungen hinzu… können wir uns vorstellen worin das endet. Hier kommen Java-Style Enums ins Spiel.

Java-Style Enums in C#

Java-Style Enums sind prinzipiell eine mehr oder weniger geschickte Zusammenstellung von records, welche einerseits die wünschenswerten Eigenschaften von Enums imitiert und andererseits die Möglichkeit ergänzt, ihnen Logik beizubringen.

Basis-Implementierung

Hier ein verkürztes Beispiel mit einem Wert:

1
2
3
4
5
6
7
8
9
10
11
public abstract record Day
{
    public static readonly Day Monday = new MondayDay();

    public abstract string TellItLikeItIs();
}

public sealed record MondayDay : Day
{
    public override string TellItLikeItIs() => "Mondays are bad.";
}

Um die Enum-typische Verwendung von z. B. Day.Monday zu gewährleisten, verwenden wir eine statische Eigenschaft. Diese gibt eine Instanz des Records (MondayDay) zurück, welche den Wert repräsentiert.

INFO Normalerweise arbeite ich nach der One-File-One-Class Convention. Allerdings bieten sich meiner Meinung nach, Java-Style Enums an, sie in einer Datei zu lassen… solange nicht allzu viel Logik in ihnen wohnt.

Dadurch sparen wir uns das switch-Statement und können unser Enum so verwenden:

1
2
Day today = week.Today; // MondayDay
today.TellItLikeItIs(); // "Mondays are bad."

Mapping auf ein normales Enum

Das ist schon nicht schlecht, allerdings haben wir gerade leider einen wichtigen Vorteil von Enums verloren: Switch Exhaustiveness Checks.

1
2
3
4
5
6
7
8
9
10
11
return day switch
{
    MondayDay    => DayContract.Monday,
    TuesdayDay   => DayContract.Tuesday,
    WednesdayDay => DayContract.Wednesday,
    ThursdayDay  => DayContract.Thursday,
    FridayDay    => DayContract.Friday,
    // SaturdayDay  => DayContract.Saturday, <- unbehandelt!
    // SundayDay    => DayContract.Sunday,   <- unbehandelt!
    _            => throw new ArgumentOutOfRangeException(nameof(day))
};

Oh, nein! Kein Wochenende! Das könnte uns bei einem klassischen Enum nicht so leicht passieren.

»In most cases, the compiler generates a warning if a switch expression doesn’t handle all possible input values.« — Microsoft

In JetBrains Rider steht dann da: »Some values of the enum are not processed inside switch: MyEnum«

Deshalb holen wir unser ursprüngliches Enum zurück und ergänzen ein beliebiges Suffix, mit dem wir uns anfreunden können. (Id, PersistenceValue, etc.) Ich habe mich für Enum entschieden:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public enum DayEnum
{
    Monday
}

public abstract record Day
{
    public static readonly Day Monday = new MondayDay();

    public abstract DayEnum DayEnum { get; }
    public abstract string TellItLikeItIs();
}

public sealed record MondayDay : Day
{
    public override DayEnum DayEnum => DayEnum.Monday;
    public override string TellItLikeItIs() => "Mondays are bad.";
}

Das ist vielleicht nicht wunderschön, hilft uns aber beim Mapping oder in der Persistenzschicht.

1
2
3
4
5
6
7
8
9
10
11
return day.DayEnum switch
{
    DayEnum.Monday    => DayContract.Monday,
    DayEnum.Tuesday   => DayContract.Tuesday,
    DayEnum.Wednesday => DayContract.Wednesday,
    DayEnum.Thursday  => DayContract.Thursday,
    DayEnum.Friday    => DayContract.Friday,
    // DayEnum.Saturday  => DayContract.Saturday, <- compiler Warnung!
    // DayEnum.Sunday    => DayContract.Sunday,   <- compiler Warnung!
    _            => throw new ArgumentOutOfRangeException(nameof(day))
};

»[…] I’m limited by the technology of my time […]« — Howard Stark

Ein vollständiges Beispiel gibt es hier: https://github.com/martin-hirsch/JavaStyleEnums

BONUS - Ardalis SmartEnum

Selbstverständlich bin ich nicht der erste, der dieses Problem angeht. Unter dem Begriff Smart Enum finden sich diverse Lösungen im Netz unter anderem: https://github.com/ardalis/SmartEnum. Das NuGet ist extrem umfangreich und bietet Lösungen für Probleme, in die ich selbst noch nicht gelaufen bin.

Dieser Eintrag ist vom Autor unter CC BY 4.0 lizensiert.

Beliebte Tags