Les primitives et les Handles

 

        A ce stade, nous ne pouvons continuer notre apprentissage du C# sans passer par une étude approfondie des primitives et des handles. Examinons donc les types fondamentaux du langage C# et la façon dont ces données sont conservées en mémoire.

 

Sommaire

1. La mémoire d'un ordinateur

2. les types

3. Où se trouvent les données manipulées par C# ?

4. Présentation des différents types du C#

5. Les primitives

6. Les types entiers

7. Le type byte

8. Le type sbyte

9. Le type short et ushort

10. Le type int et Uint

11. Le type long et ulong

12. Le type char

13. Les types à virgule flottante

14. Le type float

15. Le type double

16. Le type décimal

17. Le type bool

18. Les identificateurs

19. Savoir écrire un identificateur

20. Les mots clés

21. Les constantes

22. Constantes non nommées

23. Constantes non nommées entières

24. Constantes non nommées réelles

25. Constantes caractères

26. les séquences d'échappement

27. Constantes string (chaînes de caractères)

28. Constantes nommées

29. Les variables

30. Initialisation

31. affectation

32. Valeurs par défaut des primitives

33. Portée des identificateurs de variables ou de constantes

34. Portée des identificateurs de méthodes

35. identificateurs des objets

36. Les types fondamentaux objets

37. La classe Object

38. La classe string

39. Les objets déclarés avec const

40. Un programme final

 

La mémoire d'un ordinateur

 

        La mémoire d'un ordinateur est un ensemble "d'états binaires" qui, comme nous l'avons vu dans la première partie, se nomment "bits" (8 bits donnent un octet). Un bit peut indifféremment prendre 0 ou 1 comme valeur. Il va de soit qu'un ordinateur peut mémoriser bien plus qu'un simple bit d'information. Il manipule généralement les données par tranches de format fixe qu'on appelle mots. La taille d'un mot varie d'un ordinateur à l'autre. Sur les plus anciens, elle était de 8 ou 16 bits (soit un ou deux octets) mais on arrive aujourd'hui à 32 bits (soit quatre octets).
Les ordinateurs ont besoin de stocker de grandes quantités d'informations, telles que les instructions d'un programme ou les données que ces instructions manipulent. La mémoire centrale d'un PC est ainsi très importante. Elle contient environ 128.10^6 cellules. Une cellule mémoire est une petite unité dont la taille est d'un octet. Pour des raisons pratiques, ces cellules sont numérotées consécutivement à partir de 0 (0, 1, 2, 3, 4, 5 ). Ces numéros sont appelés adresses et chaque cellule a une adresse unique. L'adresse 0 est dite null.
Ces dans ces cellules que seront stockées les données et les instructions d'un programme.

 


Une portion de 13 octets de la mémoire d'un ordinateur. Chaque octet a son adresse

 

les types

 

        Comment un ordinateur peut savoir, avec une mémoire composée de milliards de 0 et de 1, où commence un nombre et où il finit ? De même, comment peut-il savoir si ce nombre est entier, flottant, ou s'il s'agit d'un caractère ?
C'est là le rôle d'un type. Indiquer au compilateur où se trouve une variable, comment celle-ci est codée en mémoire, et la taille de la portion qu'elle y occupe.
Pour stocker une donnée dans une cellule il sera nécessaire de fournir à la mémoire une adresse ou de stocker l'information en plus de la donnée elle-même.
De nombreux types de données peuvent tenir en un seul mot. D'autres, plus petits, n'en utilisent qu'une partie.

 

Où se trouvent les données manipulées par C# ?

 

        Nos programmes peuvent manipuler des données se trouvant dans trois endroits différents. C'est la façon dont elles seront utilisées qui déterminera leur emplacement.
Les données peuvent se situer tout d'abord dans une zone de la mémoire spéciale que l'on appelle la pile ou stack en anglais. Il s'agit d'une structure où les éléments fonctionnent comme une pile d'assiettes dans un placard ; la dernière assiette posée sur la pile est aussi la première à en être retirée. La première assiette mise sous la pile est la dernière à être retirée. Ce genre de données sert principalement pour l'exécution de tâches rapides, c'est pourquoi ce sont elles qui sont créées à l'intérieur des méthodes.
Le C# peut aussi manipuler des données dans une zone de la mémoire nommée tas ou heap dont la taille peut varier dynamiquement. C'est dans celle-ci que sont placés les objets C#. Elle s'étend au fur et à mesure des besoins du programme (dans les limites de l'espace mémoire).
Comme nous l'avons vu, c'est le C# qui se charge de gérer cette mémoire, notamment par l'intermédiaire du garbage collector permettant au programmeur de s'occuper de tâches plus nobles. Ceci réduit donc considérablement le risque d'erreur.

Enfin, évidemment les données peuvent être stockées de manière permanente lorsqu'elles se trouvent sur le disque dur ou sur des zones de mémoire morte. Ces données persistent après l'arrêt de l'ordinateur.

 

Présentation des différents types du C#

 

        En C#, les types sont soit prédéfinis, soit définis par le programmeur lui-même. Les types prédéfinis sont appelés types de données fondamentaux. Les types créés par le programmeur peuvent être soit des pointeurs, des énumérations, des tableaux (arrays), ou bien évidemment des classes (nous reviendrons sur tous ces termes).
Il existe en fondamental plusieurs sortes de types :
_Les entiers dont les spécificateurs sont int, short, long, char, sbyte, byte, ushort, uint, ulong.
_Les flottants (nombres à virgules) dont les spécificateurs sont float, double, long double, decimal.
_ Les objets (object qui est la classe mère dont nous avons parlé dans la première partie ou string qui sont les chaînes de caractères)
_ les booléens dont le spécificateur est bool

Astuce

Avant de commencer notre étude des différents types fondamentaux existant en C#, nous ne saurions trop vous conseiller de vous rendre afin de lire le cours sur le système binaire dans la partie "Introduction aux systèmes numériques "

 

Les primitives

 

        C# n'est pas un langage à 100 % objet. Il existe des éléments qu'on appelle primitives. Ils sont créés de façon différente des objets et ne sont pas manipulés dans la mémoire de la même manière. Leur existence a pour principale origine les performances ; ces primitives permettent de créer des données en mémoire et d'effectuer des opérations sur elles très rapidement. Les objets étant gérés dans le heap, leurs réactions auraient été beaucoup trop lentes pour effectuer des algorithmes rapides.

 

Les types entiers

 

        Pour la représentation des nombres entiers, c'est-à-dire ceux dépourvus de décimales, le C# permet l'utilisation des types int, short, long, char, sbyte, byte, ushort, uint, ulong. Les lettres minuscules u, précédant les types ushort, uint, ulong, et s, précédant sbyte, indiquent que les nombres peuvent être mémorisés avec un signe ou non.

 

Le type byte

 

        Une donnée de type byte occupe un octet (ou byte en anglais) en mémoire soit 8 bits, elle est utilisée pour stocker un entier faible compris entre 0 et 255 (c'est-à-dire les chiffres compris entre 0 et 2^8-1) soit un nombre positif exclusivement. Contrairement au type sbyte que nous allons voir après, la mémoire occupée par un type byte ne dépend pas de l'existence d'un signe. Le nombre codé en binaire est codé avec la totalité des 8 octets. Il n'est pas nécessaire de réserver un emplacement pour coder le signe. D'un point de vue mathématique, ce nombre sera positif, pour l'ordinateur il n'aura simplement pas de signe. Chacun de ces bits peut prendre la valeur 0 ou 1, c'est à dire deux états possibles huit fois de suite, soit 2^8 (256) combinaisons possibles, la première étant :

 
0000 0000
 

        ce qui correspond au nombre 0, et la dernière étant :

 
1111 1111
 

        c'est-à-dire le nombre 255.

 


codage du nombre 65 en décimal ou 01000001 en binaire)

 

Le type sbyte

 

        A l'inverse du type byte, sbyte permet le stockage de données sur 8 bits signées. Ces données sont des nombres codés sur 7 bits, le huitième bit étant utilisé pour contenir une information de signe.

 


structure du type sbyte en mémoire

 

        Le nombre en mémoire est considéré comme négatif ou positif selon la valeur de ce huitième bit. Si celui-ci est égal à 1 alors le nombre est considéré comme négatif, s'il vaut 0 alors le nombre est positif. Il ne reste donc plus pour les nombres positifs que 128 valeurs représentables soit de 0 à 127 (de 00000000 à 01111111). A l'opposé, les combinaisons 1000000 à 11111111 représentent les nombres négatifs -1 à 128.

 


Représentation des nombres signés 8 bits

 

        La représentation des nombres négatifs est appelée méthode du complément à deux. Le complément à deux d'un nombre binaire b est le nombre b' tel que

 
b + b' = 2^n
 

        n représente le nombre de bits du nombre binaire. Dans le cas du type sbyte n vaut 8. Le complément à deux d'un nombre binaire s'obtient en remplaçant chaque 0 de la combinaison binaire le représentant par 1, et chaque 1 par 0, puis en ajoutant 1 au résultat.
Calculons le complément à deux de 1 (qui est -1)
Le nombre 1 en sbyte est codé ainsi en mémoire :

 
00000001
 

        Si on change chaque bits 0 en 1 et vice et versa on obtient :

 
11111110
 

        on lui rajoute alors 1 :

 
