Skip to content

3. Nozioni di base sul linguaggio C#

3.1. Introduction

Tratteremo inizialmente il C# come un linguaggio di programmazione classico. Affronteremo le classi in un secondo momento. In un programma si trovano due elementi:

  • i dati
  • le istruzioni che li manipolano

In genere si cerca di separare i dati dalle istruzioni:

3.2. I dati in C#

C# utilizza i seguenti tipi di dati:

  1. i numeri interi
  1. i numeri reali
  2. i numeri decimali
  3. i caratteri e le stringhe
  4. i valori booleani
  5. gli oggetti

3.2.1. I tipi di dati predefiniti

Tipo C#
Tipo .NET
Dato rappresentato
Suffisso
dei valori letterali
Codifica
Intervallo di valori
char
Carattere (i)
carattere
 
2 byte
carattere Unicode (UTF-16)
string
Stringa (C)
stringa di caratteri
  
riferimento a una sequenza di caratteri Unicode
int
Int32 (S)
numero intero
 
4 byte
[-231, 231-1] [–2147483648, 2147483647]
uint
UInt32 (S)
..
U
4 byte
[0, 232-1] [0, 4294967295]
long
Int64 (S)
..
L
8 byte
[-263, 263 -1] [–9223372036854775808, 9223372036854775807]
ulong
UInt64 (S)
..
UL
8 byte
[0, 264 -1] [0, 18446744073709551615]
sbyte
 
..
 
1 byte
[-27 , 27 -1] [-128,+127]
byte
Byte (S)
..
 
1 byte
[0 , 28 -1] [0,255]
short
Int16 (S)
..
 
2 byte
[-215, 215-1] [-32768, 32767]
ushort
UInt16 (S)
..
 
2 byte
[0, 216-1] [0,65535]
float
Singolo (S)
numero reale
F
4 byte
[1.5 10-45, 3.4 10+38] in valore assoluto
double
Doppio (S)
..
D
8 byte
[-1.7 10+308, 1.7 10+308] in valore assoluto
decimal
Decimale (S)
numero decimale
M
16 byte
[1.0 10-28,7.9 10+28] in valore assoluto con 28 cifre significative
bool
Booleano (S)
..
 
1 byte
vero, falso
object
Oggetto (C)
riferimento all'oggetto
  
riferimento all'oggetto

Sopra, abbiamo affiancato ai tipi C# i loro equivalenti .NET con il commento (S) se si tratta di una struttura e (C) se si tratta di una classe. Si nota che esistono due possibili tipi per un intero a 32 bit: int e Int32. Il tipo int è un tipo C#. Int32 è una struttura appartenente allo spazio dei nomi System. Il suo nome completo è quindi System.Int32. Il tipo int è un alias C# che indica la struttura .NET System.Int32. Allo stesso modo, il tipo C# string è un alias per il tipo .NET System.String. System.String è una classe e non una struttura. I due concetti sono simili, ma presentano la seguente differenza fondamentale:

  • una variabile di tipo Structure viene gestita tramite il suo valore
  • una variabile di tipo Classe viene gestita tramite il proprio indirizzo (riferimento nel linguaggio orientato agli oggetti).

Sia una structure che una classe sono tipi complessi dotati di attributi e metodi. Pertanto, si potrà scrivere:

string nomDuType=3.GetType().FullName;

Nel codice sopra riportato, il letterale 3 è per impostazione predefinita di tipo C# int, quindi di tipo .NET System.Int32. Questa struttura dispone di un metodo GetType() che restituisce un oggetto che incapsula le caratteristiche del tipo di dati System.Int32. Tra queste, la proprietà FullName restituisce il nome completo del tipo. Si vede quindi che il letterale 3 è un oggetto più complesso di quanto sembri a prima vista.

Ecco un programma che illustra questi diversi punti:


using System;

namespace Chap1 {
    class P00 {
        static void Main(string[] args) {
            // esempio 1
            int ent = 2;
            float fl = 10.5F;
            double d = -4.6;
            string s = "essai";
            uint ui = 5;
            long l = 1000;
            ulong ul = 1001;
            byte octet = 5;
            short sh = -4;
            ushort ush = 10;
            decimal dec = 10.67M;
            bool b = true;
            Console.WriteLine("Type de ent[{1}] : [{0},{2}]", ent.GetType().FullName, ent,sizeof(int));
            Console.WriteLine("Type de fl[{1}]: [{0},{2}]", fl.GetType().FullName, fl, sizeof(float));
            Console.WriteLine("Type de d[{1}] : [{0},{2}]", d.GetType().FullName, d, sizeof(double));
            Console.WriteLine("Type de s[{1}] : [{0}]", s.GetType().FullName, s);
            Console.WriteLine("Type de ui[{1}] : [{0},{2}]", ui.GetType().FullName, ui, sizeof(uint));
            Console.WriteLine("Type de l[{1}] : [{0},{2}]", l.GetType().FullName, l, sizeof(long));
            Console.WriteLine("Type de ul[{1}] : [{0},{2}]", ul.GetType().FullName, ul, sizeof(ulong));
            Console.WriteLine("Type de b[{1}] : [{0},{2}]", octet.GetType().FullName, octet, sizeof(byte));
            Console.WriteLine("Type de sh[{1}] : [{0},{2}]", sh.GetType().FullName, sh, sizeof(short));
            Console.WriteLine("Type de ush[{1}] : [{0},{2}]", ush.GetType().FullName, ush, sizeof(ushort));
            Console.WriteLine("Type de dec[{1}] : [{0},{2}]", dec.GetType().FullName, dec, sizeof(decimal));
            Console.WriteLine("Type de b[{1}] : [{0},{2}]", b.GetType().FullName, b, sizeof(bool));
        }
    }
}
  • riga 7: dichiarazione di un intero ent
  • riga 19: il tipo di una variabile v può essere ottenuto tramite v.GetType().FullName. La dimensione di una struttura S può essere ottenuta tramite sizeof(S). L'istruzione Console.WriteLine("... {0} ... {1} ...", param0, param1, ...) visualizza sullo schermo il testo che costituisce il suo primo parametro, sostituendo ogni notazione {i} con il valore dell'espressione parami.
  • riga 22: poiché il tipo string indica una classe e non una struttura, non è possibile utilizzare l'operatore sizeof.

Ecco il risultato dell'esecuzione:

Type de ent[2] : [System.Int32,4]
Type de fl[10,5]: [System.Single,4]
Type de d[-4,6] : [System.Double,8]
Type de s[essai] : [System.String]
Type de ui[5] : [System.UInt32,4]
Type de l[1000] : [System.Int64,8]
Type de ul[1001] : [System.UInt64,8]
Type de b[5] : [System.Byte,1]
Type de sh[-4] : [System.Int16,2]
Type de ush[10] : [System.UInt16,2]
Type de dec[10,67] : [System.Decimal,16]
Type de b[True] : [System.Boolean,1]

La visualizzazione riporta i tipi .NET e non gli alias C#.

3.2.2. Notazione dei dati letterali

entier int (32 bits)
145, -7, 0xFF (esadecimale)
entier long (64 bits) - suffixe L
100000L
réel double
134,789, -45E-18 (-45 × 10⁻¹⁸)
réel float (suffixe F)
134.789F, -45E-18F (-45 10-18)
réel decimal (suffixe M)
100000M
caractère char
'A', 'b'
chaîne de caractères string
"oggi" "c:\\cap1\\paragrafo3" @"c:\cap1\paragrafo3"
booléen bool
vero, falso
date
new DateTime(1954,10,13) (anno, mese, giorno) per il 13/10/1954

Si notino le due stringhe letterali: "c:\\chap1\\paragraph3" e @"c:\chap1\paragraph3". Nelle stringhe letterali, il carattere \ viene interpretato. Pertanto "\n" rappresenta il carattere di fine riga e non la sequenza dei due caratteri \ e n. Se si volesse ottenere tale sequenza, bisognerebbe scrivere "\\n", dove la sequenza \\ viene interpretata come un unico carattere \. Si potrebbe scrivere anche @"\n" per ottenere lo stesso risultato. La sintassi @"testo" richiede che il testo venga preso esattamente così come è scritto. A volte questa viene chiamata stringa verbatim.

3.2.3. Dichiarazione dei dati

3.2.3.1. Ruolo delle dichiarazioni

Un programma gestisce dati caratterizzati da un nome e da un tipo. Questi dati vengono memorizzati in memoria. Al momento della compilazione del programma, il compilatore assegna a ciascun dato una posizione in memoria caratterizzata da un indirizzo e da una dimensione. Lo fa avvalendosi delle dichiarazioni effettuate dal programmatore.

