AP2

Pierre Ferrari

Analyse et programmation 2

Référentiel

Référentiel AP2

Table des matières

[TOC]

Avant-propos

Ce cours est une mise à jour du cours d’Analyse et Programmation 2 initialement écrit par M. Maire puis repris par M. Bel. Il reprend les mêmes concepts mais les met à jour avec les dernières technologies et les dernières spécificités du langage C#.

Compilation en ligne de commande

Souvent, lorsqu’on utilise des outils de développement tels que Visual Studio, on ne se rend pas compte de tout ce qui se passe en arrière-plan. On clique sur un bouton et hop, le programme se lance. Mais qu’est-ce qui se passe réellement ? Comment est-ce que Visual Studio sait comment compiler notre programme ? Comment sait-il où trouver les fichiers nécessaires à la compilation ? Comment sait-il où trouver les fichiers nécessaires à l’exécution de notre programme ?

Mais aussi, qu’est-ce qu’il a créé comme fichiers exécutable ? Est-ce que l’exécutable est autonome ou a-t-il besoin d’autres fichiers pour fonctionner ? Est-ce que l’exécutable est portable ? Peut-on le distribuer à quelqu’un d’autre et être sûr qu’il fonctionnera ?

Pour répondre à ces questions, nous allons voir comment compiler un programme en ligne de commande. Nous allons voir comment compiler un programme C pour commencer car il permet facilement de mettre en évidence le concept de librairie partagée. Puis nous compilerons un programme C# en ligne de commande. Nous allons voir comment compiler un programme C# pour qu’il soit autonome et portable et distribuable.

C

Commençons par du C. Le langage C est réputé pour être bas niveau. C’est-à-dire que c’est au développeur de savoir exactement ce qu’il fait. Le langage C est compilé à l’aide d’un compilateur tel que gcc.

En partant du traditionnel Hello, World!, on va voir comment compiler un programme C en ligne de commande sur Linux.

hello.c

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char** argv)
{
    puts("Hello World!\n");
    exit(0);
}

Compilation

# build-essential est un meta paquet contenant entre autre gcc
$ sudo apt install build-essential
# compilation
$ gcc hello.c -o hello
# exécution
$ ./hello
Hello World!
# voir sa taille
$ ls -lh hello

Si on jette un œil à la taille du programme, on trouve ~16kB. C’est déjà beaucoup pour un simple Hello, World!. Si on regarde le code présent dans le fichier ELF, on peut voir que le programme est lié à la librairie libc et qu’il utilise la fonction puts pour afficher le message et exit pour quitter le programme. Ces deux fonctions, même si elles sont écrites dans notre code, ne se trouvent pas dans notre fichier ELF mais dans une librairie partagée par tous les programmes, la libc.so.6.

$ readelf -d hello

Dynamic section at offset 0x2dc0 contains 27 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]

$ readelf -s hello

Symbol table '.dynsym' contains 8 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.2.5 (2)
     ...
     5: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND exit@GLIBC_2.2.5 (2)

$ ldd ./hello
    linux-vdso.so.1 (0x00007fff555a8000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007efc0018e000)
    /lib64/ld-linux-x86-64.so.2 (0x00007efc0038c000)

$ ls -lh /lib/x86_64-linux-gnu/libc.so.6
-rwxr-xr-x 1 root root 1.9M 23 jui 19:09 /lib/x86_64-linux-gnu/libc.so.6

La libc.so.6 contient à son tour ~1.9M de code. En résumé, pour un simple Hello, World!, on a besoin de 16K de code et 1.9M de librairie partagée.

Si on compile le programme en plaçant dans le fichier ELF tout le code nécessaire à son fonctionnement, cela donne

$ gcc -static -O3 hello.c -o hello

$ ls -lh
total 462K
-rwxrwxr-x 1 ubuntu ubuntu  17K aoû 23 14:33 hello          # 17K shared library
-rw-rw-r-- 1 ubuntu ubuntu  206 aoû 23 14:20 hello.c        # 206 source code
-rwxrwxr-x 1 ubuntu ubuntu 724K aoû 21 18:19 hello_static   # 724K static library

$ ldd ./hello_static
    not a dynamic executable

On voit que le fichier hello_static est plus gros que le fichier hello mais il contient tout le code nécessaire à son fonctionnement. Il est autonome et portable. On peut le distribuer à quelqu’un d’autre et être sûr qu’il fonctionnera.

💬 Comment le prouver, le tester ?

Pour le prouver, il faut disposer d’une installation qui ne comporte pas la libc.so.6. On peut utiliser une machine virtuelle ou un conteneur pour cela mais il faut s’assurer d’installer une distribution Linux qui ne contient pas la libc.so.6. Une solution consiste à installer une version busybox statiquement compilée.

$ incus launch images:busybox/1.36.1 busy
$ incus list                        
+---------------+---------+------+-----------------------------------------------+-----------+-----------+
|     NAME      |  STATE  | IPV4 |                     IPV6                      |   TYPE    | SNAPSHOTS |
+---------------+---------+------+-----------------------------------------------+-----------+-----------+
| busy          | RUNNING |      | fd42:e9e4:82af:ce86:216:3eff:fea3:4784 (eth0) | CONTAINER | 0         |
+---------------+---------+------+-----------------------------------------------+-----------+-----------+
$ incus exec busy sh
~ # pwd
/root

/ # ls -l /
total 10
drwxr-xr-x    2 0        0               95 Aug 21 06:01 bin
drwxr-xr-x    7 0        0              440 Aug 21 07:55 dev
drwxr-xr-x    2 0        0                4 Aug 21 06:01 etc
lrwxrwxrwx    1 0        0               12 Aug 21 06:01 linuxrc -> /bin/busybox
drwxr-xr-x    2 0        0                2 Aug 21 06:01 mnt
dr-xr-xr-x  586 65534    65534            0 Aug 21 07:55 proc
drwxr-xr-x    2 0        0                3 Aug 21 07:57 root
drwxr-xr-x    2 0        0                2 Aug 21 06:01 run
drwxr-xr-x    2 0        0               74 Aug 21 06:01 sbin
dr-xr-xr-x   13 65534    65534            0 Aug 21 07:55 sys
drwxr-xr-x    2 0        0                2 Aug 21 06:01 tmp
drwxr-xr-x    4 0        0                4 Aug 21 06:01 usr

~ # find / -name "libc*" 2> /dev/null
/sys/kernel/btf/libcrc32c
/sys/module/libcrc32c

~ # ls -l
total 441
-rwxrwxr-x    1 1000     1000         16000 Aug 21 07:58 hello
-rwxrwxr-x    1 1000     1000        740568 Aug 21 07:59 hello_static

~ # ./hello 
sh: ./hello: not found

~ # ./hello_static
Hello World!

C# dotnet

La plupart des développeurs utilisent des IDE Integrated Development Environment pour développer en C#. Lors de l’installation d’un IDE, tel que Visual Studio, un ensemble d’outils de développement est installé pour nous. Ces outils, qui font partie de l’IDE nous permettent d’éditer facilement nos fichiers C#, de compiler notre projet, de faire du débogage et de démarrer notre exécutable grâce au compilateur, au linker, au .NET SDK et au .Net Runtime. Les .Net SDK / Runtime sont installés automatiquement lors de l’installation de Visual Studio. Dès lors, on peut facilement oublier que ces outils sont nécessaires pour faire fonctionner notre programme.

Afin de bien comprendre ce qui se passe dans Visual Studio lorsqu’on édite notre code et qu’on clique ensuite sur « Démarrer », essayons de le faire sans Visual Studio !

Nous allons créer une application Console en ligne de commande à l’aide de la commande dotnet et ainsi, mieux comprendre ce qui se passe automatiquement dans Visual Studio.

dotnet est l’outil cross-platform permettant de générer des exécutables mais il permet aussi bien d’autres choses

Pré-requis

Je vous propose de réaliser cette application dans un système Linux. Vous pourrez ainsi constater que le langage C# est multiplateforme. Pour faire cela, plusieurs choix s’offre à vous:

Pour la version WSL2, vous trouverez les indications fournies par Microsoft sur la page Install Linux on Windows with WSL ainsi qu’une distribution Ubuntu 22.04.3 LTS dans le store Windows.

ubuntu_store

Remarque: si vous avez une ancienne version de Windows ou une version managée par le service informatique, vous devrez procéder à l’installation manuel

Installation dotnet

Mes exemples sont réalisés avec une distribution Debian testing

On commence par installer le SDK. On peut le trouver à cette adresse Installation manuel.

J’ai dû temporairement désactiver le Firewall Windows pour permettre la résolution DNS.
Il faudra adapter les adresses ci-dessous et pas simplement copier/coller. En effet, le SDK évolue rapidement et au moment où vous lisez ces lignes, la version aura certainement changé
## Téléchargement du SDK
tux@makina:~$ curl https://download.visualstudio.microsoft.com/download/pr/db901b0a-3144-4d07-b8ab-6e7a43e7a791/4d9d1b39b879ad969c6c0ceb6d052381/dotnet-sdk-8.0.401-linux-x64.tar.gz --output dotnet-sdk-8.0.401-linux-x64.tar.gz
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
 42  174M   42 74.9M    0     0  5225k      0  0:00:34  0:00:14  0:00:20 5303k

## Vérification de sa présence
tux@makina:~$ ls
dotnet-sdk-8.0.401-linux-x64.tar.gz

## Création d'une variable d'environnement (elles sont toutes temporaires à cette session)
tux@makina:~$ DOTNET_FILE=dotnet-sdk-8.0.401-linux-x64.tar.gz

## Vérification de la variable
tux@makina:~$ echo $DOTNET_FILE
dotnet-sdk-8.0.401-linux-x64.tar.gz

## Export de la variable
tux@makina:~$ export DOTNET_ROOT="$HOME/.dotnet"

## Création du dossier caché .dotnet dans HOME et décompression de l'archive à l'intérieur
tux@makina:~$ mkdir -p "$DOTNET_ROOT" && tar zxf "$DOTNET_FILE" -C "$DOTNET_ROOT"

## Mise à jour de la variable PATH
tux@makina:~$ export PATH=$PATH:$DOTNET_ROOT

## Essai
tux@makina:~$ dotnet --version
8.0.401

On a maintenant un SDK fonctionnel. On peut voir toutes les options de cette commande en utilisant le paramètre d’aide -h

Help
tux@makina:~$ dotnet -h
Utilisation: dotnet [runtime-options] [path-to-application] [arguments]

Exécutez une application .NET.

runtime-options:
  --additionalprobingpath <path>   Chemin contenant la stratégie de collecte et les assemblys à collecter.
  --additional-deps <path>         Chemin du fichier deps.json supplémentaire.
  --depsfile                       Chemin du fichier <application>.deps.json.
  --fx-version <version>           Version du framework partagé installé à utiliser pour exécuter l'application.
  --roll-forward <setting>         Restaurer par progression la version du framework (LatestPatch, Minor, LatestMinor, Major, LatestMajor, Disable).
  --runtimeconfig                  Chemin du fichier <application>.runtimeconfig.json.

path-to-application:
  Chemin d'un fichier .dll d'application à exécuter.

Utilisation: dotnet [sdk-options] [command] [command-options] [arguments]

Exécutez une commande du kit SDK .NET.

sdk-options:
  -d|--diagnostics  Activez la sortie des diagnostics.
  -h|--help         Affichez l'aide de la ligne de commande.
  --info            Affichez les informations sur .NET.
  --list-runtimes   Affichez les runtimes installés.
  --list-sdks       Affichez les SDK installés.
  --version         Affichez la version utilisée du kit SDK .NET.

Commandes du SDK:
  add               Ajoutez un package ou une référence à un projet .NET.
  build             Générez un projet .NET.
  build-server      Interagissez avec les serveurs démarrés par une build.
  clean             Nettoyez les sorties de build d'un projet .NET.
  format            Appliquez les préférences de style à un projet ou une solution.
  help              Affichez l'aide de la ligne de commande.
  list              Listez les références de projet d'un projet .NET.
  msbuild           Exécutez des commandes MSBuild (Microsoft Build Engine).
  new               Créez un fichier ou projet .NET.
  nuget             Fournit des commandes NuGet supplémentaires.
  pack              Créez un package NuGet.
  publish           Publiez un projet .NET à des fins de déploiement.
  remove            Supprimez un package ou une référence d'un projet .NET.
  restore           Restaurez les dépendances spécifiées dans un projet .NET.
  run               Générez et exécutez une sortie de projet .NET.
  sdk               Gérer l'installation du kit SDK .NET.
  sln               Modifiez les fichiers solution Visual Studio.
  store             Stockez les assemblys spécifiés dans le magasin de packages de runtime.
  test              Exécutez des tests unitaires à l'aide du programme Test Runner spécifié dans un projet .NET.
  tool              Installez ou gérez les outils qui étendent l'expérience .NET.
  vstest            Exécutez des commandes VSTest (Microsoft Test Engine).
  workload          Gérez les charges de travail facultatives.

Commandes supplémentaires d'outils groupés :
  dev-certs         Créez et gérez des certificats de développement.
  fsi               Démarrer F# Interactive / exécuter les scripts F#.
  user-jwts         Gérer les jetons Web JSON en développement.
  user-secrets      Gérez les secrets d'utilisateur de développement.
  watch             Démarrez un observateur de fichier qui exécute une commande quand les fichiers changent.

Pour plus d'informations sur une commande, exécutez 'dotnet [commande] --help'.

C’est parti pour la création de l’application Console. Pour ce faire, on suit partiellement le tutoriel présent à cette adresse Get started with .NET.

Quelques précisions:

La liste des applications que l’on peut créer s’obtient ainsi

tux@makina:~$ dotnet new list
These templates matched your input:

Template Name                                 Short Name      Language    Tags
--------------------------------------------  --------------  ----------  --------------------------
...
Console App                                   console         [C#],F#,VB  Common/Console

Si vous travaillé sur Windows et que dotnet est installé, ce qui est normalement le cas avec Visual Studio, vous pouvez réaliser les mêmes commandes dans un terminal PowerShell

PS C:\Moi> dotnet --list-sdks
8.0.401 [C:\Program Files\dotnet\sdk]

Publication

Ce que nous avons fait jusqu’ici c’est

On est loin de la publication de notre application sur Internet pour la distribuer au plus grand nombre… Pour y parvenir nous devons

Exercice

Le but de cet exercice est très simple. On vous demande de créer un programme console simple tel qu’un Hello, World! dans une machine Linux. Une fois le programme crée, on vous demande de faire le nécessaire pour pouvoir utiliser ce programme sur une autre machine Linux. Un peu comme si une autre personne dans le monde possédant une machine Linux téléchargeait votre programme et l’exécutait sur sa machine. Vous ne savez rien de sa machine et surtout, vous ne savez pas si cette machine possède le runtime C# mais vous partez du principe qu’il aura téléchargé la bonne version du programme, c’est-à-dire, la version prévue pour sa machine.

Lorsque vous avez fait le travail, échangé votre programme avec un autre élève et tester le programme de l’autre élève sur une machine Linux vierge fraichement installée sans ajout d’un quelconque programme.

Pour aller un peu plus loin, compilez votre programme pour Windows et testez-le sur une machine Windows là aussi, fraichement installée.

Commençons par le commencement, et créons un nouveau projet à l’aide de la commande suivante:

tux@makina:~$ dotnet new console -n Hello

Quels sont les fichiers et dossiers qui ont été générés par cette commande ?

$ tree
.
├── Hello
│   ├── Hello.csproj
│   ├── Program.cs
│   └── obj
│       ├── Hello.csproj.nuget.dgspec.json
│       ├── Hello.csproj.nuget.g.props
│       ├── Hello.csproj.nuget.g.targets
│       ├── project.assets.json
│       └── project.nuget.cache

On lance le build. Quels sont les fichiers qui ont étés générés par la commande build ?

$ ls bin/Debug/net6.0/
Hello.deps.json  Hello.dll  Hello.exe  Hello.pdb  Hello.runtimeconfig.json

Comment générer des fichiers en mode Release et non en mode Debug ?

$ dotnet build -c Release

En mode Release j’obtiens ces fichiers :

En toute logique, si je lance le programme Hello sans passer par la commande dotnet je dois obtenir un magnifique Hello, World!. Alors essayons

tux@makina:~/Hello$ bin/Release/net6.0/Hello
Hello, World!

Vous êtes un peu perdu, répondez aux questions ci-dessous.

💬 Quel sorte de fichier est-ce que ce fichier Hello généré par la commande de build ? Un fichier texte de type script bash ? Un fichier exécutable ? Un ELF à oreilles pointues ou un PE ?

$ file bin/Debug/net8.0/Hello bin/Debug/net8.0/Hello: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=44a1a474112841305c564127417d446b4eccde12, stripped

💬 Maintenant que je sais ce que c’est, est-ce que je peux simplement distribuer ce fichier via Internet pour que tout le monde puisse l’exécuter et obtenir ce fameux Hello, World! ? Vous ne pouvez pas simplement répondre oui/non mais vous devez prouvez votre réponse !

Dans l’image ci-dessous on peut voir deux machines Linux WSL.

Celle du haut est une machine Ubuntu 22.04 alors que celle du bas est une machine Ubuntu 20.04.

En lisant la suite de commandes on peut voir que dans la machine du haut on voit que le framework dotnet est installé mais pas dans celle du bas.

Dans les deux machines, on a placer les même fichiers ELF/DLL. Ces fichiers ont étés générés par la commande build en mode Release

Dans la machine qui ne contient pas le framework, sans surprise, ça ne fonctionne pas

preuve

💬 Comment faire pour pouvoir fournir ce programme Hello a une personne qui possède une machine Linux x86_64 et lui permettre de l’utiliser sans installer le framework .NET ?

On doit publier le programme en précisant qu’on souhaite obtenir un dossier contenant tout le nécessaire pour faire fonctionner le Hello. On peut encore préciser le format du programme en sortie.

tux@makina:~/Hello$ dotnet publish -c Release -r linux-x64 --self-contained true

.NET RID Catalog

Ce qui fournit un dossier contenant toutes les librairies nécessaires au fonctionnement de ce simple Hello 😲

bin/Release/net8.0/linux-x64/publish/
├── createdump
├── Hello
├── Hello.deps.json
├── Hello.dll
├── Hello.pdb
├── Hello.runtimeconfig.json
├── libclrjit.so
├── libcoreclr.so
├── libcoreclrtraceptprovider.so
├── libdbgshim.so
├── libhostfxr.so
├── libhostpolicy.so
├── libmscordaccore.so
├── libmscordbi.so
├── libSystem.Globalization.Native.so
├── libSystem.IO.Compression.Native.so
├── libSystem.Native.so
├── libSystem.Net.Security.Native.so
├── libSystem.Security.Cryptography.Native.OpenSsl.so
├── Microsoft.CSharp.dll
├── Microsoft.VisualBasic.Core.dll
├── Microsoft.VisualBasic.dll
├── Microsoft.Win32.Primitives.dll
├── Microsoft.Win32.Registry.dll
├── mscorlib.dll
├── netstandard.dll
├── System.AppContext.dll
├── System.Buffers.dll
├── System.Collections.Concurrent.dll
├── System.Collections.dll
├── System.Collections.Immutable.dll
├── System.Collections.NonGeneric.dll
├── System.Collections.Specialized.dll
├── System.ComponentModel.Annotations.dll
├── System.ComponentModel.DataAnnotations.dll
├── System.ComponentModel.dll
├── System.ComponentModel.EventBasedAsync.dll
├── System.ComponentModel.Primitives.dll
├── System.ComponentModel.TypeConverter.dll
├── System.Configuration.dll
├── System.Console.dll
├── System.Core.dll
├── System.Data.Common.dll
├── System.Data.DataSetExtensions.dll
├── System.Data.dll
├── System.Diagnostics.Contracts.dll
├── System.Diagnostics.Debug.dll
├── System.Diagnostics.DiagnosticSource.dll
├── System.Diagnostics.FileVersionInfo.dll
├── System.Diagnostics.Process.dll
├── System.Diagnostics.StackTrace.dll
├── System.Diagnostics.TextWriterTraceListener.dll
├── System.Diagnostics.Tools.dll
├── System.Diagnostics.TraceSource.dll
├── System.Diagnostics.Tracing.dll
├── System.dll
├── System.Drawing.dll
├── System.Drawing.Primitives.dll
├── System.Dynamic.Runtime.dll
├── System.Formats.Asn1.dll
├── System.Globalization.Calendars.dll
├── System.Globalization.dll
├── System.Globalization.Extensions.dll
├── System.IO.Compression.Brotli.dll
├── System.IO.Compression.dll
├── System.IO.Compression.FileSystem.dll
├── System.IO.Compression.ZipFile.dll
├── System.IO.dll
├── System.IO.FileSystem.AccessControl.dll
├── System.IO.FileSystem.dll
├── System.IO.FileSystem.DriveInfo.dll
├── System.IO.FileSystem.Primitives.dll
├── System.IO.FileSystem.Watcher.dll
├── System.IO.IsolatedStorage.dll
├── System.IO.MemoryMappedFiles.dll
├── System.IO.Pipes.AccessControl.dll
├── System.IO.Pipes.dll
├── System.IO.UnmanagedMemoryStream.dll
├── System.Linq.dll
├── System.Linq.Expressions.dll
├── System.Linq.Parallel.dll
├── System.Linq.Queryable.dll
├── System.Memory.dll
├── System.Net.dll
├── System.Net.Http.dll
├── System.Net.Http.Json.dll
├── System.Net.HttpListener.dll
├── System.Net.Mail.dll
├── System.Net.NameResolution.dll
├── System.Net.NetworkInformation.dll
├── System.Net.Ping.dll
├── System.Net.Primitives.dll
├── System.Net.Quic.dll
├── System.Net.Requests.dll
├── System.Net.Security.dll
├── System.Net.ServicePoint.dll
├── System.Net.Sockets.dll
├── System.Net.WebClient.dll
├── System.Net.WebHeaderCollection.dll
├── System.Net.WebProxy.dll
├── System.Net.WebSockets.Client.dll
├── System.Net.WebSockets.dll
├── System.Numerics.dll
├── System.Numerics.Vectors.dll
├── System.ObjectModel.dll
├── System.Private.CoreLib.dll
├── System.Private.DataContractSerialization.dll
├── System.Private.Uri.dll
├── System.Private.Xml.dll
├── System.Private.Xml.Linq.dll
├── System.Reflection.DispatchProxy.dll
├── System.Reflection.dll
├── System.Reflection.Emit.dll
├── System.Reflection.Emit.ILGeneration.dll
├── System.Reflection.Emit.Lightweight.dll
├── System.Reflection.Extensions.dll
├── System.Reflection.Metadata.dll
├── System.Reflection.Primitives.dll
├── System.Reflection.TypeExtensions.dll
├── System.Resources.Reader.dll
├── System.Resources.ResourceManager.dll
├── System.Resources.Writer.dll
├── System.Runtime.CompilerServices.Unsafe.dll
├── System.Runtime.CompilerServices.VisualC.dll
├── System.Runtime.dll
├── System.Runtime.Extensions.dll
├── System.Runtime.Handles.dll
├── System.Runtime.InteropServices.dll
├── System.Runtime.InteropServices.RuntimeInformation.dll
├── System.Runtime.Intrinsics.dll
├── System.Runtime.Loader.dll
├── System.Runtime.Numerics.dll
├── System.Runtime.Serialization.dll
├── System.Runtime.Serialization.Formatters.dll
├── System.Runtime.Serialization.Json.dll
├── System.Runtime.Serialization.Primitives.dll
├── System.Runtime.Serialization.Xml.dll
├── System.Security.AccessControl.dll
├── System.Security.Claims.dll
├── System.Security.Cryptography.Algorithms.dll
├── System.Security.Cryptography.Cng.dll
├── System.Security.Cryptography.Csp.dll
├── System.Security.Cryptography.Encoding.dll
├── System.Security.Cryptography.OpenSsl.dll
├── System.Security.Cryptography.Primitives.dll
├── System.Security.Cryptography.X509Certificates.dll
├── System.Security.dll
├── System.Security.Principal.dll
├── System.Security.Principal.Windows.dll
├── System.Security.SecureString.dll
├── System.ServiceModel.Web.dll
├── System.ServiceProcess.dll
├── System.Text.Encoding.CodePages.dll
├── System.Text.Encoding.dll
├── System.Text.Encoding.Extensions.dll
├── System.Text.Encodings.Web.dll
├── System.Text.Json.dll
├── System.Text.RegularExpressions.dll
├── System.Threading.Channels.dll
├── System.Threading.dll
├── System.Threading.Overlapped.dll
├── System.Threading.Tasks.Dataflow.dll
├── System.Threading.Tasks.dll
├── System.Threading.Tasks.Extensions.dll
├── System.Threading.Tasks.Parallel.dll
├── System.Threading.Thread.dll
├── System.Threading.ThreadPool.dll
├── System.Threading.Timer.dll
├── System.Transactions.dll
├── System.Transactions.Local.dll
├── System.ValueTuple.dll
├── System.Web.dll
├── System.Web.HttpUtility.dll
├── System.Windows.dll
├── System.Xml.dll
├── System.Xml.Linq.dll
├── System.Xml.ReaderWriter.dll
├── System.Xml.Serialization.dll
├── System.Xml.XDocument.dll
├── System.Xml.XmlDocument.dll
├── System.Xml.XmlSerializer.dll
├── System.Xml.XPath.dll
├── System.Xml.XPath.XDocument.dll
└── WindowsBase.dll

Maintenant, on peut fournir le dossier à quelqu’un qui possède une machine Linux et il pourra faire fonctionner le programme comme on peut le voir ci-dessous avec les deux mêmes machines Linux WSL. Celle du haut possède le framework mais pas celle du bas. Par contre celle du bas possède le dossier avec toutes les dépendances.

portable OK

💬 Peut-on créer un exécutable PE depuis cette machine Linux ?

Oui c’est tout à fait possible, il suffit de changer le RID

$ dotnet publish -c Release -r win-x64  --self-contained true
$ file bin/Release/net8.0/win-x64/publish/Hello.exe 
bin/Release/net8.0/win-x64/publish/Hello.exe: PE32+ executable (console) x86-64, for MS Windows, 6 sections
Portable Windows

À quoi ça sert

Voilà une question légitime. Pourquoi faire en ligne de commande quelque chose qu’on peut faire graphiquement ?

Vous avez peut-être remarqué le lien en début de tutoriel .NET In-Browser Tutorial.

in-browser

Quand on clique sur le bouton Run Code que se passe-t-il ?

  1. Le texte que vous avez écrit dans le navigateur est transmit au serveur web via une simple requête HTTP de type POST
  2. Le serveur web crée un projet avec la commande dotnet new console -n UUID_Unique
  3. Il remplace le code présent dans le fichier Program.cs par le votre, celui qu’il vient de recevoir
  4. Il démarre ensuite un conteneur en faisant une copie d’un conteneur de base contenant également la commande dotnet, par exemple celui-ci microsoft-dotnet-framework si le système de conteneur utilisé est docker
  5. Il pousse le projet dans le conteneur et il donne l’ordre au conteneur de lancer une suite de commande dont la commande dotnet build et dotnet run
  6. Il interprète le résultat reçu en retour, détruit le conteneur et génère une page html qui sera affichée dans votre navigateur en retour

Tout se passe dans un serveur web et dans un conteneur. Aucune de ces machines ne possède une interface graphique beaucoup trop gourmande en ressources. Il est donc impossible, d’utiliser Visual Studio.

On trouve même des sites qui utilisent la commande dotnet test pour démarrer des tests unitaires et ainsi faire de la validation de challenges de programmation. Par exemple le site Edabit. Les premiers challenges sont gratuits puis le site devient payant.

Le site Gitlab utilise le même principe pour faire l’intégration continue (CI/CD). Il travail avec des conteneurs LXC.

Pour finir, la documentation officielle de Microsoft regorge de ce type d’utilisation, par exemple ici conditional operator

operator_run_online

Exercice

Nous allons réaliser un exercice qui permettra de mieux comprendre ce qui se passe sur le site d’edabit et plus précisément, cet exercice Javascript qui fait office de donnée d’exercice. Bien sûr, nous le réaliseront en C# et en mode console avec la commande dotnet.

Pour faire ce travail on pourrait suivre cette marche à suivre mais je vous ai simplifié le travail en vous préparant une archive contenant déjà tout le nécessaire.

Cette archive contient une solution qui elle-même contient deux projets, Challenge et Test :

├── Challenge
│   ├── Challenge
│   │   ├── Challenge.cs
│   │   ├── Challenge.csproj
│   ├── Challenge.sln
│   └── Test
│       ├── Test.csproj
│       ├── UnitTest.cs
│       └── Usings.cs

Le projet Test contient les tests qui doivent être validés

using Challenge;

namespace Test
{
        [TestFixture]
        public class Tests
        {
                private Boomerang boomerang;

                [SetUp]
                public void Setup()
                {
                        boomerang = new Boomerang();
                }

                [Test]
                [TestCase(new int[] {9, 5, 9, 5, 1, 1, 1}, 2, TestName="Test 1" )]
                [TestCase(new int[] {5, 6, 6, 7, 6, 3, 9}, 1, TestName="Test 2" )]
                [TestCase(new int[] {4, 4, 4, 9, 9, 9, 9}, 0, TestName="Test 3" )]
                [TestCase(new int[] {5, 9, 5, 9, 5}, 3, TestName="Test 4" )]
                [TestCase(new int[] {4, 4, 4, 8, 4, 8, 4}, 3, TestName="Test 5" )]
                [TestCase(new int[] {2, 2, 2, 2, 2, 2, 3}, 0, TestName="Test 6" )]
                [TestCase(new int[] {2, 2, 2, 2, 3, 2, 3}, 2, TestName="Test 7" )]
                [TestCase(new int[] {1, 2, 1, 1, 1, 2, 1}, 2, TestName="Test 8" )]
                [TestCase(new int[] {5, 1, 1, 1, 1, 4, 1}, 1, TestName="Test 9" )]
                [TestCase(new int[] {3, 7, 3, 2, 1, 5, 1, 2, 2, -2, 2}, 3, TestName="Test 10" )]
                [TestCase(new int[] {1, 7, 1, 7, 1, 7, 1}, 5, TestName="Test 11" )]
                [TestCase(new int[] {5, 5, 5}, 0, TestName="Test 12" )]
                public void Test(int[] array, int expected)
                {
                        int result = boomerang.HowMany(array);
                        Assert.AreEqual(result, expected);
                }
        }
}

Tandis que le projet Challenge contient le code source à compléter

using System;

namespace Challenge
{
        public class Boomerang
        {
                public int HowMany(int[] array)
                {
                        /*
                         * Votre code ici
                         */
                        return -1;
                }
        }
}

Le principe est similaire à ce qui a été vu au-dessus. Vous devez compléter la méthode HowMany pour qu’elle fasse le travail souhaitez. Si vous y parvenez alors les 12 tests seront ✅ et c’est à ce moment qu’on considère le challenge comme valide.

Pour lancer les tests on utilise la commande dotnet test depuis la racine

.
├── Challenge
├── Challenge.sln
└── Test

À vous de jouer 😁. Réaliser le code qui valide les 12 tests 😉.

Outils C#

Vous avez plusieurs choix pour réaliser un programme en C#.

Boxing et unboxing

Lors de vos futures lectures de sites web ou autres ouvrages papier, vous rencontrerez probablement ces deux termes. Le boxing et l’unboxing.

Le boxing consiste généralement à convertir un type valeur en type object.

L’unboxing consiste à faire à démarche inverse, à savoir, convertir l’objet ou la variable de type object en son type d’origine.

La conversion boxing est implicite.

int i = 123;
// The following line boxes i.
object o = i;

La conversion unboxing est explicite (il faut préciser le type d’origine)

object o = 123;
int i = (int)o; // unboxing

Attention, le boxing et l’unboxing sont des opérations coûteuses en temps de calcul et il est préférable de ne pas les utiliser Performances.

Il ne faut pas confondre boxing et unboxing avec la copie de référence. En effet, le boxing et l’unboxing ne copie pas simplement une référence mais il crée une nouvelle référence sur la pile o qui pointe sur une nouvelle variable i boxed sur le tas.

Les deux variables vivent leur vie séparément. Voyez le programme ci-dessous :

int i=123;
object obj = i;
Console.WriteLine($"i   is typeof                     {i.GetType()}");
Console.WriteLine($"obj is typeof                     {obj.GetType()}");
Console.WriteLine($"i   contain                       {i}");
Console.WriteLine($"obj contain                       {obj}");
Console.WriteLine($"Change value of obj to {obj = 456}");
Console.WriteLine($"i   contain                       {i}");
Console.WriteLine($"obj contain                       {obj}");
Console.Write("i.GetType() == typeof(int) ?      ");
if(i.GetType() == typeof(int))
    Console.WriteLine("yes");
else
    Console.WriteLine("no");

Console.Write("i is int ?                        ");
if(i is int)
    Console.WriteLine("yes");
else
    Console.WriteLine("no");

Console.Write("obj.GetType() == typeof(object) ? ");
if(obj.GetType() == typeof(object))
    Console.WriteLine("yes");
else
    Console.WriteLine("no");

Console.Write("obj is object ?                   ");
if(obj is object)
    Console.WriteLine("yes");
else
    Console.WriteLine("no");

Il donne le résultat suivant :

i   is typeof                     System.Int32
obj is typeof                     System.Int32
i   contain                       123
obj contain                       123

Change value of obj to 456
i   contain                       123
obj contain                       456
i.GetType() == typeof(int) ?      yes
i is int ?                        yes
obj.GetType() == typeof(object) ? no
obj is object ?                   yes

Comme on peut le constater, i et obj sont deux variables différentes. obj est une variable de type object qui contient une copie de la valeur de i au moment du boxing.

Il ne faut pas confondre le boxing de type simple et la copie de référence d’objet

Quand utiliser le boxing et l’unboxing ?

Un exemple classique est la collection ArrayList. Une ArrayList est une collection dynamique d’object.

Autrement dit c’est une collection dans laquelle on peut mettre tout ce qu’on veut. Chaque élément sera converti implicitement en object.

Voici un exemple :

ArrayList al = new ArrayList();
// Boxing implicite en type object
al.Add(12); // Un int
al.Add("Hello"); // Une string
al.Add(new Point(12, 4)); // un point

// Unboxing:
int i = (int)al[0];
string s = (string)al[1];
Point p = (Point)al[2];

Pour plus de détails :

Boxing et unboxing

Mots clés is et as

Le mot clé is est un opérateur qui permet de tester le type d’une variable ou d’un objet.

Il s’utilise ainsi :

if (o is int)

ce qui signifie « o, qui est un object, contient-il un int ? »

Le mot clé as est un opérateur nous permettant de faire du transtypage (expression casting). Il s’utilise ainsi:

string s = al[1] as string; // Opérateur as
string s = (string)al[1];   // Expression casting

L’opérateur as ne peut être utilisé que pour les types « nullables » (string , object, etc… mais pas int par exemple) car en cas de conversion impossible, la variable se verra assigner la valeur null contrairement au casting qui lèvera une exception.

Exercice

Soit le code suivant:

Code
 1  class Program
 2  {
 3      static void Main(string[] args)
 4      {
 5          List<object> list = new List<object>();
 6
 7          list.Add(123);
 8          list.Add("C#");
 9          list.Add(null);
10          list.Add(new Person("Onime", "Anne"));
11
12          // Parcours de modification
13          foreach (object item in list)
14          {
15              if (item is Person)
16              {
17                  Person person = (Person)item;
18                  person.Nom = "Nasse";
19                  person.Prenom = "Anna";
20              }
21              else if (item is int)
22              {
23                  int n = (int)item;
24                  n++;
25              }
26              else
27              {
28                  string? chaine = item as string;
29                  chaine = "F" + chaine[1];
30              }
31          }
32
33          // Parcours d'affichage
34          foreach (object item in list)
35          {
36              Console.WriteLine(item);
37          }
38
39          // gestion des Exceptions
40          for (int i = 0; i < list.Count; i++)
41          {
42              try
43              {
44                  int n = (int)list[i];
45                  Console.WriteLine($"Unboxing list at index {i} to int is OK");
46              }
47              catch (InvalidCastException)
48              {
49                  Console.WriteLine($"List at index {i} is not possible");
50              }
51              catch (NullReferenceException)
52              {
53                  Console.WriteLine($"List at index {i} is null");
54              }
55          }
56      }
57  }
58
59  public class Person
60  {
61      public Person(string nom, string prenom)
62      {
63          Nom = nom;
64          Prenom = prenom;
65      }
66      public string Nom { get; set; }
67      public string Prenom { get; set; }
68
69      public override string ToString()
70      {
71          return $"{Prenom} {Nom}";
72      }
73  }

Répondez aux questions suivantes sans coder le programme et sans lire les réponses.

💬 Est-il possible de placer toutes ces valeurs de type différent dans une List typée ainsi (lignes 7 à 10) ? Justifier!

oui car elles descendent toutes du type object

💬 Si tout ce passe bien, quelles seront les valeurs affichées lors de la seconde boucle (lignes 34 à 37) ?

ça se passe mal, System.NullReferenceException pour l’entrée null de la liste. Sinon, seul l’objet Person est modifié car il est passé en référence

💬 Pourquoi y a-t-il un ? devant la déclaration string? chaine = item as string; (ligne 28) ?

La compilation lève un warning nous indiquant que peut-être item sera null. Le ? désactive le warning mais au moins on l’a vu

💬 Qu’est-ce qui sera affiché par la dernière boucle (lignes 40 à 55) ?

Unboxing list at index 0 to int is OK List at index 1 is not possible List at index 2 is null List at index 3 is not possible

💬 Quelle(s) correction(s) faudrait-il apporter à ce programme ?

Englober la première boucle avec un if (item is not null)

Opérateurs

En programmation informatique, un opérateur est une fonction spéciale dont l’identificateur s’écrit généralement avec des caractères non autorisés pour l’identificateur des fonctions ordinaires tels que +, -, * etc. Il s’agit souvent des équivalents aux opérateurs mathématiques pour un langage de programmation. En C#, les opérateurs peuvent être surchargés au même titre que les méthodes standards.

Mais au fait, comment est-ce possible de créer l’opérateur + pour un entier ? En effet, si on imagine la méthode pour cet opérateur elle se présenterait comme suit :

int operator +(int left, int right)
{
    return left + right; // on ne peut pas utiliser le symbole '+'
}

Oui mais voilà, on ne peut pas utiliser le symbole + puisqu’on est justement entrain de le coder 😖 !

Pour trouver une réponse à cette épineuse question, on peut se tourner vers le CLI qui est capable de fournir des instructions en langage intermédiaire (CIL) qui seront ensuite compilées en code machine. Voici un exemple de code CIL pour l’opérateur + :

.method public static int32 Add(int32, int32) cil managed {
    .maxstack 2
    ldarg.0 // load the first argument, opcode 0x02
    ldarg.1 // load the second argument, opcode 0x03
    add     // add them, opcode 0x58
    ret     // return the result, opcode 0x2A
}

Nous n’allons pas descendre aussi bas dans le détail mais sachez que le compilateur C# est capable traiter ce genre de code.

Opérateurs surchargés

On peut classer les opérateurs selon le nombre d’opérandes qu’ils acceptent.

Opérateurs standards +, -, *, /

À titre d’exemple, vous trouvez ci-dessous une classe partielle Vector2 (la classe complète se trouve ici) qui permet la manipulation de vecteur en 2 dimensions. Cette exemple contient trois opérateurs. L’opérateur d’addition + et deux opérateurs de multiplications surchargés *. À l’intérieur de ces méthodes, on retrouve l’utilisation des opérateurs internes (built-in) au type double.

public class Vector2
{
    double x, y;
    public Vector2(double x, double y)
    {
        this.x = x;
        this.y = y;
    }
    public static Vector2 operator +(Vector2 vec1, Vector2 vec2)
    {
        // usage of double '+' built-in operator
        return new Vector2(vec1.x + vec2.x, vec1.y + vec2.y);
    }
    public static Vector2 operator *(Vector2 vec1, Vector2 vec2)
    {
        // usage of double '*' built-in operator
        return new Vector2(vec1.x * vec2.x, vec1.y * vec2.y);
    }
    public static Vector2 operator *(Vector2 vec, float scaleFactor)
    {
        vec.x *= scaleFactor; // usage of double '*' built-in operator
        vec.y *= scaleFactor; // and double '=' built-in operator
        return vec;
    }
}

On peut utiliser cette classe ainsi

Vector2 vec1 = new Vector2(10.0, 3.0);
Vector2 vec2 = new Vector2(27.0, 4.0);

Vector2 res1 = vec1 + vec2;
Vector2 res2 = vec1 * vec2;
Vector3 res3 = vec1 * 34.5;

On peut encore remarquer l’utilisation de l’opérateur built-in = d’affectation de référence. On se rend compte que l’utilisation des opérateurs est omniprésente en programmation.

Exercice opérateur

On vous demande de réaliser la structure PointF dont le début est fournit ci-dessous (différence entre struct et classe hors cours). Les opérateurs ne sont pas forcement utiles et concrets mais ils servent d’exercice 😉.

public struct PointF
{
    float x;
    float y;

    public PointF(float x, float y)
    {
        this.x = x;
        this.y = y;
    }
    public bool IsEmpty
    {
        get { return x == 0f && y == 0f; }
    }
    public float X
    {
        get { return x; }
        set { x = value; }
    }
    public float Y
    {
        get { return y; }
        set { y = value; }
    }
    /// <summary>
    /// Opérateur unaire de soustraction
    /// Symbol: --
    /// Job: soustrait 1 aux deux composantes x et y
    /// Exemple:
    /// PointF pt(12f, 24f);
    /// pt--;
    /// </summary>
    /// <param name="pt"></param>
    /// <returns></returns>


    // VOTRE CODE ICI


    /// <summary>
    /// Opérateur binaire de soustraction:
    /// Symbol: -
    /// Job: Soustrait les composantes x et y de pt2 à pt1
    /// Exemple:
    /// PointF pt1(12f, 24f);
    /// PointF pt2(35f, 56f);
    /// PointF pt = pt2 - pt1;
    /// </summary>
    /// <param name="pt1"></param>
    /// <param name="pt2"></param>
    /// <returns></returns>


    // VOTRE CODE ICI


    /// <summary>
    /// Opérateur binaire de division
    /// Symbole: /
    /// Job: Divise les composantes x et y de pt1 par les composantes de pt2
    /// Exemple:
    /// PointF pt1(12f, 24f);
    /// PointF pt2(35f, 56f);
    /// PointF pt = pt2 / pt1;
    /// </summary>
    /// <param name="pt1"></param>
    /// <param name="pt2"></param>
    /// <returns></returns>


    // VOTRE CODE ICI


    /// <summary>
    /// Opérateur binaire de division
    /// Symbole: /
    /// Job: Divise les composantes x et y de pt par la valeur scale
    /// Exemple:
    /// PointF pt(12f, 24f);
    /// float scale = 2.0f;
    /// pt = pt / scale;
    /// </summary>
    /// <param name="pt"></param>
    /// <param name="scaleFactor"></param>
    /// <returns></returns>


    // VOTRE CODE ICI


}

Le code de test est le suivant

PointF pt = new PointF(11f, 86f);
PointF pt2 = new PointF(2f, 3f);

pt--;                               // pt.x = 10 et pt.y = 85
pt = pt / 5f;                       // pt.x = 2 et pt.y = 17
pt = pt / pt2;                      // pt.x = 1 et pt.y = 5.666
pt = pt - pt2;                      // pt.x = -1 et pt.y = 2.666

Corrigé

Quelques opérateurs

Il existe un grand nombre d’opérateurs en C#. Certains s’utilisent quotidiennement, comme l’opérateur +, d’autres sont plus rarement utilisés. Voici deux opérateurs qui ne s’utilise que rarement 😯.

Index depuis la fin ^

Disponible en C# 8.0 et versions ultérieures, l’opérateur indique la position de l’élément ^ à partir de la fin d’une séquence. Pour une séquence de longueur length, ^n pointe vers l’élément avec un décalage length - n à partir du début d’une séquence. Par exemple, ^1 pointe vers le dernier élément d’une séquence et ^length pointe vers le premier élément d’une séquence.

int[] xs = new[] { 0, 10, 20, 30, 40 };
int last = xs[^1];
Console.WriteLine(last);     // output: 40

string lines = new List<string> { "one", "two", "three", "four", "five" };
string prelast = lines[^2];  // lines.Length = 5 and 5 - 2 = 3 -> lines[3] = "four"
Console.WriteLine(prelast);  // output: four

string word = "Hello";
Index toFirst = ^word.Length;
char first = word[toFirst];
Console.WriteLine(first);    // output: H

Opérateur de plage ..

Disponible en C# 8.0 et versions ultérieures, l’opérateur .. spécifie le début et la fin d’une plage d’index en tant qu’opérandes. L’opérande de gauche est un début inclusif d’une plage. L’opérande de droite est une fin exclusive d’une plage. L’un des opérandes peut être un index à partir du début ou de la fin d’une séquence, comme l’illustre l’exemple suivant :

int[] numbers = new[] { 0, 10, 20, 30, 40, 50 };
int start = 1;
int amountToTake = 3;
int[] subset = numbers[start..(start + amountToTake)]; // subset: 10 20 30

int margin = 1;
int[] inner = numbers[margin..ᶺmargin];                // inner: 10 20 30 40

Vous pouvez omettre l’un des opérandes de l’opérateur .. pour obtenir une plage ouverte :

Warning

On peut vite se faire avoir avec les opérateurs qui n’en sont pas.

Exemple 1 :

int[][] numbers = new[]
{
    new[] {1,2,3},
    new[] {4,5,6},
    new[] {7,8,9}
};

List<int> values = numbers.SelectMany(_ => _.Where(_ => _ % 2 == 0)).ToList();

Dans cet exemple, le caractère _ n’est rien d’autre qu’un nom de variable autorisé 😟 au même titre que

int _ = 12;

Ou encore le code ci-dessous qui prête à confusion quand au type d’opérateur

Exemple 2 :

int n = 1234ᶺ1

Ou dans ce cas, à gauche de l’opérateur on ne trouve pas un tableau mais une variable entière et dans ce cas, on a affaire à l’opérateur XOR 😩.

Au fait, quels sont les résultats de ces deux exemples ?

L’exemple 1 fournit dans values

fournit les nombres paires présent dans chaque tableau sous forme de liste d’entiers

L’exemple 2 fournit dans n

1234₁₀ => 0100 1101 0010₂ XOR 1 = 1235₁₀

Le cas du ?

Le point d’interrogation revient régulièrement en programmation. Il peut signifier plusieurs choses mais il est intimement lié au fait qu’une référence peut être null. Hors, un référence null peut lever une exception de type NullReferenceException mais elle peut aussi être impliquée dans des attaques informatiques de type null pointer dereference. Même si l’exemple fournit avec ce lien est très vieux, cette attaque est toujours d’actualité car les programmeurs oublient de tester leurs références aux objets.

Exemple

Imaginez un code complet de plusieurs milliers de lignes parmi lesquelles se trouvent ces lignes là :

const int MAX_CLIENTS = 100;
const int MAX_CAR_TO_PRINT = 20;
string[] noms = new string[MAX_CLIENTS];

try
{
    noms = File.ReadAllLines("clients.txt");
}
catch (FileNotFoundException ex)
{
    Console.WriteLine("Le fichier n'existe pas...");
}

for (int i = 0; i < MAX_CLIENTS; i++)
{
    if (noms[i].Length > MAX_CAR_TO_PRINT)
    {
        Console.WriteLine($"Bonjour {noms[i].Substring(0, MAX_CAR_TO_PRINT - 3)}...");
    }
    else
    {
        Console.WriteLine($"Bonjour Monsieur {noms[i]}");
    }
}

Le programmeur possède bien entendu le fichier clients.txt sur son poste car il est justement entrain de régler des problèmes d’affichage sur la longueur des noms. Mais que ce passe-t-il si ce fichier n’existe pas ?

Aux lignes 16 et 18 le programme tente d’accéder aux propriétés via une référence null. Le programme entre alors dans un mode inconnu…

Type non-nullable

En C# il existe des types non-nullable. Ce sont les types de base :

Cependant, dans certaines applications, une valeur de variable peut être indéfinie ou manquante. Par exemple, un champ de base de données contenant un booléen peut contenir true ou false , mais il peut aussi contenir aucune valeur, autrement dit, NULL . Vous pouvez alors, grâce à l’opérateur ? utiliser le type bool? dans ce scénario.

Vous utilisez donc un type valeur Nullable lorsque vous devez représenter la valeur indéfinie d’un type valeur non nullable.

Imaginons l’exemple où vous faites un accès à une base de données pour obtenir l’âge lié à un profil d’utilisateur. Lorsque l’utilisateur à rempli son profil, la date de naissance était facultative.

register

Vous ne pouvez donc pas faire ceci

int age = DBProfilGetAge();

car les valeurs possibles sont une valeur entière mais aussi null si l’utilisateur n’a pas donné sa date de naissance. Par contre vous pouvez faire ceci:

int? age = DBProfilGetAge();

Ou ceci, qui revient au même:

Nullable<int> age = DBProfilGetAge();

Dans ce cas, la variable age pourra prendre comme valeur un entier ou la valeur null malgré qu’un type int n’est pas nullable.

Opérateurs de condition null ?. et ?[]

Cet opérateur s’applique au accès de type membre de ou index. Cet opérateur retourne la valeur correspondante seulement pour des valeurs non-null autrement, il retourne null.

Exemple

L’exemple ci-dessous déclenche une exception de type

System.NullReferenceException : ‘Object reference not set to an instance of an object.’

car la référence person est null

public static class NullProject
{
    public static void Main()
    {
        Person person = null;
        Console.WriteLine(person.Name);
    }
}

public class Person
{
    public string Name { get; set; }
}

Si on écrit le même programme avec l’opérateur ? alors l’accès au membre Name sera protégé dans le sens où il ne déclenchera pas d’exception mais il renverra la valeur null et Console.WriteLine(null) revient à afficher une chaîne de caractère null, dans ce cas, la méthode WriteLine affiche simplement un retour à la ligne.

public static void Main()
{
    Person person = null;
    Console.WriteLine(person?.Name);
}

Exemple 2

Imaginons qu’on souhaite créer une méthode ayant la signature suivante

int FirstOrDefault(string search, string[] strings)

Cette méthode retourne l’index de la première valeur search présente dans le tableau strings. Si la méthode ne trouve pas la valeur search elle retourne la valeur -1. Une approche simple serait

int FirstOrDefault(string search, string[] strings)
{
    int i = 0;

    do
    {
        if (strings[i] == search)
            return i;
    }
    while(i++ < strings.Length);

    return -1;
}

Et les tests seraient

int foundAt = -1;
foundAt = FirstOrDefault("C", null));                         // foundAt: -1
foundAt = FirstOrDefault("C", new string[0]));                // foundAt: -1
foundAt = FirstOrDefault("C", new string[] { null }));        // foundAt: -1
foundAt = FirstOrDefault("C", new string[] {"A", "B", "C"})); // foundAt:  2