111111111
 

        pour obtenir la combinaison de bits représentant le nombre -1. Si vous cherchez le complément à deux de la suite binaire 11111111 vous obtiendrez évidemment 00000001. En additionnant 11111111 à 00000001 on obtient le nombre 256 qui est égal à 2^8.

 


passage d'un nombre positif à son opposé négatif et inversement

 

        Il convient de rester prudent quand au stockage de nombres avec le type sbyte. Voyons le stockage de deux nombres 65 et 227 avec les types byte et sbyte.

 


le codage de 65 avec les types byte et sbyte ne pose pas de problème

 

        65 étant inférieur à 127, il est représentable sur 7 bits et ne modifie pas le huitième bit de signe. Celui-ci ne servant pas reste à 0, laissant le nombre en positif.
Pourtant, si on prend un nombre qui ne peut être codé que sur 8 bits du fait de sa grandeur comme 227, il va se poser un problème :

 


le codage de 227 avec les types byte et sbyte pose problème

 

        En byte on obtient 227 car le huitième bit sert au codage du nombre mais pas à donner le signe. En sbyte, ce bit va servir à indiquer le signe du nombre codé par les sept autres bits. La combinaison binaire qui était égale à 227 en byte va être lu comme -29 par le C# (le négatif venant du huitième bit à 1).

 

Le type short et ushort

 

        Les types short et ushort prennent en mémoire la même place que le type char : 2 octets. Leur existence peut donc sembler inutile. En fait on les utilise pour une raison d'esthétique du code ; si un char (comme nous le verrons plus loin) peut coder un nombre, il est surtout d'usage de l'utiliser pour représenter les caractères en mémoire. Il est donc plus logique de coder un nombre avec le type short. Enfin le type char en mémoire ne représente que des valeurs positives (c'est à dire les codes des caractères) alors que le short permet de coder les nombres signés sur deux octets.
Ushort et short occupent en mémoire 2 octets soit 16 bits, c'est-à-dire 2^16 (65536) nombres différents. Le type ushort étant non signé, permet de représenter les nombres dans l'intervalle 0 à 65535, short permet de représenter les nombres positifs par l'intermédiaire du bit de signe (le seizième):

 
0000 0000  0000 0000
 

        à

 
0111 1111  1111 1111
 

        c'est à dire les nombres 0 à 32767 et les nombres négatifs -1 à -32768 :

 
1111 1111  1111 1111
 

        et

 
1000 0000  0000 0000
 

        short permet donc de mémoriser un nombre dans un intervalle compris entre -32768 et 32767.

 


représentation d'un short ou Ushort en mémoire ; ici les deux octets du type ont pris arbitrairement les valeurs 5120 et 5121

 

Le type int et Uint

 

        Le type int et son équivalent non signé uint représentent les types entiers standards qui sont le plus souvent utilisés par les programmeurs. Ils occupent en mémoire 4 octets soit 32 bits. Ils fonctionnent sur le même principe que short et ushort, mais la plage des nombres représentables est beaucoup plus importante puisqu'il est possible de combiner 2^32 (4294967296) entiers. uint représentera les entiers entre 0 et 4294967295, et int les entiers positifs entre 0 et 2147483647 et négatifs entre -1 et 2147483648.

 


représentation d'un int ou uint en mémoire

 

Le type long et ulong

 

        Les types long et ulong permettent de stocker des entiers très grands puisqu'ils sont tous les deux stockés sur 64 bits (8 octets). Il est donc possible de coder 2^64 (1.844*10^19) nombres ! Ulong codera des entiers compris entre 0 et 2^64 et long permettra de coder des nombres entre -2^63 et 2^63-1 (soit entre -9 223 372 036 854 775 808 et + 9 223 372 036 854 775 807) !

 