Inoltre, queste consentono al compilatore di individuare eventuali errori di programmazione. Ad esempio, l’operazione

x=x*2;

sarà segnalata come errata se x è, ad esempio, una stringa di caratteri.

3.2.3.2. Dichiarazione delle costanti

La sintassi per dichiarare una costante è la seguente:

    const type nom=valeur;          //definisce una costante nome=valore

Ad esempio:

const float myPI=3.141592F;    

Perché dichiarare le costanti?

  1. La lettura del programma risulterà più agevole se alla costante viene assegnato un nome significativo:
    const  float taux_tva=0.186F;
  1. Modificare il programma sarà più semplice se la «costante» dovesse cambiare. Quindi, nel caso precedente, se l’aliquota IVA passasse al 33%, l’unica modifica da apportare sarebbe quella di modificare l’istruzione che ne definisce il valore:
    const float taux_tva=0.33F;

Se invece si fosse utilizzato esplicitamente 0,186 nel programma, sarebbe stato necessario modificare numerose istruzioni.

3.2.3.3. Dichiarazione delle variabili

Una variabile è identificata da un nome e si riferisce a un tipo di dati. C# distingue tra maiuscole e minuscole. Pertanto, le variabili FIN e fin sono diverse.

Le variabili possono essere inizializzate al momento della loro dichiarazione. La sintassi per dichiarare una o più variabili è:

 Identificateur_de_type variable1[=valeur1],variable2=[valeur2],...;

dove Identificateur_de_type è un tipo predefinito o un tipo definito dal programmatore. Facoltativamente, una variabile può essere inizializzata al momento della dichiarazione.

È anche possibile non specificare il tipo esatto di una variabile utilizzando la parola chiave var al posto di Identificateur_de_type:

 var variable1=valeur1,variable2=valeur2,...;

La parola chiave var non significa che le variabili non abbiano un tipo preciso. La variabile variablei ha il tipo del dato valeuri che le viene assegnato. L'inizializzazione è qui obbligatoria affinché il compilatore possa dedurne il tipo della variabile.

Ecco un esempio:


using System;

namespace Chap1 {
    class P00 {
        static void Main(string[] args) {
            int i=2;
            Console.WriteLine("Type de int i=2 : {0},{1}",i.GetType().Name,i.GetType().FullName);
            var j = 3;
            Console.WriteLine("Type de var j=3 : {0},{1}", j.GetType().Name, j.GetType().FullName);
            var aujourdhui = DateTime.Now;
            Console.WriteLine("Type de var aujourdhui : {0},{1}", aujourdhui.GetType().Name, aujourdhui.GetType().FullName);
        }
    }
}
  • riga 6: un dato tipizzato esplicitamente
  • riga 7: (dato).GetType().Name è il nome abbreviato di (dato), (dato).GetType().FullName è il nome completo di (dato)
  • riga 8: un dato tipizzato implicitamente. Poiché 3 è di tipo int, j sarà di tipo int.
  • riga 10: un dato tipizzato implicitamente. Poiché DateTime.Now è di tipo DateTime, aujourdhui sarà di tipo DateTime.

All'esecuzione, si ottiene il seguente risultato:

1
2
3
Type de int i=2 : Int32,System.Int32
Type de var j=3 : Int32,System.Int32
Type de var aujourdhui : DateTime,System.DateTime

Una variabile tipizzata implicitamente dalla parola chiave var non può successivamente cambiare tipo. Pertanto, dopo la riga 10 del codice non si potrebbe scrivere la riga:


            var aujourdhui = "aujourd'hui";

Vedremo in seguito che è possibile dichiarare un tipo “al volo” all’interno di un’espressione. Si tratta in questo caso di un tipo anonimo, ovvero un tipo a cui l’utente non ha assegnato un nome. Sarà il compilatore ad assegnare un nome a questo nuovo tipo. Se un dato di tipo anonimo deve essere assegnato a una variabile, l’unico modo per dichiararla è utilizzare la parola chiave var.

3.2.4. Le conversioni tra numeri e stringhe di caratteri

nombre -> chaîne
nombre.ToString()
chaine -> int
int.Parse(stringa) oppure System.Int32.Parse
chaîne -> long
long.Parse(stringa) oppure System.Int64.Parse
chaîne -> double
double.Parse (stringa) o System.Double.Parse (stringa)
chaîne -> float
float.Parse (stringa) o System.Float.Parse (stringa)

La conversione di una stringa in un numero potrebbe non andare a buon fine se la stringa non rappresenta un numero valido. In tal caso viene generato un errore irreversibile denominato eccezione. Questo errore può essere gestito tramite la seguente clausola try/catch:


try{
    appel de la fonction susceptible de générer l'exception
} catch (Exception e){
    traiter l'exception e
}
instruction suivante

Se la funzione non genera un'eccezione, si passa all'istruzione successiva; in caso contrario, si passa al corpo della clausola catch e poi all'istruzione successiva. Torneremo in seguito sulla gestione delle eccezioni. Ecco un programma che illustra alcune tecniche di conversione tra numeri e stringhe di caratteri. In questo esempio la funzione affiche visualizza sullo schermo il valore del proprio parametro. Pertanto, affiche(S) visualizza sullo schermo il valore di S, dove S è di tipo string.


using System;

namespace Chap1 {
    class P01 {
        static void Main(string[] args) {

            // dati
            const int i = 10;
            const long l = 100000;
            const float f = 45.78F;
            double d = -14.98;

            // numero --> stringa
            affiche(i.ToString());
            affiche(l.ToString());
            affiche(f.ToString());
            affiche(d.ToString());

            //booleano --> stringa
            const bool b = false;
            affiche(b.ToString());

            // stringa --> int
            int i1;
            i1 = int.Parse("10");
            affiche(i1.ToString());
            try {
                i1 = int.Parse("10.67");
                affiche(i1.ToString());
            } catch (Exception e) {
                affiche("Erreur : " + e.Message);
            }

            // stringa --> long
            long l1;
            l1 = long.Parse("100");
            affiche(l1.ToString());
            try {
                l1 = long.Parse("10.675");
                affiche(l1.ToString());
            } catch (Exception e) {
                affiche("Erreur : " + e.Message);
            }

            // stringa --> double
            double d1;
            d1 = double.Parse("100,87");
            affiche(d1.ToString());
            try {
                d1 = double.Parse("abcd");
                affiche(d1.ToString());
            } catch (Exception e) {
                affiche("Erreur : " + e.Message);
            }

            // stringa --> float
            float f1;
            f1 = float.Parse("100,87");
            affiche(f1.ToString());
            try {
                d1 = float.Parse("abcd");
                affiche(f1.ToString());
            } catch (Exception e) {
                affiche("Erreur : " + e.Message);
            }

        }// fine main

        public static void affiche(string S) {
            Console.Out.WriteLine("S={0}",S);
        }
    }// fine classe
}

Righe 30-32: si gestisce l'eventuale eccezione che potrebbe verificarsi. e.Message è il messaggio di errore associato all'eccezione e.

I risultati ottenuti sono i seguenti:

S=10
S=100000
S=45,78
S=-14,98
S=False
S=10
S=Erreur : Input string was not in a correct format.
S=100
S=Erreur : Input string was not in a correct format.
S=100,87
S=Erreur : Input string was not in a correct format.
S=100,87
S=Erreur : Input string was not in a correct format.

Si noti che i numeri reali sotto forma di stringa di caratteri devono utilizzare la virgola e non il punto decimale. Pertanto si scriverà

double d1=10.7; 

ma

double d2=int.Parse("10,7");

3.2.5. Gli array di dati

Un array in C# è un oggetto che consente di raggruppare sotto un unico identificatore dati dello stesso tipo. La sua dichiarazione è la seguente:

Type[] tableau[]=new Type[n]

n è il numero di dati che l'array può contenere. La sintassi Array[i] indica il dato n. i dove i appartiene all'intervallo [0,n-1]. Qualsiasi riferimento al dato Tableau[i] in cui i non appartiene all’intervallo [0,n-1] provocherà un’eccezione. Un array può essere inizializzato contemporaneamente alla sua dichiarazione:

    int[] entiers=new int[] {0,10,20,30};

oppure, più semplicemente:

    int[] entiers={0,10,20,30};

Gli array hanno una proprietà Length che indica il numero di elementi dell'array.

Un array bidimensionale può essere dichiarato come segue:

Type[,] array=new Type[n,m];

dove n è il numero di righe, m il numero di colonne. La sintassi Tableau[i,j] indica l'elemento j della riga i dell'array. Anche l'array bidimensionale può essere inizializzato contemporaneamente alla sua dichiarazione:

    double[,] réels=new double[,] { {0.5, 1.7}, {8.4, -6}};