Malheureusement, avec la première ligne de test on obtiendra

System.NullReferenceException : ‘Object reference not set to an instance of an object.’

lors de l’accès à l’index strings[i] car strings est null. On peut alors placer l’opérateur ? pour protéger l’accès comme ceci strings?[i]. Cette fois, la valeur retournée sera null et le test null == search est tout à fait possible 😎.

On peut aussi se dire qu’on peut tester la taille du tableau avant d’accéder à une cellule

if (strings.Length == 0)
    return -1;

Malheureusement, le programme plantera lors de l’accès au membre strings.Length toujours parce que strings est null. À nouveau, on peut protéger l’accès au membre avec l’opérateur ? de cette manière strings?.Length. Le programme devient alors

int FirstOrDefault(string search, string[] strings)
{
    int i = 0;

    if (strings?.Length == 0)
        return -1;

    do
    {
        if (strings?[i] == search)
            return i;
        i++;
    }
    while(i < strings?.Length);

    return -1;
}

Attention: comme il est stipulé dans la documentation officielle, l’opérateur ? ne vous protège pas contre les autres exceptions. Ainsi la seconde ligne du test déclenchera une exception

System.IndexOutOfRangeException : ‘Index was outside the bounds of the array.’

car l’index 0 n’existe pas dans le tableau string[0]

Une autre manière de procéder serait de tester la valeur de strings au début du programme ainsi