Le type char

 

        Le type de données char (de l'anglais caractère) occupe une place de 2 octets en mémoire. Il sert à manipuler les caractères par l'intermédiaire d'Unicode. Ce dernier est un système 16 bits permettant de représenter 65536 caractères (2^16).

Remarque

Code ASCII
Les 128 premiers caractères d'Unicode sont ceux du code ASCII

 

        Chaque nombre de cet intervalle correspond à un caractère ou à un idéogramme permettant de coder les signes de toutes les langues du monde. Par exemple la lettre A possède la valeur 65 dans le code Unicode/ASCII.

 

Les types à virgule flottante

 

        Les types à virgules flottantes (appelés également réels ou tout simplement flottants) permettent de stocker en mémoire les nombres qui possèdent des décimales comme 3.14 ou -0.01258. En C# la virgule est représentée par un point décimal. Ce point peut se déplacer en n'importe quel endroit du nombre :

 
45.123
 

        est équivalent à

 
4.5123*10^1
 

        mais aussi à

 
451.23*10^-1
 

        c'est la multiplication par différentes puissances de dix qui fait que ces diverses représentations sont une seule et même valeur. En C# la représentation des nombres flottants est différente des nombres entiers. L'emplacement mémoire occupé par un nombre flottant est divisé en trois parties : le signe, l'exposant et la mantisse :

 


représentation des nombres réels en mémoire

 

        La partie signe fonctionne de la même manière que le bit de signe des types entiers, soit à 1 pour les nombres négatifs et à 0 pour les positifs. La mantisse est la suite de chiffres qui constitue le nombre flottant. Dans le nombre 45.123 la mantisse serait ainsi égale à 45123. Enfin l'exposant est la puissance nécessaire pour écrire le nombre dans la forme voulue.
Cette forme voulue étant :

 
0.mantisse * 10^exposant
 

        c'est-à-dire en forme binaire :

 
0.mantisse-codée-en-binaire * 2^exposant
 

        notre nombre 45.123 s'écrira ainsi en décimal :

 
0.45123*10^2
 

        et en binaire

 
0.0101101*2^5
 

Le type float

 

        Le type float occupe 4 octets en mémoire, divisés en trois parties énoncées ci-dessous. Les 32 bits sont divisés en 24 bits pour coder la mantisse, 7 bits pour coder l'exposant et 1 bit de signe. Le type float peut mémoriser des nombres compris entre 3.4*10^-38 et 3.4*10^38 avec une précision de 7 digits.

 


représentation d'un flottant en mémoire

 

Le type double

 

        Le type double occupe 8 octets en mémoire avec une mantisse de 53 bits et un exposant de 10 bits assurant une couverture des nombres de 1.7*10^-308 à 1.7*10^308 offrant une précision de 15 à 16 digits.

 


représentation d'un double en mémoire

 

Le type décimal

 

        Le type décimal en occupant 128 bits en mémoire offre une plage de nombres plus petite que les deux autres types flottants, mais avec une très grande précision avec 28-29 digits significatifs. Les nombres pouvant être mémorisés sont compris entre 1.0 à10^-28 à 7.9 à10^28.

 

Le type bool

 

        Le type bool n'occupe théoriquement que 1 bit en mémoire. En fait celui-ci prend au minimum un octet. Car une cellule ne peut pas être subdivisée en plusieurs parties. Une variable de type bool ne peut prendre que deux valeurs littérales, soit true ou false. Elles sont donc très utiles pour les programmes nécessitant un résultat ne pouvant prendre que deux états. Comme le lancer d'une pièce de monnaie par exemple : soit pile, soit face.

 

Les identificateurs

 

        Tout comme les handles permettent de donner un nom à un objet, il est possible d'attribuer à une donnée un identificateur. Cet identificateur doit cependant respecter certaines règles édictées par le langage. Celles-ci s'appliquent aussi bien aux noms attribués aux données qu'à ceux des handles.

 

Savoir écrire un identificateur

 

        En principe un identificateur doit être constitué d'un ou plusieurs caractères. Ceux-ci peuvent être des lettres, des chiffres, ou bien l'underscore ( _ ) ; le premier caractère de l'identificateur devant être soit une lettre soit un underscore mais jamais un chiffre.

 
pepette                //bon
pepette2               //bon
2pepette               //mauvais : commence par un chiffre
MA_VARIABLE            //bon
MES_2variables         //bon
fichier_13             //bon
#_de_participants      //mauvais : utilisation du caractère #
nombre de participants //mauvais : utilisation d'un caractère d'espacement
nombre-1               //mauvais : utilisation du caractère -
 

        Le compilateur fait aussi une distinction entre les majuscules et les minuscules, ce qui est souvent source d'erreurs au moment de la compilation.

 
pepette
 

        est ainsi différent de

 
Pepette
 

Les mots clés

 

        Les identificateurs sont aussi soumis à une autre forme de restriction ; ils ne doivent surtout pas correspondre à un mot clé du langage C#. Ainsi, il serait faux d'utiliser using ou public en tant qu'identificateurs.
Le C# dispose de 74 mots clés ; le tableau ci-dessous les énumère :


abstract

base

bool

break

byte

case

catch

char

checked

class

const

continue

decimal

default

delegate

do

double

else

enum

event

explicit

extern

false

finally

fixed

float

for

foreach

goto

if

implicit

in

int

interface

internal

is

lock

long

namespace

new

null

object

operator

out

override

params

private

protected

public

readonly

ref

return

sbyte

sealed

short

sizeof

static

string

struct

switch

this

throw

true

try

typeof

uint

ulong

unchecked

unsafe

ushort

using

virtual

void

while




nous y retrouvons évidemment les types float, int, long etc. dont nous avons parlé précédemment. La signification des mots clés que contient le tableau ci-dessus sera évidemment expliquée dans la suite de l'ouvrage.

 

Les constantes

 

        Les données ne se caractérisent pas uniquement par leur type (ce que nous venons de voir) ou leur valeur, mais aussi par les constantes et les variables. Ces deux derniers éléments vont nous permettre de modifier leur valeur.
Il y a deux catégories de constantes, les non-nommées qui ont une valeur et un type, et les constantes nommées qui possèdent en plus un nom pour les manipuler (à la manière des handles).

 

Constantes non nommées

 

        Les constantes non nommées sont soit entières pour affecter une valeur à un entier, soit flottantes pour les flottants, soit chaînes de caractères pour les strings.

 

Constantes non nommées entières

 

        Ces constantes sont une suite de chiffres décimaux ou hexadécimaux.

 

        Constantes décimales

Les constantes décimales sont évidemment exprimées dans la base de 10 (c'est-à-dire avec des chiffres compris entre 0 et 9).

 
1
45
1256489
895
 

        Constantes hexadécimales


Les constantes hexadécimales sont exprimées en base 16, c'est-à-dire avec les digits 0 1 2 3 4 5 6 7 8 9 A B C D E F a b c d e f. Les six dernières lettres (majuscules et minuscules sont ici équivalentes) correspondent aux valeurs décimales 10 à 15. Pour indiquer au compilateur que la valeur est hexadécimale on fait précéder le nombre du symbole 0x ou 0X. Voici les chiffres de l'exemple sur les constantes décimales en hexadécimal :

 
0x1
0x2D
0x132C29
0x37F
 

        Constantes négatives


Pour rendre une constante décimale ou hexadécimale négative, il suffit de la précéder du signe moins ( - ).

 
-1
-0x132C29
-01577
 

        définir les constantes entières


Le type d'une constante dépend de sa valeur. Nous avons vu que les valeurs représentables par les types dépendent de la place occupée en mémoire. Une constante de type ushort ne pourra pas contenir des nombres plus grands que 2^16. Ainsi, si une constante a une valeur trop importante pour un ushort, elle se verra attribuer le type int, si celui si est encore trop petit ce sera uint puis long jusqu'à ulong. Cette règle s'applique quelle que soit la base du nombre.
Il est possible pour le programmeur d'indiquer lui-même le type d'une constante dans un programme, en terminant la valeur par une lettre. Par exemple :

 
22L
 

        force le compilateur à donner le type long à cette constante grâce à la lettre L la terminant, alors qu'elle tenait fort bien dans un byte ou un short.
Une constante littérale suit les règles suivantes :

  • S'il n'y a pas de suffixe dans la constante, alors le premier de ces types sera pris dans l'ordre de grandeur de la valeur : int, uint, long, ulong.

  • Si le suffixe est la lettre U ou u, alors le premier de ces types sera pris dans l'ordre de grandeur de la valeur : uint, ulong

  • Si le suffixe est la lettre L ou l alors le premier de ces types sera pris dans l'ordre de grandeur de la valeur : long, ulong

  • Si le suffixe est UL, ul, uL, LU, lU, ou lu alors le type sera ulong

  • Si la valeur entière de la constante est plus grande que ce que permet un ulong, le compilateur génère une erreur.

 

Constantes non nommées réelles

 

        Les constantes à virgules flottantes se composent d'une partie entière (c'est-à-dire les chiffres situés avant le point décimal), d'une partie décimale (soit les chiffres après le point) et éventuellement d'un exposant. Ils se trouvent toujours exprimés en base 10.


représentation


La partie entière et les décimales dans une constante réelle peuvent être omises. La présence du point décimal est elle aussi facultative. L'exposant doit s'exprimer en base 10 avec l'aide de la lettre E ou e suivie d'un nombre entier (positif ou négatif). Par exemple :

 
45.12E-1
 

        et

 
45.12e-1
 

        représentent le nombre 4.512 c'est-à-dire :

 
45.12*10^-1
 

        Nous aurions aussi pu écrire ce nombre ainsi :

 
0.4512e1
 

        Lorsque la partie entière est nulle comme ici elle peut être omise :

 
.4512e1
 

        Si c'est la partie décimale qui est nulle, nous pouvons écrire :

 
4512.e-3
 

        qui est équivalent à

 
4512E-3
 

        Les réels négatifs s'obtiennent tout simplement en rajoutant le préfixe moins ( - ).

 

        définir les constantes réelles

La gestion des types suivant la valeur de la constante réelle est identique aux constantes entières :

Si aucun suffixe n'est spécifié, le type de la constante est double sinon le type est choisi par le compilateur en suivant les règles suivantes :

  • Un réel suffixé par un F ou un f est de type float par exemple : 1f, 1.5F ou 456E-3F sont de type float

  • Un réel suffixé par D ou d est de type double par exemple : 1d, 1.5D ou 1e10d sont tous de type double

  • Un réel suffixé par M ou m est de type décimal : par exemple les constantes 1m, 1.5m ou 4547.7893214597871365m sont tous de type décimal.

 

Constantes caractères

 

        Une constante caractère se compose d'un ou plusieurs caractères placés entre une paire d'apostrophes. Exemple :

 
'A'     //caractère majuscule A
'1'     //caractère 1 (à ne pas confondre avec la valeur numérique)
 

        Les constantes de caractères sont de type char et pourraient se classer dans les constante entières comme nous l'avons vu lors de l'étude du type char. Ces constantes représentent les codes entiers du jeu de caractères Unicode. Pour l'ordinateur la constante 'A' équivaut à la valeur numérique 65 et '1' à la valeur numérique 49 (et non 1 comme on pourrait le penser). A chaque nombre de 0 à 65535 correspond un caractère imprimable par l'ordinateur.

Astuce

Vous trouverez dans la rubrique langage les 255 premiers caractères Unicode et leur valeur numérique

 

        Il faut bien comprendre que les caractères '1', '2' ou '3' ne sont pas équivalents aux entiers 1, 2 et 3. Les premiers correspondent aux nombres 49, 50 et 51 et les seconds aux valeurs 1 à 3.
Une question peut nous venir à l'esprit : comment représenter le caractère apostrophe puisque celui-ci sert déjà à délimiter les caractères ?
La réponse à cette question est "les séquences d'échappement"

 

les séquences d'échappement

 

        Certains caractères ne peuvent pas être représentés sous une forme imprimable. L'apostrophe est un bon exemple. Pour le compilateur elle représente un séparateur.
Si nous écrivons :

 
Console.Out.Write(''');
 

        Nous obtenons une erreur à la compilation :

 
TroisiemeProgramme.cs(29,23): error CS1010: Newline in constant

Danger

Attention
Guillemets et Apostrophes
dans l'instruction Console.Out.Write('''); nous n'utilisons pas les guillemets mais des apostrophes. Les guillemets permettent de délimiter une chaîne de caractères, les apostrophes délimitent un caractère.

Remarque

La méthode Write tout comme WriteLine fait partie de l'objet Out, à la différence de WriteLine elle ne revient pas à la ligne après l'affichage.

 

        Pour représenter l'apostrophe, il faut la faire précéder d'une barre oblique inversée ( \ ) ou backslash :

 
Console.Out.Write('\'');
 

        La compilation se déroule ici sans erreur. Le backslash masque au compilateur l'utilité du caractère apostrophe en le faisant passer pour un caractère ordinaire, permettant son affichage sur la sortie standard.

 
//TroisièmeProgramme.cs
/**************************************************/
/*                                                */
/*    troisème programme d'apprentissage du C#    */
/*              (premiere version)                */
/*                                                */
/**************************************************/



//utiliser le namespace System 
using System ;



/**
 * objet qui contient notre fonction main
 */
class TroisiemeProgramme
{


 /**
  * méthode principale du programme
  */
  public static void Main()
  {
        Console.Out.Write(' ');

  }

}
 

        Les caractères introduits par un backslash s'appellent les séquences d'échappement.



Caractère

Séquence d'échappement

nom

code Unicode hexadécimal

'

\'

Single quote

0x0027

"

\"

Double quote

0x0022

\

\\

Backslash

0x005C

caractère nul(NUL)

\0

Null

0x0000

signal sonore(Bel)

\a

Alert

0x0007

retour arrière

\b

Backspace

0x0008

saut de page

\f

Form feed

0x000C

saut de ligne

\n

New line

0x000A

retour chariot

\r

Carriage return

0x000D

tabulation horizontale

\t

Horizontal tab

0x0009

tabulation verticale

\v

Vertical tab

0x000B

nombre hexadécimal

\xhhh

--

hhh prend la valeur hexadécimale voulue





Le guillemet n'a pas besoin d'un backslash pour être représenté en simple caractère

 
Console.Out.Write('"');
 

        Par contre à l'intérieur d'une constante string (chaîne de caractères) la séquence d'échappement est obligatoire

 
Console.Out.WriteLine("et Cesar dit : \" les dés sont jetés\" ");
 

        A l'inverse, si le caractère apostrophe nécessite obligatoirement une barre oblique inversée en tant que caractère, à l'intérieur d'une string il peut être affiché directement.

 
Console.Out.WriteLine("l'alibi d'la donzelle l'a protégé");
 

        Il existe un grand nombre de caractères non imprimables mais très souvent utilisés par les programmes C#. Ils ont le code décimal 0 à 31 dans l'Unicode ; le signal sonore ( \a ) permet d'émettre un Bip, le caractère Backspace (\b) permet de faire reculer d'un caractère la position du curseur etc. Ainsi l'instruction :

 
Console.Out.WriteLine("le cours sur le C*\b# est bien ");
 

        affichera :

 
le cours sur le C# est bien
 

        La tabulation et le saut de ligne permettent de modifier la présentation de ce que nous écrivons à l'écran

 
Console.Out.WriteLine("1ere ligne\ ligne\n3emeligne vive le C\");
 

        La séquence d'échappement \f permet d'effectuer des sauts de pages pour l'impression de données. Par exemple copiez ce code dans un fichier que vous nommerez texte.cs

 
//texte.cs
/**************************************************/
/*                                                */
/*                affiche 3 pages                 */
/*                                                */
/**************************************************/



//utiliser le namespace System 
using System ;



/**
 * objet qui contient notre fonction main
 */
class TroisiemeProgramme
{


 /**
  * méthode principale du programme
  */
  public static void Main()
  {
    Console.Out.Write("première page");
    Console.Out.WriteLine('\f');
    Console.Out.Write("seconde page");
    Console.Out.WriteLine('\f');
    Console.Out.WriteLine("troisième page");
  }

}
 

        si vous l'exécutez, vous obtiendrez une sortie avec des caractères incompréhensibles. En redirigeant (par l'intermédiaire du caractère supérieur) la sortie vers l'imprimante grâce à la commande prn, vous imprimerez trois feuilles avec en haut de chacune d'elles première page, seconde page et troisième page.

 


form feed permet d'écrire sur chaque page après redirection de la sortie vers l'imprimante

 

        Terminons notre étude des caractères d'échappement en étudiant ceux qui comportent un nombre hexadécimal. Tous les caractères Unicode peuvent être représentés avec leur code hexadécimal. Les séquences d'échappement hexadécimales sont constituées d'une barre oblique inversée suivie d'une valeur en base 16. Le caractère # est codé en hexadécimal par la valeur 23. Ainsi l'instruction :

 
Console.Out.WriteLine("langage C\x23");
 

        donnera à la sortie :

 
langage C#
 

Constantes string (chaînes de caractères)

 

        Les constantes chaînes de caractères permettent l'affichage de texte. Elle sont constituées d'une suite de caractères placés entre guillemets. Notre premier programme contenait ainsi la string

 
"Bonjour de la part de C#"
 

        Comme nous l'avons vu précédemment les string peuvent contenir des caractères ordinaires mais aussi des séquences d'échappement.

 

        Séquences d'échappement hexadécimales dans les string


Utiliser les séquences d'échappement à l'intérieur des chaînes de caractères peut s'avérer dangereux. Imaginons que nous voulons utiliser le point d'exclamation en l'écrivant avec son code hexadécimal :

 
\x21
 

        Employée dans un WriteLine seule, cette séquence ne posera pas de problème :

 
Console.Out.WriteLine("\x21");
 

        Et donnera la sortie :

 
!
 

        Mais si nous voulons afficher la sortie !exclamation une erreur risque de se produire :

 
Console.Out.WriteLine("\x21exclamation");
 

        donne la sortie

 
?xclamation
 

        ce qui n'est pas ce que nous attendions. En réalité le compilateur n'a pas lu la séquence \x21 mais \x21e qui représente un caractère tout autre sur le point d'exclamation en Unicode. Ceci est souvent pour le programmeur une source d'erreurs incompréhensible et pourtant facilement débuggable .

 

        Les strings verbatim


Il arrive que certaines strings contiennent un grand nombre de séquences d'échappement. Il est alors difficile de les lire ou d'y trouver une erreur d'écriture. Ainsi si nous voulons afficher le path :

 
\\server\share\file.txt
 

        il va nous falloir utiliser la string :

 
"\\\\sever\\share\\file.txt";
 

        dans l'instruction :

 
Console.Out.WriteLine("\\\\sever\\share\\file.txt");
 

        Heureusement C# permet de rendre cette instruction beaucoup plus représentable à l'aide de l'opérateur verbatim ( @ ). Celui-ci placé dans une chaîne de caractères demande au compilateur d'ignorer toutes les séquences d'échappement jusqu'au dernier guillemet de l'instruction et de les afficher telles quelles.

 
Console.Out.WriteLine( @"\\sever\share\file.txt" );
 

        Le code est ainsi beaucoup plus lisible.
La puissance du @ ne s'arrête pas là. Il est ainsi possible de créer des instructions comme ceci :

 
Console.Out.WriteLine(@"ce livre a pour titre ""rivières pourpres"" ;\t il est bien");
 

        Ce qui donnera la sortie :

 
ce livre a pour titre "rivières pourpres" ;\t il est bien
 

        Le verbatim a permis ici l'inclusion d'une sous string "rivières pourpres" et a laissé la séquence d'échappement \t telle quelle.
Il est enfin possible de faire directement des sauts de lignes dans nos textes, comme ceci :

 
Console.Out.WriteLine(@"ligne 1
ligne 2
ligne 3");
 

        Ce qui donnera la sortie :

 
ligne 1
ligne 2
ligne 3
 

        Les string verbatim sont à utiliser avec modération, si elles peuvent rendre le code source plus lisibles dans bien des cas, elles peuvent leur rendre incompréhensible en cas d'utilisation trop importante.

 

Constantes nommées

 

        Intéressons-nous dès maintenant aux constantes nommées. Celles-ci sont des variables auxquelles il est impossible de changer une valeur. Contrairement aux constantes non nommées, les constantes de ce type disposent d'un identificateur auquel est associé une valeur dans le code source. Elles nécessitent une déclaration dans votre code source.

 

        La déclaration d'une constante


Une déclaration de constante s'avère pratiquement identique à une déclaration de handle ou de variable (comme nous le verrons plus loin) :

 
const type_de_la_constante nom_de_la_constante = valeur_de_la_constante 
[ nom_de_la_constante = valeur_de_la_constante,  ];
 

        Les crochets indiquent le facultatif : une instruction peut définir plusieurs constantes. Par exemple, l'instruction :

 
const int jours = 365;
 

        définit une constante de type int appelée jours qui a pour valeur 365.
L'instruction :

 
const byte âge= 23, taille = 185;
 

        permet la création, en une instruction, de deux constantes de type byte nommées âge et taille, en leur donnant respectivement la valeur 23 et 185. Notons que si, dans les deux derniers exemples, nous omettions le mot clé const, nous déclarerions alors une variable.
Nous avons dit qu'une constante gardait sa valeur à vie, contrairement à une variable ou à un handle. Il est donc nécessaire de lui donner une valeur au moment de sa déclaration. Ainsi :

 
Const short x;
 

        Provoquera une erreur au moment de la compilation :

 
constante.cs(28,18): error CS0145: A const field requires a value to be provided
 

        L'initialisation d'une constante s'effectue à l'aide de l'opérateur d'affectation = qui copie la valeur se trouvant à sa droite (right value) dans l'opérande de gauche (left value)
Après la déclaration d'une constante, il est impossible d'en changer la valeur ; ainsi l'instruction :

 
x = 92;
 

        provoquera une erreur :

 
constante.cs(30,5): error CS0131: The left-hand side of an assignment must be 
a variable, property or indexer
 

        Tant qu'il existera, x représentera une valeur dans le programme. Celle-ci ne pourra plus être modifiée après sa création.

 

        Où déclarer ?

 

        Une déclaration de constante, tout comme une déclaration de variable, peut se faire n'importe où à l'intérieur d'une classe ou d'une méthode. Il est ainsi possible de générer un code ainsi :

 
//constante.cs
/**************************************************/
/*                                                */
/*    troisème programme d'apprentissage du C#    */
/*              (premiere version)                */
/*                                                */
/**************************************************/



//utiliser le namespace System 
using System ;



/**
 * objet qui contient notre fonction main
 */
class Constante
{


 /**
  * méthode principale du programme
  */
  public static void Main()
  {
    Console.Out.Write("par ");
    const short x = 3;
    Console.Out.Write( x );
    Console.Out.Write(" fois mon record au lancer de poids a été de ");
    const sbyte RECORD = -23;
    Console.Out.Write(RECORD);
    Console.Out.WriteLine(" metres. La valeur de PI vaut :");
    const float PI = 3.14F;
    Console.Out.WriteLine( PI );
    Console.Out.Write(". Queen a vendu ");
    const uint NOMBRE_ALBUMS = 175000000;
    Console.Out.Write(NOMBRE_ALBUMS);
    Console.Out.WriteLine(" albums dans le monde et un Euro vaut");
    const decimal EURO = 6.7556624787854254857m;
    Console.Out.Write(EURO);
    Console.Out.WriteLine(" francs");

   }

}
 

        Pour afficher la valeur d'une constante, d'une variable ou d'un handle, il suffit de donner en paramètre à la méthode WriteLine() ou Write() le nom de la constante voulue. La valeur de celle-ci sera alors affichée sur la sortie standard.
Si le code ci-dessus fonctionne sans aucun problème, il souffre malheureusement d'une très mauvaise lisibilité. Il est d'usage de regrouper les déclarations de constantes, tout comme celles de variables ou de handles, au tout début d'un bloc :

 
//constante.cs
/**************************************************/
/*                                                */
/*    troisème programme d'apprentissage du C#    */
/*              (seconde version)                 */
/*                                                */
/**************************************************/



//utiliser le namespace System 
using System ;



/**
 * objet qui contient notre fonction main
 */
class Constante
{


 /**
  * méthode principale du programme
  */
  public static void Main()
  {
    const sbyte RECORD = -23;
    const short x = 3;
    const float PI = 3.14F;
    const decimal EURO = 6.7556624787854254857m;
    const uint NOMBRE_ALBUMS = 175000000;


    Console.Out.Write("par ");
    Console.Out.Write( x );
    Console.Out.Write(" fois mon record au lancer de poids a été de ");
    Console.Out.Write(RECORD);
    Console.Out.WriteLine(" metres. La valeur de PI vaut :");
    Console.Out.WriteLine( PI );
    Console.Out.Write(". Queen a vendu ");
    Console.Out.Write(NOMBRE_ALBUMS);
    Console.Out.WriteLine(" albums dans le monde et un Euro vaut");
    Console.Out.Write(EURO);
    Console.Out.WriteLine(" Francs");

   }

}
 

        Les constantes nommées permettent une sécurité du code en interdisant le programmeur d'en modifier la valeur par inadvertance, et en rendant le code source un peu plus lisible grâce au remplacement des valeurs numériques par des noms explicites.

 


les constantes nommées permettent de garder des valeurs que l'on pourrait qualifier de références sur lesquelles un programme s'appuie

Remarque

Dans ce chapitre, nous allons voir qu'il est possible d'afficher directement dans une string la valeur d'une constante, d'une variable ou d'un handle. Le code ci-dessus est très redondant mais nous apprendrons à le rendre plus efficace et mieux construit plus tard.

 

        Lorsque la constante primitive est déclarée à l'intérieur d'une classe et à l'extérieur d'une méthode, c'est un membre de classe qui contient une valeur de référence. Si nous avions créé une classe mammifères, nous aurions pu lui donner le membre :

 
class Mammifere
{
  public const sbyte NOMBRE_DE_PATTES = 4;


  //...
}
 

        car tous les mammifères ont quatre membres et cela ne risque pas de changer au cours des vingt prochains millions d'années. Un programmeur utilisant cette classe plus tard ne pourra pas changer la valeur de NOMBRE_DE_PATTES à 18 par inadvertance.

 

Les variables

 

        Si les constantes sont un atout pour rendre un code clair en créant des données références dont la valeur ne peut changer, elles souffrent d'un grave défaut : elles sont constantes ! Heureusement le programmeur pour aussi faire appel à des variables qui, elles, peuvent contenir au cours de leur vie un grand nombre de valeurs (qui seront toujours du même type de la variable). Une variable est un emplacement mémoire d'un ou de plusieurs octets (suivant le type) qui fixe la manière dont ces octets sont à interpréter. Ainsi pour un emplacement de quatre octets, c'est en connaissant le type de la variable que nous saurons s'il s'agit d'un int ou d'un float. Pour que la variable soit localisable en mémoire centrale, on lui donne une adresse. Cette adresse nous importe peu pour l'instant car avec le nom de la variable nous allons pouvoir la manipuler dans nos programmes.

 


Une variable de type short nommée x en mémoire

 

        Ici notre variable a pour adresse 5423. L'emplacement d'une variable est totalement imprévisible, il dépend de l'état de la mémoire au moment de l'exécution du programme.
La déclaration d'une variable est identique à celle d'une constante nommée à laquelle on enlève le mot clé const :

 
type_de_la_constante nom_de_la_constante [= valeur_de_la_constante]
 [ nom_de_la_constante = valeur_de_la_constante,  ];
 

        comme on le voit ici, une variable ne doit pas nécessairement être initialisée lors de sa déclaration. Ainsi l'instruction :

 
long population;
 

        déclare une variable de type long nommée population.
L'instruction :

 
int resultat1, resultat2, resultat3;
 

        Déclare trois variables nommées respectivement resultat1, resultat2 et resultat3 de type int. Sachant qu'un int occupe quatre octets en mémoire, nous réservons donc 4 * 3 octets soit 12 octets.

 

Initialisation

 

        En l'absence d'initialisation d'une variable, le compilateur générera une erreur si le programmeur veut l'utiliser. Le code suivant :

 
//variable.cs
/**************************************************/
/*                                                */
/*            programme montrant le               */
/*         fonctionnement des variables           */
/*                                                */
/**************************************************/



//utiliser le namespace System 
using System ;



/**
 * objet qui contient notre fonction main
 */
class Variable
{


 /**
  * méthode principale du programme
  */
  public static void Main()
  {
    long population;

     Console.Out.WriteLine(population);

  }

}
 

        génèrera l'erreur suivante :

 


ne jamais oublier de donner une valeur à une variable avant de l'utiliser ou de l'afficher

 

        L'initialisation se fait ici aussi à l'aide du signe égal lors de la création de la variable :

 
 /**
  * méthode principale du programme
  */
  public static void Main()
  {
    long population = 3;

     Console.Out.WriteLine(population);

  }

}
 

        En initialisant la variable population, comme ci-dessus, la compilation se déroulera sans aucun problème.
Dans le cas de plusieurs déclarations dans une même instruction nous ferons :

 
int resultat1 = 15, resultat2 = 12, resultat3 = 6;
 

affectation

 

        La valeur d'une variable peut être modifiée après sa déclaration. Qu'il y ait eu ou non initialisation. On utilise ici aussi l'opérateur égal = pour affecter à la variable une nouvelle valeur.

 
ushort poids; 

//...
poids =  423;
 

        Dans le code ci-dessus, après la déclaration de la variable, on affecte la valeur 423 à la variable poids. Si poids possédait déjà une valeur, elle aurait été remplacée par la nouvelle. Nous aurions pu évidemment écrire :

 
ushort poids =  423 ;
 

        mais il s'agirait ici d'une initialisation et non une affectation. L'initialisation d'une variable se fait au moment de sa déclaration, alors que l'affectation se fait à n'importe quel moment et dans un nombre indéfini de fois. L'opérande située à droite de l'opérateur = n'est pas forcément une constance non nommée. Avec les déclarations suivantes :

 
int a = 1, b = 7;
const int c = 12;
 

        il est possible de faire :

 
b = a;
 

        mais aussi

 
a = c;
 

        Nous transférons tout d'abord la valeur contenue par a dans b. Cette dernière est maintenant égale à la valeur de a (soit 1). Dans la deuxième instruction, nous transférons la valeur de la constante nommée c dans a. La variable a prend la valeur 12.

 

Valeurs par défaut des primitives

 

        Nous venons de voir que les primitives déclarées à l'intérieur d'une méthode devaient obligatoirement être initialisées avant leur utilisation. Nous pouvons en déduire qu'elles n'ont donc pas de valeur par défaut lors de leur création.
Lorsque les primitives sont utilisées comme membres de classe, elles reçoivent une valeur par défaut au moment de leur déclaration.
Vérifions cela en améliorant l'exemple de notre classe Chien

 
//troisiemeProgramme.cs
/**************************************************/
/*                                                */
/*   troisième programme d'apprentissage du C#    */
/*                                                */
/**************************************************/


//utiliser le namespace System 
using System ;



/**
 * objet qui contient notre fonction main
 */
class TroisiemeProgramme
{


 /**
  * méthode principale du programme
  */
  public static void Main()
  {
    Chien rex;

  }

}




/**
 * classe qui permet la création d'un objet de type Chien
 */
class Chien 
{

  /**
   * membre qui contient l'âge du chien
   */
  byte age;



 /**
  * constructeur de notre classe chien
  */
  public Chien()
  {

    Console.Out.WriteLine("création d'un chien");

  }


 /**
  * méthode qui permet à notre chien d'aboyer
  */
  public void Aboyer()
  {

    Console.Out.WriteLine("Ouah ! ouah ! grrrrrrrrrrrrrrrr Ouah !");

  }


 /**
  * méthode qui permet à notre chien de couiner
  */
  public void Couiner()
  {

    Console.Out.WriteLine("Kaï Kaï Kaï !!!!");

  }


 /**
  * méthode qui permet à notre chien de sentir
  */
  public void Sentir()
  {

    Console.Out.WriteLine("snif ? snif ! snif ?!");

  }



}
 

        Nous avons ajouté ici un membre de type byte, nommé âge, à la classe de type byte. Celui-ci est déclaré comme public afin que nous puissions accéder au membre depuis notre fonction Main. Il contiendra évidemment l'âge du chien. Voyons maintenant ce qui va se passer si, dans la fonction main, nous demandons d'afficher l'âge du chien sans avoir donné de valeur à ce membre :

 
/**
  * méthode principale du programme
  */
  public static void Main()
  {
    Chien rex;

    rex = new Chien();

    Console.Out.Write("Rex a ");
    Console.Out.Write(rex.age);
    Console.Out.WriteLine(" an(s)");


  }
 

        la compilation se déroule sans erreur mais avec un warning (le compilateur pense qu'il peut y avoir un problème) :

 
TroisiemeProgramme.cs(51,15): warning CS0649: Field 'Chien.age' is never assigned to,
 and will always have its default value 0
 

        Il nous prévient ici que le membre âge de chien ne prend jamais de valeur et qu'il lui en assigne une par défaut (0). Il nous met donc en garde au cas où nous n'aurions pas pensé à cette affectation forcée.
Nous obtenons effectivement la sortie :

 
création d'un chien
Rex a 0 an(s)
 

        Remarquons que nous utilisons ici aussi le point pour relier l'objet Chien rex à une de ses variables d'instances(ici âge) tout comme nous le faisions pour le relier à une de ses méthodes dans le capitre précédent.
Ce qui prouve bien que âge a été initialisé par défaut à 0.
Chaque type est initialisé avec une valeur par défaut comme spécifié ci-dessous :


  • Pour les types sbyte, byte, short, ushort, int, uint, long, and ulong, la valeur par défaut est 0.

  • Pour les types char, la valeur par défaut est '\x0000' (soit le premier code Unicode).

  • Pour les types float, la valeur par défaut est 0.0f.

  • Pour les types double, la valeur par défaut est 0.0d.

  • Pour les types decimal, la valeur par défaut est 0.0m.

  • Pour les types bool, la valeur par défaut est false.

 

Portée des identificateurs de variables ou de constantes

 

        Pour faire référence à un objet, une variable ou une constance nommée, nous utilisons un identificateur. Cet identificateur va nous permettre de manipuler soit une primitive (pour les variables et les constantes), soit un objet (pour les handles).
La vie d'un objet dure de l'instant de sa création à l'aide d'un constructeur, jusqu'au moment de sa destruction par le garbage collector. Les membres de ses objets, qu'ils soit primitives ou handle existeront tant que l'objet vivra. En revanche pour les primitives et handles déclarés dans des méthodes, la donne est différente. Elles n'ont pas de durée proprement dite mais une visibilité. Les primitives ne sont visibles que dans certaines parties de notre programme et invisibles dans d'autres.
On appelle portée (ou scope en anglais) l'étendue du programme dans lequel un identificateur existe.
Nous savons que la majeur partie d'un programme se déroule à l'intérieur de blocs (délimités par une accolade ouvrante { et une accolade fermante } ). La portée d'un identificateur s'étend de l'endroit où il est créé, jusqu'à la fin du bloc (en incluant les blocs imbriqués). L'image ci-dessous nous montre la portée des identificateurs dans un programme.

 


portée des différents identificateurs de cette classe ; à chaque zone de gris correspond une portée

 

        Voyons cela concrètement en compilant le programme suivant :

 
//exemplevisibilite.cs
/**************************************************/
/*                                                */
/*        programme montre la portée et la        */
/*       la visibilitée des  identificateurs       */
/*                                                */
/**************************************************/


//utiliser le namespace System 
using System ;



/**
 * objet qui contient notre fonction main
 */
class ExempleVisibilite
{
  static char membreCaractere = 'S';


 /**
  * méthode principale du programme
  */
  public static void Main()
  {
    int donnee = 10;

    Console.Out.Write("voici la valeur du membre variable membreCaractere: ");
    Console.Out.Write(" dans la méthode Main() : ");
    Console.Out.WriteLine(ExempleVisibilite.membreCaractere );

    Console.Out.Write("voici la valeur de la variable donnée dans Main() : ");
    Console.Out.WriteLine(donnee);


    //nouveau bloc
    {
      string donneeBloc = "me voyez vous ?";
      Console.Out.Write("voici la valeur de la variable donneeBloc dans Main() : ");
      Console.Out.WriteLine(donneeBloc);
    }     


    //appel de la méthode visibilité
    ExempleVisibilite.Visibilite();


    //enlevez le commentaire ci-dessous pour générer une erreur
    //Console.Out.WriteLine(donneeBloc);
  }


 /**
  * méthode pour démontrer les principes de la visibilité
  */
  public static void Visibilite()
  {
    double donnee = 54.123589D;

    Console.Out.Write("voici la valeur de la variable donnée dans la");
    Console.Out.Write(" méthode Visibilite(): ");
    Console.Out.WriteLine(donnee);

    Console.Out.Write("voici la valeur du membre variable membreCaractere ");
    Console.Out.Write(" dans la méthode Visibilite() : ");
    Console.Out.WriteLine(ExempleVisibilite.membreCaractere );


  }

}
 

        nous obtenons la sortie :

 
voici la valeur du membre variable membreCaractere dans la méthode Main() : S
voici la valeur de la variable donnee dans Main() : 10
voici la valeur de la variable donneeBloc dans Main() : me voyez vous ?
voici la valeur de la variable donnee dans la méthode Visibilite: 54.123589
voici la valeur du membre variable membreCaractere dans la méthode Visibilite(): S
 

        Dans ce programme, il y a plusieurs remarques à formuler :
Tout d'abord, nous avons déclaré le membre membreCaractere de la classe ExempleVisibilite en static. Ceci nous permet de l'utiliser dans notre programme en faisant référence à la classe et non à un objet de la classe. La variable membreCaractere appartient en effet à la classe et non pas à une de ses instances.
La méthode Visibilite() est déclarée en static pour les mêmes raisons.
Nous déclarons une variable de type int nommée donnee dans la méthode principale Main(). Elle est accessible tout au long de notre méthode Main() mais pas depuis l'extérieur. La méthode Visibilité peut nous prouver cela de deux manières : en déclarant une variable nommée donnee mais de type double, et en affichant sa valeur, nous obtiendrons une valeur de type double comme le montre la quatrième ligne de notre sortie :

 
voici la valeur de la variable donnee dans la méthode Visibilite: 54.123589
 

        La variable de type int nommée donnee n'a donc pas de visibilité ici ; elle est cachée par la variable de même nom déclarée dans Visibilite().
Si nous effaçons la déclaration

 
double donnee = 54.123589D;
 

        alors le compilateur générera l'erreur suivante :

 
exemplevisibilite.cs(62,27): error CS0103: The name 'donnee' does not exist in the class 
or namespace 'ExempleVisibilite'
 

        nous indiquant qu'il n'y a aucun identificateur visible se nommant donnee dans la méthode. La variable donnee de Main() n'est évidemment plus visible après l'accolade fermante du corps de cette méthode.
De même, comme le spécifie le code suivant :

 
    //enlevez le commentaire ci-dessous pour générer une erreur
    //Console.Out.WriteLine(donneeBloc);
 

        si nous enlevons le commentaire, une erreur se produira. La variable nommée donneeBloc n'est en effet pas accessible en dehors du petit bloc dans lequel elle a été déclarée.

 

Portée des identificateurs de méthodes

 

        Contrairement aux handles et aux noms de primitives, un identificateur de méthode a une portée qui s'étend à la totalité de la classe qui la contient. Ainsi dans le programme ci-dessus, la méthode Visibilite() se trouve après la méthode Main(). Mais lorsque cette dernière l'appelle, le compilateur ne génère pas d'erreur car il sait qu'elle existe.

 

identificateurs des objets

 

        Les objets n'ont pas de portée. Seul leur identificateur en a une (c'est-à-dire le handle).
Ce qu'il faut bien comprendre, c'est que même en dehors de la portée d'un handle, ce dernier continue d'exister en mémoire.

 

Les types fondamentaux objets

 

        Tous les types de C# ne sont évidemment pas des primitives. Nous allons maintenant voir deux classes dont nous allons souvent nous servir dans nos programmes ; la classe Object et la class String.

 

La classe Object

 

        Le type object est le type ultime de C#. Une instance de cette classe peut revêtir la forme de n'importe quel objet ou primitive.
Le nom object est en fait un alias de System.Object. Son existence permet de rester dans l'esprit des primitives dont le nom des types ne commence pas par une majuscule (à la différence des classes). Dans notre programme objets.cs, dont le code se trouve ci-dessous :

 
//objets.cs
/**************************************************/
/*    programme montant le type Object en action  */
/*                                                */
/*                                                */
/**************************************************/


//utiliser le namespace System 
using System ;



/**
 * objet qui contient notre fonction main
 */
class Objet
{


 /**
  * méthode principale du programme
  */
  public static void Main()
  {
    Object        premierObjet;  
    System.Object secondObjet;  
    object        troisiemeObjet;  


  }



}
 

        nous créons trois variables de type Object à l'intérieur de la méthode principale Main() :

 
/**
  * méthode principale du programme
  */
  public static void Main()
  {
    Object        premierObjet;  
    System.Object secondObjet;  
    object        troisiemeObjet;  


  }
 

        Dans le premier cas :

 
Object        premierObjet;  
 

        Nous faisons appel à l'alias, le compilateur lit donc cette ligne ainsi :

 
System.Object premierObjet;  
 

        C'est à dire le namespace où se trouve les classes que nous utilisons (System) et le nom de la classe elle-même. A la seconde ligne de la méthode, cette notation est d'ailleurs présente en complet pour la création d'un deuxième objet de type Object. Enfin, en troisième ligne de Main(), nous utilisons le nom de la classe directement pour créer une variable. Ceci est possible car au début du programme nous avons donné l'instruction :

 
using System ;
 

        qui indique au compilateur que s'il ne connaît pas une classe dans le source, c'est dans le namespace System qu'il la trouvera.
Comme nous l'avons dit plus haut, il est possible d'assigner à un objet de type object une donnée de n'importe quel type dans une opération qu'on appelle boxing.
Considérons la déclaration suivante d'une variable de type int :

 
int i = 123;
 

        La ligne suivante applique une opération de boxing sur la variable i :

 
object o = i;
 

        Par cette instruction, un objet de type Object est créé sur la pile. Celui-ci référence une valeur de type int sur le heap (tas). Cette valeur est une copie de la valeur de i.

 


différence entre les variables i et o

 

        Voyons un exemple concret :

Remarque

Nous reviendrons sur la classe object lorsque nous aborderons le polymorphisme.

 

La classe string

 

        Dans le même principe que la classe Object, le type string est un alias de System.String. Il est donc possible de créer un objet de type String de trois manières différentes :

 
 /**
  * méthode principale du programme
  */
  public static void Main()
  {
    string        premierObjet;  
    System.String secondObjet;  
    String        troisiemeObjet;  
  }
 

        Une string est une chaîne de caractères Unicode. Contrairement aux primitives standards, leur place en mémoire est totalement aléatoire et dépend de la longueur de la chaîne. Testons le comportement des strings :

 
//strings.cs
/**************************************************/
/*   programme montrant le type String en action  */
/*                                                */
/*                                                */
/**************************************************/


//utiliser le namespace System 
using System ;



/**
 * objet qui contient notre fonction main
 */
class ChaineCaracteres
{


 /**
  * méthode principale du programme
  */
  public static void Main()
  {
    string chaine1 = new String("les strings...");  

    Console.Out.WriteLine(chaine1);

    string chaine2 = "se déchainent...";  

    Console.Out.WriteLine(chaine2);

    string chaine3 = chaine1;

    Console.Out.WriteLine(chaine3);

    chaine3 = "sont des objets spéciaux...";

    Console.Out.WriteLine(chaine3);
  }

}
 

        Notons tout d'abord que nous initialisons les strings de trois manières différentes . L'instruction :

 
string chaine1 = new String("les strings...");  
 

        crée une string comme un objet. La constante string "les strings..." située entre les parenthèses du constructeur est un paramètre.du constructeur.

Remarque

nous verrons en détail les paramètres dans notre prochain chapitre

 

        Le constructeur va s'en servir pour initialiser la string. L'instruction :

 
string chaine2 = "se déchainent...";  
 

        crée une string comme nous le ferions avec une primitive. Cette notation est équivalente à

 
string chaine1 = new String("se déchainent...");
 

        pour le compilateur. Ce cas est une exception dans le principe de création d'objets qui permet de pouvoir donner la possibilité au programmeur de gérer le type string comme une primitive.
Enfin la troisième string est initialisée en pointant sur le même objet que celui du handle chaine1. Après son affichage, nous lui affectons une nouvelle valeur à la manière d'une primitive :

 
chaine3 = "sont des objets spéciaux...";
 

        Le programme nous donne la sortie :

 
les strings...
se déchainent...
les strings...
sont des objets spéciaux...
 

        Etudions plus en détail l'exemple de la variable chaine3 qui pointe sur le même objet que chaine1. Que se passera t'il si après une telle instruction nous changeons la valeur de chaine1 et que nous affichons chaine3 ? Voyons cela en changeant le programme comme ci-dessous :

 
/**
  * méthode principale du programme
  */
  public static void Main()
  {
    string chaine1 = new String("les strings...");  

    string chaine3 = chaine1;

    Console.Out.WriteLine(chaine3);

    Chaine1 = "sont des objets spéciaux...";

    Console.Out.WriteLine(chaine3);
  }

}
 

        nous obtenons la sortie :

 