oppure, più semplicemente:

    double[,] réels={ {0.5, 1.7}, {8.4, -6}};

Il numero di elementi in ciascuna delle dimensioni può essere ottenuto tramite il metodo GetLength(i), dove i=0 rappresenta la dimensione corrispondente al primo indice, i=1 quella corrispondente al secondo indice, …

Il numero totale di dimensioni si ottiene con la proprietà Rank, mentre il numero totale di elementi con la proprietà Length.

Un array di array viene dichiarato come segue:

Type[][] array = new Type[n][];

La dichiarazione sopra riportata crea un array di n righe. Ogni elemento array[i] è un riferimento a un array unidimensionale. Questi riferimenti array[i] non vengono inizializzati nella dichiarazione sopra riportata. Il loro valore è il riferimento null.

L'esempio seguente illustra la creazione di un array di array:


            // un array di array
            string[][] noms = new string[3][];
            for (int i = 0; i < noms.Length; i++) {
                noms[i] = new string[i + 1];
            }//for
            // inizializzazione
            for (int i = 0; i < noms.Length; i++) {
                for (int j = 0; j < noms[i].Length; j++) {
                    noms[i][j] = "nom" + i + j;
                }//for j
            }//for i
  • riga 2: un array noms composto da 3 elementi di tipo string[][]. Ogni elemento è un puntatore ad array (un riferimento a un oggetto) i cui elementi sono di tipo string[].
  • righe 3-5: i 3 elementi dell'array noms vengono inizializzati. Ciascuno di essi ora «punta» a un array di elementi di tipo string[]. noms[i][j] è l’elemento j dell’array di tipo string [] a cui fa riferimento noms[i].
  • riga 9: inizializzazione dell'elemento nomi[i][j] all'interno di un doppio ciclo. In questo caso, noms[i] è un array di i+1 elementi. Poiché noms[i] è un array, noms[i].Length rappresenta il numero dei suoi elementi.

Ecco un esempio che riunisce i tre tipi di array che abbiamo appena presentato:


using System;

namespace Chap1 {
    // matrici

    using System;

    // classe di test
    public class P02 {
        public static void Main() {
            // un array unidimensionale inizializzato
            int[] entiers = new int[] { 0, 10, 20, 30 };
            for (int i = 0; i < entiers.Length; i++) {
                Console.Out.WriteLine("entiers[{0}]={1}", i, entiers[i]);
            }//per

            // un array bidimensionale inizializzato
            double[,] réels = new double[,] { { 0.5, 1.7 }, { 8.4, -6 } };
            for (int i = 0; i < réels.GetLength(0); i++) {
                for (int j = 0; j < réels.GetLength(1); j++) {
                    Console.Out.WriteLine("réels[{0},{1}]={2}", i, j, réels[i, j]);
                }//for j
            }//for i

            // un array di array
            string[][] noms = new string[3][];
            for (int i = 0; i < noms.Length; i++) {
                noms[i] = new string[i + 1];
            }//per
            // inizializzazione
            for (int i = 0; i < noms.Length; i++) {
                for (int j = 0; j < noms[i].Length; j++) {
                    noms[i][j] = "nom" + i + j;
                }//per j
            }//per i
            // visualizzazione
            for (int i = 0; i < noms.Length; i++) {
                for (int j = 0; j < noms[i].Length; j++) {
                    Console.Out.WriteLine("noms[{0}][{1}]={2}", i, j, noms[i][j]);
                }//per j
            }//per i
        }//Principale
    }//classe
}//spazio dei nomi

All’esecuzione, otteniamo i seguenti risultati:

entiers[0]=0
entiers[1]=10
entiers[2]=20
entiers[3]=30
réels[0,0]=0,5
réels[0,1]=1,7
réels[1,0]=8,4
réels[1,1]=-6
noms[0][0]=nom00
noms[1][0]=nom10
noms[1][1]=nom11
noms[2][0]=nom20
noms[2][1]=nom21
noms[2][2]=nom22

3.3. Le istruzioni di base di C#

Si distinguono

1 le istruzioni elementari eseguite dal computer.

2 le istruzioni di controllo dello svolgimento del programma.

Le istruzioni elementari risultano evidenti se si considera la struttura di un microcomputer e delle sue periferiche.

  1. lettura delle informazioni provenienti dalla tastiera

  2. elaborazione delle informazioni

  3. scrittura di informazioni sullo schermo

3.3.1. Scrittura sullo schermo

Esistono diverse istruzioni per la scrittura sullo schermo:

Console.Out.WriteLine(expression)
Console.WriteLine(expression)
Console.Error.WriteLine (expression)

dove expression rappresenta qualsiasi tipo di dato che possa essere convertito in una stringa di caratteri per essere visualizzato sullo schermo. Tutti gli oggetti C# o .NET dispongono di un metodo ToString() utilizzato per effettuare tale conversione.

La classe System.Console consente di accedere alle operazioni di scrittura su schermo (Write, WriteLine). La classe Console dispone di due proprietà, Out ed Error, che sono flussi di scrittura di tipo TextWriter:

  • Console.WriteLine() è equivalente a Console.Out.WriteLine() e scrive sul flusso Out solitamente associato alla schermata.
  • Console.Error.WriteLine() scrive sul flusso Error, solitamente anch'esso associato allo schermo.

I flussi Out e Error possono essere reindirizzati verso file di testo durante l'esecuzione del programma, come vedremo tra poco.

3.3.2. Lettura dei dati digitati sulla tastiera

Il flusso di dati proveniente dalla tastiera è rappresentato dall’oggetto Console.In di tipo TextReader. Questo tipo di oggetti consente di leggere una riga di testo con il metodo ReadLine:

    string ligne=Console.In.ReadLine();

La classe Console offre un metodo ReadLine associato per impostazione predefinita al flusso In. È quindi possibile scrivere:

    string ligne=Console.ReadLine();

La riga digitata sulla tastiera viene memorizzata nella variabile ligne e può quindi essere utilizzata dal programma. Il flusso In può essere reindirizzato verso un file, così come i flussi Out ed Error.

3.3.3. Esempio di operazioni di I/O

Ecco un breve programma che illustra le operazioni di input-output da tastiera/schermo:


using System;

namespace Chap1 {
    // classe di test
    public class P03 {
        public static void Main() {

            // scrittura sul flusso Out
            object obj = new object();
            Console.Out.WriteLine(obj);

            // scrittura sul flusso Error
            int i = 10;
            Console.Error.WriteLine("i=" + i);

            // lettura di una riga immessa dalla tastiera
            Console.Write("Tapez une ligne : ");
            string ligne = Console.ReadLine();
            Console.WriteLine("ligne={0}", ligne);
        }//fine main
    }//fine classe
}
  • riga 9: obj è un riferimento a un oggetto
  • riga 10: obj viene visualizzato sullo schermo. Per impostazione predefinita, viene chiamato il metodo obj.ToString().
  • riga 14: è anche possibile scrivere:

            Console.Error.WriteLine("i={0}",i);

Il primo parametro "i={0}" è il formato di visualizzazione, gli altri parametri sono le espressioni da visualizzare. Gli elementi {n} sono parametri "posizionali". All'esecuzione, il parametro {n} viene sostituito dal valore dell'espressione n. n.

Il risultato dell’esecuzione è il seguente:

1
2
3
4
System.Object
i=10
Tapez une ligne : je suis là
ligne=je suis là
  • riga 1: il risultato visualizzato dalla riga 10 del codice. Il metodo obj.ToString() ha visualizzato il nome del tipo della variabile obj: System.Object. Il tipo object è un alias C# del tipo .NET System.Object.

3.3.4. Reindirizzamento delle I/O

In DOS e UNIX sono presenti tre periferiche standard denominate:

  1. periferica di ingresso standard - indica per impostazione predefinita la tastiera e porta il n. 0
  2. periferica di uscita standard - indica per impostazione predefinita lo schermo e ha il n. 1
  3. periferica di errore standard - indica per impostazione predefinita lo schermo e ha il numero 2

In C#, il flusso di scrittura Console.Out scrive sul dispositivo 1, il flusso di scrittura Console.Error scrive sul dispositivo 2 e il flusso di lettura Console.In legge i dati provenienti dal dispositivo 0.

Quando si avvia un programma in DOS o Unix, è possibile specificare quali saranno i dispositivi 0, 1 e 2 per il programma in esecuzione. Consideriamo la seguente riga di comando:

pg arg1 arg2 .. argn

Aggiungendo gli argomenti argi al programma pg, è possibile reindirizzare le periferiche di I/O standard verso dei file:

0<in.txt
il flusso di ingresso standard n. 0 viene reindirizzato al file in.txt. Nel programma, il flusso Console.In preleverà quindi i propri dati dal file in.txt.
1>out.txt
reindirizza l'uscita n. 1 verso il file out.txt. Di conseguenza, nel programma il flusso Console.Out scriverà i propri dati nel file out.txt
1>>out.txt
idem, ma i dati scritti vengono aggiunti al contenuto attuale del file out.txt.
2>error.txt
reindirizza l'uscita n. 2 verso il file error.txt. Di conseguenza, nel programma il flusso Console.Error scriverà i propri dati nel file error.txt
2>>error.txt
lo stesso, ma i dati scritti vengono aggiunti al contenuto attuale del file error.txt.
1>out.txt 2>error.txt
Le periferiche 1 e 2 vengono entrambe reindirizzate verso dei file

Si noti che per reindirizzare i flussi di I/O del programma pg verso dei file, non è necessario modificare il programma pg. È il sistema operativo a determinare la natura delle periferiche 0, 1 e 2. Consideriamo il seguente programma:


using System;

namespace Chap1 {

    // reindirizzamenti
    public class P04 {
        public static void Main(string[] args) {
            // lettura flusso In
            string data = Console.In.ReadLine();
            // scrittura flusso Out
            Console.Out.WriteLine("écriture dans flux Out : " + data);
            // scrittura flusso Error
            Console.Error.WriteLine("écriture dans flux Error : " + data);
        }//Main
    }//classe
}

Generiamo l'eseguibile da questo codice sorgente:

  • in [1]: l'eseguibile viene creato facendo clic con il tasto destro del mouse sul progetto / Build
  • in [2]: in una finestra DOS, l’eseguibile 04.exe è stato creato nella cartella bin/Release del progetto.

Eseguiamo i seguenti comandi nella finestra DOS [2]:

1
2
3
4
5
6
7
8
...\04\bin\Release>echo test >in.txt
...\04\bin\Release>more in.txt
test
...\04\bin\Release>04 0<in.txt 1>out.txt 2>err.txt
...\04\bin\Release>more out.txt
écriture dans flux Out : test
...\04\bin\Release>more err.txt
écriture dans flux Error : test
  • riga 1: si inserisce la stringa test nel file in.txt
  • righe 2-3: visualizziamo il contenuto del file in.txt per verifica
  • riga 4: esecuzione del programma 04.exe. Il flusso In viene reindirizzato al file in.txt, il flusso Out verso il file out.txt, il flusso Error verso il file err.txt. L'esecuzione non genera alcuna visualizzazione.
  • righe 5-6: contenuto del file out.txt. Questo contenuto ci mostra che:
  • il file in.txt è stato letto
  • l'output su schermo è stato reindirizzato al file out.txt
  • righe 7-8: verifica analoga per il file err.txt

Si vede chiaramente che i flussi Out e In non scrivono sugli stessi dispositivi, poiché è stato possibile reindirizzarli separatamente.

3.3.5. Assegnazione del valore di un'espressione a una variabile

Ci interessa qui l’operazione variable=expression;

L’espressione può essere di tipo: aritmetico, relazionale, booleano, stringa

3.3.5.1. Interpretazione dell’operazione di assegnazione

L'operazione variable=expression;

è essa stessa un'espressione la cui valutazione avviene nel modo seguente:

  • viene valutata la parte destra dell'assegnazione: il risultato è un valore V.
  • il valore V viene assegnato alla variabile
  • il valore V è anche il valore dell'assegnazione, considerata questa volta come espressione.

È così che l’operazione

    V1=V2=expression

è valida. A causa della priorità, verrà valutato l'operatore = più a destra. Si ha quindi

    V1=(V2=expression)

L'espressione V2=espressione viene valutata e ha come valore V. La valutazione di questa espressione ha provocato l'assegnazione di V a V2. L'operatore = successivo viene quindi valutato nella forma:

    V1=V

Il valore di questa espressione è ancora V. La sua valutazione determina l'assegnazione di V a V1.

Pertanto, l'operazione V1=V2=espressione

è un'espressione la cui valutazione

  • provoca l'assegnazione del valore di expression alle variabili V1 e V2
  • restituisce come risultato il valore expression.

È possibile generalizzare l'espressione del tipo:

V1=V2=....=Vn=expression

3.3.5.2. Espressione aritmetica

Gli operatori delle espressioni aritmetiche sono i seguenti:

  • somma

  • sottrazione

* moltiplicazione

/ divisione: il risultato è il quoziente esatto se almeno uno degli operandi è un numero reale. Se entrambi gli operandi sono interi, il risultato è il quoziente intero. Quindi 5/2 -> 2 e 5,0/2 -> 2,5.

% divisione: il risultato è il resto indipendentemente dalla natura degli operandi, mentre il quoziente è intero. Si tratta quindi dell'operazione modulo.

Esistono diverse funzioni matematiche. Eccone alcune:

double Sqrt(double x)
radice quadrata
double Cos(double x)
Coseno
double Sin(double x)
Sen
double Tan(double x)
Tangente
double Pow(double x,double y)
x alla potenza y (x>0)
double Exp(double x)
Esponenziale
double Log(double x)
Logaritmo naturale
double Abs(double x)
valore assoluto

ecc...

Tutte queste funzioni sono definite in una classe C# denominata Math. Quando le si utilizza, è necessario anteporre il nome della classe in cui sono definite. Pertanto si scriverà:

double x, y=4;
x=Math.Sqrt(y);

La definizione completa della classe Math è la seguente:

Image

Image

Image

Image

Image

3.3.5.3. Priorità nella valutazione delle espressioni aritmetiche

L'ordine di precedenza degli operatori nella valutazione di un'espressione aritmetica è il seguente (dal più prioritario al meno prioritario):

[fonctions], [ ( )],[ *, /, %], [+, -]

Gli operatori appartenenti allo stesso blocco [ ] hanno la stessa priorità.

3.3.5.4. Espressioni relazionali

Gli operatori sono i seguenti:

       <, <=, ==, !=, >, >=

Priorità degli operatori

>, >=, <, <=
==, !=

Il risultato di un'espressione relazionale è il valore booleano false se l'espressione è falsa, true in caso contrario.

      bool fin;
      int x=...;
      fin=x>4;

Confronto tra due caratteri

Siano dati due caratteri C1 e C2. È possibile confrontarli utilizzando gli operatori

    <, <=, ==, !=, >, >=

Vengono quindi confrontati i loro codici Unicode, che sono numeri. Secondo l'ordine Unicode si hanno le seguenti relazioni:

espace < .. < '0' < '1' < .. < '9' < .. < 'A' < 'B' < .. < 'Z' < .. < 'a' < 'b' < .. <'z'

Confronto tra due stringhe di caratteri

Vengono confrontate carattere per carattere. La prima disuguaglianza riscontrata tra due caratteri determina una disuguaglianza dello stesso segno tra le stringhe.

Esempi:

Si considerino le stringhe "Gatto" e "Cane"

Quest'ultima disuguaglianza permette di affermare che "Gatto" < "Cane".

Supponiamo di confrontare le stringhe "Gatto" e "Gattino". C'è uguaglianza per tutto il tempo fino all'esaurimento della stringa "Gatto". In questo caso, la stringa esaurita viene dichiarata la più "piccola". Si ha quindi la relazione

    "Chat" < "Chaton".

Funzioni di confronto tra due stringhe

È possibile utilizzare gli operatori relazionali == e != per verificare se due stringhe sono uguali o meno, oppure il metodo Equals della classe System.String. Per le relazioni < <= > >=, occorre utilizzare il metodo CompareTo della classe System.String:


using System;

namespace Chap1 {
    class P05 {
        static void Main(string[] args) {
            string chaine1="chat", chaine2="chien";
            int n = chaine1.CompareTo(chaine2);
            bool egal = chaine1.Equals(chaine2);
            Console.WriteLine("i={0}, egal={1}", n, egal);
            Console.WriteLine("chien==chaine1:{0},chien!=chaine2:{1}", "chien"==chaine1,"chien" != chaine2);
        }
    }
}

Riga 7, la variabile i avrà il valore:

0 se le due stringhe sono uguali

1 se la stringa n. 1 è maggiore della stringa n. 2

-1 se la stringa n. 1 è minore della stringa n. 2

Riga 8: la variabile egal assumerà il valore true se le due stringhe sono uguali, false in caso contrario. Riga 10: si utilizzano gli operatori == e != per verificare se due stringhe sono uguali o meno.

Risultati dell'esecuzione:

i=-1, egal=False
chien==chaine1:False,chien!=chaine2:False

3.3.5.5. Espressioni booleane

