Journal Port complet de TapTempo en C# 9

Posté par  . Licence CC By‑SA.
14
11
jan.
2021

Sommaire

Bonjour 'nal,

Y'a plein de poussière ici dis-moi. Faut dire que ça fait longtemps que je ne t'ai plus écrit.

Ce WE, j'ai voulu faire honneur à Pierre Tramo avec un langage qui (à ses débuts) était une copie de Java avec quelques améliorations pour pas que le prof voit qu'on a copié sur le voisin.

Je veux bien sûr parler de C#.

Les sources

C'est par là

Pourquoi ?

Parce que j'en avais envie. Pour le fun. Parce que ça manquait (ou pas), surtout face au port Java.

Ce qui a été porté

Tout. Les traductions, les tests unitaires, le mode jeu, l'aspect orienté-objet…

Ce qui est propre à ce port

J'ai voulu utiliser au plus les possibilités modernes de C# 9 et précédents.

Bon y'a pas de pattern matching ni d'async/await ni de stackalloc, ni plein d'autres choses, mais c'est pour très une bonne raison : on n'en a pas besoin.

Le point d'entrée utilise des top level statements (C# 9) (et une lambda) :

using CmdTempo;

using CommandLine;
using CommandLine.Text;

using LibTempo;

// Set sentence builder to localizable
SentenceBuilder.Factory = () => new LocalizableSentenceBuilder();
Parser.Default.ParseArguments<Options>(args).WithParsed((options) => Runner.RunOptions(options));

Comme pour async dans un point d'entrée de programme console, ce n'est que du sucre syntaxique.
Le compilateur s'occupe de rajouter tout la sauce (namespace, classe statique, static void Main (string[] args)).

Mais ça fait du bien à mes petits doigts potelés de ne pas avoir à l'écrire, et à mes yeux de ne pas avoir à les lire. Et ça, c'est bon !

On utilise parfois des ranges sur des chaînes de caractère (C# 8) :

return
  String.Format(Resource.SentenceMutuallyExclusiveSetErrors, names[0..^2], incompat[0..^2]);

C'est beaucoup plus rapide à écrire qu'avec SubString, et ça me rappelle mes années Ruby.

Les options sont une classe essayant de forcer l'immutabilité (un record) (C# 9) :

namespace LibTempo
{
    using CommandLine;

    public record Options
    {
        public const uint DefaultSampleSize = 5;
        public const uint DefaultResetTime = 5;
        public const uint DefaultPrecision = 0;
        public const uint MaxPrecision = 5;

        [Option('g', "game", Required = false, Default = false, HelpText = nameof(IsGamingMode), ResourceType = typeof(Resource))]
        public bool IsGamingMode { get; }

        [Option('s', "sample-size", Required = true, Default = DefaultSampleSize, HelpText = nameof(SampleSize), ResourceType = typeof(Resource))]
        public uint SampleSize { get; }

        [Option('r', "reset-time", Required = true, Default = DefaultResetTime, HelpText = nameof(ResetTime), ResourceType = typeof(Resource))]
        public uint ResetTime { get; }

        [Option('p', "precision", Required = true, Default = DefaultPrecision, HelpText = nameof(Precision), ResourceType = typeof(Resource))]
        public uint Precision { get; }

        public Options(bool isGamingMode, uint sampleSize, uint resetTime, uint precision)
        {
            IsGamingMode = isGamingMode;
            SampleSize = sampleSize == 0 ? DefaultSampleSize : sampleSize;
            ResetTime = resetTime == 0 ? DefaultResetTime : resetTime;
            Precision = precision > MaxPrecision ? MaxPrecision : precision == 0 ? DefaultPrecision : precision;
        }
    }
}

On utilise des extensions pour faire croire que nous aussi on a Back, Front, et IsEmpty dans la classe générique Queue (C# 2 pour les Generics et 3 pour LINQ, ça nous rajeunit pas) :

using System.Collections.Generic;
using System.Linq;

namespace LibTempo
{
    internal static class QueueExtensions
    {
        public static bool IsEmpty<T>(this Queue<T> queue) => queue.Count == 0;

        public static T Back<T>(this Queue<T> queue) => queue.Last();

        public static T Front<T>(this Queue<T> queue) => queue.First();
    }
}

On utilise partout des expression body pour des méthodes car les {} et return c'est has-been (C# 7) :

private double ComputeNewSecretBPM() => _betterRng.Next(50, 200);

On écrit le BPM avec la précision demandée en utilisant beaucoup moins le clavier et avec de l'interpolation (C# 7) :

protected string BPMToStringWithPrecision(double bpm) => bpm.ToString($"G{_precision}", CultureInfo.CurrentCulture);

Et le pattern Fluent pour des tests plus rapidement écrits (mais ça c'est un package Nuget) :

        [Fact]
        public void InvalidArsShouldReturnDefaultOptions()
        {
            var result = Parser.Default.ParseArguments<Options>(args: new string[] { "0", "0" });
            result.Tag.Should().Be(ParserResultType.NotParsed);
        }

On a activé les Nullable Reference Types partout, parce que c'est la guerre contre les NullReferenceExceptions depuis C# 8 (comme beaucoup de choses, C# a piqué ça à F# où NULL n'existe pas) :

    <Nullable>enable</Nullable>
    <TreatWarningsAsErrors>True</TreatWarningsAsErrors>

Ainsi, un accès à null (qui provoque un crash) est détecté à la compilation et traité comme une erreur.
Mais je n'ai pas eu l’occasion d'utiliser les annotations, donc pas de code à montrer. Le compilateur n'a rien dit.

Ce que j'ai loupé

J'aurais pu convertir le projet en VB.NET en un tour de main grâce au compilateur Roslyn, afficher la console dans le navigateur avec Blazor.Console… Peut-être plus tard ?

J'aurais pu aussi utiliser NGetText pour éviter les fichiers RESX et leur intrusion avec leurs clés en Pascal Case dans le code, ce qui le rend moins lisible. Et ainsi garder le fichier .PO d'origine.

Console.WriteLine(Resource.HitEnterForEachTempoOrQToQuit);

Enfin, j'ai loupé mon week-end, et j'aurais pu me coucher plus tôt.

Sacré Pierre Tramo !

Note à moi-même

Ce n'est pas parce que c'est plus facile de compiler et déboguer TapTempo quand on est sous Windows avec WSL qu'il fallait forcément t'y intéresser !

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.