URL: https://linuxfr.org/users/xcomcmdr/journaux/port-complet-de-taptempo-en-c-9 Title: Port complet de TapTempo en C# 9 Authors: xcomcmdr Date: 2021年01月11日T00:02:22+01:00 License: CC By-SA Tags: dotnet, terminal, linux, windows, mac, arm et taptempo Score: 14 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à](https://github.com/maximilien-noal/SharpTempo) 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](https://docs.microsoft.com/en-us/archive/msdn-magazine/2019/may/csharp-8-0-pattern-matching-in-csharp-8-0) ni d'async/await ni de [stackalloc](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/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](https://docs.microsoft.com/en-us/dotnet/csharp/tutorials/exploration/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(args).WithParsed((options) => Runner.RunOptions(options)); ``` Comme pour [async dans un point d'entrée de programme console](https://blog.stephencleary.com/2012/02/async-console-programs.html), 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](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-8.0/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](https://docs.microsoft.com/en-us/dotnet/api/system.string.substring?view=net-5.0), et ça me rappelle [mes années Ruby](https://ruby-doc.org/core-2.5.1/Range.html). Les options sont une classe essayant de forcer l'immutabilité ([un record](https://devblogs.microsoft.com/dotnet/c-9-0-on-the-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](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/extension-methods) pour faire croire que nous aussi on a Back, Front, et IsEmpty dans la classe générique [Queue](https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.queue-1?view=net-5.0) (C# 2 pour les [Generics](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/) et 3 pour [LINQ](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/linq/introduction-to-linq-queries), ça nous rajeunit pas) : ``` using System.Collections.Generic; using System.Linq; namespace LibTempo { internal static class QueueExtensions { public static bool IsEmpty(this Queue queue) => queue.Count == 0; public static T Back(this Queue queue) => queue.Last(); public static T Front(this Queue queue) => queue.First(); } } ``` On utilise partout des [_expression body_](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/expression-bodied-members) 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](https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-numeric-format-strings) et avec de l'[interpolation](https://docs.microsoft.com/en-us/dotnet/csharp/tutorials/string-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(args: new string[] { "0", "0" }); result.Tag.Should().Be(ParserResultType.NotParsed); } ``` On a activé les [Nullable Reference Types](https://docs.microsoft.com/en-us/dotnet/csharp/nullable-references) 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) : ``` enable
True ``` 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](https://marketplace.visualstudio.com/items?itemName=SharpDevelopTeam.CodeConverter), afficher la console dans le navigateur avec [Blazor.Console](https://github.com/ardacetinkaya/Blazor.Console)... Peut-être plus tard ? J'aurais pu aussi utiliser [NGetText](https://github.com/VitaliiTsilnyk/NGettext) pour éviter les fichiers RESX et leur intrusion avec leurs clés en [Pascal Case](https://wiki.c2.com/?PascalCase) 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](https://taptempo.tuxfamily.org/) quand on est sous Windows avec [WSL](https://doc.ubuntu-fr.org/wsl) qu'il fallait forcément t'y intéresser !

AltStyle によって変換されたページ (->オリジナル) /