[TOC]
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#.
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.
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)
{
("Hello World!\n");
puts(0);
exit}
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!
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.
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.
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
On commence par installer le SDK. On peut le trouver à cette adresse Installation manuel.
## 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
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]
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
EXE
pour WindowsLe 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 :
Hello.deps.json
Hello.dll
Hello
Hello.pdb
Hello.runtimeconfig.json
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
💬 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
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.
💬 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
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.
Quand on clique sur le bouton Run Code
que se passe-t-il
?
dotnet new console -n UUID_Unique
Program.cs
par le votre, celui qu’il vient de recevoirdotnet
, par exemple
celui-ci microsoft-dotnet-framework
si le système de conteneur utilisé est dockerdotnet build
et dotnet run
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
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()
{
= new Boomerang();
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);
.AreEqual(result, expected);
Assert}
}
}
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 😉.
Vous avez plusieurs choix pour réaliser un programme en C#.
dotnet
tel que présenté ci-dessus et un éditeur à
coloration syntaxique. Cette solution est très légère mais elle n’est
pas adaptée à un apprentissage du langage C#.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;
.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) ? ");
Consoleif(i.GetType() == typeof(int))
.WriteLine("yes");
Consoleelse
.WriteLine("no");
Console
.Write("i is int ? ");
Consoleif(i is int)
.WriteLine("yes");
Consoleelse
.WriteLine("no");
Console
.Write("obj.GetType() == typeof(object) ? ");
Consoleif(obj.GetType() == typeof(object))
.WriteLine("yes");
Consoleelse
.WriteLine("no");
Console
.Write("obj is object ? ");
Consoleif(obj is object)
.WriteLine("yes");
Consoleelse
.WriteLine("no"); Console
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.
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 :
= new ArrayList();
ArrayList al // Boxing implicite en type object
.Add(12); // Un int
al.Add("Hello"); // Une string
al.Add(new Point(12, 4)); // un point
al
// Unboxing:
int i = (int)al[0];
string s = (string)al[1];
= (Point)al[2]; Point p
Pour plus de détails :
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.
Soit le code suivant:
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)
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
.0 // load the first argument, opcode 0x02
ldarg.1 // load the second argument, opcode 0x03
ldarg// add them, opcode 0x58
add // return the result, opcode 0x2A
ret }
Nous n’allons pas descendre aussi bas dans le détail mais sachez que le compilateur C# est capable traiter ce genre de code.
On peut classer les opérateurs selon le nombre d’opérandes qu’ils acceptent.
Une opération unaire accepte un seul argument
// operator
int operator -(int value)
{
// code ...
}
// usage
int testValue = 12;
int test = -testValue; // test = -12
Une opération binaire accepte deux arguments
// operator
int operator +(int left, int right)
{
// code ...
}
// usage
int testValue1 = 12;
int testValue1 = 21;
int test = testValue1 + testValue2; // test = 33
Un opérateur ternaire accepte trois arguments
int age = 12;
string condition = (age >= 18) ? "Majeur" : "Mineur";
+
,
-
, *
, /
À 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)
{
.x *= scaleFactor; // usage of double '*' built-in operator
vec.y *= scaleFactor; // and double '=' built-in operator
vecreturn vec;
}
}
On peut utiliser cette classe ainsi
= new Vector2(10.0, 3.0);
Vector2 vec1 = new Vector2(27.0, 4.0);
Vector2 vec2
= vec1 + vec2;
Vector2 res1 = vec1 * vec2;
Vector2 res2 = vec1 * 34.5; Vector3 res3
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.
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
= new PointF(11f, 86f);
PointF pt = new PointF(2f, 3f);
PointF pt2
--; // 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 pt
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
😯.
^
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];
.WriteLine(last); // output: 40
Console
string lines = new List<string> { "one", "two", "three", "four", "five" };
string prelast = lines[^2]; // lines.Length = 5 and 5 - 2 = 3 -> lines[3] = "four"
.WriteLine(prelast); // output: four
Console
string word = "Hello";
= ^word.Length;
Index toFirst char first = word[toFirst];
.WriteLine(first); // output: H Console
..
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 :
a..
équivaut à a..ᶺ0
..b
équivaut à 0..b
..
équivaut à 0..ᶺ0
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}
};
<int> values = numbers.SelectMany(_ => _.Where(_ => _ % 2 == 0)).ToList(); List
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
…
L’exemple 2 fournit dans n
…
?
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 ?
En C# il existe des types non-nullable. Ce sont les types de base :
int
double
char
bool
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.
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:
<int> age = DBProfilGetAge(); Nullable
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.
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()
{
= null;
Person person .WriteLine(person.Name);
Console}
}
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()
{
= null;
Person person .WriteLine(person?.Name);
Console}
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;
= 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 foundAt
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 !
?:
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
? b : c ? d : e a
est évaluée comme étant
? b : (c ? d : e) a
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
<int> numbers = null;
List// Dans ce cas, on lui affecte une nouvelle liste de nombres entiers
// et numbers n'est plus null
??= new List<int> { 1, 2, 3, 4, 5, 6 };
numbers // comme numbers n'est plus null, la ligne ci dessous ne fera rien
??= new List<int> { 10, 20, 30, 40, 50, 60 };
numbers // numbers contient 1, 2, 3, 4, 5, 6
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
.
?
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();
.WriteLine("La méthode à retourné: {0}", /* votre opérateur ternaire */)
Console}
public static int DBProfilGetAge()
{
= new Random();
Random rand int randomAge = rand.Next(1, 101);
return /* votre opérateur ternaire */;
}
=>
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!";
.WriteLine(GetWeatherDisplay(15)); // output: Cold.
Console.WriteLine(GetWeatherDisplay(27)); // output: Perfect! Console
Code testable ici
Dans cet exemple, une méthode GetWeatherDisplay
a été
définie avec, comme corps d’expression
tempInCelsius < 20.0 ? "Cold." : "Perfect!";
.
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}";
}
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 qui a une expression comme corps :
(input-parameters) => expression
Instruction lambda qui a un bloc d’instructions comme corps :
(input-parameters) => { <sequence-of-statements> }
<int> listSupSix = listEnVrac.FindAll(x => x > 6); List
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.
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:
.SendCompleted += new EventHandler(smtpClient_SendCompleted);
smtpClient
private void smtpClient_SendCompleted(object sender, EventArgs e)
{
.WriteLine("Email sent");
Console}
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
.SendCompleted += (s, a) => Console.WriteLine("Email sent"); smtpClient
Dans le framework .NET MAUI il existe des Animation
qui
permettent, par exemple, de créer un spinner de
téléchargement
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
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)
{
.Rotation = animationValue;
LabelLoad}
= new Animation(Animation_Callback, 0, 360, Easing.Linear); Animation animate0To360
Cette syntaxe peut être abrégée ainsi avec les Lambda
= new Animation(
Animation animate0To360 (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
.Commit(
animate0To360this, //owner
"rotate", //name of your choice
16, //number of milliseconds between each callback
1000, //duration of the animation, in milliseconds
.Linear, //animation type
Easing(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.
public partial class MainPage : ContentPage
{
private Animation animate0To360;
private bool animate;
public MainPage()
{
InitializeComponent();
}
protected override void OnAppearing()
{
= false;
animate .Text = "Animate please";
ToogleAnim= new Animation((deg) => LabelLoad.Rotation = deg, 0, 360, Easing.CubicInOut);
animate0To360 }
public void Toogle_Click(object sender, EventArgs e)
{
if (!animate)
{
.Commit(this, "rotate", 16, 1000, Easing.CubicInOut,
animate0To360(v, c) => LabelLoad.Rotation = 0,
() => animate
);
}
else
{
this.AbortAnimation("rotate");
}
= !animate;
animate .Text = animate ? "Animated" : "Animate please";
ToogleAnim}
}
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.
Vous devez coder chaque exercice. Vous devez créer un projet par exercice et placer tous les projets dans une seule solution.
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)
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)
{
<Person> persons = new List<Person> {
Listnew Person("Alain", "Proviste"),
new Person("Anne", "Onime"),
new Person("Déborah", "Dirouge"),
new Person("Cécile", "Our")
};
//you can use string.StartsWith
<Person> filtered = persons.Where( /* votre lambda */ ).ToList();
List}
}
(fn => fn.FullName.StartsWith(“A”))
Soit le programme suivant
class Program
{
static void TimerCallback(Object? state)
{
.WriteLine("Timer tick at : {0}", DateTime.Now.ToString("H:mm:ss.fff", CultureInfo.CurrentCulture));
Console}
static void Main(string[] args)
{
int attenteAvantStart = 2000; // 2 secondes
int interval = 100; // 100 ms
= new Timer(callback: TimerCallback); // <-- ICI ICI ICI ICI
Timer timer
.Change(attenteAvantStart, interval);
timer
.WriteLine("Press any key to quit");
Console// "Tant que" aucune touche n'est pressée
while (!Console.KeyAvailable) ;
// Supprime le timer
.Dispose();
timer}
}
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)));
Soit les exemples de méthode ci-dessous
= () => Console.WriteLine();
Action line <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; Func
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:
<double, double> carre = (x) => x * x;
Funcdouble 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”);
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
= f(4); // result = 24 result
Func<int, int> f = (n) => Enumerable.Range(1,
n).Aggregate(1, (p, item) => p * item);
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)
{
.Default.ParseArguments<Options>(args)
Parser.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.
WithParsed
utilisera un opérateur ternaire sur l’option
Verbose
pour afficher le Message
si
Verbose
est true
et le texte
"Nothing"
la cas échéant.WithNotParsed
transformera le IEnumerable
en List
grâce à la méthode ToList
pour pouvoir
utiliser la méthode ForEach
de la List
. Dans
la méthode ForEach
de la liste, vous placerez une
expression Lambda qui affichera le message
"Le parser à rencontrer une erreur de type: {Erreur.Tag}"
Pour terminer, vous n’utiliserez pas le Parser.Default
mais votre propre Parser
que vous instancierez ainsi
= new Parser(config => config.HelpWriter = null); Parser parser
Dans mon programme j’ai fait des tests avec les arguments suivants
"Nothing"
-m Mon_message
↣ affichage de
"Nothing"
-m Mon_message -v
↣ Affichage de
"Mon_message"
-m Mon_message -v -x
↣ Affichage de
"Le parser à rencontrer une erreur de type: UnknownOptionError"
Parser parser = new Parser(config => config.HelpWriter =
null); parser.ParseArguments
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)
{
= new Cylindre(10.0, 2.3);
Cylindre cylindre
= File.OpenWrite("temp.dat"); // sauvegarde dans un fichier
FileStream f string jsonString = JsonSerializer.Serialize<Cylindre>(cylindre); // sérialsation
.WriteAsync(Encoding.UTF8.GetBytes(jsonString)); // écriture asynchrone
await f.DisposeAsync(); // fermeture du fichier
await f
.ReadKey();
Console}
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)
{
= File.OpenRead("temp.dat");
FileStream openStream ? cylindre = await JsonSerializer.DeserializeAsync<Cylindre>(openStream);
Cylindre.DisposeAsync();
await openStream
.WriteLine($"Hauteur: {cylindre?.Hauteur}\nRayon: {cylindre?.Rayon}\nSurface: {cylindre?.Surface():F2}");
Console.ReadKey();
Console}
Hauteur: 10
Rayon: 2.3
Surface: 177.75
Imaginons que nous soyons en possession d’un flux JSON tel que celui ci-dessous
{
"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
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
double
ne serait pas définie déclenchera un Exception
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"version": "1.0" <--- Exception
},
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.
Pour vous remettre en jambe, ou vous remettre dans le bain, je vous propose un exercice basé sur un exemple réel.
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.
![]() |
![]() |
---|
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 :
{
"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"
}
}
}
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.
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.
Librairie utilisé : WinFormsMapControl
ToString()
==
. La
comparaison se fera sur le champ recevied_at
. Pour faire
cet opérateur, vous devrez implémenter l’opérateur !=
et
les méthodes GetHashCode
et Equals
.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.
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.
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
Plus le trait est haut, plus la musique à cette fréquence était forte
que se soit en dB
ou 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 consiste à créer un ensemble de class permettant au programmeur d’avoir une représentation graphique de la musique qu’il entend. Par exemple:
Le code du programmeur se résumerai, par exemple, à quelque chose comme :
private Equalizeur equalizeur;
public MainForm()
{
= new Equalizeur(width:600, height:400);
equalizeur .Location = new Point(0, 0);
equalizeur
this.Controls.Add(equalizeur);
this.Paint += (s, e) =>
{
= equalizeur.Spectrum;
spectrum // Code de dessin
}
private void Timer_Tick(object sender, EventArgs e)
{
Invalidate()
}
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 :
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 :
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
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
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.
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 :
Equalizeur
hérite de
Control
Color
qui n’existe que dans le Framework .NET FormsEt 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
.
La documentation est une partie importante de la programmation.
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
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
{
...
}
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 the sample](/samples/dotnet/maui-samples/userinterface-absolutelayout)
:::image type="content" source="media/absolutelayout/layouts.png" alt-text=".NET MAUI AbsoluteLayout." border="false":::
<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. The .NET Multi-platform App UI (.NET MAUI)
Qui offre le rendu suivant sur le site de Microsoft
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.
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.
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
.
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 :
main
et que
ce commit contient un tag
, j’aimerais démarrer un
conteneur.Dans ce conteneur:
documentation/Doxyfile
je pourrai lancer la commande
$ doxygen documentation/Doxyfile
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.
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
.
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
/root/
. Vous devez créer un dossier à votre nom,
par exemple /root/ferrari
. C’est dans ce dossier que vous
ferez votre publication.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..gitlab-ci.yml
pour
ajouter une étape de publication FTP..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
$ 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.
Imaginons une modélisation de classes comprenant:
Un bouton |
![]() |
Une boîte de regroupement | |
Une case à cocher |
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.
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()
{
= height = 10;
width = y = 1;
x = "button";
text = Color.Control;
color }
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.
}
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()
{
= height = 10;
width = y = 1;
x = "checkbox";
text = Color.Control;
color = false;
Checked }
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.
}
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()
{
= height = 10;
width = y = 1;
x = "groupbox";
text = Color.Control;
color }
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
}
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.
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
.
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
.
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.
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)
{
= dateDuPermis;
dtDatePermisPoidsLourds }
public new string Titre
{
=> bEstMasculin ? "Conducteur" : "Conductrice";
get }
// 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
{
=> bEstMasculin ? "M." : "Mme.";
get }
// Constructeur et méthodes (y compris getter)
public Personne(string nom, string prenom, int anneeDeNaissance, bool estMasculin)
{
this.Nom = nom;
this.Prenom = prenom;
= anneeDeNaissance;
iAnneeNaissance = estMasculin;
bEstMasculin }
public int Age
{
=> System.DateTime.Now.Year - iAnneeNaissance;
get }
public override string ToString() { ... }
}
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
Deux cas peuvent se produire :
Form
possède un constructeur par
défaut et donc, quand vous instanciez votre fenêtre Form1
,
vous utiliser implicitement le constructeur par défaut de
Form
.public Form1() {...}
.Run(new Form1()); Application
Vous pouvez bien sûr utiliser des paramètres comme tout constructeur spécifique.
La classe de base ne possède que des constructeurs paramétrés. Vous devez donc appeler le constructeur paramétré de la classe de base après la fermeture de la parenthèse de votre constructeur. Les paramètres fournis au constructeur de la classe de base ne peuvent être que des valeurs en dur ou des noms de paramètres provenant de votre constructeur, sans indiquer de type. Par exemple, si la signature du constructeur de la classe de base Personne est :
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.
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
{
=> bEstMasculin ? "Conducteur" : "Conductrice";
get }
// 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:
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 :
En regardant le tableau ci-dessus, on constate que
protected
a le niveau d’accès suivant :
protected
sont visibles comme en public
.Personne
ou de
Chauffeur
), les attributs protected
sont
invisibles, comme en private
.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
.
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
{
=> dtDatePermisPoidsLourds;
get }
public new string Titre
{
=> bEstMasculin ? "Conducteur" : "Conductrice";
get }
// 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)
{
= dateDuPermis;
dtDatePermisPoidsLourds }
}
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
{
=> bEstMasculin ? "M." : "Mme.";
get }
// Méthodes
public int Age
{
=> System.DateTime.Now.Year - iAnneeNaissance;
get }
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;
= anneeDeNaissance;
iAnneeNaissance = estMasculin;
bEstMasculin }
}
// 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)
{
=new Chauffeur("Dupont", "Toto", 1980, true, new DateTime(2022, 12, 24));
Chauffeur chauffeurCamion= new Camion(new Moteur("Scania"), 28000);
Camion camion .Camion = camion;
chauffeurCamion.Show(chauffeurCamion.ToString();
MessageBox}
Ci-dessous vous trouvez trois documents qui résume la modélisation ainsi que la syntaxe C# correspondante:
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.
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
.
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.
![]() |
![]() |
---|
Quelques liens utiles
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.
Créez une class Animate
selon les spécifications
suivantes:
Animate
est un PictureBox
Animate
possède un attribut _images
de
type List<Image>
Animate
possède une méthode Anime
qui
lance une animation, c’est-à-dire, qui positionne l’attribut interne ou
la propriété automatique IsAnimate
à true
Animate
possède une propriété permettant de savoir si
l’animation est en coursImage
de la collection, il y a une durée
associée, par exemple, l’image 0 à une durée d’affichage de 100ms,
l’image 1 à une durée de 200ms etc.Image vidéo 2 (150ms).png
Animate
possède une propriété CurrentImage
qui retourne l’Image
couranteAnimate
possède une méthode MoveToNext
qui
permet de sélectionner la prochaine (ou la première si on a fait le
tour) Image
Animate
possède une méthode Add
permettant
d’ajouter des imagesImage
possède une propriété entière permettant de
connaître la durée d’affichage d’une image en millisecondesImage
possède une propriété permettant d’obtenir
l’attribut Bitmap
interneVous ê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.
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.
Pour comprendre les interfaces, nous allons commencer par poser un contexte.
Prenons deux objets qui, à priori, n’ont rien en commun.
Voiture
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
.
Cependant :
Si on place la méthode dans la class Animal
, ça peut
prêter à confusion. En effet, est-il possible de comparer un
Chien
et un Canard
? Est-ce que cela a du sens
malgré qu’ils héritent tous les deux de la class
Animal
?
La comparaison ne se fera pas toujours sur la taille. Par
exemple, une Vache
pourrait être comparée à une autre
Vache
par rapport à sa production de lait.
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 :
[] tabChiens = new Chien[2];
Chien[0] = new Chien("Rantanplan", 50); // Rantanplan a une taille de 50cm
tabChiens[1] = new Chien("Volt", 34); // Volt a une taille de 34cm
tabChiensint 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.
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.
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
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.
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 :
[] tabInts = new Int32[] { 1, 34, 54, 98, -12, 34, -128, 45 };
Int32.Sort(tabInts); // Et voilà notre tableau est trié
Array.WriteLine(String.Join(",", Array.ConvertAll<int, String>(tabInts, Convert.ToString))); Console
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 :
-1 si l’objet est inférieur selon le critère de tri
0 si l’objet est identique selon le critère de tri
1 si l’objet est supérieur selon le critère de tri
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.
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.
public abstract class Animal
{
public Animal(string nom) { Nom = nom; }
public string Nom { get; set ; }
}
public class Chien : Animal, IComparable
{
public Chien(string nom) : base(nom) { }
public uint Taille { get; set; }
public int CompareTo(object? obj)
{
? LautreChien = obj as Chien;
Chienif (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 class Vache : Animal, IComparable
{
public Vache(string nom) : base(nom) { }
public uint ProductionLait { get; set; }
public int CompareTo(object? obj)
{
? LautreVache = obj as Vache;
Vacheif (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}"; }
}
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)
{
<string> nomVaches = new() { "Trompette", "Minnie", "Rosette", "Blondie", "Miss Caloway", "Noisette", "Betty" };
List<string> nomChiens = new() { "Nala", "Maya", "Lucky", "Luna", "Joy", "Naya", "Jack" };
Listconst int NB_ANIMALS = 5; // Nombre d’animaux
= new Random(DateTime.Now.Millisecond); // Générateur de valeurs aléatoires
Random rnd
[] chiens = new Chien[NB_ANIMALS]; // Tableau de Chien
Chien
[] vaches = new Vache[NB_ANIMALS]; // Tableau de Vache
Vache
for (int i = 0; i < NB_ANIMALS; i++) // Remplissage des tableaux
{
string nom = nomChiens.ElementAt(rnd.Next(0, nomVaches.Count()));
.Remove(nom);
nomChiens[i] = new Chien(nom);
chiens[i].Taille = (uint)rnd.Next(20, 100); // Taille en cm
chiens
= nomVaches.ElementAt(rnd.Next(0, nomVaches.Count()));
nom .Remove(nom);
nomVaches[i] = new Vache(nom);
vaches[i].ProductionLait = (uint)rnd.Next(10, 30); // Production de lait en litres
vaches}
// Affiche le tableau avant le tri
.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)));
Console
// Maintenant on peut trier le tableau
.Sort(chiens);
Array.Sort(vaches);
Array
// Affiche le tableau après le tri
.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)));
Console}
}
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
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.
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
.
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)
{
? LautreChien = obj as Chien;
Chienif (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.
<IParler> lstParleurs = new List<IParler>();
List.Add(new Chien());
lstParleurs.Add(new Personne()); lstParleurs
Visual Studio présente les interfaces ainsi dans les diagrammes de classes
On ne peut pas instancier d’objet issus d’une interface. On ne pourrait pas, par exemple, créer un objet IParler Compiler Error CS0526, Compiler Error CS0144
A partir de C# 8.0, une interface peut contenir une implémentation explicite par défaut. L’exemple ci-dessous est donc valide.
public interface IControl
{
void Paint() => Console.WriteLine("Default Paint method");
}
public class SampleClass : IControl
{
// Paint() is inherited from IControl.
}
...
= new SampleClass();
SampleClass sample //sample.Paint();// "Paint" isn't accessible.
= sample as IControl;
IControl control .Paint(); control
L’idée est de permettre l’ajout d’une fonctionnalité à une interface existante sans devoir ré-écrire toutes les classes qui implémentaient l’ancienne version démonstration en Java. Tant qu’on peut y échapper, on évitera de mettre un corps aux méthodes présentent dans les interfaces.
Une classe ou un struct
qui implémente l’interface
doit implémenter tous ses membres.
Une classe ou un struct
peuvent implémenter
plusieurs interfaces. Une classe peut hériter d’une classe de base et
également implémenter une ou plusieurs interfaces.
On ne peut pas définir de niveau d’accessibilité aux membres
d’une interface. Par défaut ils sont public
et
abstract
.
On vous demande de réaliser une class PointXY. Cette classe est décrite ci-dessous :
Le nom de la classe est PointXY.
Il y a un attribut X et un attribut Y. Ils sont tous les deux de type int.
Chaque attribut possède une propriété du même nom.
Les valeurs de X et Y ne peuvent pas être négatives.
Il n’existe qu’un constructeur spécifique prenant en paramètre une valeur pour chaque attribut.
La classe comporte une méthode ToString retournant les valeurs des deux attributs ainsi "valeurX:valeurY".
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
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 :
Un PointXY est considéré comme plus grand qu’un autre PointXY si sa valeur C est plus grande.
Un PointXY est considéré comme plus petit qu’un autre PointXY si sa valeur C est plus petite.
Un PointXY est considéré comme équivalent à un autre PointXY si les deux valeurs C sont identiques.
Vous réaliserez ensuite une Form
qui affichera des
PointXY aléatoires mais triés par éloignement ainsi
Pour avoir des PointXY le plus aléatoire possible, vous devez initialiser le générateur avec un sel ainsi:
= new Random(DateTime.Now.Millisecond); Random rnd
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
<PointXY> pts = new List<PointXY>();
List
for(...)
{
.Add(randomPoint)
pts}
// pts contient (596:240), (596:240), (53:555), (446:312), (289:565), (230:488), (230:488)
Avec une liste HashSet
<PointXY> pts = new HashSet<PointXY>();
HashSet
for(...)
{
.Add(randomPoint)
pts}
// pts contient (596:240), (53:555), (446:312), (289:565), (230:488)
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
kv
, par exemple :
config.kv
#
sont des
commentairesFichier 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>
kvx
, par exemple :
config.kvx
/
sont
des commentairesDé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
filename
reçu via
le constructeurcontent
est remplit via la méthode
ReadFile
WriteFile
permet la sauvegarde des
modifications apportées via SetValueForKey
dans un nouveau
fichier reçu en paramètrecontent
. On ne peut pas non plu l’excraser avec
la méthode WriteFile
ReadFile
est laissé public
à des fins de
débugage. Si content
est vide, on lit le fichier sinon on
affiche content
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
.
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.
Nous souhaitons mettre à disposition une classe permettant l’affichage de texte selon un degré de gravité. Il existe trois degré de gravités :
INFO
: c’est un message d’information qui n’a aucune
incidence sur le fonctionnement du programme
WARNING
: c’est un message qui nous informe que le
programme a rencontré une situation imprévue. Le programme peut
continuer de fonctionner sans problème mais les résultats à venir
peuvent être erronés
ERROR
: une erreur est survenue et le programme ne
peut pas continuer de fonctionner normalement
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)
{
= new Logger();
Logger logger int i;
.Info("Vous devriez initialiser votre variable i");
logger}
}
public class Logger : Messager
{
public override void WriteMessage(string text)
{
.WriteLine(text);
Console}
}
Mode graphique:
public partial class Form1 : Form
{
= new Logger();
Logger logger public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
int i;
.Info("Vous devriez initialiser votre variable i");
logger}
}
public class Logger : Messager
{
public override void WriteMessage(string text)
{
.Show(text);
MessageBox}
}
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.
Ils sont identiques aux classes standards. On distingue une classe abstraite par son nom en italique.
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
kv
, par exemple :
config.kv
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>
kvx
, par exemple :
config.kvx
Travail à réaliser
On vous demande de réaliser l’implémentation des classes représentées
dans le diagramme ci-dessous :
ReadFile
devra compléter la liste de type
Dictionary<string, string>
avec le contenu du fichier
dont le nom et le chemin d’accès seront passé en paramètreDictionary
, la première valeur sera la clé, et
la seconde, la valeur. Ce qui pourrait s’écrire en français
Dictionary<clé, valeur>
FileNotFoundException
FileFormatException
Dictionary
dans une
ListBox
graphiqueListBox
sera bindée avec le
Dictionary
via la propriété DataSource
fileName
sera obtenu via un
openFileDialog
Modélisez et implémentez les éléments suivants
Toutes les figures possèdent
double Surface()
qui retourne la surface de
la figuredouble Perimetre()
qui retourne le
périmètre de la figurevoid MoveTo(int x, int y)
qui déplace la
figurestring ToString()
qui retourne une chaîne
de caractères représentant la figureLes 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.
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 :
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
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
= new Arithmetique();
Arithmetique am
int resultat = am.Addition(12, 34);
double resultat = am.Addition(12.0, 34.0),
🤔 et si le programmeur fait ça ?
= new Arithmetique();
Arithmetique am
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 fait appel à la généricité. Pour comprendre ce type de polymorphisme, il faut comprendre la généricité. Prenons un exemple.
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.
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
Noeud
public class Noeud
{
public Noeud? Suivant { get; set; } = null;
public int Valeur
{
get; private set;
}
public Noeud(int val)
{
= val;
Valeur }
public Noeud(Noeud n)
{
= n.Valeur;
Valeur }
}
ListeEntier
public class ListeEntiers
{
private Noeud? tete;
private Noeud? fin;
private int taille;
public ListeEntiers() { }
public void Ajouter(int v)
{
= new Noeud(v);
Noeud p if (Vide)
{
= fin = p; // tete et fin pointe sur la référence de p
tete }
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.
*/
.Suivant = p;
fin
/*
Maintenant, on déplace fin sur la référence du nouveau noeud p.
*/
= p;
fin }
++;
taille}
public void Vider()
{
= 0;
taille = fin = null;
tete }
public bool Vide => tete == null;
public int Taille => taille;
}
Program
class Program
{
static void Main(string[] args)
{
= new ListeEntiers();
ListeEntiers listeEntiers .Ajouter(1);
listeEntiers.Ajouter(2);
listeEntiers.Ajouter(3);
listeEntiers}
}
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
Liste et Noeud
public class Noeud<T>
{
public Noeud<T>? Suivant { get; set; } = null;
public T Valeur
{
get; private set;
}
public Noeud(T val)
{
= val;
Valeur }
public Noeud(Noeud<T> n)
{
= n.Valeur;
Valeur }
}
class Liste<T>
{
private Noeud<T>? tete;
private Noeud<T>? fin;
private int taille;
public Liste() { }
public void Ajouter(T v)
{
<T> p = new Noeud<T>(v);
Noeudif (Vide)
{
= fin = p;
tete }
else
{
if (fin != null)
{
.Suivant = p;
fin= p;
fin }
}
++;
taille}
public void Vider()
{
= 0;
taille = fin = null;
tete }
public bool Vide => tete == null;
public int Taille => taille;
}
Program
class Program
{
static void Main(string[] args)
{
<int> listeEntiers = new Liste<int>();
Liste.Ajouter (1);
listeEntiers.Ajouter (2);
listeEntiers.Ajouter (3);
listeEntiers
<string> listeStrings = new Liste<string>();
Liste.Ajouter("Lorem");
listeStrings.Ajouter("Ipsum");
listeStrings.Ajouter("Adamet");
listeStrings}
}
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.
Vous pouvez vous rendre dans la partie Les génériques et réaliser l’exercice correspondant.
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 Roi
etc.
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().
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)
{
<Piece> LesBlancs = new List<Piece>();
List
.Add(new Roi(1, 4));
LesBlancs.Add(new Reine(1, 5));
LesBlancs.Add(new Tour(1, 3));
LesBlancs.Add(new Tour(1, 6));
LesBlancs
// On pourrait encore faire les autres pièces...
// Appel des méthodes mouvement de toutes les pièces actuelles
foreach (Piece piece in LesBlancs)
{
.Mouvement(); // Polymorphisme -> la bonne méthode est appelée
piece}
}
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()
!
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)
{
<Rond> pieces = new();
List.Add(new Rond(2.3));
pieces.Add(new Cylindre(10.0, 2.3));
pieces
foreach (var piece in pieces)
{
.WriteLine($"Type:{piece.GetType().Name} -> {piece.Surface():F2}");
Console}
.ReadKey();
Console}
Type:Rond -> 33.24
Type:Cylindre -> 177.75
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
<IPiece> LesBlancs = new List<IPiece>();
List...
[0].Mouvement(); LesBlancs
Version classe abstraite
<Piece> LesBlancs = new List<Piece>();
List...
[0].Mouvement(); LesBlancs
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. |
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;
= new T[capacite];
tableau = 0;
index }
public void Add(T str)
{
if(index < capacite)
{
[index++] = str;
tableau}
}
public T? GetElementAt(uint index)
{
return (index < capacite) ? tableau[index] : default(T); // null, 0, false en fonction du type....
}
}
Voici un exemple d’utilisation de la classe ci-dessus :
public class Program
{
static void Main(string[] args)
{
<string> coureurs = new MaListeGenerique<string>(10);
MaListeGenerique.Add("Albert");
coureurs.Add("Jacques");
coureursstring? nom = coureurs.GetElementAt(8000);
<int> entiers = new MaListeGenerique<int>(10);
MaListeGenerique.Add(1);
entiers.Add(2);
entiersint v = entiers.GetElementAt(8000);
}
}
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.
On aimerait une classe générique qui ne conserve que les nombres paires.
static void Main(string[] args)
{
<char> listChar = new Pair<char>();
Pair.Add('2');
listChar.Add('7');
listChar.Add('A');
listChar.WriteLine(listChar);
Console
<int> listInt = new Pair<int>();
Pairfor (int i = 0; i < 10; i++) { listInt.Add(i); }
.WriteLine(listInt);
Console
<string> listStr = new Pair<string>();
Pair.Add("23");
listStr.Add("12954572");
listStr.Add("124");
listStr.Add("125");
listStr.Add("abs125");
listStr.WriteLine(listStr);
Console
<double> listDbl = new Pair<double>();
Pairfor (double i = 0.0; i < 10.0; i++) { listDbl.Add(i); }
.WriteLine(listDbl);
Console}
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….
La suisse possède un système de coordonnée en 2 dimensions qui lui est propre. Ce système se nomme 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.
<mn95> coordMN95 = new GeoCoordinate<mn95>(1200000, 2600000); // Berne
GeoCoordinatebool estEnSuisse = coordMN95.InSwitzerland; // True
<wgs84> coordWGS84 = new GeoCoordinate<wgs84>(46.95580, 7.42103); // Berne aussi
GeoCoordinatebool estEnSuisse = coordWGS84.InSwitzerland; // True