
Smart enums
Enumy sprawiają dużo problemów. Dlaczego? Oto przykład.
Załóżmy taki kawałek kodu:
Mamy enuma oznaczającego typ przeciwnika.
public enum EnemyType { Soldier, ToughSoldier, Captain }
Mamy klasę przeciwnik.
public class Enemy { public EnemyType Type { get; } public int MaxHp { get; } public Enemy(EnemyType type) { Type = type; switch (type) { case EnemyType.Soldier: MaxHp = 100; break; case EnemyType.ToughSoldier: MaxHp = 200; break; case EnemyType.Captain: MaxHp = 300; break; default: throw new ArgumentOutOfRangeException(nameof(type), type, null); } } public string GetDescription() { switch (Type) { case EnemyType.Soldier: return "This is soldier."; case EnemyType.ToughSoldier: return "This is tough soldier."; case EnemyType.Captain: return "This is captain."; default: throw new ArgumentOutOfRangeException(); } } }
Samo stworzenie przeciwnika jest bardzo proste:
Enemy newEnemy = new Enemy(EnemyType.Soldier);
Załóżmy, że teraz chcemy dodać nowy typ przeciwnika – bossa:
public enum EnemyType { Soldier, ToughSoldier, Captain, Boss } public class Enemy { public EnemyType Type { get; } public int MaxHp { get; } public Enemy(EnemyType type) { Type = type; switch (type) { case EnemyType.Soldier: MaxHp = 100; break; case EnemyType.ToughSoldier: MaxHp = 200; break; case EnemyType.Captain: MaxHp = 300; break; case EnemyType.Boss: MaxHp = 500; break; default: throw new ArgumentOutOfRangeException(nameof(type), type, null); } } public string GetDescription() { switch (Type) { case EnemyType.Soldier: return "This is soldier."; case EnemyType.ToughSoldier: return "This is tough soldier."; case EnemyType.Captain: return "This is captain."; case EnemyType.Boss: return "This is boss."; default: throw new ArgumentOutOfRangeException(); } } }
Konieczne było dodanie kolejnej wartości do enuma i dopisanie kolejnych przypadków do switchy. A co byłoby gdybyśmy takie switche mieli także w innej części programu? W każdym takim miejscu konieczne byłoby obsłużenie tej zmiany. Mamy tutaj problem z zależnościami – każdy switch jest zależny od wszystkich wartości enuma.
Przedstawiony problem można rozwiązać w bardzo prosty sposób – poprzez smart enums.
Zacznijmy od kodu:
public sealed class EnemyType { public static readonly EnemyType Soldier = new EnemyType(100, "This is soldier."); public static readonly EnemyType ToughSoldier = new EnemyType(200, "This is tough soldier."); public static readonly EnemyType Captain = new EnemyType(300, "This is captain."); public static readonly EnemyType Boss = new EnemyType(500, "This is boss."); public string Description { get; } public int MaxHp { get; } private EnemyType(int maxHp, string description) { MaxHp = maxHp; Description = description; } }
Teraz wartości opisów i życia są powiązane z możliwymi wartościami smart enuma. Warto tu zwrócić uwagę na prywatny konstruktor – stworzenie nowej wartości smart enum nie będzie możliwe z zewnątrz. Słowo kluczowe sealed przed nazwą klasy sprawia że z tego typu (tzn. klasy naszego enuma) nie będzie można dziedziczyć.
Klasa przeciwnik znacznie się też zmniejszyła:
public class Enemy { public EnemyType Type { get; } public int MaxHp { get; } public Enemy(EnemyType type) { MaxHp = type.MaxHp; Type = type; } public string GetDescription() { return Type.Description; } }
Stworzenie nowego przeciwnika, bazując na typie jest identyczne jak poprzednio:
Enemy newEnemy = new Enemy(EnemyType.Soldier);
A jeśli chcielibyśmy sprawdzić typ utworzonego przeciwnika to zapis nie różniłby się w ogóle od zapisu na zwykłych enumach.
if (newEnemy.Type == EnemyType.Soldier) { Console.WriteLine("This is simple solder - no threat."); }
Wróćmy teraz do początku – skąd wyniknął problem ze zwykłym enumem? Dlatego że konieczna była modyfikacja samego typu wyliczeniowego i dane zależne jego wartości nie były powiązane z nim samym. Teraz powinno pojawić się pytanie – czy powinno stosować się enumy? Tak, ale tylko do zestawu wartości, co do którego mamy pewność, że będzie niezmienny.
Smart enumy pozwalają w prosty sposób powiązać dodatkowe dane (np. opis i maksymalne hp) z samymi wartościami wyliczenia. Można nawet pójść o krok dalej – smart enums mogą nawet zawierać metody. Można podać do nich jakieś zachowanie (np. poprzez delegatę, interferjs) w konstruktorze. Jako że robimy to w jednym miejscu, zmiana powiązanych ze smart enumem danych też jest bardzo prosta – nie trzeba przeglądać kodu w poszukiwaniu problematycznych switchy.
Ponadto od c# 7.0 smart enum może być także łatwo używany w instrukcjach switch:
switch (newEnemy.Type) { case var enemyType when enemyType == EnemyType.Captain: Console.WriteLine("This is captain - it will be hard."); break; case var enemyType when enemyType == EnemyType.Boss: Console.WriteLine("This is boss - run ASAP!"); break; }