les strings...
sont des objets spéciaux...
 

        contrairement aux variables et aux constantes, qui ont leur propre emplacement mémoire contenant une valeur, les handle chaine1 et chaine3 pointent toutes les deux sur un même objet situé dans le tas. C'est cet objet qui a été modifié par l'intermédiaire du handle chaine1 et c'est toujours lui qui a été affiché par l'intermédiaire du handle chaine3.
Etudions un autre cas pour illustrer le travail du garbage collector :

 
/**
  * méthode principale du programme
  */
  public static void Main()
  {
    string chaine1 = "les strings...";  

    Chaine1 = "sont des objets spéciaux...";

  }

}
 

        dans cet exemple nous créons un premier objet string qui contient la chaîne "les strings..." :

 
string chaine1 = "les strings...";  
 

        Dans un second temps, nous faisons pointer notre handle sur un autre objet. Le premier objet qui contient la chaîne "les strings..." est donc perdu en mémoire. Le Garbage collector le détruira à sa prochaine activation.

 


l'objet qui n'est plus référencé par aucun handle est perdu en mémoire

 

        L'utilisation des strings verbatim est bien évidemment possible comme le montre l'exemple suivant :

 
/**
  * méthode principale du programme
  */
  public static void Main()
  {
    string chaine1 = @"c:\Docs\Source\a.txt" 

    Console.Out.WriteLine(chaine1)

  }

}
 

        qui donnera la sortie suivante :

 