Gli operatori utilizzabili sono AND (&&) OR (||) NOT (!). Il risultato di un'espressione booleana è un valore booleano.

Priorità degli operatori:

  1. !
  2. &&
  3. ||

            double x = 3.5;
            bool valide = x > 2 && x < 4;

Gli operatori relazionali hanno la precedenza sugli operatori && e ||.

3.3.5.6. Operazioni bit a bit

Gli operatori

Siano i e j due numeri interi.

i<<n
sposta i di n bit a sinistra. I bit di riempimento sono zeri.
i>>n
sposta i di n bit a destra. Se i è un numero intero con segno (signed char, int, long), il bit di segno viene mantenuto.
i & j
esegue l'OR logico bit per bit tra i e j.
i | j
esegue la logica bit per bit tra i e j (OU).
~i
completa i a 1
i^j
esegue l'operazione OU EXCLUSIF su i e j

Si consideri il seguente codice:


short i = 100, j = -13;
ushort k = 0xF123;
Console.WriteLine("i=0x{0:x4}, j=0x{1:x4}, k=0x{2:x4}", i,j,k);
Console.WriteLine("i<<4=0x{0:x4}, i>>4=0x{1:x4},k>>4=0x{2:x4},i&j=0x{3:x4},i|j=0x{4:x4},~i=0x{5:x4},j<<2=0x{6:x4},j>>2=0x{7:x4}", i << 4, i >> 4, k >> 4, (short)(i & j), (short)(i | j), (short)(~i), (short)(j << 2), (short)(j >> 2));
  • il formato {0:x4} visualizza il parametro n. 0 in formato esadecimale (x) con 4 caratteri (4).

I risultati dell'esecuzione sono i seguenti:

i=0x0064, j=0xfff3, k=0xf123
i<<4=0x0640, i>>4=0x0006,k>>4=0x0f12,i&j=0x0060,i|j=0xfff7,~i=0xff9b,j<<2=0xffcc,j>>2=0xfffc

3.3.5.7. Combinazione di operatori

a=a+b può essere scritto come a+=b

a=a-b può essere scritto come a-=b

Lo stesso vale per gli operatori /, %, *, <<, >>, &, |, ^. Pertanto, a=a/2; può essere scritto come a/=2;

3.3.5.8. Operatori di incremento e decremento

La notazione variable++ significa variable=variable+1 oppure variable+=1

La notazione variable-- indica variable=variable-1 oppure variable-=1.

3.3.5.9. L'operatore ternario?

L'espressione

    expr_cond ? expr1:expr2

viene calcolata come segue:

1 viene valutata l'espressione expr_cond. Si tratta di un'espressione condizionale con valore vrai o faux

2 Se è vera, il valore dell'espressione è quello di expr1 e expr2 non viene valutata.

3 Se è falsa, avviene il contrario: il valore dell’espressione è quello di expr2 e expr1 non viene valutato.

L'operazione i=(j>4 ? j+1:j-1); assegnerà alla variabile i il valore j+1 se j>4, altrimenti j-1. È come scrivere if(j>4) i=j+1; else i=j-1; ma è più conciso.

3.3.5.10. Priorità generale degli operatori

() []  fonction                  
gd
! ~ ++ --                        
dg
new (type) opérateurs cast       
dg
*  /  %                          
gd
+  -                             
gd
<<  >>                           
gd
< <=  > >= instanceof            
gd
==    !=                         
gd
&                                
gd
^                                
gd
|                                
gd
&&                               
gd
||                               
gd
?   :                            
dg
= += -= etc. .                   
dg

gd indica che, a parità di priorità, viene osservata la priorità da sinistra a destra. Ciò significa che quando in un'espressione sono presenti operatori con la stessa priorità, viene valutato per primo l'operatore più a sinistra nell'espressione. dg indica una priorità da destra a sinistra.

3.3.5.11. I cambiamenti di tipo

In un'espressione è possibile modificare temporaneamente la codifica di un valore. Questo processo è noto come "cambio di tipo" o, in inglese, "type casting". La sintassi per il cambio di tipo di un valore in un'espressione è la seguente:

    (type) valeur

Il valore assume quindi il tipo indicato. Ciò comporta una modifica della codifica del valore.


using System;

namespace Chap1 {
    class P06 {
        static void Main(string[] args) {
            int i = 3, j = 4;
            float f1=i/j;
            float f2=(float)i/j;
            Console.WriteLine("f1={0}, f2={1}",f1,f2);
        }
    }
}
  • alla riga 7, f1 avrà il valore 0,0. La divisione 3/4 è una divisione intera poiché entrambi gli operandi sono di tipo int.
  • riga 8, (float)i è il valore di i convertito in float. Ora si ha una divisione tra un numero reale di tipo float e un numero intero di tipo int. Viene quindi eseguita la divisione tra i due numeri reali. Anche il valore di j verrà convertito nel tipo float, dopodiché verrà eseguita la divisione tra i due numeri reali. f2 avrà quindi il valore 0,75.

Ecco i risultati dell'esecuzione:

f1=0, f2=0,75

Nell’operazione (float)i:

  • i è un valore codificato in modo esatto su 2 byte
  • (float) i è lo stesso valore codificato in modo approssimativo come numero reale su 4 byte

Si verifica quindi una transcodifica del valore di i. Tale transcodifica avviene solo per la durata di un calcolo, mentre la variabile i mantiene sempre il proprio tipo int.

3.4. Le istruzioni di controllo dello svolgimento del programma

3.4.1. Arresto

Il metodo Exit definito nella classe Environment consente di interrompere l'esecuzione di un programma.

syntaxe        void Exit(int status)
action        arrête le processus en cours et rend la valeur status au processus père

Exit provoca la fine del processo in corso e restituisce il controllo al processo chiamante. Il valore di status può essere utilizzato da quest'ultimo. In DOS, questa variabile di stato viene restituita nella variabile di sistema ERRORLEVEL, il cui valore può essere verificato in un file batch. In Unix, con l'interprete di comandi Bourne Shell, è la variabile $? che recupera il valore di status.

    Environment.Exit(0);

interromperà l’esecuzione del programma con un valore di stato pari a 0.

3.4.2. Struttura di selezione semplice

 syntaxe :  if (condition) {actions_condition_vraie;} else {actions_condition_fausse;}

Note:

  • la condizione è racchiusa tra parentesi.
  • Ogni azione è terminata da un punto e virgola.
  • le parentesi graffe non sono seguite da un punto e virgola.
  • le parentesi graffe sono necessarie solo se c'è più di un'azione.
  • la clausola else può essere assente.
  • Non è presente la clausola then.

L'equivalente algoritmico di questa struttura è la struttura «se ... allora ... altrimenti»:

Esempio


    if (x>0)  { nx=nx+1;sx=sx+x;} else dx=dx-x;

È possibile annidare le strutture di selezione:

if(condition1)
if (condition2)
        {......}
      else         //condizione2
         {......}
else         //condizione1
     {.......}

A volte si presenta il seguente problema:


using System;

namespace Chap1 {
    class P07 {
        static void Main(string[] args) {
            int n = 5;
            if (n > 1)
                if (n > 6)
                    Console.Out.WriteLine(">6");
                else Console.Out.WriteLine("<=6");
        }
    }
}

Nell'esempio precedente, a quale if si riferisce il else della riga 10? La regola prevede che un else si riferisca sempre al if più vicino: if(n>6), alla riga 8, nell’esempio. Consideriamo un altro esempio:


            if (n2 > 1) {
                if (n2 > 6) Console.Out.WriteLine(">6");
       } else Console.Out.WriteLine("<=1");    

In questo caso volevamo associare un else al if(n2>1) e non un else al if(n2>6). A causa di quanto osservato in precedenza, siamo costretti a inserire le parentesi graffe in if(n2>1) {...} else ...

3.4.3. Struttura dei casi

La sintassi è la seguente:

switch(expression) {
    case v1:     
            actions1;
            break;
    case v2:     
            actions2;
            break;
         . .. .. .. .. ..
    default:     
            actions_sinon;
            break;
}

note

  • il valore dell'espressione di controllo di switch può essere un numero intero, un carattere o una stringa di caratteri
  • L'espressione di controllo è racchiusa tra parentesi.
  • La clausola default può essere assente.
  • I valori vi sono valori possibili dell'espressione. Se l'espressione ha come valore vi, vengono eseguite le azioni successive alla clausola case vi.
  • L'istruzione break esce dalla struttura case.
  • Ogni blocco di istruzioni associato al valore vi deve terminare con un'istruzione di salto (break, goto, return, ...); in caso contrario, il compilatore segnala un errore.

Esempio

