• Li chevron_right

      Port complet de TapTempo en C# 9

      xcomcmdr · pubsub.eckmul.net / linuxfr · Sunday, 10 January, 2021 - 23:02 · 8 minutes

    <h2 class="sommaire">Sommaire</h2> <ul class="toc"> <li><a href="#toc-les-sources">Les sources</a></li> <li><a href="#toc-pourquoi">Pourquoi ?</a></li> <li><a href="#toc-ce-qui-a-%C3%A9t%C3%A9-port%C3%A9">Ce qui a été porté</a></li> <li><a href="#toc-ce-qui-est-propre-%C3%A0-ce-port">Ce qui est propre à ce port</a></li> <li><a href="#toc-ce-que-jai-loup%C3%A9">Ce que j'ai loupé</a></li> <li><a href="#toc-note-%C3%A0-moi-m%C3%AAme">Note à moi-même</a></li> </ul> <p>Bonjour 'nal,</p> <p>Y'a plein de poussière ici dit moi. Faut dire que ça fait longtemps que je ne t'ai plus écrit.</p> <p>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.</p> <p>Je veux bien sûr parler de C#.</p> <h2 id="toc-les-sources">Les sources</h2> <p><a href="https://github.com/maximilien-noal/SharpTempo">C'est par là</a></p> <h2 id="toc-pourquoi">Pourquoi ?</h2> <p>Parce que j'en avais envie. Pour le fun. Parce que ça manquait (ou pas), surtout face au port Java.</p> <h2 id="toc-ce-qui-a-été-porté">Ce qui a été porté</h2> <p>Tout. Les traductions, les tests unitaires, le mode jeu, l'aspect orienté-objet, …</p> <h2 id="toc-ce-qui-est-propre-à-ce-port">Ce qui est propre à ce port</h2> <p>J'ai voulu utiliser au plus les possibilités modernes de C# 9 et précédents.</p> <p>Bon y'a pas de <a href="https://docs.microsoft.com/en-us/archive/msdn-magazine/2019/may/csharp-8-0-pattern-matching-in-csharp-8-0">pattern matching</a> ni d'async/await ni de <a href="https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/stackalloc">stackalloc</a>, ni plein d'autres choses, mais c'est pour très une bonne raison : on en a pas besoin.</p> <p>Le point d'entré utilise des <em>top level statements</em> (C# 9) (et une lambda) :</p> <pre><code>using CmdTempo; using CommandLine; using CommandLine.Text; using LibTempo; // Set sentence builder to localizable SentenceBuilder.Factory = () =&gt; new LocalizableSentenceBuilder(); Parser.Default.ParseArguments&lt;Options&gt;(args).WithParsed((options) =&gt; Runner.RunOptions(options)); </code></pre> <p>Comme pour <a href="https://blog.stephencleary.com/2012/02/async-console-programs.html">async dans un point d'entrée de programme console</a>, ce n'est que du sucre syntaxique.<br> Le compilateur s'occupe de rajouter tout la sauce (namespace, classe statique, static void Main (string[] args)).</p> <p>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 !</p> <p>On utilise parfois des <a href="https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-8.0/ranges">ranges</a> sur des chaînes de caractère (C# 8) :</p> <pre><code>return String.Format(Resource.SentenceMutuallyExclusiveSetErrors, names[0..^2], incompat[0..^2]); </code></pre> <p>C'est beaucoup plus rapide à écrire qu'avec <a href="https://docs.microsoft.com/en-us/dotnet/api/system.string.substring?view=net-5.0">SubString</a>, et ça me rappelle <a href="https://ruby-doc.org/core-2.5.1/Range.html">mes années Ruby</a>.</p> <p>Les options sont une classe essayant de forcer l'immutabilité (<a href="https://devblogs.microsoft.com/dotnet/c-9-0-on-the-record/">un record</a>) (C# 9) :</p> <pre><code>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 &gt; MaxPrecision ? MaxPrecision : precision == 0 ? DefaultPrecision : precision; } } } </code></pre> <p>On utilise des extensions pour faire croire que nous aussi on a Back, Front, et IsEmpty dans la classe générique <a href="https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.queue-1?view=net-5.0">Queue</a> (C# 2 pour les <a href="https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/">Generics</a> et 3 pour <a href="https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/linq/introduction-to-linq-queries">LINQ</a>, ça nous rajeunit pas) :</p> <pre><code>using System.Collections.Generic; using System.Linq; namespace LibTempo { internal static class QueueExtensions { public static bool IsEmpty&lt;T&gt;(this Queue&lt;T&gt; queue) =&gt; queue.Count == 0; public static T Back&lt;T&gt;(this Queue&lt;T&gt; queue) =&gt; queue.Last(); public static T Front&lt;T&gt;(this Queue&lt;T&gt; queue) =&gt; queue.First(); } } </code></pre> <p>On utilise partout des <a href="https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/expression-bodied-members"><em>expression body</em></a> pour des méthodes car les {} et return c'est <em>has-been</em> (C# 7) :</p> <pre><code>private double ComputeNewSecretBPM() =&gt; _betterRng.Next(50, 200); </code></pre> <p>On écrit le BPM avec la précision demandée <a href="https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-numeric-format-strings">en utilisant beaucoup moins le clavier</a> et avec de l'<a href="https://docs.microsoft.com/en-us/dotnet/csharp/tutorials/string-interpolation">interpolation</a> (C# 7) :</p> <pre><code>protected string BPMToStringWithPrecision(double bpm) =&gt; bpm.ToString($"G{_precision}", CultureInfo.CurrentCulture); </code></pre> <p>Et le pattern Fluent pour des tests plus rapidement écrits (mais ça c'est un package Nuget) :</p> <pre><code> [Fact] public void InvalidArsShouldReturnDefaultOptions() { var result = Parser.Default.ParseArguments&lt;Options&gt;(args: new string[] { "0", "0" }); result.Tag.Should().Be(ParserResultType.NotParsed); } </code></pre> <p>On a activé les <a href="https://docs.microsoft.com/en-us/dotnet/csharp/nullable-references">Nullable Reference Types</a> partout, parce que c'est la gueere contre les NullReferenceExceptions depuis C# 8 (comme beaucoup de choses, C# a piqué ça à F# où NULL n'existe pas) :</p> <pre><code> &lt;Nullable&gt;enable&lt;/Nullable&gt; &lt;TreatWarningsAsErrors&gt;True&lt;/TreatWarningsAsErrors&gt; </code></pre> <p>Ainsi, un accès à null (qui provoque un crash) est détecté à la compilation et traité comme une erreur.<br> Mais je n'ai pas eu l’occasion d'utiliser les annotations, donc pas de code à montrer. Le compilateur n'a rien dit.</p> <h2 id="toc-ce-que-jai-loupé">Ce que j'ai loupé</h2> <p>J'aurais pu convertir le projet en <a href="https://marketplace.visualstudio.com/items?itemName=SharpDevelopTeam.CodeConverter">VB.NET en un tour de main grâce au compilateur Roslyn</a>, afficher la console dans le navigateur avec <a href="https://github.com/ardacetinkaya/Blazor.Console">Blazor.Console</a>… Peut-être plus tard ?</p> <p>J'aurais pu aussi utiliser <a href="https://github.com/VitaliiTsilnyk/NGettext">NGetText</a> pour éviter les fichiers RESX et leur intrusion avec leurs clés en <a href="https://wiki.c2.com/?PascalCase">Pascal Case</a> dans le code, ce qui le rend moins lisible. Et ainsi garder le fichier .PO d'origine.</p> <pre><code>Console.WriteLine(Resource.HitEnterForEachTempoOrQToQuit); </code></pre> <p>Enfin, j'ai loupé mon week-end, et j'aurais pu me coucher plus tôt.</p> <p>Sacré Pierre Tramo !</p> <h2 id="toc-note-à-moi-même">Note à moi-même</h2> <p>Ce n'est pas parce que c'est plus facile de compiler et déboguer <a href="https://taptempo.tuxfamily.org/">TapTempo</a> quand on est sous Windows avec <a href="https://doc.ubuntu-fr.org/wsl">WSL</a> qu'il fallait forcément t'y intéresser !</p> <div><a href="https://linuxfr.org/users/xcomcmdr/journaux/port-complet-de-taptempo-en-c-9.epub">Télécharger ce contenu au format EPUB</a></div> <p> <strong>Commentaires :</strong> <a href="//linuxfr.org/nodes/122890/comments.atom">voir le flux Atom</a> <a href="https://linuxfr.org/users/xcomcmdr/journaux/port-complet-de-taptempo-en-c-9#comments">ouvrir dans le navigateur</a> </p>