c:\\Docs\\Source\\a.txt"
 

Les objets déclarés avec const

 

        Lorsqu'un handle est déclaré avec le mot clé const la contrainte ne s'applique qu'à lui ; le handle ne pourra jamais plus pointer sur un autre objet que celui qu'on lui a donné à référencer lors de sa déclaration. Les caractéristiques de cet objets peuvent néanmoins être modifiées à la condition qu'elle ne soit pas elles-même déclarées avec const.

 

Un programme final

 

        Nous terminerons ce chapitre par l'étude d'un programme en mettant en avant les diverses notions que nous avons apprises :

 
//ordinateurs.cs
/**************************************************/
/*                                                */
/* programme de démonstration finale des handles  */ 
/*               et des primitives                */
/**************************************************/


//utiliser le namespace System 
using System ;



/**
 * objet qui contient notre fonction main
 */
class TroisiemeProgramme
{


 /**
  * méthode principale du programme
  */
  public static void Main()
  {
    double tailleInstallationWindows = .6D;
    double memoireUtiliseeParWindows = 90.12D;


    Console.Out.Write("Aujourd'hui les pc possèdent des disques durs ");
    Console.Out.Write(" d'une capacité de ");
    Console.Out.Write(DisqueDur.capaciteMaximale);
    Console.Out.Write(" gigas(s) et une mémoire de ");
    Console.Out.Write(Memoire.capaciteMaximale);
    Console.Out.WriteLine(" mégas\n");

    Ordinateur monPC = new Ordinateur();


    monPC.hdd.memoireUtilisee = 0;
    Console.Out.Write("\nnous formatons le disque dur.\nla taille utlisée");
    Console.Out.Write(" sur le disque est de :");
    Console.Out.WriteLine(monPC.hdd.memoireUtilisee);


    monPC.hdd.memoireUtilisee = tailleInstallationWindows ;
    Console.Out.Write("\nnous installons Windows sur le pc,\nla taille utilisée ");
    Console.Out.Write(" sur le disque est de :");
    Console.Out.WriteLine(monPC.hdd.memoireUtilisee);


    monPC.ram.memoireUtilisee = memoireUtiliseeParWindows ;
    Console.Out.Write("\nnous démarrons Windows sur le pc,\nmémoire prise est de :");
    Console.Out.WriteLine(monPC.ram.memoireUtilisee);

    Console.Out.Write("\nbref il reste ");
    Console.Out.Write(Memoire.capaciteMaximale - monPC.ram.memoireUtilisee);
    Console.Out.Write(" mégas en ram et ");
    Console.Out.Write(DisqueDur.capaciteMaximale - monPC.hdd.memoireUtilisee);
    Console.Out.Write(" giga(s) sur le disque dur");






  }

}