int FirstOrDefault(string search, string[] strings)
{
    if(strings is null)
        return -1;

En programmation, il y a toujours plusieurs chemin pour atteindre un but 😉. L’important c’est de faire les tests !

Opérateur ternaire ?:

C’est une façon raccourcie d’écrire un if / else. L’opérateur ternaire évalue une condition qui doit retourner un booléen et effectue l’une des deux actions en conséquence.

Écriture générique

condition ? action_si_vrai : action_si_faux

Exemple

int age = Input("Quel est ton âge ?");
string catAge = (age >= 18) ? "Majeur" : "Mineur";

Dans cet exemple, la chaîne de caractères catAge prend la valeur Majeur ou Mineur en fonction du résultat de l’expression booléenne age >= 18. Les parenthèses sont facultatives.

L’opérateur ternaire est associatif à droite, autrement dit, une expression de la forme

a ? b : c ? d : e

est évaluée comme étant

a ? b : (c ? d : e)

Affectation si null, l’opérateur ??=

L’opérateur ??= d’affectation null-coalescing affecte la valeur de son opérande de droite à son opérande de gauche uniquement si l’opérande de gauche est évalué à null.

// numbers est null
List<int> numbers = null;
// Dans ce cas, on lui affecte une nouvelle liste de nombres entiers
// et numbers n'est plus null
numbers ??= new List<int> { 1, 2, 3, 4, 5, 6 };
// comme numbers n'est plus null, la ligne ci dessous ne fera rien
numbers ??= new List<int> { 10, 20, 30, 40, 50, 60 };
// numbers contient 1, 2, 3, 4, 5, 6

Test de valeur null, l’opérateur ??

L’opérateur de test null-coalescing ?? retourne la valeur de l’opérande de gauche si elle n’est pas null sinon, il évalue l’opérande de droite et retourne son résultat.

Reprenons l’exemple du profil plus haut. Si l’utilisateur n’a pas fourni sa date de naissance une valeur NULL est fournie par la base de données. Dans ce cas, on peut utiliser cet opérateur pour remplacer cette valeur null ainsi

int age = DBProfilGetAge() ?? 0;

Si DBProfilGetAge() fourni une valeur non-null c’est cette valeur qui sera placée dans la variable age. Si DBProfilGetAge() retourne une valeur null, elle sera remplacée par la valeur 0.

Exercice ?

Comme on ne possède pas de base de données, on vous demande de réaliser une méthode de test DBProfilGetAge. Cette méthode simulera des valeurs aléatoires comprises entre 50 et 100 ou null. Chaque appel de la méthode retourne une seule valeur. Le résultat escompté est le suivant:

La méthode à retourné: null
La méthode à retourné: 81
La méthode à retourné: 61

Votre apprenti a commencé une ébauche de programme mais il ne sait pas comment faire des opérateurs ternaires et son programme ne se compile pas. Il vous appel à l’aide. Vous arrivez vers lui et avec vos connaissances vous écrivez les deux opérateurs ternaires et à ajouter les ? manquants 😎.

public static void Main(string[] args)
{
    int age = DBProfilGetAge();
    Console.WriteLine("La méthode à retourné: {0}", /* votre opérateur ternaire */)
}

public static int DBProfilGetAge()
{
    Random rand = new Random();
    int randomAge = rand.Next(1, 101);
    return /* votre opérateur ternaire */;
}

Corrigé

L’opérateur =>

On trouve cet opérateur en tant que corps d’expression. Il est lié aux expressions Lambda mais il peut également être utilisé ainsi :

string GetWeatherDisplay(double tempInCelsius) => tempInCelsius < 20.0 ? "Cold." : "Perfect!";

Console.WriteLine(GetWeatherDisplay(15));  // output: Cold.
Console.WriteLine(GetWeatherDisplay(27));  // output: Perfect!

Code testable ici

Dans cet exemple, une méthode GetWeatherDisplay a été définie avec, comme corps d’expression tempInCelsius < 20.0 ? "Cold." : "Perfect!";.

Exemple avec une propriété

Dans certaines classes on trouve des propriétés en lecture seule qui vont simplement nous fournir le contenu d’un attribut. Le code sans l’opérateur sera équivalent à ceci:

public class Person
{
    string firstName;
    string lastName;

    ...

    public string FullName
    {
        get
        {
            return $"{firstName} {lastName}";
        }
    }
}

En utilisant l’opérateur =>, on peut résumer cette syntaxe ainsi

public class Person
{
    string firstName;
    string lastName;

    ...

    public string FullName => $"{firstName} {lastName}";
}

Lambda

Une expression lambda est une syntaxe permettant de créer des fonctions anonymes en ligne.

Une expression lambda est créée à l’aide de l’opérateur =>. On place tous les paramètres sur le côté gauche de l’opérateur et sur le côté droit on place une expression qui peut utiliser ces paramètres. Cette expression résoudra la valeur de retour de la fonction. Si nécessaire, un {code block} complet peut être utilisé à droite. Si le type de retour n’est pas null, le bloc contiendra une déclaration de retour return.

Expression Lambda en tant que paramètre de méthode

List<int> listSupSix = listEnVrac.FindAll(x => x > 6);

x => x > 6 est une expression lambda agissant comme un prédicat qui garantit que seuls les éléments supérieurs à 6 de listEnVrac sont renvoyés. x est un nom de variable arbitraire et il contiendra tour à tour, toutes les valeurs de la liste.

Expression Lambda en tant que gestionnaire d’événements

Les expressions Lambda peuvent être utilisées pour gérer des événements, ce qui est utile lorsque:

Ci-dessous, un exemple de gestionnaire d’événements lambda :

smtpClient.SendCompleted += (sender, args) => Console.WriteLine("Email sent");

Sans utiliser les expressions Lambda, on aurait eu le code suivant:

smtpClient.SendCompleted += new EventHandler(smtpClient_SendCompleted);

private void smtpClient_SendCompleted(object sender, EventArgs e)
{
    Console.WriteLine("Email sent");
}

On remarque que c’est bien le nom de la méthode qui disparaît avec l’utilisation des Lambda. Le nom des paramètres est arbitraire là-aussi. On aurait pu écrire

smtpClient.SendCompleted += (s, a) => Console.WriteLine("Email sent");

Exemple réel, les Animations

Dans le framework .NET MAUI il existe des Animation qui permettent, par exemple, de créer un spinner de téléchargement

spinner_loading

L’idée est simple. On choisit un caractère de type spinner, par exemple celui fourni par fontawesome que l’on place dans un Label

spinner

On crée ensuite une animation linéaire d’une valeur entière allant de 0 à 360 et on affecte une méthode de callback qui, après chaque changement de valeur de l’animation, va affecter la nouvelle valeur à la propriété Rotation du Label. Ainsi, imaginons que l’Animation augmente sa valeur de 0 à 1, et bien la nouvelle valeur 1 sera affectée à la propriété Rotation du Label et le Label tournera de 1°.

private void Animation_Callback(double animationValue)
{
    LabelLoad.Rotation = animationValue;
}

Animation animate0To360 = new Animation(Animation_Callback, 0, 360, Easing.Linear);

Cette syntaxe peut être abrégée ainsi avec les Lambda

Animation animate0To360 = new Animation(
    (deg) => LabelLoad.Rotation = deg, /* expression Lambda "deg" est un nom de variable arbitraire */
    0, 360, Easing.Linear);

Selon le même principe, la méthode Commit de la classe Animation possède deux méthodes de callback

animate0To360.Commit(
    this,                            //owner
    "rotate",                        //name of your choice
    16,                              //number of milliseconds between each callback
    1000,                            //duration of the animation, in milliseconds
    Easing.Linear,                   //animation type
    (v, c) => LabelLoad.Rotation = 0,//finished callback, what's append at the end of this Animation
    () => true                       //repeat callback, if true, repeat this Animation
);

Le callback finished contient deux paramètres. v est la valeur final au moment où l’animation est arrivée à la fin et c est un booléen permettant d’arrêter l’animation. Dans ce callback, comme on est arrivé au bout de l’animation et donc que notre Label à fait un tour sur lui-même, on le remet dans sa position initiale LabelLoad.Rotation = 0. On n’utilise aucun des deux paramètres.

Le callback repeat n’a pas de paramètre et il retourne une valeur booléenne. true recommence l’animation, false l’arrête.

Remarque

L’aide en ligne de Microsoft ainsi que les tutoriels présents sur Internet vous présenterons tous leurs codes avec des méthodes Lambda. Il est donc impératif de les comprendre.

Code complet .NET MAUI

public partial class MainPage : ContentPage
{
    private Animation animate0To360;
    private bool animate;

    public MainPage()
    {
        InitializeComponent();
    }

    protected override void OnAppearing()
    {
        animate = false;
        ToogleAnim.Text = "Animate please";
        animate0To360 = new Animation((deg) => LabelLoad.Rotation = deg, 0, 360, Easing.CubicInOut);
    }

    public void Toogle_Click(object sender, EventArgs e)
    {
        if (!animate)
        {
            animate0To360.Commit(this, "rotate", 16, 1000, Easing.CubicInOut,
                (v, c) => LabelLoad.Rotation = 0,
                () => animate
            );
        }
        else
        {
            this.AbortAnimation("rotate");
        }

        animate = !animate;
        ToogleAnim.Text = animate ? "Animated" : "Animate please";
    }
}

animation_record

Exercice Spinner Forms

En C# Forms il n’existe pas d’Animations telles qu’on les trouve dans les Framework basés sur du MVVM tel que WPF. Est-ce que ça signifie qu’il n’est pas possible de faire un spinner ?

Bien sûr que non. À vous de jouer ! Trouvez la solution la plus simple possible. Une fois que vous aurez réalisé votre solution, vous pouvez prendre le code ci-dessous.

Le code de la solution ci-dessus

Vous y trouverez deux gestionnaires d’événements Click, un par bouton.

Spinner.Designer.cs

...
//
// btnVisible
//
this.btnVisible.Location = new System.Drawing.Point(51, 195);
this.btnVisible.Name = "btnVisible";
this.btnVisible.Size = new System.Drawing.Size(75, 23);
this.btnVisible.TabIndex = 2;
this.btnVisible.Text = "Hide";
this.btnVisible.UseVisualStyleBackColor = true;
this.btnVisible.Click += new System.EventHandler(this.btnVisible_Click);
...

Spinner.cs

private void btnVisible_Click(object sender, EventArgs e)
{
    // Bla bla...
}

Transformez ces deux événements en Lambda, c’est-à-dire, en deux méthodes anonymes.

Version Lambda

Exercices Lambda

Vous devez coder chaque exercice. Vous devez créer un projet par exercice et placer tous les projets dans une seule solution.

Exercice 1

Soit le tableau d’entier data ci-dessous. On vous demande de compléter l’expression Lambda permettant d’obtenir un nouveau tableau contenant uniquement les valeurs paires

int[] data = { 1, 2, 4, 5, 6, 10 };
int[] even = data.Where(     /* votre lambda */     ).ToArray();

(fn => fn % 2 == 0)

Exercice 2

Soit la liste de personnes ci-dessous. On vous demande de compléter l’expression Lambda permettant d’obtenir les personnes dont le FullName commence par “A”

public class Person
{
    private string firstName;
    private string lastName;
    public Person(string firstName, string lastName)
    {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public string FullName => $"{firstName} {lastName}";
}

class Program
{
    static void Main(string[] args)
    {
        List<Person> persons = new List<Person> {
            new Person("Alain", "Proviste"),
            new Person("Anne", "Onime"),
            new Person("Déborah", "Dirouge"),
            new Person("Cécile", "Our")
        };

        //you can use string.StartsWith
        List<Person> filtered = persons.Where(  /* votre lambda */  ).ToList();
    }
}

(fn => fn.FullName.StartsWith(“A”))

Exercice 3

Soit le programme suivant

class Program
{
    static void TimerCallback(Object? state)
    {
        Console.WriteLine("Timer tick at : {0}", DateTime.Now.ToString("H:mm:ss.fff", CultureInfo.CurrentCulture));
    }
    static void Main(string[] args)
    {
        int attenteAvantStart = 2000; // 2 secondes
        int interval = 100; // 100 ms

        Timer timer = new Timer(callback: TimerCallback); // <-- ICI ICI ICI ICI

        timer.Change(attenteAvantStart, interval);

        Console.WriteLine("Press any key to quit");
        // "Tant que" aucune touche n'est pressée
        while (!Console.KeyAvailable) ;
        // Supprime le timer
        timer.Dispose();
    }
}

On vous demande d’écrire le même programme en remplaçant la méthode de callback TimerCallback par une fonction Lambda.

Timer timer = new Timer(callback => Console.WriteLine(“Timer tick at : {0}”, DateTime.Now.ToString(“H:mm:ss.fff”, CultureInfo.CurrentCulture)));

Exercice 4

Soit les exemples de méthode ci-dessous

Action line = () => Console.WriteLine();
Func<double, double> cube = x => x * x * x;
Func<int, int, bool> testForEquality = (x, y) => x == y;
Func<int, string, bool> isTooLong = (int x, string s) => s.Length > x;

En vous aidant si besoin de l’aide en ligne de Microsoft sur les Action et les Func<T,TResult>, on vous demande d’utiliser chacune de ces méthodes dans un programme simple. Par exemple:

Func<double, double> carre = (x) => x * x;
double result = carre(3);// result = 9

Console.WriteLine(“Exercice 4”); line(); double c = 3.0; Console.WriteLine($“{c} au cube donne {cube(c)}”); line(); int a = 2; int b = 3; Console.WriteLine(“This two values {0} and {1} are {2}”, a, b, testForEquality(a, b) ? “equal” : “not equal”); line(); string s = “Hello World!”; Console.WriteLine(“Is this string ‘{0}’ to long ? {1}”, s, isTooLong(10, s) ? “yes” : “no”);

Exercice 5

On vous demande de réaliser une méthode Lambda permettant de calculer la factorielle d’un nombre x de manière à pouvoir l’utiliser ainsi

int result = f(3); // result = 6
result = f(4); // result = 24

Func<int, int> f = (n) => Enumerable.Range(1, n).Aggregate(1, (p, item) => p * item);

Exercice 6

Pour terminer, réaliser un exercice plus réaliste. Sur le site github vous trouvez un paquet NuGet permettant de parser les paramètres de la ligne de commande. Le paquet se nomme CommandLineParser.

Sur la page d’accueil du site on trouve plusieurs exemple que l’on peut mixer pour obtenir le code ci-dessous

using CommandLine;

public class Options
{
    [Option('v', "verbose", Required = false, HelpText = "Set output to verbose messages.")]
    public bool Verbose { get; set; }

    [Option('m', "message", Required = true, HelpText = "Set output message.")]
    public string? Message { get; set; }
}

class Exercice6
{
    static void Main(string[] args)
    {
        Parser.Default.ParseArguments<Options>(args)
          .WithParsed(RunOptions)
          .WithNotParsed(HandleParseError);
    }
    static void RunOptions(Options opts)
    {
        // opérateur ternaire sur Verbose
    }
    static void HandleParseError(IEnumerable<Error> errs)
    {
        // IEnumerable<Error> -> List.Foreach( votre lambda )
    }
}

Votre travail consiste à remplacer les deux méthodes par des expressions Lambda.

Pour terminer, vous n’utiliserez pas le Parser.Default mais votre propre Parser que vous instancierez ainsi

Parser parser = new Parser(config => config.HelpWriter = null);
Dans Visual Studio il est possible de définir des arguments via le menu Déboguer/Propriétés de débogage de…/Argument de la ligne de commande

Dans mon programme j’ai fait des tests avec les arguments suivants

Parser parser = new Parser(config => config.HelpWriter = null); parser.ParseArguments(args) .WithParsed(opts => Console.WriteLine(“{0}”, opts.Verbose ? opts.Message : “Nothing”)) .WithNotParsed(errs => errs.ToList().ForEach(err => Console.WriteLine($“Le parser à rencontrer une erreur de type: {err.Tag}”)));

La sérialisation

Sérialisation Json

Commençons par un peu de terminologie. JSON signifie « JavaScript Object Notation » et la sérialisation est le procédé qui permet de passer d’un état d’objet vers une forme de données qui peut être persistée, par exemple dans un fichier texte, ou transportée sur l’Internet via HTTP.

Voici un exemple simple de sérialisation d’un objet Cylindre vu plus bas.

public static async Task Main(string[] args)
{
    Cylindre cylindre = new Cylindre(10.0, 2.3);

    FileStream f = File.OpenWrite("temp.dat");                          // sauvegarde dans un fichier
    string jsonString = JsonSerializer.Serialize<Cylindre>(cylindre);   // sérialsation
    await f.WriteAsync(Encoding.UTF8.GetBytes(jsonString));             // écriture asynchrone
    await f.DisposeAsync();                                             // fermeture du fichier

    Console.ReadKey();
}

Le fichier temp.dat contient alors

{"Hauteur":10,"Rayon":2.3}

On peut imaginer transférer ce fichier à une autre personne qui sera en mesure de re-créer un objet Cylindre pour autant qu’elle possède la classe.

public static async Task Main(string[] args)
{
    FileStream openStream = File.OpenRead("temp.dat");
    Cylindre? cylindre = await JsonSerializer.DeserializeAsync<Cylindre>(openStream);
    await openStream.DisposeAsync();

    Console.WriteLine($"Hauteur: {cylindre?.Hauteur}\nRayon: {cylindre?.Rayon}\nSurface: {cylindre?.Surface():F2}");
    Console.ReadKey();
}
Hauteur: 10
Rayon: 2.3
Surface: 177.75

Génération de classes C♯

Imaginons que nous soyons en possession d’un flux JSON tel que celui ci-dessous

Flux JSON
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {
        "version": 1.0
      },
      "geometry": {
        "type": "LineString",
        "coordinates": [
          [6.866025924682617, 46.96244775337095],
          [6.86920166015625, 46.96438077654255],
          [6.870081424713135, 46.965230113121564],
          [6.870746612548827, 46.96634301650308]
        ]
      }
    }
  ]
}

Il est possible de générer les classes C# dont nous aurons besoin dans notre application en utilisant des outils. Parmi ces outils, on trouve le site web

Voici ce que le site nous propose comme class pour le flux ci-dessus:

public class Feature
{
    public string type { get; set; }
    public Properties properties { get; set; }
    public Geometry geometry { get; set; }
}

public class Geometry
{
    public string type { get; set; }
    public List<List<double>> coordinates { get; set; }
}

public class Properties
{
    public double version { get; set; }
}

public class Root
{
    public string type { get; set; }
    public List<Feature> features { get; set; }
}

La classe Root devient votre classe de base, par exemple, GeoJSON.

À partir de la version Visual Studio 2022, il est également possible d’utiliser l’outil intégré. Pour cela, il faut copier votre flux JSON dans le presse-papier et ensuite, dans Visual Studio, Menu contextuel du Projet / Ajouter / Classe. Dans le fichier créer, on supprime tout à part le namespace ainsi

namespace MonNamespace;

Ensuite, Edition / Collage spécial / Coller le code JSON en tant que classes

namespace MonNamespace;

public class Rootobject
{
    public string type { get; set; }
    public Feature[] features { get; set; }
}

public class Feature
{
    public string type { get; set; }
    public Properties properties { get; set; }
    public Geometry geometry { get; set; }
}

public class Properties
{
    public float version { get; set; }
}

public class Geometry
{
    public string type { get; set; }
    public float[][] coordinates { get; set; }
}

On peut ensuite désérialiser nos données

deserialize

On peut remarquer que les classes ne sont pas pareilles. On peut noter que l’outil de Visual Studio préfère les tableaux au List

public List<Feature> features { get; set; } // json2csharp
public Feature[] features { get; set; }     // visual studio
public List<List<double>> coordinates { get; set; }  // json2csharp
public float[][] coordinates { get; set; }           // visual studio
public double version { get; set; } // json2csharp
public float version { get; set; }  // visual studio
On notera également qu’aucune vérification n’est faite sur les valeurs mises dans les propriétés. Ainsi, un flux dont la version serait fournie en chaîne de caractères au lieu d’une valeur double ne serait pas définie déclenchera un Exception
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {
          "version": "1.0" <--- Exception
      },

Les classes

Vous avez déjà vu les concepts liés à la programmation orientée objet en Java. À quelques exceptions, notamment les propriétés qui remplacent les méthodes getter et setter du Java, programmer des classes et instancier des objets en C# revient au même.

Le site de Microsoft regorge d’exemples et de tutoriels liés aux classes. Par exemple :

Le site developpez.com propose aussi des cours complets.

Exercice traqueur

Pour vous remettre en jambe, ou vous remettre dans le bain, je vous propose un exercice basé sur un exemple réel.

Contexte

Pour suivre un vélo, un colis ou une voiture, on peut lui attacher un traqueur GPS. En utilisant le réseau LoRa par intermédiaire des passerelles thethingsnetwork on peut faire du suivit de position gratuitement sans payer de redevance à un opérateur de téléphonie. Le capteur Dragino LGT-92 remplit exactement cette fonction.

LGT-92-20 LGT-92-10

Après avoir configurer l’application sur le site thethingsnetwork on peut recevoir sur notre propre site, les informations relayées par une des passerelles du réseau thethingsnetwork. Les informations se présentent ainsi :

Flux JSON
{
  "end_device_ids": {
    "device_id": "eui-a8404134e182ff4c",
    "application_ids": {
      "application_id": "pif-bike"
    },
    "dev_eui": "A8404134E182FF4C",
    "join_eui": "A000000000000102",
    "dev_addr": "260BCA94"
  },
  "correlation_ids": [
    "as:up:01GB2N4TJEE8Q26066RY87SXA5",
    "gs:conn:01G9YKB58R24D89HRXSW9T0RQT",
    "gs:up:host:01G9YKB5CXYBDGS8HDYQTYGA72",
    "gs:uplink:01GB2N4TBYJ8D74N34XK19P9P8",
    "ns:uplink:01GB2N4TBZ7FQQDVD7YCVD984C",
    "rpc:/ttn.lorawan.v3.GsNs/HandleUplink:01GB2N4TBZH9B0DRAAQ1Q1ABK9",
    "rpc:/ttn.lorawan.v3.NsAs/HandleUplink:01GB2N4TJDMVQT86W8FBDBYW0M"
  ],
  "received_at": "2022-08-22T11:31:28.974361916Z",
  "uplink_message": {
    "session_key_id": "AYLBMdoVTjlPoGNikaD47g==",
    "f_port": 2,
    "f_cnt": 68,
    "frm_payload": "AsziFABowQgPxmQ=",
    "decoded_payload": {
      "ALARM_status": false,
      "BatV": 4.038,
      "FW": 164,
      "LON": true,
      "MD": "Move",
      "latitude": 46.981652,
      "longitude": 6.86516
    },
    "rx_metadata": [
      {
        "gateway_ids": {
          "gateway_id": "chneuch-fcc23dfffe0ddcf3",
          "eui": "FCC23DFFFE0DDCF3"
        },
        "time": "2022-08-22T11:31:28.753438Z",
        "timestamp": 2772742172,
        "rssi": -111,
        "channel_rssi": -111,
        "snr": -18,
        "location": {
          "latitude": 46.99963179059538,
          "longitude": 6.948739886283875,
          "altitude": 550,
          "source": "SOURCE_REGISTRY"
        },
        "uplink_token": "CiYKJAoYY2huZXVjaC1mY2MyM2RmZmZlMGRkY2YzEgj8wj3//g3c8xCc4JKqChoMCJDSjZgGEKrfxe0CIODav6LZgpQCKgwIkNKNmAYQsJqi5wI=",
        "channel_index": 7,
        "received_at": "2022-08-22T11:31:28.766603178Z"
      },
      {
        "gateway_ids": {
          "gateway_id": "a840411ecc544150",
          "eui": "A840411ECC544150"
        },
        "time": "2022-08-22T11:31:28.786141Z",
        "timestamp": 84387636,
        "rssi": -144,
        "channel_rssi": -144,
        "snr": -20.8,
        "location": {
          "latitude": 46.95884322684129,
          "longitude": 6.833724081516267,
          "altitude": 490,
          "source": "SOURCE_REGISTRY"
        },
        "uplink_token": "Ch4KHAoQYTg0MDQxMWVjYzU0NDE1MBIIqEBBHsxUQVAQtM6eKBoMCJDSjZgGEN7frPkCIKDGlK+6AioMCJDSjZgGEMie7vYC",
        "channel_index": 7,
        "received_at": "2022-08-22T11:31:28.791359454Z"
      }
    ],
    "settings": {
      "data_rate": {
        "lora": {
          "bandwidth": 125000,
          "spreading_factor": 12
        }
      },
      "coding_rate": "4/5",
      "frequency": "867900000",
      "timestamp": 2772742172,
      "time": "2022-08-22T11:31:28.753438Z"
    },
    "received_at": "2022-08-22T11:31:28.767295430Z",
    "consumed_airtime": "1.482752s",
    "locations": {
      "frm-payload": {
        "latitude": 46.981652,
        "longitude": 6.86516,
        "source": "SOURCE_GPS"
      }
    },
    "version_ids": {
      "brand_id": "dragino",
      "model_id": "lgt92",
      "hardware_version": "_unknown_hw_version_",
      "firmware_version": "1.6.6",
      "band_id": "EU_863_870"
    },
    "network_ids": {
      "net_id": "000013",
      "tenant_id": "ttn",
      "cluster_id": "eu1",
      "cluster_address": "eu1.cloud.thethings.network"
    }
  }
}

Travail

Il n’y a pas de notion d’héritage dans ce flux JSON, uniquement des relations de type A UN ou A PLUSIEURS.

Les fichiers se trouvent ici.

json_files_list

On vous demande de mettre en place la(les) classe(s) C# permettant d’accueil le flux JSON. Attention, il y a deux types de fichier et donc deux types de flux JSON :

Ils ne contiennent pas les mêmes informations. Dans les fichiers uplink_message il y a des informations supplémentaires telles que :

Une fois en possession des positions GPS, on vous demande d’afficher le trajet sur une carte telle qu’OpenStreetMap.

Ci-dessous vous pouvez voir un exemple d’application fonctionnelle. On y voit en rouge le trajet d’un vélo qui a fait un aller / retour à Marin-Centre depuis Cormondrèche.

traqueur_csharp

Librairie utilisé : WinFormsMapControl

Suppléments

Pour trouver le rectangle qui engloble le trajet, il faut faire un raisonnement simple.