In algoritmica

                selon la valeur de choix
                    cas 0
                         fin du module
                    cas 1
                         exécuter module M1
                    cas 2
                        exécuter module M2
                    sinon
                         erreur<--vrai
                findescas

In C#

int choix = 2; 
bool erreur = false;
switch (choix) {
   case 0: return;
   case 1: M1(); break;
   case 2: M2(); break;
   default: erreur = true; break;
}
}// fine Main

static void M1() {
    Console.WriteLine("M1");
}

static void M2() {
    Console.WriteLine("M2");
}
}

3.4.4. Strutture di ripetizione

3.4.4.1. Numero di ripetizioni noto

Struttura for

La sintassi è la seguente:


    for (i=id;i<=if;i=i+ip){ 
       actions; 
      } 

Note

  • i 3 argomenti di for sono racchiusi tra parentesi e separati da punti e virgola.
  • Ogni azione di for termina con un punto e virgola.
  • La parentesi graffa è necessaria solo se vi è più di un'azione.
  • La parentesi graffa non è seguita da un punto e virgola.

L'equivalente algoritmico è la struttura pour:

pour i variant de id à if avec un pas de ip
    actions
finpour

che può essere tradotta in una struttura tantque:

    i  id
    tantque i<=if
        actions
        i i+ip
    fintantque

Struttura foreach

La sintassi è la seguente:


foreach (Type variable in collection)
    instructions; 
}

Note

  • collection è una collezione di oggetti enumerabile. La collezione di oggetti enumerabile che già conosciamo è l'array
  • Type è il tipo degli oggetti della collezione. Nel caso di un array, sarebbe il tipo degli elementi dell'array
  • variable è una variabile locale al ciclo che assumerà in successione tutti i valori della collezione.

Pertanto, il codice seguente:


            string[] amis = { "paul", "hélène", "jacques", "sylvie" };
            foreach (string nom in amis) {
                Console.WriteLine(nom);
         }

visualizzerebbe:

paul
hélène
jacques
sylvie

3.4.4.2. Numero di ripetizioni sconosciuto

In C# esistono numerose strutture per questo caso.

Struttura «while»


    while(condition){
          actions;
        } 

Il ciclo continua finché la condizione è vera. Il ciclo potrebbe non essere mai eseguito.

Note:

  • la condizione è racchiusa tra parentesi.
  • Ogni azione è seguita da un punto e virgola.
  • la parentesi graffa è necessaria solo se c'è più di un'azione.
  • la parentesi graffa non è seguita da un punto e virgola.

La struttura algoritmica corrispondente è la struttura «finché»:

tantque condition
    actions
fintantque

Struttura "ripeti fino a" (do while)

La sintassi è la seguente:


    do{
       instructions;
    }while(condition); 

Il ciclo continua finché la condizione non diventa falsa. In questo caso il ciclo viene eseguito almeno una volta.

Note

  • la condizione è racchiusa tra parentesi.
  • Ogni azione è seguita da un punto e virgola.
  • la parentesi graffa è necessaria solo se c'è più di un'azione.
  • La parentesi graffa non è seguita da un punto e virgola.

La struttura algoritmica corrispondente è la struttura «ripeti … fino a»:

répéter
    actions
jusqu'à condition

Struttura «for» (for)

La sintassi è la seguente:


for(instructions_départ;condition;instructions_fin_boucle){
    instructions; 
}

Il ciclo continua finché la condizione è vera (valutata prima di ogni iterazione). Le istruzioni Instructions_départ vengono eseguite prima di entrare nel ciclo per la prima volta. Le istruzioni Instructions_fin_boucle vengono eseguite dopo ogni iterazione.

note

  • le diverse istruzioni in instructions_depart e instructions_fin_boucle sono separate da virgole.

La struttura algoritmica corrispondente è la seguente:

instructions_départ
tantque condition
    actions
    instructions_fin_boucle
fintantque

Esempi

I seguenti frammenti di codice calcolano tutti la somma dei primi 10 numeri interi.

            int i, somme, n=10;
            for (i = 1, somme = 0; i <= n; i = i + 1)
                somme = somme + i;

            for (i = 1, somme = 0; i <= n; somme = somme + i, i = i + 1) ;

            i = 1; somme = 0;
            while (i <= n) { somme += i; i++; }

            i = 1; somme = 0;
            do somme += i++;
            while (i <= n);

3.4.4.3. Istruzioni per la gestione dei cicli

break
esce dal ciclo for, while, do ... while.
continue
passa all'iterazione successiva dei cicli for, while, do ... while

3.5. Gestione delle eccezioni

Numerose funzioni C# possono generare eccezioni, ovvero errori. Quando una funzione è suscettibile di generare un'eccezione, il programmatore dovrebbe gestirla al fine di ottenere programmi più resistenti agli errori: occorre sempre evitare il "blocco" improvviso di un'applicazione.

La gestione di un'eccezione avviene secondo lo schema seguente:

try{
    code susceptible de générer une exception
} catch (Exception e){
    traiter l'exception e
}
instruction suivante

Se la funzione non genera un'eccezione, si passa all'istruzione successiva; in caso contrario, si passa al corpo della clausola catch e poi all'istruzione successiva. Si notino i seguenti punti:

  • e è un oggetto di tipo Exception o derivato. È possibile essere più precisi utilizzando tipi quali IndexOutOfRangeException, FormatException, SystemException, ecc.: esistono diversi tipi di eccezioni. Scrivendo catch (Exception e), si indica che si desidera gestire tutti i tipi di eccezioni. Se il codice della clausola try è in grado di generare diversi tipi di eccezioni, si può voler essere più precisi gestendo l’eccezione con più clausole catch:
try{
    code susceptible de générer les exceptions
} catch ( IndexOutOfRangeException e1){
    traiter l'exception e1
}
} catch ( FormatException e2){
    traiter l'exception e2
}
instruction suivante
  • È possibile aggiungere alle clausole try/catch una clausola finally:
try{
    code susceptible de générer une exception
} catch (Exception e){
    traiter l'exception e
}
finally{
    code exécuté après try ou catch
}
instruction suivante

Che vi sia o meno un'eccezione, il codice della clausola finally verrà sempre eseguito.

  • Nella clausola catch, potrebbe non essere necessario utilizzare l'oggetto Exception disponibile. Invece di scrivere catch (Exception e){..}, si scrive quindi catch(Exception){...} o, più semplicemente, catch {...}.
  • La classe Exception ha una proprietà Message che contiene un messaggio che descrive in dettaglio l’errore verificatosi. Pertanto, se si desidera visualizzarlo, si scriverà:
catch (Exception ex){
    Console.WriteLine("L'erreur suivante s'est produite : {0}",ex.Message);
    ...
}//catch
  • La classe Exception dispone di un metodo ToString che restituisce una stringa di caratteri indicante il tipo dell’eccezione e il valore della proprietà Message. Si potrà quindi scrivere:
catch (Exception ex){
    Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.ToString());
    ...
}//catch

Si può scrivere anche:

catch (Exception ex){
    Console.WriteLine("L'erreur suivante s'est produite : {0}",ex);
    ...
}//catch

Il compilatore assegnerà al parametro {0} il valore ex.ToString().

L'esempio seguente mostra un'eccezione generata dall'utilizzo di un elemento di array inesistente:


using System;

namespace Chap1 {
    class P08 {
        static void Main(string[] args) {
            // dichiarazione e inizializzazione di un array
            int[] tab = { 0, 1, 2, 3 };
            int i;
            // visualizzazione dell'array con un ciclo for
            for (i = 0; i < tab.Length; i++)
                Console.WriteLine("tab[{0}]={1}", i, tab[i]);
            // visualizzazione di un array con un ciclo for each
            foreach (int élmt in tab) {
                Console.WriteLine(élmt);
            }
            // generazione di un'eccezione
            try {
                tab[100] = 6;
            } catch (Exception e) {
                Console.Error.WriteLine("L'erreur suivante s'est produite : " + e);
                return;
            }//try-catch
            finally {
                Console.WriteLine("finally ...");
            }
        }
    }
}

Nella riga 18 sopra riportata verrà generata un'eccezione poiché l'array tab non contiene l'elemento n. 100. L'esecuzione del programma produce i seguenti risultati:

tab[0]=0
tab[1]=1
tab[2]=2
tab[3]=3
0
1
2
3
L'erreur suivante s'est produite : System.IndexOutOfRangeException: L'index se trouve en dehors des limites du tableau.
   à Chap1.P08.Main(String[] args) dans C:\data\travail\2007-2008\c# 2008\poly\Chap1\08\Program.cs:riga 7