class Ordinateur
{
  /**
   * membre disque dur de l'ordinateur
   */
   public DisqueDur hdd;

  /**
   * membre mémoire de l'ordinateur
   */
   public Memoire ram;



  /**
   * constructeur de la classe
   */
   public Ordinateur()
   {
     Console.Out.WriteLine("un ordinateur a été construit");
     Console.Out.WriteLine("il possède :");

     //faire pointer le handle sur un nouvel objet disque dur
     hdd = new DisqueDur();

     //faire pointer le handle sur un nouvel objet disque dur
     ram = new Memoire();

   }

}



class DisqueDur
{

  /**
   * membre indiquant la capacité maximale du disque dur
   * il est static car toutes les instances auront la même capacité.
   */
   public static double capaciteMaximale = 45.0D;

  /**
   * membre indiquant la mémoire déjà occupée sur le disque dur
   * ce membre n'est pas statique car tous les disques durs ne sont pas
   * remplis de la même façon
   */
   public double memoireUtilisee = 0;



   public DisqueDur()
   {
     Console.Out.Write("un disque Dur d'une capacité de ");
     Console.Out.Write(capaciteMaximale );
     Console.Out.WriteLine(" giga(s) a été créé");
   }



}




class Memoire
{
  /**
   * membre indiquant la capacité maximale de la mémoire
   * il est statique car toutes les instances auront la même capacité.
   */
   public statique double capaciteMaximale = 128.0D;

