Ma relation amour-haine avec switch en C#

.NETCleanCode
Jean-Baptiste Raulin - 08/08/2023 à 14:37:070 commentaire

Voilà bientôt 21 ans que j'écris en C#. Au fur et à mesure des années, ma relation avec l'opérateur switch a changé. Faire le bon choix peut-être compliqué.


2002-2004 - Erreurs de jeunesse

Au début, la découverte. C'est pratique, plus concis qu'un enchaînement de if else et ça permet de gérer beaucoup de cas.

 public void DoSwitch(int someValue)
 {
     switch (someValue)
     {
         case 0:
             Console.WriteLine("number: Zéro");
             break;

         case 2:
             Console.WriteLine("number: Zwei");
             break;

         case 3:
             Console.WriteLine("number: さん");
             break;

         case 5:
             Console.WriteLine("number: Cinqo");
             break;

         default:
             Console.WriteLine("Some other integer");
             break;
      }
 }


2004-2019 - Pas si joli finalement

Déjà, le switch est syntaxiquement étrange avec ses ":" et ses "break" incessants.

De plus, sa logique fait souvent réécrire aux développeurs des mêmes portions de code.

Et quand on se retrouve finalement avec des gros pâtés de code dans chaque case, ça devient carrément illisible.


Afin d'éviter tout ça, j'ai refusé beaucoup de code avec du switch.

Que faire à la place?


Pour les cas simples, on peut faire du if else.

Ou, comme ci-dessous, un dictionnaire

public readonly Dictionary<int, string> _numberMatchDictionary = new Dictionary<int, string>()
{
    {0, "Zéro" },
    {2, "Zwei" },
    {3, "さん" },
    {5, "Cinqo" }
};

public void DoNotSwitch(int someValue)
{
    if (!_numberMatchDictionary.Keys.Contains(someValue))
    {
        Console.WriteLine("Some other integer");
        return;
    }
    Console.WriteLine($"number: {_numberMatchDictionary[someValue]}");
}

La partie spécifique de chaque choix étant définie dans le dictionnaire, il devient plus simple de faire évoluer le code selon nos besoins.

On voit de manière évidente le lien entre la condition (clé du dictionnaire) et sa résultante (valeur du dictionnaire).


Pour des cas plus compliqués, on peut instancier une classe réutilisable.

Par exemple pour un switch comme suit :

switch (pressedInteger)
{
    case 1:
    case 2:
    case 3:
    case 5:
        isValidated = DoSomethingPrimary();
        break;

    case 4:
        break;

    case 6:
        DoSomethingDividedByThree();
        isValidated = true;
        break;

    default:
        isValidated = true;
        break;
}


On créé les classes MatchCase et CaseOption qui nous permettront d'utiliser le switch case quel que soit le type de switch et le type de fonction à appeler :

public class CaseOption<TMatchType, TActionType> 
    where TActioType : Delegate
{
    public CaseOption(TMatchType[] matches, TActionType action)
    {
        Matches = matches;
        Action = action;
    }

    public CaseOption(TMatchType match, TActionType consequence)
    {
        Matches = new[] { match };
        Action = consequence;
    }

    public TActionType Action { get; }
    public TMatchType[] Matches { get; }
}

public class MatchCase<TMatchType, TActionType> : 
    IEnumerable<CaseOption<TMatchType, TActionType>> 
    where TActionType : Delegate
{
    private List<CaseOption<TMatchType, TActionType>> _internalList = new();

    public TActionType? Default { get; set; }

    public void Add(CaseOption<TMatchType, TActionType> item)
    {
        _internalList.Add(item);
    }

    public void Add(TMatchType match, TActionType action)
    {
        _internalList.Add(new CaseOption<TMatchType, TActionType>(match, action));
    }

    public void Add(TMatchType[] matches, TActionType action)
    {
        _internalList.Add(new CaseOption<TMatchType, TActionType>(matches, action));
    }

    public IEnumerator GetEnumerator()
    {
        return _internalList.GetEnumerator();
    }

    IEnumerator<CaseOption<TMatchType, TActionType>> IEnumerable<CaseOption<TMatchType, TActionType>>.GetEnumerator()
    {
        return _internalList.GetEnumerator();
    }

    public object? Process(TMatchType valueToMatch, params object[] parameters)
    {
        if (!Any(valueToMatch))
        {
            return Default?.DynamicInvoke(parameters);
        }
        return _internalList
            .First(item => item.Matches.Contains(valueToMatch))
            .Action.DynamicInvoke(parameters);
    }

    private bool Any(TMatchType valueToMatch)
    {
        return _internalList.Any(item => item.Matches.Contains(valueToMatch));
    }
}