finally ...
  • riga 9: si è verificata l'eccezione [System.IndexOutOfRangeException]
  • riga 11: è stata eseguita la clausola finally (righe 23-25) del codice, nonostante alla riga 21 fosse presente un'istruzione return per uscire dal metodo. Si noti che la clausola finally viene sempre eseguita.

Ecco un altro esempio in cui si gestisce l’eccezione causata dall’assegnazione di una stringa di caratteri a una variabile di tipo intero quando la stringa non rappresenta un numero intero:


using System;

namespace Chap1 {
    class P08 {
        static void Main(string[] args) {

            // esempio 2
            // Viene richiesto il nome
            Console.Write("Nom : ");
            // lettura della risposta
            string nom = Console.ReadLine();
            // viene richiesta l'età
            int age = 0;
            bool ageOK = false;
            while (!ageOK) {
                // domanda
                Console.Write("âge : ");
                // lettura e verifica della risposta
                try {
                    age = int.Parse(Console.ReadLine());
                    ageOK = age>=1;
                } catch {
                }//try-catch
                if (!ageOK) {
                    Console.WriteLine("Age incorrect, recommencez...");
                }
            }//while
            // visualizzazione finale
            Console.WriteLine("Vous vous appelez {0} et vous avez {1} an(s)",nom,age);
        }
    }
}
  • righe 15-27: il ciclo di immissione dell’età di una persona
  • riga 20: la riga digitata sulla tastiera viene trasformata in un numero intero dal metodo int.Parse. Questo metodo genera un'eccezione se la conversione non è possibile. Per questo motivo, l'operazione è stata inserita in un blocco try/catch.
  • righe 22-23: se viene generata un'eccezione, si passa al blocco catch dove non viene eseguita alcuna operazione. Pertanto, la variabile booleana ageOK, impostata a false alla riga 14, rimarrà a false.
  • riga 21: se si arriva a questa riga, significa che la conversione da stringa a intero è andata a buon fine. Si verifica tuttavia che l’intero ottenuto sia effettivamente maggiore o uguale a 1.
  • righe 24-26: viene emesso un messaggio di errore se l’età non è corretta.

Alcuni risultati dell’esecuzione:

1
2
3
Nom : dupont
âge : 23
Vous vous appelez dupont et vous avez 23 an(s)
1
2
3
4
5
6
7
Nom : durand
âge : x
Age incorrect, recommencez...
âge : -4
Age incorrect, recommencez...
âge : 12
Vous vous appelez durand et vous avez 12 an(s)

3.6. Esempio di applicazione - V1

Ci proponiamo di scrivere un programma che consenta di calcolare l’imposta di un contribuente. Consideriamo il caso semplificato di un contribuente che abbia da dichiarare solo il proprio stipendio (dati del 2004 relativi ai redditi del 2003):

  • si calcola il numero di quote del dipendente nbParts=nbEnfants/2 +1 se non è sposato, nbEnfants/2+2 se è sposato, dove nbEnfants è il numero dei suoi figli.
  • se ha almeno tre figli, gli spetta una mezza quota in più
  • si calcola il suo reddito imponibile R=0,72*S, dove S è il suo stipendio annuale
  • si calcola il suo coefficiente familiare QF = R/nbParts
  • si calcola l’imposta I. Consideriamo la seguente tabella:
4262
0
0
8382
0,0683
291,09
14753
0,1914
1322,92
23.888
0,2826
2668,39
38.868
0,3738
4846,98
47932
0,4262
6883,66
0
0,4809
9505,54

Ogni riga contiene 3 campi. Per calcolare l'imposta I, si cerca la prima riga in cui QF ≤ campo1. Ad esempio, se QF = 5000, si troverà la riga

    8382        0.0683        291.09

L'imposta I è quindi pari a 0,0683*R - 291,09*nbParts. Se QF è tale che la relazione QF<=campo1 non è mai soddisfatta, allora vengono utilizzati i coefficienti dell’ultima riga. In questo caso:

0 0,4809 9505,54

il che dà l'imposta I=0,4809*R - 9505,54*nbParts.

Il programma C# corrispondente è il seguente:


using System;

namespace Chap1 {
    class Impots {
        static void Main(string[] args) {
            // tabelle di dati necessarie per il calcolo dell'imposta
            decimal[] limites = { 4962M, 8382M, 14753M, 23888M, 38868M, 47932M, 0M };
            decimal[] coeffR = { 0M, 0.068M, 0.191M, 0.283M, 0.374M, 0.426M, 0.481M };
            decimal[] coeffN = { 0M, 291.09M, 1322.92M, 2668.39M, 4846.98M, 6883.66M, 9505.54M };

            // si recupera lo stato civile
            bool OK = false;
            string reponse = null;
            while (!OK) {
                Console.Write("Etes-vous marié(e) (O/N) ? ");
                reponse = Console.ReadLine().Trim().ToLower();
                if (reponse != "o" && reponse != "n")
                    Console.Error.WriteLine("Réponse incorrecte. Recommencez");
                else OK = true;
            }//while
            bool marie = reponse == "o";

            // numero di figli
            OK = false;
            int nbEnfants = 0;
            while (!OK) {
                Console.Write("Nombre d'enfants : ");
                try {
                    nbEnfants = int.Parse(Console.ReadLine());
                    OK = nbEnfants >= 0;
                } catch {
                }// try
                if (!OK) {
                    Console.WriteLine("Réponse incorrecte. Recommencez");
                }
            }// while

            // stipendio
            OK = false;
            int salaire = 0;
            while (!OK) {
                Console.Write("Salaire annuel : ");
                try {
                    salaire = int.Parse(Console.ReadLine());
                    OK = salaire >= 0;
                } catch {
                }// try
                if (!OK) {
                    Console.WriteLine("Réponse incorrecte. Recommencez");
                }
            }// while

            // calcolo del numero di quote
            decimal nbParts;
            if (marie) nbParts = (decimal)nbEnfants / 2 + 2;
            else nbParts = (decimal)nbEnfants / 2 + 1;
            if (nbEnfants >= 3) nbParts += 0.5M;

            // reddito imponibile
            decimal revenu = 0.72M * salaire;

            // quoziente familiare
            decimal QF = revenu / nbParts;

            // ricerca della fascia di imposta corrispondente a QF
            int i;
            int nbTranches = limites.Length;
            limites[nbTranches - 1] = QF;
            i = 0;
            while (QF > limites[i]) i++;
            // l'imposta
            int impots = (int)(coeffR[i] * revenu - coeffN[i] * nbParts);

            // viene visualizzato il risultato
            Console.WriteLine("Impôt à payer : {0} euros", impots);
        }
    }
}
  • righe 7-9: i valori numerici sono seguiti dal suffisso M (Money) affinché siano di tipo decimal.
  • riga 16:
  • Console.ReadLine() restituisce la stringa C1 digitata dalla tastiera
  • C1.Trim() rimuove gli spazi iniziali e finali da C1 - restituisce la stringa C2
  • C2.ToLower() restituisce la stringa C3, che è la stringa C2 convertita in minuscolo.
  • riga 21: il valore booleano marie riceve il valore true o false dalla relazione reponse=="o"
  • riga 29: la stringa digitata sulla tastiera viene convertita nel tipo int. Se la conversione non va a buon fine, viene generata un'eccezione.
  • riga 30: il valore booleano OK riceve il valore true o false dalla relazione nbEnfants>=0
  • righe 55-56: non è possibile scrivere semplicemente nbEnfants/2. Se nbEnfants fosse pari a 3, si avrebbe 3/2, una divisione intera che darebbe 1 e non 1,5. Pertanto, si scrive (decimal)nbEnfants per rendere reale uno degli operandi della divisione e ottenere così una divisione tra numeri reali.

Ecco alcuni esempi di esecuzione:

Etes-vous marié(e) (O/N) ? o
Nombre d'enfants : 2
Salaire annuel : 60000
Impôt à payer : 4282 euros
Etes-vous marié(e) (O/N) ? oui
Réponse incorrecte. Recommencez
Etes-vous marié(e) (O/N) ? o
Nombre d'enfants : trois
Réponse incorrecte. Recommencez
Nombre d'enfants : 3
Salaire annuel : 60000 euros
Réponse incorrecte. Recommencez
Salaire annuel : 60000
Impôt à payer : 2959 euros

3.7. Argomenti del programma principale

La funzione principale Main può accettare come parametro un array di stringhe: String[] (o string[]). Questo array contiene gli argomenti della riga di comando utilizzata per avviare l'applicazione. Pertanto, se si avvia il programma P con il seguente comando (DOS):


        P arg0 arg1 … argn

e se la funzione Main è dichiarata come segue:

public static void Main(string[] args)

si avrà args[0]="arg0", args[1]="arg1" … Ecco un esempio:


using System;