On cherche les valeurs minimales et maximales de latitude et de longitude. On peut alors créer un rectangle à partir de ces valeurs.

Pour connaître le niveau de zoom à appliquer, c’est un peu plus compliqué. Il faut calculer la distance entre les deux points les plus éloignés, par exemple, le coin haut-gauche et le coin bas-droite du précédent rectangle. On peut alors calculer le niveau de zoom à appliquer en fonction de cette distance.

Pour calculer la distance entre deux points GPS, on peut utiliser le code présent ici.

Pour déterminer le niveau de zoom à appliquer, il faut comprendre comment fonctionne le zoom dans OpenStreetMap. On peut se simplifier la tâche en utilisant uniquement les valeurs à l’équateur.

Source du programme ci-dessus

Exercice analyse spectrale

On vous fournit un programme fonctionnel permettant la capture de son provenant du Système. Par exemple, si vous êtes entrain de visionner une vidéo YouTube et bien ce programme en capture le son.

Source

spectrum

Ce programme est un programme d’exercice ! Il n’est pas sécurisé contre les mauvaises manipulations du programmeur!

Dans le code, on trouve deux variables principales. fftFreq et fftPower de type double[]. fftFreq contient les fréquences musicales auxquels les mesures présentent dans fftPower ont étés réalisées. Les mesures peuvent être faite en dB ou en W via la variable booléenne decibel.

Voici par exemple l’écoute d’un son pur à 1000Hz

1000Hz

Plus le trait est haut, plus la musique à cette fréquence était forte que se soit en dBou en W.

Le fonctionnement qui se cache derrière ce programme ce nomme Transformée de Fourier rapide, en anglais Fast Fourier Transformation ou FFT. Comme on peut le voir sur l’image ci-dessous, une FFT décompose un son ♫ ♬ complexe (en rouge à gauche) en son ♩ ♪ de fréquence unique (en bleu à droite). On passe alors d’une représentation temporelle à une représentation fréquentielle (axe des abscisses).

C’est les valeurs de ce graphique bleu qui se trouvent dans le tableau fftPower et l’axe des abscisses dans fftFreq.

Vous pouvez trouver une bonne explication de cette transformée de Fourier dans la vidéo ci-dessous

Le programme fournit utilise plusieurs librairies:

Votre travail

Votre travail consiste à créer un ensemble de class permettant au programmeur d’avoir une représentation graphique de la musique qu’il entend. Par exemple:

spectrum

Le code du programmeur se résumerai, par exemple, à quelque chose comme :

private Equalizeur equalizeur;