On l'utilisera comme suit :

public MatchCase<int, Func<bool>> _numbersMatchCase;

/// Initialisation de "switch case"
public void InitializeCharFuncMatchList()
{
    _numbersMatchCase = new MatchCase<int, Func<bool>>
    {
        { new[]{ 1, 2, 3, 5}, DoSomethingPrimary },
        { 4, () => false },
        { 6, () => {  DoSomethingDividedByThree(); return true; } },
    };
    _numbersMatchCase.Default = () => true;
}

/// Appel
public void DoNotBigSwitchClean(int operation, out bool isValidated)
{
    isValidated = (bool)_numbersMatchCase.Process(operation);
}


On pourrait réutiliser MatchCase pour tout autre besoin de type switch case.

Par exemple :

public MatchCase<DayOfWeek, Func<DateTime, bool>> _daysMatchCase;

public void InitializeWorkOrNotList()
{
    _daysMatchCase = new MatchCase<DayOfWeek, Func<DateTime, bool>>
    {
        { EnumTools.Range(DayOfWeek.Monday, DayOfWeek.Friday), (day) => !IsBankHolliday(day) },
        { new []{ DayOfWeek.Saturday, DayOfWeek.Sunday }, (day) => false}
    };
    _daysMatchCase.Default = (day) => throw new Exception("Not a valid day");
}

public bool IsWorking(DateTime day)
{
    return (bool)_daysMatchCase.Process(day.DayOfWeek, day);
}


Avec cette méthode, on a une écriture plus concise qui invite le développeur à refactoriser son code.



Et puis un jour switch case a évolué.


Depuis 2019 : On s'est retrouvé et notre relation a changé


switch case permet depuis les versions de C#8 jusqu'à C#11 d'autres utilisations.


On peut retourner le choix à stocker dans une variable :

string message = someValue switch
{
    0 => "number: Zéro",
    2 => "number: Zwei",
    3 => "number: さん",
    5 => "number: Cinqo",
    _ => "Some other integer"
};
Console.WriteLine(message);


ou juste définir nos actions en fonction du choix :

 command switch
   {
       Operation.SystemTest => RunDiagnostics(),
       Operation.Start => StartSystem(),
       Operation.Stop => StopSystem(),
       Operation.Reset => ResetToReady(),
       _ => throw new ArgumentException("Invalid enum value for command", nameof(command)),
   };


On peut faire des choix sur des plages de valeur :

static string Classify(double measurement) => measurement switch
{
    < -4.0 => "Too low",
    > 10.0 => "Too high",
    double.NaN => "Unknown",
    _ => "Acceptable",
};


ou plusieurs choix possibles :

static string GetCalendarSeason(DateTime date) => date.Month switch
{
    3 or 4 or 5 => "spring",
    6 or 7 or 8 => "summer",
    9 or 10 or 11 => "autumn",
    12 or 1 or 2 => "winter",
    _ => throw new ArgumentOutOfRangeException(nameof(date), $"Date with unexpected month: {date.Month}."),
};


Comparer des listes :

int[] numbers = { 24, 320, 32, 700 };

string arrayDesc = numbers switch
{
    [24, 320, 32, 700] => "It's the array we expected!",
    _ => "no match"
};


On peut également préciser nos choix :

static Point Transform(Point point) => point switch
{
    { X: 0, Y: 0 }                    => new Point(0, 0),
    { X: var x, Y: var y } when x < y => new Point(x + y, y),
    { X: var x, Y: var y } when x > y => new Point(x - y, y),
    { X: var x, Y: var y }            => new Point(2 * x, 2 * y),
};


Plein d'options qui rendent l'opérateur switch plus versatile. C'est toutes ces nouveautés qui me plaisent chez lui.


Plus d'information sur les expressions switch

Plus d'information sur le pattern matching en particulier


Voilà. En 21 ans, des rapports compliqués.

Qui sait ce qu'il nous réserve pour l'avenir.

Commentaires :

Aucun commentaires pour le moment


Laissez un commentaire :

Réalisé par
Expaceo