  /**
   * membre indiquant la mémoire déjà utilisée
   * ce membre n'est pas statique car toute la mémoire utilisée
   * dépend de l'utilisation de l'ordinateur.
   */
   public double memoireUtilisee;



   public Memoire()
   {
     Console.Out.Write("une mémoire d'une capacité de ");
     Console.Out.Write(capaciteMaximale );
     Console.Out.WriteLine(" mégas a été créée");
   }

}
 

        Enregistrez ce programme sous le nom ordinateurs.cs et compilez-le. A l'exécution vous obtenez la sortie :

 

        Aujourd'hui les pc possèdent des disques durs d'une capacité de 45 gigas(s) et une mémoire de 128 mégas

un ordinateur a été construit
il possède :
un disque Dur d'une capacité de 45 giga(s) a été créé
une mémoire d'une capacité de 128 mégas a été créée

nous formatons le disque dur.
la taille utlisée sur le disque est de :0

nous installons Windows sur le pc,
la taille utilisée sur le disque est de :0.6

nous démarrons Windows sur le pc,
mémoire prise est de :90.12

bref il reste 37.879999999999995 mégas en ram et 44.4 giga(s) sur le disque dur

 

        Etudions ce programme plus en détails :
Celui-ci est composé de quatre classes. Une classe qui sert de "lanceur" nommée TroisiemeProgramme. Elle contient :
la méthode Main qui va lancer le programme et manipuler les différents objets créés.
Une classe nommée Ordinateur qui permettra de créer des objets ordinateurs.
Une class DisqueDur, et une classe Memoire.
La classe Ordinateur possède deux membres. Un handle pour pointer sur des objets de type DisqueDur, et un handle pour pointer sur des objets de type Mémoire. Les Classes DisqueDur et Memoire disposent toutes deux des membres : capaciteMaximale qui indiquent la taille maximale de la mémoire (qu'il s'agisse de celle du disque dur ou de la mémoire centrale) et memoireUtilisee qui indique la mémoire déjà utilisée. Les membres capaciteMaximale de DisqueDur et de Memoire sont déclarés static car nous savons que tous les objets de type DisqueDur auront la même capacité maximale et que toutes les instances de Memoire auront la même capacité maximale aussi. Il sera donc plus judicieux d'en faire une variable de classe plutôt qu'une variable d'instance.
Revenons maintenant à notre méthode Main().

 
/**
  * méthode principale du programme
  */
  public static void Main()
  {
    double tailleInstallationWindows = .6D;
    double memoireUtiliseeParWindows = 90.12D;


    Console.Out.Write("Aujourd'hui les pc possèdent des disques durs");
    Console.Out.Write(" d'une capacité de ");
    Console.Out.Write(DisqueDur.capaciteMaximale);
    Console.Out.Write(" gigas(s) et une mémoire de ");
    Console.Out.Write(Memoire.capaciteMaximale);
    Console.Out.WriteLine(" mégas\n");

    Ordinateur monPC = new Ordinateur();


    monPC.hdd.memoireUtilisee = 0;
    Console.Out.Write("\nnous formatons le disque dur.\nla taille utlisée");
    Console.Out.Write(" sur le disque est de :");
    Console.Out.WriteLine(monPC.hdd.memoireUtilisee);


    monPC.hdd.memoireUtilisee = tailleInstallationWindows ;
    Console.Out.Write("\nnous installons Windows sur le pc,\nla taille utilisée");
    Console.Out.Write(" sur le disque est de :");
    Console.Out.WriteLine(monPC.hdd.memoireUtilisee);


    monPC.ram.memoireUtilisee = memoireUtiliseeParWindows ;
    Console.Out.Write("\nnous démarrons Windows sur le pc,\nmémoire prise est de :");
    Console.Out.WriteLine(monPC.ram.memoireUtilisee);

    Console.Out.Write("\nbref il reste ");
    Console.Out.Write(Memoire.capaciteMaximale - monPC.ram.memoireUtilisee);
    Console.Out.Write(" mégas en ram et ");
    Console.Out.Write(DisqueDur.capaciteMaximale - monPC.hdd.memoireUtilisee);
    Console.Out.Write(" giga(s) sur le disque dur");

  }
 

        Ici, nous avons créé deux variables nommées tailleInstallationWindows qui indiquent la taille que prend Windows sur le disque dur et memoireUtiliseeParWindows qui donne la mémoire utilisée par Windows lorsque celui-ci est en fonctionnement. Le groupe d'instructions :

 