public MainForm()
{
    equalizeur = new Equalizeur(width:600, height:400);
    equalizeur.Location = new Point(0, 0);

    this.Controls.Add(equalizeur);
    this.Paint += (s, e) =>
    {
        spectrum = equalizeur.Spectrum;
        // Code de dessin
    }

    private void Timer_Tick(object sender, EventArgs e)
    {
        Invalidate()
    }

Diagramme de classes

L’enseignant s’est prêté à l’exercice et il a imaginé le diagramme ci-dessous.

L’Equalizeur comporte des Column. Les Column représentent une bande de fréquence, par exemple, de 20Hz (FMin) à 75Hz (FMax). Chaque Column contient des View. Les vues permettent d’afficher la puissance moyenne du signal dans une barre. Ce qui pourrai donner par exemple :

Vous ne devez pas faire le même diagramme que l’enseignant. Vous devez faire le votre. Vous pouvez vous inspirer de celui de l’enseignant mais vous devez le faire à votre sauce. C’est pour cette raison que vous n’aurez pas d’explication sur ce diagramme.

Et en fonctionnement

La bande de fréquence audible par l’oreille humaine va de 20Hz à 20kHz. Si on place 10 colonnes dans la largeur de l’équaliseur, on peut dire que chaque colonne représente une bande de fréquence de :

f=200002010=1998Hz f=\frac{20000-20}{10}=1998Hz

Plus il y a de colonne, plus l’équaliseur est précis. En effet, dans cet exemple, chaque colonne montre la valeur moyenne de 1998 fréquences, hors si on imagine que sur ces 1998 fréquences, 1900 ont une puissance de 0 et 98 ont une puissance de 100, il est difficile de représenter cette situation. Si on fait une simple moyenne on obtient

Pm=(1900*0)+(98*100)1998=98001998=4.9 Pm=\frac{(1900*0)+(98*100)}{1998}=\frac{9800}{1998}=4.9

On aurai donc une colonne avec des vues très basse, pratiquement tout en bas, seul le premier carré vert serait allumé alors qu’il y a quand même 98 fréquences qui étaient au maximum! Au contraire, si on ne prend que la valeur maximale, on aura un colonne au maximum alors qu’il y avait 1900 fréquences à 0.

Avec 64 barres

f=200002064=312Hz f=\frac{20000-20}{64}=312Hz

Pm=(1900*0)+(98*100)312=9800312=31.4 Pm=\frac{(1900*0)+(98*100)}{312}=\frac{9800}{312}=31.4

C’est déjà mieux…

Après moult expériences, il s’avère qu’une bande de fréquence comprise entre 20Hz et 3kHz et préférable pour l’affichage. Le reste des fréquences est peut représentatif et peut comporter des fréquences parasites dite de repliement.

MIS⊥∀ʞƎ

Attention, suivant votre modélisation il peut y avoir potentiellement un gros problème. En effet, si votre diagramme de classe est construit autour du Framework .NET Forms, par exemple :

Et bien cela rendra vos classes inutilisables avec une autre Framework tel que WPF, Unity, Console, Raylib, SDL ou Löve.

Il y a donc deux chemins possibles.

C’est cette dernière version qui vous est demandée.

Une fois votre modélisation terminée, je pourrai vous imposer un Framework pour mettre en œuvre votre équaliseur. Je vous dirai par exemple, faite un POC avec Löve heart.

Documentation de code

La documentation est une partie importante de la programmation.

ph documentation

Elle est souvent générée avec des outils automatiques tel que doxygen, Sandcastle, Hugo ou encore pandoc. Ces outils sont capables de faire beaucoup de choses tout seul. Par exemple, ils sont capables de voir dans quel assembly se trouve votre classe, quelles sont ses méthodes, ses propriétés et ses attributs, quels sont les types de retour, les types des paramètres, quelle est la version actuelle, la précédente etc. mais ils ne sont pas capables de mettre un texte humain tel que:

Accelerometer data of the acceleration of the device in three dimensional space

C’est là votre job. C’est à vous d’écrire le texte compréhensible sur lequel un humain s’appuiera pour comprendre le fonctionnement de votre code.

// If you're reading this, that means you have been put in charge of my
//  previous project. I am so, so sorry for you. God speed.

Exemple

Prenons comme exemple cette documentation de Microsoft .NET MAUI

documentation ms

Il n’y a pas, chez Microsoft, une personne en charge de coder un site HTML statique correspondant à celui-ci. Si c’était le cas, cette personne devrait dialoguer avec le(s) développeur(s) pour bien comprendre ce que fait son code et ensuite transcrire cela en HTML. Elle devrait également mettre à jour le site à chaque fois que le codeur fait une modification dans son code source. Par exemple, si le codeur ajoute un paramètre à une méthode, ou change un type de retour😢. On pourrait aussi demander au codeur de réaliser ce site web en collaboration avec tous les autres codeurs. Ce n’est pas réaliste, ça coûterai cher et ce serait du travail de 🤡. On va plutôt demander au codeur de faire le nécessaire pour qu’un script réalise automatiquement ce site 😎.

Le codeur placera directement dans les fichiers sources les informations nécessaires à la réalisation du site web. Si on se rend dans le code source de l’Accelerometer sur github, on y trouve plusieurs commentaires.

Accelerometer.shared.cs

/// <summary>
/// Accelerometer data of the acceleration of the device in three dimensional space.
/// </summary>
public static class Accelerometer
{
    ...
}
On se rend compte au passage, que tout le code de Microsoft n’est pas entièrement documenté 😞

ph documentation 2

Documentation généraliste

Un autre type de documentation correspond au passage de fichiers Markdown à un site web d’informations plus généraliste. On ne détail pas telle ou telle méthode avec ses paramètres et son canal de retour mais on explique plus généralement l’utilisation, par exemple, d’un composant graphique. On peut le voir avec cette source AbsolutLayout

---
title: "AbsoluteLayout"
description: "The .NET MAUI AbsoluteLayout is used to position and size elements using explicit values, or values proportional to the size of the layout."
ms.date: 01/09/2023
---

# AbsoluteLayout

[![Browse sample.](~/media/code-sample.png) Browse the sample](/samples/dotnet/maui-samples/userinterface-absolutelayout)

:::image type="content" source="media/absolutelayout/layouts.png" alt-text=".NET MAUI AbsoluteLayout." border="false":::

The .NET Multi-platform App UI (.NET MAUI) <xref:Microsoft.Maui.Controls.AbsoluteLayout> is used to position and size children using explicit values. The position is specified by the upper-left corner of the child relative to the upper-left corner of the <xref:Microsoft.Maui.Controls.AbsoluteLayout>, in device-independent units. <xref:Microsoft.Maui.Controls.AbsoluteLayout> also implements a proportional positioning and sizing feature. In addition, unlike some other layout classes, <xref:Microsoft.Maui.Controls.AbsoluteLayout> is able to position children so that they overlap.

Qui offre le rendu suivant sur le site de Microsoft

rendu

Le principe reste le même. Le codeur écrit directement son fichier Markdown dans son dépôt git. Ce fichier est ensuite utiliser pour générer le site web d’informations.

On peut voir au début du fichier Markdown du YAML Front Matter. Ce sont un ensemble de valeur qui seront utilisées par le script de génération du site web pour, par exemple, définir le titre de la page.

Votre documentation

Vos classes de l’équaliseur seront documentées, par exemple en utilisant les balises XML Tags de Microsoft (d’autres styles de balises peuvent être utilisées) de manière à rendre votre code compréhensible par un autre programmeur.

Pour votre documentation, on vous demande d’utiliser l’outil doxygen pour construire une documentation HTML.

Vidéo explicative doxygen

Vous pouvez (devez) utiliser le wizard pour créer votre premier fichier de configuration.

Le wizard vous permettra d’avoir un fichier Doxyfile de base avec tous les paramètres courants correctement définit.

N’utiliser que des chemins d’accès relatifs car lors de la génération automatique, le script ne se lancera pas sur votre poste mais dans un conteneur sur le serveur git

Par la suite, vous devrez être en mesure d’utiliser doxygen en ligne de commande pour pouvoir générer automatiquement votre documentation via un script par exemple.

ferrarip@LMB-101-00 MINGW64 /d/spectrum/doxygen (main)
$ doxygen Doxyfile
D:/spectrum/Program.cs:8: warning: Compound Program is not documented.
D:/spectrum/ColorUtils.cs:8: warning: Compound RaySpectrum::ColorRGB is not documented.
D:/spectrum/ColorUtils.cs:22: warning: Compound RaySpectrum::ColorUtils is not documented.
D:/spectrum/Column.cs:6: warning: Compound RaySpectrum::Column is not documented.
D:/spectrum/View.cs:3: warning: Compound RaySpectrum::View is not documented.
D:/spectrum/ColorUtils.cs:10: warning: Member ColorRGB(byte r=0, byte g=0, byte b=0) (function) of struct RaySpectrum::ColorRGB is not documented.

Cette documentation doit pouvoir être générée depuis un poste Linux, par exemple, une machine Ubuntu WSL (vue en première leçon).

Remarques

Lors du lancement de la commande doxygen depuis un poste Linux, j’ai obtenu des messages d’erreurs supplémentaires. Sans doute parce que le fichier de configuration Doxyfile a été crée par le wizard depuis Windows ou alors parce que les versions installées sur Windows et sur Linux ne sont pas les mêmes.

ferrarip@LMB-101-00:/mnt/d/spectrum$ doxygen doxygen/Doxyfile
warning: ignoring unsupported tag 'CREATE_SUBDIRS_LEVEL' at line 93, file doxygen/Doxyfile
warning: ignoring unsupported tag 'SHOW_HEADERFILE' at line 632, file doxygen/Doxyfile
warning: ignoring unsupported tag 'WARN_IF_INCOMPLETE_DOC' at line 850, file doxygen/Doxyfile

...

Computing class relations...
/mnt/d/spectrum/Program.cs:8: warning: Compound Program is not documented.
/mnt/d/spectrum/ColorUtils.cs:8: warning: Compound RaySpectrum::ColorRGB is not documented.
/mnt/d/spectrum/ColorUtils.cs:22: warning: Compound RaySpectrum::ColorUtils is not documented.

Comme on peut le voir, les warning nous indiquent que certaines parties de code ne sont pas documentées. Il ne devrait pas y avoir de warning dans votre documentation.

Une fois que la documentation se génère correctement, sans erreurs et sans warnings depuis le poste Linux, on peut mettre en place une génération automatique de documentation lors d’un push sur un gestionnaire de version tel que gitlab.

Génération automatique

M. Steeve Droz a réalisé un cours sur le DevOps.

Son cours a été réalisé sur le serveur git de l’école mais il peut évidement être réalisé sur le serveur git de M. Huguenin.

Dans une utilisation standard d’un dépôt de code source, on publie généralement notre code sur une branche dev. Lorsque le code est prêt à être publié, on le fusionne avec la branche main. Quand le code est prêt à être publié en tant que nouvelle version, on ajoute un tag sur la branche main. Par exemple v1.0.0. Pour les numéros de version, on utilise le Semantic Versioning.

En respectant cette méthode de travail, on peut mettre en place un système de génération automatique de la documentation. Par exemple, on peut imaginer que :

Dans ce conteneur:

graph LR;
    A[Poste développement] -->|PUSH| B[Serveur git];
    B[Serveur git] -->|IF 'main' && 'tag'| C[Conteneur LXC ou docker avec copie dépôt];
    C((Conteneur LXC ou docker<br>génération HTML))-->D;
    D[Conteneur LXC ou docker]--> |ftp|E[starks.s2dev.ch];

Si on ne possède pas de serveur distant pour publier le site web, on pourra se contenter de vérifier dans les artifacts que la documentation s’est bien générée.

job_artifacts

Sans publication, c’est-à-dire, uniquement pour la génération de la documentation HTML, le fichier YAML se résume à ceci

stages:
  - build-doc

workflow:
  rules:
    - if: "$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH" # si on travail sur la branche main uniquement

build-job:
  stage: build-doc
  before_script:
    - apt-get update # mise-à-jour des dépôts du conteneur
    - apt-get install -y doxygen # installation de doxygen

  script:
    - doxygen doxygen/Doxyfile # exécute la génération de la documentation

  artifacts:
    expire_in: 10min # les anciens résultats seront disponibles pendant 10 minutes
    untracked: true # prend tout les fichiers mêmes ceux ignorés par .gitignore
    paths:
      - doc # on place dans l'artifact le dossier "doc"

Output

Running with gitlab-runner 15.1.0 (76984217)
...

$ doxygen doxygen/Doxyfile
Doxygen version used: 1.9.1

...

Generating docs for namespace RaySpectrum
Generating docs for compound RaySpectrum::ColorRGB...

...

Uploading artifacts for successful job 00:01
Uploading artifacts...

...

Job succeeded

Bien sûr, cela suppose que le dossier doxygen et son fichier de configuration Doxyfile existe. La commande va générer la documentation dans un dossier doc et c’est ce dossier qui est placer dans l’artifact. Attention, si la génération se fait dans le dossier doc c’est parce que c’est explicitement demandé dans le fichier Doxyfile.

La publication sur le site web

Nous sommes contraints par des règles de sécurité du réseau et des configurations que nous ne maitrisons pas. Seul le service informatique de l’école à la possibilité de modifier, d’ouvrir, de permettre un publication FTP. Jusqu’en décembre 2024, il n’était pas possible de faire une publication FTP depuis le serveur git de l’école. Maintenant, les choses ont changées. Voici comment procéder.

publication FTP

  1. L’enseignant vous fournit un compte FTP (username / password) permettant la connection au serveur starks.s2dev.ch
  2. Par défaut, lors d’une connexion FTP, vous vous retrouvez dans le dossier /root/. Vous devez créer un dossier à votre nom, par exemple /root/ferrari. C’est dans ce dossier que vous ferez votre publication.
  3. Votre dépôt comportera des variables CI/CD FTP_USER et FTP_PASS qui contiendront respectivement votre nom d’utilisateur et votre mot de passe FTP. Votre dépôt ne doit donc pas être public.
  4. Vous devrez modifier votre fichier .gitlab-ci.yml pour ajouter une étape de publication FTP.
  5. Dans le fichier .gitlab-ci.yml, en utilisant le tag docker, vous pourrez utiliser un conteneur plus rapide que les anciens lxc

VM

La VM doit avoir un répertoire partagé avec l’hôte car c’est depuis l’hôte qu’on va ftp. Dans les settings de VMWare.

Settings de la VM -> Options -> Shared Folders 
  x Always enabled

  Add 
    Name : ftp
    Host path: X:/Chemin/de/votre/choix/ftp
    Attributes 
          x Enabled

Dans la VM

$ sudo nano /etc/fuse.conf

# uncomment the line
user_allow_other
 
$ /usr/bin/vmhgfs-fuse .host:/ftp  /home/ubuntu/ftp -o  subtype=vmhgfs-fuse,allow_other
$ mount
...
vmhgfs-fuse on /home/ubuntu/ftp type fuse.vmhgfs-fuse (rw,nosuid,nodev,relatime,user_id=1000,group_id=1000,allow_other)
$ ls -l ftp
total 0

Maintenant les deux dossier sont liés.

On lance FtpWatcher avec la config suivante

LOCAL_FOLDER="X:/Chemin/de/votre/choix/ftp"
REMOTE_FOLDER="/destination/dans/serveur/ftp"

FTP_HOST="cie-101.s2dev.ch"
FTP_USER="sd-eleves"
FTP_PASS="******************"
FTP_TIME_WAIT=5000

LOCAL_FOLDER correspond au dossier qu’on vient de lier dans la VM REMOTE_FOLDER correspond à l’endroit où on souhaite pousser le contenu en FTP FTP_TIME_WAIT permet d’attendre que tous les événements du watcher soient passer avant de pousser en FTP

Runner

Installation

$ curl -LJO "https://s3.dualstack.us-east-1.amazonaws.com/gitlab-runner-downloads/latest/deb/gitlab-runner_amd64.deb"
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
 34  508M   34  173M    0     0  5179k      0  0:01:40  0:00:34  0:01:06 5355k
$ sudo dpkg -i gitlab-runner_amd64.deb
Sélection du paquet gitlab-runner précédemment désélectionné.
(Lecture de la base de données... 80887 fichiers et répertoires déjà installés.)
Préparation du dépaquetage de gitlab-runner_amd64.deb ...
Dépaquetage de gitlab-runner (17.5.3-1) 
Paramétrage de gitlab-runner (17.5.3-1) ...
GitLab Runner: creating gitlab-runner...
Home directory skeleton not used
Runtime platform                                    arch=amd64 os=linux pid=1670 revision=12030cf4 version=17.5.3
gitlab-runner: the service is not installed
Runtime platform                                    arch=amd64 os=linux pid=1678 revision=12030cf4 version=17.5.3
gitlab-ci-multi-runner: the service is not installed
Runtime platform                                    arch=amd64 os=linux pid=1697 revision=12030cf4 version=17.5.3
Runtime platform                                    arch=amd64 os=linux pid=1770 revision=12030cf4 version=17.5.3

Check and remove all unused containers (both dangling and unreferenced)
-----------------------------------------------------------------------
Total reclaimed space: 0B

Depuis la page du dépôt, on crée un runner et on sélectionne Run untagged. On ne touche rien d’autre!

Dpeuis la machine Ubuntu, on lance la commande proposée par le site, du genre:

 gitlab-runner register  
   --url https://git.s2.rpn.ch  
   --token glrt-N9SR32WCAVk_XYZXnyyD

On répond aux questions:

Enter the GitLab instance URL (for example, https://gitlab.com/):

[https://git.s2.rpn.ch]:

Enter a name for the runner. This is stored only in the local config.toml file:

[m347]: fptwatcher

Enter an executor: docker, docker-windows, instance, shell, ssh, virtualbox, docker+machine, kubernetes, docker-autoscaler, custom, parallels:

shell

On démarre ensuite le Runner avec gitlab-runner run

Notre Runner apparait dans les Runners du dépôt. Il faut enlever les autres.

Le job CICD doit être configuré pour générer la documentation et la copier dans le dossier partagé. Une fois la copie effectuée, le FtpWatcher va automatiquement la publier sur le compte FTP.

Héritage

Exemple presque réel

Imaginons une modélisation de classes comprenant:

Un bouton
Une boîte de regroupement
Une case à cocher

Le bouton

Le bouton comporte un attribut permettant de définir sa largeur ainsi qu’un attribut permettant de définir sa hauteur. Il possède également une position X et une position Y permettant de le positionner en fonction de son parent. Il y a une chaîne de caractères pour définir le texte affiché sur le bouton et pourquoi pas, une couleur. Il y a sûrement d’autres attributs mais contentons-nous de ceux-ci.

Il possède un constructeur par défaut qui initialise les attributs avec des valeurs par défaut.

Chaque attribut se verra doté de setter et de getter garantissant que la valeur placée dans l’attribut soit correct.

En C# les setter et les getter son remplacé par des propriétés
public class Button
{
    int width;
    int height;
    int x;
    int y;
    string text;
    Color color;

    public Button()
    {
        width = height = 10;
        x = y = 1;
        text = "button";
        color = Color.Control;
    }

    public int Width
    {
        get { return width; }
        set { width = (value > 0) ? value : 1; }
    }

    // Autres getter/setter (propriétés) similaires pour la largeur, la position etc.
}

La case à cocher

Elle est sensiblement identique au bouton. Elle possède les mêmes attributs. Ce qui diffère du Button avec la case à cocher c’est son nom CheckBox ainsi qu’un attribut supplémentaire tel que checked de type bool qui permet de savoir si on doit afficher ceci ou ceci .

public class CheckBox
{
    int width;
    int height;
    int x;
    int y;
    string text;
    Color color;

    public CheckBox()
    {
        width = height = 10;
        x = y = 1;
        text = "checkbox";
        color = Color.Control;
        Checked = false;
    }

    public int Width
    {
        get { return width; }
        set { width = (value > 0) ? value : 1; }
    }

    // Ceci est un propriété automatique. Pas besoin de déclarer un attribut
    public bool Checked { get; set; }

    // Autres getter/setter (propriétés) similaires pour la largeur, la position etc.
}

La boîte de regroupement

Sans surprise, la boîte de regroupement contient les mêmes attributs que le bouton avec en plus, un attribut de type Collection qui contiendra toutes les références des objets présents dans la boîte.

public class GroupBox
{
    int width;
    int height;
    int x;
    int y;
    string text;
    Color color;

    public GroupBox()
    {
        width = height = 10;
        x = y = 1;
        text = "groupbox";
        color = Color.Control;
    }

    public int Width
    {
        get { return width; }
        set { width = (value > 0) ? value : 1; }
    }

    // Ceci est un propriété automatique. Pas besoin de déclarer un attribut
    public List<object> Objects { get; set; } = new List<object>();

    // Autres getter/setter similaire
}

Constatations

On peut le voir dans ces trois exemples, le programmeur écrit plusieurs fois le même code. Pire encore, il devra écrire autant de fois les tests unitaires. En effet, il devra écrire un test unitaire pour tester la propriété Width du Button et il devra ré-écrire le même test pour la propriété Width du GroupBox ainsi que celle du CheckBox. On peut faire nettement mieux 🤔.

Plutôt que de vous expliquez comment je ferai cette modélisation, je vous présente celle de Microsoft.

poo_result

Les trois classes Button, CheckBox et GroupBox héritent de la classe Control. Les propriétés communes telles que la propriété Width se trouve dans la classe Control.

La classe ci-dessous a été simplifiée pour des questions de lisibilité

poo_control

Tous les classes qui héritent de Control possèdent automatiquement ces propriétés. Le programmeur écrit donc une seule fois le code de ces propriétés. Pour les tests c’est la même chose. On ne test qu’une seule fois la classe Control. Les autres classes ne seront testées que sur leurs propriétés spécifiques. On se concentre donc uniquement sur la spécialisation d’une classe. Par exemple, pour la classe CheckBox, la programmeur se concentrera uniquement sur la propriété Checked .

Vue d’ensemble des relations

On trouve dans ce schéma, 3 types de relations

Dans l’exemple ci-dessus, la différence entre une personne et un chauffeur, c’est que le chauffeur, en plus d’être une personne, possède un permis de conduire. Un chauffeur est donc une spécialisation d’une personne.

Dans la même veine, quand vous créez une fiche dans un programme .NET Windows Desktop, le template vous propose le code suivant

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }
}

Il s’agit d’une nouvelle classe, appelée Form1, qui hérite de Form. La class Form est le modèle de fenêtre de base qui contient tous les comportements par défaut d’une fenêtre tels qu’une taille, une position, le menu système, les boutons de gestion de la fenêtre, etc. Votre classe Form1 sera une spécialisation de la classe parente Form, avec des boutons, des textes, des zones de saisie, des évènements etc. Tout le code que vous allez y apporter sera la spécialisation de cette fenêtre.

Un héritage consiste à créer une nouvelle classe qui reprend tous les avantages de la classe de base, nommé la classe parente et à laquelle on ajoute quelque chose. La classe qui hérite est donc une spécialisation de la classe parente !

Implémentation de l’héritage

Une classe enfant améliore, complète ou spécialise la classe parent. Par exemple Chauffeur est une Personne. On n’a pas besoin de coder ce que la classe Personne fait déjà, on se contente d’ajouter un attribut dtDateDuPermis pour la compléter.

Cet héritage se représente par une flèche.

public class Chauffeur : Personne // Le chauffeur est une Personne...
{
    // Attributs / Propriétés automatiques
    private DateTime dtDatePermisPoidsLourds;   // date d'obtention du permis poids lourd
    public Camion Camion { get; set; }          // et qui peut être lié à un camion

    // Propriétés
    public DateTime DateDuPermis
    {
        get
        {
            return dtDatePermisPoidsLourds;
        }
    }

    // Constructeur
    public Chauffeur(string nom, string prenom, int anneeDeNaissance, bool estMasculin, DateTime dateDuPermis)
        :base(nom, prenom, anneeDeNaissance, estMasculin)
    {
        dtDatePermisPoidsLourds = dateDuPermis;
    }

    public new string Titre
    {
        get => bEstMasculin ? "Conducteur" : "Conductrice";
    }

    // Méthode permettant d'accéder à base.Titre
    public string getTitre()
    {
        return base.Titre;
    }
    // Méthode ToString utilisant un accès à la méthode ToString de la classe de base
    public override string ToString()
    {
        if (Camion == null)
            return Titre + ": " + base.ToString() + " sans camion";
        else
            return Titre + ": " + base.ToString() + " avec " + Camion;
    }
}

public class Personne
{
    // Attributs et propriétés
    public string Nom { get; set; }             // Nom avec propriétés automatiques R/W
    public string Prenom { get; private set; }  // Prénom en lecture seule
    private int iAnneeNaissance;                // Donnée inaccessible en cas d'héritage
    protected bool bEstMasculin;                // Accès possible en cas d'héritage
    public string Titre
    {
        get => bEstMasculin ? "M." : "Mme.";
    }

    // Constructeur et méthodes (y compris getter)
    public Personne(string nom, string prenom, int anneeDeNaissance, bool estMasculin)
    {
        this.Nom = nom;
        this.Prenom = prenom;
        iAnneeNaissance = anneeDeNaissance;
        bEstMasculin = estMasculin;
    }

    public int Age
    {
        get => System.DateTime.Now.Year - iAnneeNaissance;
    }

    public override string ToString() { ... }
}

Utiliser une relation d’héritage

Etablissement de la relation d’héritage

Pour dire qu’une classe hérite d’une autre, on indique simplement le parent après le signe :

Form1 hérite de Form

public class Form1 : Form

Chauffeur hérite de Personne

public class Chauffeur : Personne

Constructeur d’une classe avec héritage

Deux cas peuvent se produire :

public Form1() {...}

Application.Run(new Form1());
public Personne(string nom, string prenom, int anneeDeNaissance, bool estMasculin)

Alors le constructeur de la classe Chauffeur sera :

public Chauffeur(string nom, string prenom, int anneeDeNaissance, bool estMasculin, DateTime dateDuPermis)
    : base(nom, prenom, anneeDeNaissance, estMasculin)

Ce cas est très courant. Vos constructeurs seront donc souvent avec une très longue signature.

Masquage d’un membre hérité

Si, dans la classe Chauffeur, on met une méthode

public override string ToString()

celle-ci va nous masquer la méthode ToString de la classe Personne. Pour pouvoir tout de même l’atteindre, il suffit d’ajouter le mot-clé base.

Exemple:

public override string ToString()
{
    return $"{Titre}: {base.ToString()}";
}

Ce problème est identique avec les autres membres, méthodes, propriétés, etc. Dans notre exemple la propriété Titre de la classe Chauffeur masque celle de la classe Personne.

Pour atteindre cette propriété Titre, on a ajouté un getter dans la classe Chauffeur !

// Propriété Titre qui masque la même propriété de la classe de base
public new string Titre
{
    get => bEstMasculin ? "Conducteur" : "Conductrice";
}

// Getter pour pouvoir atteindre la propriété Titre de la classe de base
public string getTitre()
{
    return base.Titre;
}

Notez le mot clé new qui indique notre volonté de masquage, signalée par un avertissement:

Accès aux membres de la classe de base

Comme la classe enfant hérite de son parent, elle hérite de tous ses membres public, signalés par le signe + ou l’absence d’icône de verrouillage mais aussi de ses membre protected, signalés par le signe # ou l’icône (), mais pas les private (signe moins -, icône )

Voici les tableaux des accès possibles :

public, private et protected

En regardant le tableau ci-dessus, on constate que protected a le niveau d’accès suivant :

Il convient donc, quand on crée une classe, de définir les aux attributs devant être accessibles aux futures classes filles et dans ce cas, choisir le niveau d’accès protected plutôt que private.

Code complet de Chauffeur et Personne

public class Chauffeur : Personne // Le chauffeur est une Personne...
{   // Attributs et propriétés automatiques
    private DateTime dtDatePermisPoidsLourds;   // ...qui a un permis poids lourd
    public Camion Camion { get; set; }          // et qui peut avoir un camion

    // Propriétés
    public DateTime DateDuPermis
    {
        get => dtDatePermisPoidsLourds;
    }

    public new string Titre
    {
        get => bEstMasculin ? "Conducteur" : "Conductrice";
    }

    // Méthode getter permettant d'à nouveau accéder à base.Titre
    public string getTitre()
    {
        return base.Titre;
    }

    // Méthode ToString utilisant un accès à la méthode ToSting de la classe de base
    public override string ToString()
    {
        if (Camion == null)
            return Titre + ": " + base.ToString() + " sans camion";
        else
            return Titre + ": " + base.ToString() + " avec " + Camion;
    }

    // Constructeur utilisant obligatoirement l'unique constructeur de la classe de base
    public Chauffeur(string nom, string prenom, int anneeDeNaissance, bool estMasculin, DateTime dateDuPermis)
        : base(nom, prenom, anneeDeNaissance, estMasculin)
    {
        dtDatePermisPoidsLourds = dateDuPermis;
    }
}

public class Personne
{
    // Attributs et propriétés automatiques
    public string Nom { get; set; }             // Nom avec propriétés automatiques R/W
    public string Prenom { get; private set; }  // Prénom en lecture seule
    private int iAnneeNaissance;                // Donnée inaccessible en cas d'héritage
    protected bool bEstMasculin;                // Donnée accessible en cas d'héritage

    // Propriété
    public string Titre
    {
        get => bEstMasculin ? "M." : "Mme.";
    }

    // Méthodes
    public int Age
    {
        get => System.DateTime.Now.Year - iAnneeNaissance;
    }

    public override string ToString()
    {
        return $"{Titre} {Prenom} {Nom} ({Age} ans)";
    }

    // Constructeur unique de la classe Personne
    public Personne(string nom, string prenom, int anneeDeNaissance, bool estMasculin)
    {
        this.Nom = nom;
        this.Prenom = prenom;
        iAnneeNaissance = anneeDeNaissance;
        bEstMasculin = estMasculin;
    }
}

// Exemple de création d'un chauffeur de camion, avec son camion en cas de clic sur un btn
private void btnChauffeurDeCamion_Click(object sender, EventArgs e)
{
    Chauffeur chauffeurCamion=new Chauffeur("Dupont", "Toto", 1980, true, new DateTime(2022, 12, 24));
    Camion camion = new Camion(new Moteur("Scania"), 28000);
    chauffeurCamion.Camion = camion;
    MessageBox.Show(chauffeurCamion.ToString();
}

Ci-dessous vous trouvez trois documents qui résume la modélisation ainsi que la syntaxe C# correspondante:

Exercice

Une LED est une petite lampe qui peut s’allumer et s’éteindre (comme toutes les lampes 🤪). Sa particularité est une très faible consommation d’électricité et la possibilité d’émettre une lumière d’une seule couleur précise.

Si on souhaite créer une LED en C# et qu’on se trouve dans un environnement graphique tel que le .NET Windows Desktop, il y a fort à parier qu’on puisse hériter d’un composant de base qui se chargera d’offrir une taille, une position, des évènements ainsi que leur binding sur des gestionnaires d’événements, bref, toutes les tâches de base. En fonction du diagramme de classe du Framework .NET déjà vu précédemment, on peut décider de se baser sur l’ancêtre Control.

Nous n’allons pas faire une LED très élaborée mais juste une LED basique que vous pourrez personnaliser par la suite.

led

Nom Type Description
active bool C’est l’attribut qui déterminera si la LED est active ou pas et donc la couleur de cette dernière
colorOff Color C’est la couleur de la LED lorsqu’elle est inactive
colorOn Color C’est la couleur de la LED lorsqu’elle est active
Active bool Propriété liée à l’attribut active (rw)
ColorOn Color Propriété liée à l’attribut colorOn (rw)
ColorOff Color Propriété liée à l’attribut colorOff (rw)

Dans le constructeur, on définira la taille de la LED en utilisant les propriétés héritées Width et Height. On définira également la propriété DoubleBuffered à true.

L’événement Paint sera déclaré en version Lambda. Dans cet événement on dessinera la LED en utilisant l’objet Graphics présent dans le second paramètre de l’événement. La méthode FillEllipse dessinera la LED en utilisant un pinceau de la couleur définie dans ColorOn/ColorOff et en fonction de la propriété Active. Tout le dessin ne se fera que si la propriété Enable héritée de Control est à true.

Le code de la LED sera placé dans un fichier séparé nommé LED.cs.

Comme la classe LED hérite de Control, Visual Studio nous la présente dans la boîte à outils. On peut donc la placer sur notre Form comme n’importe quel autre composant.

Voici un exemple de fonctionnement. La LED est activée ou désactivée grâce à un bouton.

led_off led_off

Quelques liens utiles

Correction

Tetris

Cet exercice est un peu particulier. Il apparaît maintenant dans le cours et ce n’est pas par hasard. Cet exercice est particulier parce qu’il correspond un peu au principe de classe inversée. L’idée est la suivante:

Plutôt que l’enseignant passe 1 heure à vous expliquez un concept, vous allez commencer par le mettre en pratique. Pour ce faire, vous allez suivre ce tutoriel

La seule différence par rapport à ce tutoriel c’est que vous devrez faire des adaptations pour que votre code fonctionne avec autre chose que du WPF. Vous avez le choix entre Forms, Console, Raylib, SDL ou Löve.

L’enseignant c’est prêté à l’exercice en utilisant Raylib.

Une fois que votre programme sera fonctionnel, alors vous expliquerez à l’enseignant le concept particulier que vous avez mis en œuvre dans ce programme.

Exercice héritage, Animation

Créez une class Animate selon les spécifications suivantes:

Vous êtes libre d’ajouter des propriétés et/ou des méthodes à vos classes. La modélisation ci-dessous est donnée à titre d’exemple.

Vous devrez gérer le fait qu’il existe déjà une classe Image dans le framework

Réalisez un programme Windows Form qui met en pratique votre class Animate en réalisant une animation similaire à celle-ci. C’est au programme principal qu’il revient de gérer le timing d’animation.

Si l’image semble saccadée c’est parce que le programme met en évidence une durée différente pour chaque image.

Pour vos images, il suffit de télécharger une image gif animée depuis l’Internet et de l’ouvrir avec le logiciel Gimp. Ce dernier vous permettra l’exporte de chaque image composant l’animation.

Interfaces et classes abstraites

Le contexte

Pour comprendre les interfaces, nous allons commencer par poser un contexte.

Prenons deux objets qui, à priori, n’ont rien en commun.

  1. Voiture

  2. Chien

Ces deux objets possèdent des attributs, des propriétés et des méthodes que nous n’avons pas besoin de connaître.

Imaginons maintenant, que nous possédons un tableau de Voiture ainsi qu’un tableau de Chien et que nous souhaiterions pouvoir trier ces deux tableaux !

Il est évident que trier des Voiture et des Chien ne se fera pas de la même manière. On peut imaginer trier des Voiture par rapport à leur puissance en chevaux vapeur (CV) mais les Chien seront plutôt triés par rapport à leur taille.

Pour permettre le tri d’un tableau de Chien, on pourrait choisir de créer une nouvelle méthode dans la classe Chien, ou si notre hiérarchie d’héritage le permet, on pourrait créer cette méthode dans une class de plus haut niveau. Par exemple, si la classe Chien hérite de la classe Animal et que tous les animaux héritant de la classe Animal se trie de la même manière, on pourrait mettre cette méthode dans la class Animal.

heritage.png

Cependant :

La méthode ne sera pas directement une méthode de tri mais plutôt une méthode de comparaison (utilisée pour réaliser le tri). En effet, en étant dans la class Chien ou Animal, on ne voit qu’un Chien (ou qu’un Animal) et pas tous les Chien du tableau. Un Chien peut alors se comparer à un autre Chien qu’il reçoit en paramètre.

Une fois que la méthode est en place, on devra informer le programmeur de l’existence de notre méthode de comparaison. Ce dernier pourrait alors faire quelque chose comme :

Chien[] tabChiens = new Chien[2];
tabChiens[0] = new Chien("Rantanplan", 50);               // Rantanplan a une taille de 50cm
tabChiens[1] = new Chien("Volt", 34);                     // Volt a une taille de 34cm
int iResultat = tabChiens[0].CompareChiens(tabChiens[1]); // Compare les deux Chien

Le programmeur devra répéter l’opération pour tous les Chien du tableau et mettre en place un algorithme de tri pour trier tout le tableau.

Prenons maintenant le cas de la Voiture. Selon le même principe, on peut imaginer comparer des Voiture ou, si la Voiture hérite d’une class Vehicule, on peut comparer des Vehicule. On aura alors une méthode CompareVehicules ou CompareVoiture.

Avec toutes ces méthodes de comparaisons, le programmeur aura vite fait de se perdre dans les méandres de vos class même si vous respectez une bonne convention de nommage.

C’est ici qu’interviennent les interfaces.

Définition

Les interfaces sont un contrat qu’un objet s’engage à respecter. Ce contrat comporte les méthodes et les propriétés que l’objet devra implémenter à sa manière pour respecter le contrat.

Exemple

Une classe peut implémenter plusieurs interfaces, voyez plutôt la class BindingList<T>


public class BindingList<T> : Collection<T>, IBindingList, ICancelAddNew, IRaiseItemChangedEvents
{
    ...
}

public class Collection<T>: IList<T>, IList, IReadOnlyList<T>
{
    ...
}

public interface IList<T> : ICollection<T>
{
    ...
}

public interface ICollection<T> : IEnumerable<T>
{
    ...
}

public interface IEnumerable<out T> : IEnumerable
{
    ...
}

Elle implémente IBindingList, ICancelAddNew, IRaiseItemChangedEvents, IList, IEnumerable, ICollection

Les interfaces permettent d’ajouter des fonctionnalités à une classe. Elle répondent au quoi mais elles ne fournissent pas l’implémentation et donc ne répondent pas au comment! C’est très important !

Exemple

Grâce à l’interface, .NET MAUI peut fournir une abstraction de plateforme. Voyez l’image ci-dessous:

Le programmeur .NET MAUI utilise une classe Button qui implémente l’interface IButton. Dans l’interface, on trouve tous les quoi mais pas les comment. En effet, les comment dépendent de l’OS final. Un bouton sur Android n’est pas l’équivalent d’un bouton sur Windows. Au moment de la génération du code finale pour la plateforme choisie, par exemple, au moment de la génération de l’APK pour Android, les comment seront réalisés par le AppCompatButton du Framework Android.

Reprenons notre exemple de tri.

Dans le framework .NET, il existe un assembly contenant la class Array. Cette classe propose une méthode Sort qui se propose de trier un tableau. Ainsi, si on possède un tableau d’entier, on peut les trier facilement comme ceci :

Int32[] tabInts = new Int32[] { 1, 34, 54, 98, -12, 34, -128, 45 };
Array.Sort(tabInts); // Et voilà notre tableau est trié
Console.WriteLine(String.Join(",", Array.ConvertAll<int, String>(tabInts, Convert.ToString)));

Sortie:

-128,-12,1,34,34,45,54,98

Dans ce cas, on a qu’à demander à Sort de trier notre tableau de Chien, ou de Voiture ! Et bien ce n’est pas si facile. En effet, comment la méthode Sort peut-elle connaître le critère de tri ? Comment peut-elle savoir comment trier des Voiture ? Des Chien ? Des Fleur ? Des Pixel etc…

Rappelez-vous, l’interface fournit le quoi pas le comment.

Au passage, comment Sort a-t-elle pu trier des Int32 ? La réponse est ici. On peut voir que la struct Int32 implémente l’interface IComparable. Cette interface impose l’implémentation de la méthode CompareTo et c’est cette méthode qui sera utilisée pour trier le tableau d’entier par la méthode Sort.

public struct Int32 : IComparable, IFormattable, IConvertible, IComparable<Int32>, IEquatable<Int32>
{
    internal int m_value;

    public const int MaxValue = 0x7fffffff;
    public const int MinValue = unchecked((int)0x80000000);

    // Compares this object to another object, returning an integer that
    // indicates the relationship.
    // Returns a value less than zero if this  object
    // null is considered to be less than any instance.
    // If object is not of type Int32, this method throws an ArgumentException.
    //
    public int CompareTo(Object value) {
        if (value == null) {
            return 1;
        }
        if (value is Int32) {
            // Need to use compare because subtraction will wrap
            // to positive for very large neg numbers, etc.
            int i = (int)value;
            if (m_value < i) return -1;
            if (m_value > i) return 1;
            return 0;
        }
        throw new ArgumentException (Environment.GetResourceString("Arg_MustBeInt32"));
    }

    ...
}

En réalité, la class Array vous propose de souscrire au contrat de l’interface IComparable.

L’interface IComparable ne propose qu’une seule méthode CompareTo(Object) dans son contrat.

Cette méthode compare deux objets selon un critère choisi par le programmeur et retourne une valeur entière qui peut-être :

Ainsi, si notre class Chien souscrit au contrat de l’interface IComparable, qu’elle respecte ce contrat en implémentant la méthode CompareTo(object), alors nous pourrons utiliser la méthode Sort de la class Array. En d’autre terme, nous avons lié Array et Chien par le contrat IComparable. À l’aide de l’interface on agrémente notre classe d’un quoi et dans la méthode CompareTo on fournit le comment.

Exemple complet avec Animal, Chien et Vache :

Dans cet exemple, on crée deux tableaux. Un contenant des Chien et l’autre des Vache. Tous les deux héritent de la class Animal. On leur attribue un nom ainsi qu’une taille aléatoire en cm pour les Chien et une production de lait aléatoire en litres pour les Vache.

En plus de l’héritage à Animal (Chien EST UN Animal ; Vache EST UN Animal), les deux class Chien et Vache souscrivent au contrat IComparable. Pour respecter ce contrat, elles implémentent la méthode CompareTo. Ces deux class peuvent donc maintenant être appelées par la méthode Sort de la class Array puisqu’elles respectent le contrat.

Classe Animal

public abstract class Animal
{
    public Animal(string nom) { Nom = nom; }
    public string Nom { get; set ; }
}

Classe Chien

public class Chien : Animal, IComparable
{
    public Chien(string nom) : base(nom) { }
    public uint Taille { get; set; }
    public int CompareTo(object? obj)
    {
        Chien? LautreChien = obj as Chien;
        if (LautreChien is not null)
        {
            if (this.Taille < LautreChien.Taille) return -1;
            if (this.Taille > LautreChien.Taille) return 1;
            return 0;
        }
        throw new ArgumentException("On ne peut comparer que des Chiens");
    }
    public override string ToString() { return $"{Nom} : {Taille}"; }
}

Classe Vache

public class Vache : Animal, IComparable
{
    public Vache(string nom) : base(nom) { }
    public uint ProductionLait { get; set; }
    public int CompareTo(object? obj)
    {
        Vache? LautreVache = obj as Vache;
        if (LautreVache is not null)
        {
            if (this.ProductionLait < LautreVache.ProductionLait) return -1;
            if (this.ProductionLait > LautreVache.ProductionLait) return 1;
            return 0;
        }
        throw new ArgumentException("On ne peut comparer que des Vaches");
    }
    public override string ToString() { return $"{Nom} : {ProductionLait}"; }
}

Programme principal

public class Program
{
    public static string converter(Animal animal)
    {
        if (animal == null)
            return "Empty";

        return String.Format("{0,20}", animal);
    }

    public static void Main(string[] args)
    {
        List<string> nomVaches = new() { "Trompette", "Minnie", "Rosette", "Blondie", "Miss Caloway", "Noisette", "Betty" };
        List<string> nomChiens = new() { "Nala", "Maya", "Lucky", "Luna", "Joy", "Naya", "Jack" };
        const int NB_ANIMALS = 5; // Nombre d’animaux

        Random rnd = new Random(DateTime.Now.Millisecond); // Générateur de valeurs aléatoires

        Chien[] chiens = new Chien[NB_ANIMALS]; // Tableau de Chien

        Vache[] vaches = new Vache[NB_ANIMALS]; // Tableau de Vache

        for (int i = 0; i < NB_ANIMALS; i++) // Remplissage des tableaux
        {
            string nom = nomChiens.ElementAt(rnd.Next(0, nomVaches.Count()));
            nomChiens.Remove(nom);
            chiens[i] = new Chien(nom);
            chiens[i].Taille = (uint)rnd.Next(20, 100); // Taille en cm

            nom = nomVaches.ElementAt(rnd.Next(0, nomVaches.Count()));
            nomVaches.Remove(nom);
            vaches[i] = new Vache(nom);
            vaches[i].ProductionLait = (uint)rnd.Next(10, 30); // Production de lait en litres
        }

        // Affiche le tableau avant le tri
        Console.WriteLine($"{Environment.NewLine}Les chiens:");
        Console.WriteLine(String.Join(Environment.NewLine, Array.ConvertAll<Animal, String>(chiens, converter)));


        Console.WriteLine($"{Environment.NewLine}Les vaches:");
        Console.WriteLine(String.Join(Environment.NewLine, Array.ConvertAll<Animal, String>(vaches, converter)));

        // Maintenant on peut trier le tableau
        Array.Sort(chiens);
        Array.Sort(vaches);

        // Affiche le tableau après le tri
        Console.WriteLine($"{Environment.NewLine}Les chiens:");
        Console.WriteLine(String.Join(Environment.NewLine, Array.ConvertAll<Animal, String>(chiens, converter)));


        Console.WriteLine($"{Environment.NewLine}Les vaches:");
        Console.WriteLine(String.Join(Environment.NewLine, Array.ConvertAll<Animal, String>(vaches, converter)));
    }
}

Attention,

Pour trier des tableaux standards, on invoque la méthode Sort de la classe Array :

Array.Sort(LeTableau) ;

Pour ce qui est des structures dynamiques telles que ArrayList ou List<T> on invoque directement la méthode Sort de la collection :

LaCollection.Sort() ;

Résultat :

Avant le tri :

Les chiens:
           Luna : 51
            Joy : 50
           Naya : 95
           Maya : 89
           Nala : 54

Les vaches:
        Blondie : 13
      Trompette : 19
         Minnie : 15
   Miss Caloway : 14
        Rosette : 15

Après le tri :

Les chiens:
            Joy : 50
           Luna : 51
           Nala : 54
           Maya : 89
           Naya : 95

Les vaches:
        Blondie : 13
   Miss Caloway : 14
         Minnie : 15
        Rosette : 15
      Trompette : 19

Extension de classe

Comme vous avez pu le constater, l’interface permet d’étendre les « capacités » de notre class.

class Chien : Animal, IComparable

Notre class Chien hérite de la classe Animal mais on étend ses possibilités grâce à l’interface IComparable.

C’est une notion importante en C# car, contrairement à d’autres langages tel que le C++, le C# ne supporte pas l’héritage multiple.

Création d’une interface

La création d’une interface se fait de la même manière que la création d’une classe. On remplace le mot clé class par le mot clé interface.

Par contre, à la différence des class, il n’y a aucun corps aux méthodes. Dit autrement, dans l’interface, on ne trouve que les signatures des méthodes mais par leur implémentation. C’est normal car, si on reprend notre exemple de Chien et de Vache, les programmeurs de Microsoft ne pouvaient pas savoir comment je souhaitais comparer mes Chien et mes Vache au moment où ils ont créé l’interface IComparable.

Exemple de création d’interface

Nous allons créer une interface permettant d’étendre les possibilités des classe en leur ajoutant la méthode Parler. Nous nommerons cette interface IParler. La convention de nommage implique qu’on commence le nom de l’interface avec un I majuscule. Ensuite, c’est du CamelCase standard.

public interface IParler
{
    string Parler(); // On ne peut pas définir le niveau d’acccessibilité (public, …)
}

Nous allons également ajouter une classe Personne. Nous étendons ses possibilités en souscrivant au contrat de l’interface IParler. Pour respecter le contrat, nous devons implémenter la méthode Parler.

public class Personne : IParler
{
    public string Nom { get; set; }
    public string Parler()
    {
        return "Bonjour!";
    }
}

Nous pouvons également étendre les possibilités de la class Chien en souscrivant au contrat de l’interface IParler.

public class Chien : Animal, IComparable, IParler
{
    public Chien(string nom) : base(nom) { }
    public uint Taille { get; set; }
    public int CompareTo(object? obj)
    {
        Chien? LautreChien = obj as Chien;
        if (LautreChien is not null)
        {
            if (this.Taille < LautreChien.Taille) return -1;
            if (this.Taille > LautreChien.Taille) return 1;
            return 0;
        }
        throw new ArgumentException("On ne peut comparer que des Chiens");
    }
    public override string ToString() { return $"{Nom} : {Taille}"; }
    public string Parler()
    {
        return "Waff!";
    }
}

Notre Chien sait maintenant parler.

Un autre avantage des interfaces, c’est qu’on peut également réunir des objets, qui à priori, n’ont rien en commun, grâce à l’interface commune.

List<IParler> lstParleurs = new List<IParler>();
lstParleurs.Add(new Chien());
lstParleurs.Add(new Personne());

Les diagrammes UML des interfaces

Visual Studio présente les interfaces ainsi dans les diagrammes de classes

image8

Résumé

Exercices

PointXY

On vous demande de réaliser une class PointXY. Cette classe est décrite ci-dessous :

Une fois que la classe est réalisée, on vous demande d’implémenter l’interface IComparable. La comparaison de deux PointXY se fait ainsi :

Comme un PointXY est composé de deux variables X et Y, on peut en calculer une hypoténuse selon la formule

c2=a2+b2 c^2=a^2+b^2

On peut alors représenter C ainsi :

Et dire que C représente l’éloignement du point par rapport à l’origine de la fenêtre qui se trouve en haut à gauche. Ainsi, par rapport à cette origine, on dira que :

Vous réaliserez ensuite une Form qui affichera des PointXY aléatoires mais triés par éloignement ainsi

pointsxy

Pour avoir des PointXY le plus aléatoire possible, vous devez initialiser le générateur avec un sel ainsi:

Random rnd = new Random(DateTime.Now.Millisecond);

Dans le framework .NET il existe une interface IEquatable. Son but est de tester l’égalité de deux objets selon les critères du programmeur. Dans notre cas, on considère que deux PointXY sont équivalent si leurs coordonnées X et Y sont équivalentes.

On vous demande donc d’ajouter l’implémentation de cette interface à votre class PointXY.

Comme la liste de pointXY est générée aléatoirement, il est possible qu’elle contienne deux ou plusieurs pointXY ayant exactement les mêmes valeurs pour X et Y.

Pour éviter cela, on vous demande d’utiliser une List qui gère elle-même le faite de ne pas ajouter deux fois le même élément.

The HashSet<T> class provides high-performance set operations. A set is a collection that contains no duplicate elements, and whose elements are in no particular order.

Imaginons que le générateur fournisse cette liste de points aléatoires contenant des doublons

// (596:240), (596:240), (53:555), (446:312), (289:565), (230:488), (230:488)

Avec une List classique

List<PointXY> pts = new List<PointXY>();

for(...)
{
    pts.Add(randomPoint)
}
// pts contient (596:240), (596:240), (53:555), (446:312), (289:565), (230:488), (230:488)

Avec une liste HashSet

HashSet<PointXY> pts = new HashSet<PointXY>();

for(...)
{
    pts.Add(randomPoint)
}

// pts contient (596:240), (53:555), (446:312), (289:565), (230:488)

Corrigé

IConfigFile

Contexte

On aimerait définir une interface. Les interfaces précisent uniquement le quoi mais pas le comment alors voici les quois:

Ce qui donne:

public interface IConfigFile
{
    string ReadFile();
    void WriteFile(string filename);
    string GetValueFromKey(string key);
    void SetValueForKey(string key, string value);
}

En tant que programmeur, vous voulez implémenter cette interface pour deux types de fichiers.

Fichier =

Les fichiers = sauvegardent les valeurs de configuration selon le mode clé=valeur. On trouve ces fichiers avec le nom .env dans beaucoup de framework. Ils se présentent ainsi :

key1="value with spaces"
key2= value2

key3=    value3

Fichier x

Les fichiers x sauvegardent les valeurs de configuration selon le mode <clé>valeur</clé>. Le fichier de votre projet C# utilise ce format. Ils se présentent ainsi :

<key1>value with spaces</key1>
<key2> value 2</key2>

<key3>value3</key3>

Travail

Développeur de la librairie

On vous demande de réaliser l’interface ainsi que l’implémentation de cette interface dans deux classes abstraites EqualConfigFile et XmlConfigFile. Chacune des deux classes implémentera les spécificités son format.

Vous aurez alors une nano librairie pour travailler avec les fichiers de configuration.

Précisions

Développeur utilisant la librairie

Vous pouvez vous placer maintenant du point de vue de l’utilisateur qui doit travailler avec ce fichier de configuration:

# Size of Windows
WIDTH = 800
HEIGHT = 600
# Title
TITLE = TChat
# Cache folder
FOLDER_CACHE = "C:\Users\%USERNAME%\AppData\Local\Temp"
# Background color
BACK_COLOR = "#97f7de"

Il va utiliser la classe abstraite EqualConfigFile pour travailler avec ce fichier. Il va créer sa classe ConfigFile pour lire le fichier de configuration.

On vous demande de réaliser cette implémentation et d’utiliser les valeurs dans une application WinForm. Dans le FOLDER_CACHE, vous stockerez une copie du fichier .env.

Les classes abstraites

En programmation orientée objet (POO), une classe abstraite est une classe dont l’implémentation n’est pas complète et qui n’est pas instanciable. Elle sert de base à d’autres classes dérivées (héritées).

À la différence des interfaces, les classes abstraites contiennent des parties déjà implémentées mais peuvent aussi contenir des méthodes abstraites, c’est-à-dire, vide, sans code.

La classe abstraite se situe à mi-chemin entre la classe standard et l’interface. Comme l’interface, elle propose, si elle possède des méthodes abstraites, un contrat que le programmeur devra respecter et comme une classe standard, elle propose des méthodes déjà fonctionnelles.

Exemple de classe abstraite

Nous souhaitons mettre à disposition une classe permettant l’affichage de texte selon un degré de gravité. Il existe trois degré de gravités :

On souhaite que cette classe soit en mesure d’afficher les messages aussi bien en mode console qu’en mode graphique. On pourrait même imaginer afficher des messages dans d’autre mode tel que dans une application WPF, dans un jeu Unity, dans un site web etc…

On comprend tout de suite que c’est l’affichage qui sera « le problème ». Chaque mode d’affichage sera implémenté différemment et on ne maitrise pas forcément tous les modes. C’est pour cette raison qu’on choisira une classe abstraite. Voici ce que ça donne :

public abstract class Messager
{
    public void Info(string msg)
    {
        WriteMessage("INFO : " + msg);
    }

    public void Error(string msg)
    {
        WriteMessage("ERROR : " + msg);
    }

    public void Warn(string msg)
    {
        WriteMessage("WARNING : " + msg);
    }

    public abstract void WriteMessage(string text);
}

On voit que la méthode WriteMessage est vide. C’est normal puisque le contenu de cette méthode dépend du mode d’affichage ! Par contre, le reste de la classe est déjà codé. On force donc le programmeur à adopter une structure déjà établie, comme une interface.

Remarque : on ne peut pas instancier une classe abstraite ! Elle ne peut qu’être utilisée dans l’héritage.

Mode console:

public class Program
{
    static void Main(string[] args)
    {
        Logger logger = new Logger();
        int i;
        logger.Info("Vous devriez initialiser votre variable i");
    }
}

public class Logger : Messager
{
    public override void WriteMessage(string text)
    {
        Console.WriteLine(text);
    }
}

Mode graphique:

public partial class Form1 : Form
{
    Logger logger = new Logger();
    public Form1()
    {
        InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        int i;
        logger.Info("Vous devriez initialiser votre variable i");
    }
}

public class Logger : Messager
{
    public override void WriteMessage(string text)
    {
        MessageBox.Show(text);
    }
}

Classes abstraites, interfaces, que choisir ?

Une interface est un contrat proposant une structure et un fonctionnement commun à plusieurs classes. Aucun code n’est présent dans l’interface, seule la structure est présente.

Une classe peut implémenter plusieurs interfaces.

Une classe abstraite propose souvent une structure et un fonctionnement commun à plusieurs classes. Une partie du code est déjà réalisé. Il y a souvent des méthodes abstraites, c’est-à-dire qui devront être implémentées par le programmeur.

Une classe peut hériter d’une seule classe ou classe abstraite.

Comment choisir ?

Une classe abstraite est généralement utilisée pour construire des classes similaires. Elles auront toutes une implémentation en commun, celle de la classe abstraite.

Une interface est généralement utilisée pour définir des capacités (le contrat), même si les classes n’ont pas grand-chose en commun.

Si on peut dire EST UN : il s’agit d’une classe abstraite.
Si on peut dire EST CAPABLE DE : il s’agit d’une interface

Les diagrammes UML des classe abstraite

Ils sont identiques aux classes standards. On distingue une classe abstraite par son nom en italique.

Exercices

ReadFile Abstrait

Un programme faisant partie d’un système d’exploitation, permet la sauvegarde de sa configuration dans deux types de fichier différents.

Fichier =

Les fichiers = sauvegardent les valeurs de configuration selon le mode clé=valeur. Ils se présentent ainsi:

key1="value with spaces"
key2= value2

key3 =  value3

Fichier x Les fichiers x sauvegardent les valeurs de configuration selon le mode <key>valeur</key>. Ils se présentent ainsi:

<key1>value with spaces</key1>
<key2> value2</key2>

<key3>value3</key3>

Travail à réaliser

On vous demande de réaliser l’implémentation des classes représentées dans le diagramme ci-dessous : ReadFile

Exercice classe abstraite, Figure

Modélisez et implémentez les éléments suivants

Toutes les figures possèdent

Les figures peuvent se comparer entre-elles selon leur surface. Elles implémentent donc l’interface IComparable.

Vous ferez un programme de démonstration qui dessine au moins un exemplaire de chaque figure sur une WinForm en utilisant la méthode OnPaint de la Form et l’objet Graphics GDI. Les formes se déplaceront en rebondissant sur les bords de la Form.

Pour mettre en évidence la taille, vous afficherez les figures avec une couleur allant de rouge (0°) à cyan (180°). Plus la figure est grande, plus la couleur est proche du cyan.

Le polymorphisme

Ce nom vient du grec, poly qui signifie « plusieurs », morphê qui signifie « forme ». Polymorphisme signifie donc « qui peut prendre plusieurs formes ».

Il y a généralement trois formes de polymorphisme :

Le polymorphisme ad hoc

Polymorphisme de coercition

C’est la version la plus facile à comprendre. Elle se passe de commentaire.

int positionX = 2;
double mousePositionX = 2.2;
double finalPositionX = positionX + mousePositionX; // Conversion implicite de positionX en double uniquement pour ce calcul

...
// ici, positionX est toujours une variable entière

Le polymorphisme de surcharge

On parle dans ce cas de surcharge des méthodes. Cela représente le fait d’avoir plusieurs méthodes du même nom dans une classe. Ce qui permet au système de faire la distinction entre ces diverses méthodes ce sont le nombre de paramètres ou leurs types (la signature).

Ainsi nous pourrons définir plusieurs méthodes Addition() dans une classe Arithmetique.

public class Arithmetique
{
    // retourne la somme de deux entiers
    public int Addition (int a, int b) { return a + b; }

    // retourne la somme de deux double
    public double Addition (double a, double b) { return a + b; }
}

Il ne serait pas possible d’ajouter une méthode telle que public static int Addition(double a, double b) { return Convert.ToInt32(a + b); }.

Du point de vue de l’utilisateur de la classe, pour ce qui est de l’utilisation de la classe, il n’y verra absolument rien

Arithmetique am = new Arithmetique();

int resultat = am.Addition(12, 34);
double resultat = am.Addition(12.0, 34.0),

🤔 et si le programmeur fait ça ?

Arithmetique am = new Arithmetique();

int resultat = am.Addition(1.2, 3);

Et bien le compilateur va lever une exception. En effet, les paramètres de la méthode lui ferai appeler la version double de la méthode mais il ne peut pas le faire à cause du type de retour int resultat.

Le polymorphisme paramétrique

Le polymorphisme paramétrique fait appel à la généricité. Pour comprendre ce type de polymorphisme, il faut comprendre la généricité. Prenons un exemple.

Liste simplement chainée

Une liste simplement chainée est une structure de donnée où les éléments sont organisés de façon linéaire.

graph LR;
    A((Tête)) --> B["val1 [suivant ⇢]"];
    B --> C["val2 [suivant ⇢]"];
    C --> D["val3 [suivant ⇢]"];
    D --> E((Fin))

Chaque élément est un noeud qui contient une valeur valx ainsi qu’un pointeur [suivant ⇢] sur l’élément suivant. Le pointeur est symbolisé par la flèche qui relie l’élément au suivant. La valeur peut-être un simple int mais peut également être une valeur complexe.

Bien que dans le graphique ci-dessus les noeuds sont présentés comme s’ils se suivaient en mémoire, il est très probable que ceux-ci soient éparpillés au travers des millions d’octets de la mémoire vive.

array_vs_list

On peut faire une implémentation simplifiée, dans le sens où toutes les fonctionnalités de la liste ne seront pas implémentée, ainsi

Implémentation

Noeud

public class Noeud
{
    public Noeud? Suivant { get; set; } = null;
    public int Valeur
    {
        get; private set;
    }
    public Noeud(int val)
    {
        Valeur = val;
    }
    public Noeud(Noeud n)
    {
        Valeur = n.Valeur;
    }
}

ListeEntier

public class ListeEntiers
{
    private Noeud? tete;
    private Noeud? fin;
    private int taille;
    public ListeEntiers() { }
    public void Ajouter(int v)
    {
        Noeud p = new Noeud(v);
        if (Vide)
        {
            tete = fin = p; // tete et fin pointe sur la référence de p
        }
        else
        {
            /*
            fin est actuellement la référence du dernier noeud AVANT l'ajout de p. On fait donc pointer ce noeud
            sur la référence du prochain noeud qui sera ajouter, à savoir p.
            */
            fin.Suivant = p;

            /*
            Maintenant, on déplace fin sur la référence du nouveau noeud p.
            */
            fin = p;
        }
        taille++;
    }
    public void Vider()
    {
        taille = 0;
        tete = fin = null;
    }
    public bool Vide => tete == null;
    public int Taille => taille;
}

Program

class Program
{
    static void Main(string[] args)
    {
        ListeEntiers listeEntiers = new ListeEntiers();
        listeEntiers.Ajouter(1);
        listeEntiers.Ajouter(2);
        listeEntiers.Ajouter(3);
    }
}

listes chainées

Imaginons maintenant qu’on souhaite créer une liste de double, ou encore, une liste de string. Doit-on ré-écrire tout le code à chaque fois ? Non bien sûr. Et c’est là qu’entre en scène le polymorphisme paramétrique.

Au lieu de crée une classe pouvant accueillir un type en particulier, on va créer une classe d’un type générique. Par convention, on utilise la lettre T. L’implémentation devient alors

Implémentation générique

Liste et Noeud

public class Noeud<T>
{
    public Noeud<T>? Suivant { get; set; } = null;
    public T Valeur
    {
        get; private set;
    }
    public Noeud(T val)
    {
        Valeur = val;
    }
    public Noeud(Noeud<T> n)
    {
        Valeur = n.Valeur;
    }
}
class Liste<T>
{
    private Noeud<T>? tete;
    private Noeud<T>? fin;
    private int taille;
    public Liste() { }
    public void Ajouter(T v)
    {
        Noeud<T> p = new Noeud<T>(v);
        if (Vide)
        {
            tete = fin = p;
        }
        else
        {
            if (fin != null)
            {
                fin.Suivant = p;
                fin = p;
            }
        }
        taille++;
    }
    public void Vider()
    {
        taille = 0;
        tete = fin = null;
    }
    public bool Vide => tete == null;
    public int Taille => taille;
}

Program

class Program
{
    static void Main(string[] args)
    {
        Liste<int> listeEntiers = new Liste<int>();
        listeEntiers.Ajouter (1);
        listeEntiers.Ajouter (2);
        listeEntiers.Ajouter (3);

        Liste<string> listeStrings = new Liste<string>();
        listeStrings.Ajouter("Lorem");
        listeStrings.Ajouter("Ipsum");
        listeStrings.Ajouter("Adamet");
    }
}

Comme on peut le voir, il n’y a qu’une implémentation de la classe Liste et de la classe Noeud. C’est ensuite, au compilateur de faire le code correct en fonction du type qu’on souhaite placer dans cette liste.

Du point de vue du programmeur, il y a création d’une liste typée et appel de la méthode Ajouter quel que soit le type de valeur à ajouter.

Exercice

Vous pouvez vous rendre dans la partie Les génériques et réaliser l’exercice correspondant.

Le polymorphisme d’héritage

La possibilité de redéfinir une méthode dans des classes héritant d’une classe de base s’appelle la spécialisation. Il est alors possible d’appeler la méthode d’un objet sans se soucier de son type intrinsèque : il s’agit du polymorphisme d’héritage. Ceci permet de faire abstraction des détails des classes spécialisées d’une famille d’objet, en les masquant par une interface commune qui est la classe de base.

Le Hello World (exemple classique) du polymorphisme d’héritage et le jeu d’échec.

Dans un jeu d’échec on trouve des instances de Roi, Reine, Fou, Cavalier, Tour et Pion, héritant chacun de la classe Piece.

La particularité du jeu d’échec est que le mouvement de chacune de ces pièces est régit par des règles différentes. Ainsi, un Cavalier ne se déplace pas de la même manière qu’un Fou qui lui même ne peut pas se déplacer au même moment qu’un Roietc.

La méthode Mouvement() qui sera spécifique à chaque type de pièce pourra, grâce au polymorphisme d’héritage, effectuer le mouvement approprié en fonction de la classe de l’objet référencé au moment de l’appel. Cela permettra au programme de dire piece.Mouvement() sans avoir à se préoccuper de la classe de la pièce.

Dans l’exemple qui suit, nous avons comme classe parente une classe abstraite. L’exemple aurait aussi été faisable avec une interface qui nous imposait d’écrire une méthode Mouvement().

Exemple

public abstract class Piece
{
    public int PosX {get; set ; }
    public int PosY { get; set; }
    public Piece(int posX, int posY)
    {
        PosX = posX;
        PosY = posY;
    }
    public abstract void Mouvement();
}
public class Roi : Piece
{
    public Roi(int posX, int posY) : base(posX, posY) { }
    public override void Mouvement() => Debug.WriteLine("Déplacement de roi");
}
public class Reine : Piece
{
    public Reine(int posX, int posY) : base(posX, posY) { }
    public override void Mouvement() => Debug.WriteLine("Déplacement de la reine");
}
static void Main(string[] args)
{
    List<Piece> LesBlancs = new List<Piece>();

    LesBlancs.Add(new Roi(1, 4));
    LesBlancs.Add(new Reine(1, 5));
    LesBlancs.Add(new Tour(1, 3));
    LesBlancs.Add(new Tour(1, 6));

    // On pourrait encore faire les autres pièces...

    // Appel des méthodes mouvement de toutes les pièces actuelles
    foreach (Piece piece in LesBlancs)
    {
        piece.Mouvement(); // Polymorphisme -> la bonne méthode est appelée
    }
}

Ce qui nous intéresse donc dans le polymorphisme c’est que lorsque nous avons des objets qui implémentent une interface ou dérivent d’une classe de base abstraite, nous déclarons ces même objets en tant qu’objets de la classe de base ou de l’interface. Ici Piece.

Lorsque nous accédons à nos objets, il nous suffit d’invoquer leur méthode Mouvement() sans avoir à nous préoccuper de savoir s’il s’agit d’un Roi, d’une Reine. La bonne méthode est automatiquement appelée.

Si nous avions créé la classe Piece sans la méthode abstraite Mouvement(), et implémenté Mouvement() normalement dans toutes nos classes dérivées, cela n’aurait pas été possible et le programme principal n’aurait pas compilé puisque la classe Piece n’a pas de méthode Mouvement() !

Les règles de base

Pour profiter du polymorphisme d’héritage il faut absolument respecter ces règles :

public interface IPiece
{
    bool Mouvement();
}
public abstract class Piece
{
    public abstract bool Mouvement();
}
public class Rond
{
    public virtual double Surface()
    {
        return 2 * Rayon * Rayon * Math.PI;
    }
}

Et redéfinie dans la classe héritante (mot clé override)

public class Cylindre : Rond
{
    public override double Surface()
    {
        return base.Surface() + 2 * Math.PI * Rayon * Hauteur;
    }
}

La classe Rond est instanciable et on peut invoquer Surface() sans problèmes. Nous pourrions mélanger des ronds et ces cylindres dans une List de ronds. La bonne méthode Surface() sera appelée selon le type d’objet. On comprend aussi l’intérêt du type caméléon var utilisé ici dans la boucle for.

public static void Main(string[] args)
{
    List<Rond> pieces = new();
    pieces.Add(new Rond(2.3));
    pieces.Add(new Cylindre(10.0, 2.3));

    foreach (var piece in pieces)
    {
        Console.WriteLine($"Type:{piece.GetType().Name} -> {piece.Surface():F2}");
    }

    Console.ReadKey();
}
Type:Rond -> 33.24
Type:Cylindre -> 177.75
Si nous n’avions pas ajouté le mot clé virtual dans Rond et donc non plus override dans Cylindre cela ne fonctionnerait pas et nous appellerions toujours la méthode Surface() de Rond

Version Interface

List<IPiece> LesBlancs = new List<IPiece>();
...
LesBlancs[0].Mouvement();

Version classe abstraite

List<Piece> LesBlancs = new List<Piece>();
...
LesBlancs[0].Mouvement();

Les génériques

Les classes génériques

Les classes génériques sont détaillées dans la section Le polymorphisme paramétrique.

Dans le Framework .NET, il existe une multitude de classes génériques. Elles sont référencées dans l’espace de nom System.Collections.Generic.

Nom Utilisation
Dictionary<TKey,TValue>.KeyCollection Représente la collection de clés dans Dictionary<TKey,TValue>. Cette classe ne peut pas être héritée.
Dictionary<TKey,TValue>.ValueCollection Représente la collection de valeurs dans un Dictionary<TKey,TValue>. Cette classe ne peut pas être héritée.
Dictionary<TKey,TValue> Représente une collection de clés et de valeurs.
HashSet<T> Représente un ensemble de valeurs.
KeyedByTypeCollection<TItem> Fournit une collection dont les éléments sont des types qui font office de clés.
LinkedList<T> Représente une liste doublement liée.
LinkedListNode<T> Représente un nœud dans un LinkedList<T>. Cette classe ne peut pas être héritée.
List<T> Représente une liste fortement typée d’objets accessibles par index. Fournit des méthodes de recherche, de tri et de manipulation de listes.
PriorityQueue<TElement,TPriority>.UnorderedItemsCollection Énumère le contenu d’un PriorityQueue<TElement,TPriority>, sans aucune garantie d’ordre.
PriorityQueue<TElement,TPriority> Représente une collection d’éléments qui ont une valeur et une priorité. Lors de la file d’attente, l’élément dont la valeur de priorité est la plus basse est supprimée.
Queue<T> Représente une collection d’objets premier entré, premier sorti.
SortedDictionary<TKey,TValue>.KeyCollection Représente la collection de clés dans SortedDictionary<TKey,TValue>. Cette classe ne peut pas être héritée.
SortedDictionary<TKey,TValue>.ValueCollection Représente la collection de valeurs dans un SortedDictionary<TKey,TValue>. Cette classe ne peut pas être héritée.
SortedDictionary<TKey,TValue> Représente une collection de paires clé/valeur triées sur la clé.
SortedList<TKey,TValue> Représente une collection de paires clé/valeur triées par clé en fonction de l’implémentation IComparer<T> associée.
SortedSet<T> Représente une collection d’objets tenue à jour en ordre trié.
Stack<T> Représente une collection d’instances à taille variable de type dernier entré, premier sorti (LIFO) du même type spécifié.
SynchronizedCollection<T> Fournit une collection thread-safe qui contient des objets d’un type spécifié à l’aide du paramètre générique sous la forme d’éléments.
SynchronizedKeyedCollection<K,T> Fournit une collection thread-safe qui contient des objets d’un type spécifiés à l’aide d’un paramètre générique et regroupés à l’aide de clés.
SynchronizedReadOnlyCollection<T> Fournit une collection thread-safe en lecture seule qui contient des objets d’un type spécifié à l’aide du paramètre générique sous la forme d’éléments.

Création d’une classe générique

public class MaListeGenerique<T> { }

Par convention nous donnerons le nom T au type générique. En interne nous pourrons définir des membres manipulant des données de type T

public class MaListeGenerique<T>
{
    private uint capacite;
    private uint index;
    private T[] tableau;
    public MaListeGenerique(uint capacite)
    {
        this.capacite = capacite;
        tableau = new T[capacite];
        index = 0;
    }

    public void Add(T str)
    {
        if(index < capacite)
        {
            tableau[index++] = str;
        }
    }

    public T? GetElementAt(uint index)
    {
        return (index < capacite) ? tableau[index] : default(T); // null, 0, false en fonction du type....
    }
}

Utilisation

Voici un exemple d’utilisation de la classe ci-dessus :

public class Program
{
    static void Main(string[] args)
    {
        MaListeGenerique<string> coureurs = new MaListeGenerique<string>(10);
        coureurs.Add("Albert");
        coureurs.Add("Jacques");
        string? nom = coureurs.GetElementAt(8000);

        MaListeGenerique<int> entiers = new MaListeGenerique<int>(10);
        entiers.Add(1);
        entiers.Add(2);
        int v = entiers.GetElementAt(8000);
    }
}

Le mot clé « default »

Comme on ne sait pas à l’avance quel type de données seront placées dans cette liste simple on ne sait pas non plus quelle valeur retournée en tant que valeur par défaut. En effet, si la liste contient des valeurs entières, la valeur par défaut sera 0 mais dans le cas d’une chaîne de caractères, la valeur par défaut est null. C’est exactement pour cette raison qu’il existe le mot clé default.

Dans l’exemple d’utilisation ci-dessus, une erreur à volontairement été faite avec la tentative de récupération de la cellule 8000 d’une liste n’en contenant que 10. Dans le cas où la liste contient des string, la valeur reçue dans la chaîne nom est null alors que pour la variable v la valeur reçue est 0.

Vous pouvez aller voir le code source de la List<T> de Microsoft.

Exercice 1

On aimerait une classe générique qui ne conserve que les nombres paires.

static void Main(string[] args)
{
    Pair<char> listChar = new Pair<char>();
    listChar.Add('2');
    listChar.Add('7');
    listChar.Add('A');
    Console.WriteLine(listChar);

    Pair<int> listInt = new Pair<int>();
    for (int i = 0; i < 10; i++) { listInt.Add(i); }
    Console.WriteLine(listInt);

    Pair<string> listStr = new Pair<string>();
    listStr.Add("23");
    listStr.Add("12954572");
    listStr.Add("124");
    listStr.Add("125");
    listStr.Add("abs125");
    Console.WriteLine(listStr);

    Pair<double> listDbl = new Pair<double>();
    for (double i = 0.0; i < 10.0; i++) { listDbl.Add(i); }
    Console.WriteLine(listDbl);
}

Ce programme affiche ceci

2
0;2;4;6;8
12954572;124
0;2;4;6;8

La classe fait la conversion de type pour savoir s’il s’agit d’un nombre paire. Par exemple, quand on place la valeur “238857” c’est bien le nombre 238857 qui est évalué pour savoir s’il s’agit d’un nombre paire. Ce n’est pas une bidouille du genre est-ce que le dernier caractère fait parti de l’ensemble 2,4,6,8,0….

Exercice 2

La suisse possède un système de coordonnée en 2 dimensions qui lui est propre. Ce système se nomme MN95.

mn95

Il existe également un document qui permet la transformation d’un système suisse MN95 en un système universel tel que WGS84.

On aimerait une classe générique qui permette de savoir si une coordonnée se situe en suisse.

GeoCoordinate<mn95> coordMN95 = new GeoCoordinate<mn95>(1200000, 2600000);      // Berne
bool estEnSuisse = coordMN95.InSwitzerland;                                     // True

GeoCoordinate<wgs84> coordWGS84 = new GeoCoordinate<wgs84>(46.95580, 7.42103);  // Berne aussi
bool estEnSuisse = coordWGS84.InSwitzerland;                                    // True