Java-Style Enums in C#
Damit Logik dort steht, wo sie fachlich hingehört.
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.