Console.Out.Write("Aujourd'hui les pc possèdent des disques durs d'une capacité de ");
Console.Out.Write(DisqueDur.capaciteMaximale);
Console.Out.Write(" gigas(s) et une mémoire de ");
Console.Out.Write(Memoire.capaciteMaximale);
Console.Out.WriteLine(" mégas\n");
 

        est intéressant dans la mesure où nous affichons les membres capaciteMaximale et memoireUtilisee en les référant par rapport à la classe et non à l'objet (car ils sont statiques). Jusqu'ici en effet pour accéder aux membres d'un objet nous indiquions le handle de l'objet suivi d'un point puis du membre lui-même. Ici c'est le nom de la classe qui est utilisé à la place du handle :

 
Console.Out.Write(DisqueDur.capaciteMaximale);
 

        et

 
Console.Out.Write(Memoire.capaciteMaximale);
 

        Le programme crée alors un objet Ordinateur :

 
Ordinateur monPC = new Ordinateur();
 

        Cette instruction va appeler le constructeur de la classe Ordinateur :

 
/**
   * constructeur de la classe
   */
   public Ordinateur()
   {
     Console.Out.WriteLine("un ordinateur a été construit");
     Console.Out.WriteLine("il possède :");

     //faire pointer le handle sur un nouvel objet disque dur
     hdd = new DisqueDur();

     //faire pointer le handle sur un nouvel objet disque dur
     ram = new Memoire();

   }
 

        Le constructeur crée deux objets de type DisqueDur et de type Memoire provoquant l'appel des constructeurs respectifs de ces deux classes. Il fait alors pointer les handles hdd et ram sur les deux instances ainsi créées. Le constructeur de DisqueDur ne fait rien d'autre que d'afficher sur la sortie qu'un objet a été créé :

 
public DisqueDur()
   {
     Console.Out.Write("un disque Dur d'une capacité de ");
     Console.Out.Write(capaciteMaximale );
     Console.Out.WriteLine(" giga(s) a été créé");
   }
 

        celui de la classe Memoire fait lui aussi la même chose.

 
public Memoire()
{
  Console.Out.Write("une mémoire d'une capacité de ");
  Console.Out.Write(capaciteMaximale );
  Console.Out.WriteLine(" mégas a été créée");
}
 

        Dans le groupe d'instruction suivant nous manipulons les membres des deux objets hdd et ram appartenant à notre objet monPC

 
    monPC.hdd.memoireUtilisee = 0;
    Console.Out.Write("\nnous formatons le disque dur.\nla taille utlisée");
    Console.Out.Write(" sur le disque est de :");
    Console.Out.WriteLine(monPC.hdd.memoireUtilisee);


    monPC.hdd.memoireUtilisee = tailleInstallationWindows ;
    Console.Out.Write("\nnous installons Windows sur le pc,\nla taille utilisée");
    Console.Out.Write(" sur le disque est de :");
    Console.Out.WriteLine(monPC.hdd.memoireUtilisee);
 

        Nous initialisons tout d'abord la place prise sur le disque dur de notre ordinateur à 0 !

 
monPC.hdd.memoireUtilisee = 0;
 

        Pour accéder à ce membre nous donnons le nom de notre objet ( monPC) suivi du nom de l'objet de type DisqueDur qui lui appartient (hdd). Puis nous donnons le membre de cet objet que nous voulons atteindre, le tout séparé par des points. Nous pourrions lire l'instruction ci-dessus comme "le membre mémoireUtilisé de l'objet hdd de l'objet monPC doit prendre la valeur 0". Les trois lignes de code suivantes :

 
    monPC.hdd.memoireUtilisee = tailleInstallationWindows ;
    Console.Out.Write("\nnous installons Windows sur le pc,\nla taille utilisée");
    Console.Out.Write(" sur le disque est de :");
    Console.Out.WriteLine(monPC.hdd.memoireUtilisee);
 

        font la même chose mais avec le membre ram de l'objet monPC. Enfin le dernier groupe d'instructions :

 
monPC.ram.memoireUtilisee = memoireUtiliseeParWindows ;
    Console.Out.Write("\nnous démarrons Windows sur le pc,\nmémoire prise est de :");
    Console.Out.WriteLine(monPC.ram.memoireUtilisee);

    Console.Out.Write("\nbref il reste ");
    Console.Out.Write(Memoire.capaciteMaximale - monPC.ram.memoireUtilisee);
    Console.Out.Write(" mégas en ram et ");
    Console.Out.Write(DisqueDur.capaciteMaximale - monPC.hdd.memoireUtilisee);
    Console.Out.Write(" giga(s) sur le disque dur");
 

        Se contente d'afficher les valeurs des différents membres des objets de monPC. Notons ici l'utilisation par deux fois de l'opérateur de soustraction - :

 
Console.Out.Write(Memoire.capaciteMaximale - monPC.ram.memoireUtilisee);
 

        et

 
Console.Out.Write(DisqueDur.capaciteMaximale - monPC.hdd.memoireUtilisee);
 

        Qui demande au compilateur de soustraire la capacitéMaximal de la mémoire ou du disque dur, par la mémoire déjà occupée et d'afficher le résultat (nous reviendrons sur les opérateurs dans le chapitre 6).

Remarque

Ceux qui auront bien suivi le cours de programmation objet situé dans la partie langage du Monde De la Programmation pourrons déceler ici une amélioration possible. Il serait plus judicieux, en effet, de faire une classe mère que nous nommerions Stockage et qui aurait les membres capaciteMaximale et memoireUtilisee. Nous ferions alors hériter nos classes DisqueDur et Memoire de celle-ci.

Remarque

Nous comprenons mieux maintenant l'instruction :
Console.Out.WriteLine("")
Out est un objet static appartenant à la classe Console, nous pouvons donc l'utiliser sans avoir à créer d'objets de type Console. De même la méthode WriteLine() (ainsi que Write() ) est elle aussi static. Nous n'avons pas besoin de créer d'objets de type Out pour l'utiliser. Dans ces deux cas le nom de la classe suivi d'un point et du nom du membre suffit.

 

[ Précédent | Index | Suivant ]