namespace Chap1 {
    class P10 {
        static void Main(string[] args) {
            // si elencano i parametri ricevuti
            Console.WriteLine("Il y a  " + args.Length + " arguments");
            for (int i = 0; i < args.Length; i++) {
                Console.Out.WriteLine("arguments[" + i + "]=" + args[i]);
            }
        }
    }
}

Per passare argomenti al codice eseguito, si procederà come segue:

  • in [1]: clic destro sul progetto / Proprietà
  • in [2]: scheda [Debug]
  • in [3]: inserire gli argomenti

L'esecuzione fornisce i seguenti risultati:

1
2
3
4
5
Il y a  4 arguments
arguments[0]=a0
arguments[1]=a1
arguments[2]=a2
arguments[3]=a3

Si noti che la firma

public static void Main()

è valida se la funzione Main non richiede parametri.

3.8. Le enumerazioni

Un'enumerazione è un tipo di dati il cui dominio di valori è un insieme di costanti intere. Consideriamo un programma che deve gestire le menzioni relative a un esame. Ce ne sarebbero cinque: Passable, AssezBien, Bien, TrèsBien, Excellent.

Si potrebbe quindi definire un'enumerazione per queste cinque costanti:


        enum Mentions { Passable, AssezBien, Bien, TrèsBien, Excellent };

Internamente, queste cinque costanti sono codificate da numeri interi consecutivi che iniziano con 0 per la prima costante, 1 per la successiva e così via. Una variabile può essere dichiarata in modo da assumere questi valori nell'enumerazione:


            // una variabile che assume i propri valori dall'enumerazione "Mentions"
Mentions maMention = Mentions.Passable;

È possibile confrontare una variabile con i diversi valori possibili dell’enumerazione:


            if (maMention == Mentions.Passable) {
                Console.WriteLine("Peut mieux faire");
}

È possibile ottenere tutti i valori dell’enumerazione:


            // elenco delle menzioni sotto forma di stringhe
            foreach (Mentions m in Enum.GetValues(maMention.GetType())) {
                Console.WriteLine(m);
}

Allo stesso modo in cui il tipo semplice int è equivalente alla struttura System.Int32, il tipo semplice enum è equivalente alla struttura System.Enum. Questa struttura dispone di un metodo statico GetValues che consente di ottenere tutti i valori di un tipo enumerato passato come parametro. Quest’ultimo deve essere un oggetto di tipo Type, ovvero una classe che contiene informazioni sul tipo di un dato. Il tipo di una variabile v si ottiene tramite v.GetType(). Il tipo di un tipo T si ottiene tramite typeof(T). Quindi, in questo caso, maMention.GetType() restituisce l’oggetto Type dell’enumerazione Mentions, mentre Enum.GetValues(maMention.GetType()) restituisce l'elenco dei valori dell'enumerazione Mentions.

Se ora scriviamo


            //elenco delle menzioni sotto forma di numeri interi
            foreach (int m in Enum.GetValues(typeof(Mentions))) {
                Console.WriteLine(m);
}

Riga 2, la variabile del ciclo è di tipo intero. Si ottiene quindi l'elenco dei valori dell'enumerazione sotto forma di interi. L'oggetto di tipo System.Type corrispondente al tipo di dati Mentions si ottiene tramite typeof(Mentions). Si sarebbe potuto scrivere, come in precedenza, maMention.GetType().

Il programma seguente illustra quanto appena descritto:


using System;

namespace Chap1 {
    class P11 {
        enum Mentions { Passable, AssezBien, Bien, TrèsBien, Excellent };
        static void Main(string[] args) {
            // una variabile che assume i propri valori dall'enumerazione Mentions
            Mentions maMention = Mentions.Passable;
            // visualizzazione del valore della variabile
            Console.WriteLine("mention=" + maMention);
            // test con valore dell'enumerazione
            if (maMention == Mentions.Passable) {
                Console.WriteLine("Peut mieux faire");
            }
            // elenco delle menzioni sotto forma di stringhe
            foreach (Mentions m in Enum.GetValues(maMention.GetType())) {
                Console.WriteLine(m);
            }
            //elenco delle menzioni sotto forma di numeri interi
            foreach (int m in Enum.GetValues(typeof(Mentions))) {
                Console.WriteLine(m);
            }
        }
    }
}

I risultati dell'esecuzione sono i seguenti:

mention=Passable
Peut mieux faire
Passable
AssezBien
Bien
TrèsBien
Excellent
0
1
2
3
4

3.9. Passaggio dei parametri a una funzione

In questa sede ci interessiamo alle modalità di passaggio dei parametri a una funzione. Consideriamo la seguente funzione statica:


        private static void ChangeInt(int a) {
            a = 30;
            Console.WriteLine("Paramètre formel a=" + a);
}

Nella definizione della funzione, riga 1, a è definito come parametro formale. È presente solo ai fini della definizione della funzione changeInt. Avrebbe potuto benissimo chiamarsi b. Consideriamo ora un utilizzo di questa funzione:


        public static void Main() {
            int age = 20;
            ChangeInt(age);
            Console.WriteLine("Paramètre effectif age=" + age);
}

Qui, nell’istruzione della riga 3, ChangeInt(age), age è il parametro effettivo che trasmetterà il proprio valore al parametro formale a. Ci interessa capire in che modo un parametro formale recupera il valore di un parametro effettivo.

3.9.1. Passaggio per valore

L’esempio seguente mostra che i parametri di una funzione vengono passati per impostazione predefinita per valore, ovvero il valore del parametro effettivo viene copiato nel parametro formale corrispondente. Si tratta di due entità distinte. Se la funzione modifica il parametro formale, il parametro effettivo non subisce alcuna modifica.


using System;

namespace Chap1 {
    class P12 {
        public static void Main() {
            int age = 20;
            ChangeInt(age);
            Console.WriteLine("Paramètre effectif age=" + age);
        }
        private static void ChangeInt(int a) {
            a = 30;
            Console.WriteLine("Paramètre formel a=" + a);
        }
    }
}

I risultati ottenuti sono i seguenti:

Paramètre formel a=30
Paramètre effectif age=20

Il valore 20 del parametro effettivo age è stato copiato nel parametro formale a (riga 10). Quest'ultimo è stato poi modificato (riga 11). Il parametro effettivo è rimasto invece invariato. Questa modalità di passaggio è adatta ai parametri di ingresso di una funzione.

3.9.2. Passaggio per riferimento

In un passaggio per riferimento, il parametro effettivo e il parametro formale sono la stessa entità. Se la funzione modifica il parametro formale, anche il parametro effettivo viene modificato. In C#, entrambi devono essere preceduti dalla parola chiave ref:

Ecco un esempio:


using System;

namespace Chap1 {
    class P12 {
        public static void Main() {
            // esempio 2
            int age2 = 20;
            ChangeInt2(ref age2);
            Console.WriteLine("Paramètre effectif age2=" + age2);
        }
        private static void ChangeInt2(ref int a2) {
            a2 = 30;
            Console.WriteLine("Paramètre formel a2=" + a2);
        }
    }
}

e i risultati dell’esecuzione:

Paramètre formel a2=30
Paramètre effectif age2=30

Il parametro effettivo ha seguito la modifica del parametro formale. Questa modalità di passaggio è adatta ai parametri di uscita di una funzione.

3.9.3. Passaggio per riferimento con la parola chiave out

Consideriamo l’esempio precedente in cui la variabile age2 non sarebbe inizializzata prima della chiamata alla funzione changeInt:


using System;

namespace Chap1 {
    class P12 {
        public static void Main() {
            // esempio 2
            int age2;
            ChangeInt2(ref age2);
            Console.WriteLine("Paramètre effectif age2=" + age2);
        }
        private static void ChangeInt2(ref int a2) {
            a2 = 30;
            Console.WriteLine("Paramètre formel a2=" + a2);
        }
    }
}

Quando si compila questo programma, si verifica un errore:

    Use of unassigned local variable 'age2'

È possibile aggirare l'ostacolo assegnando un valore iniziale a age2. È anche possibile sostituire la parola chiave ref con la parola chiave out. In questo modo si specifica che il parametro è esclusivamente un parametro di uscita e non necessita quindi di un valore iniziale:


using System;

namespace Chap1 {
    class P12 {
        public static void Main() {
            // esempio 3
            int age3;
            ChangeInt3(out age3);
            Console.WriteLine("Paramètre effectif age3=" + age3);
        }
        private static void ChangeInt3(out int a3) {
            a3 = 30;
            Console.WriteLine("Paramètre formel a3=" + a3);
        }
    }
}

I risultati dell'esecuzione sono i seguenti:

Paramètre formel a3=30
Paramètre effectif age3=30