USER
5.2 Il ciclo while
Il costrutto while permette di eseguire un blocco di istruzioni zero o più volte in base al valore di un’espressione
booleana, secondo la seguente sintassi:
Pseudocodifica
MENTRE (condizione){
blocco codice
} FINE MENTRE
Linguaggio C#
while(espressione condizionale) {//ripeti
blocco_istruzioni
}
Fino a quando l’espressione condizionale restituisce valore true, il blocco istruzioni verrà eseguito.
È dunque necessario, per uscire dal ciclo while, che l’espressione ad un certo punto diventi false, oppure che
all’interno del ciclo while ci sia un’istruzione break, che fa saltare l’esecuzione alla fine del ciclo while, cioè
all’istruzione immediatamente successiva, o comunque al di fuori di esso:
int i=0;
while(i<10)// Esce dal ciclo solo quando la i vale 10 ATTENZIONE !!!
{
Console.WriteLine(“i={0}”,i); // Stampa i valori da 0 a 9 contenuti nella variabile i
i=i+1; //incrementa di un unità la i
}
/*L’esempio precedente esegue il blocco fra parentesi graffe fino a che il valore della variabile i non sia pari a 10, per tale valore infatti
l’espressione i<10 diventa false.*/
Come detto prima è possibile anche terminare il ciclo con un break, dunque anche da un ciclo while la cui
espressione di valutazione sia sempre true, come nel caso seguente, è possibile uscire.
int i=0;
while(true)
{
Console.WriteLine(“i={0}”,i);
i=i+1;
if(i==8)
break;
}
5.3 Il ciclo do
Il ciclo do è analogo al precedente, con l’unica differenza che il blocco di istruzioni incorporato viene eseguito
almeno una volta, in quanto l’espressione booleana viene valutata alla fine del primo ciclo. A questo punto, se
l’espressione assume valore true, si esegue un nuovo ciclo, altrimenti si esce da esso.
int i=10;
do {
Console.WriteLine("i={0}",i);
i=i-1;int i=10;
do {
Console.WriteLine("i={0}",i);
i=i-1;
} while(i>0); /*Il valore di i è 10 in partenza, ciò nonostante il ciclo viene eseguito ugualmente e stampa il valore di i.*/
15
Un esempio utile verificare se il valore inserito e conforme alle richieste del problema.
Come ad esempio che il valore inserito sia maggiore di zero.
int i=0;
do {
Console.WriteLine("Inserisci un valore per i: ");
i= Convert.ToInt32(Console.ReadLine());
if (i<=0)
{
Console.WriteLine("Hai inserito un valore minore/uguale a zero");
}
}while(i<=0);
5.4 Il ciclo for
Il ciclo for è uno dei costrutti più comuni in qualsiasi linguaggio di programmazione, e anche nella
pseudocodifica segue una struttura molto simile.
Viene utilizzato per ripetere una serie di istruzioni un numero predefinito di volte.
In pseudocodifica, un ciclo for può essere descritto nel seguente modo generale:
Per i da valore_iniziale a valore_finale fai {
// Blocco di istruzioni da eseguire
} FinePer
i: Variabile contatore che viene incrementata o modificata ad ogni iterazione.
valore_iniziale: Il valore da cui la variabile contatore parte.
valore_finale: Il valore massimo (o minimo) che la variabile contatore può assumere, prima di
terminare il ciclo.
FinePer: Termina il ciclo for. In alcuni linguaggi si utilizza anche end for o simili.
In linguaggio ci C#
for (inizializzazione; condizione; incremento){
// blocco di codice da eseguire
}
inizializzazione: Viene eseguita una sola volta, prima dell'inizio del ciclo. Di solito viene utilizzata
per dichiarare e inizializzare la variabile di controllo.
condizione: La condizione viene valutata prima di ogni iterazione. Se è true, il ciclo continua; se è
false, il ciclo si interrompe.
incremento: Viene eseguito alla fine di ogni iterazione, e di solito viene usato per aggiornare la
variabile di controllo (come l'incremento di un contatore).
Esempio
for (int i = 0; i < 5; i++) {
Console.WriteLine("Iterazione numero: " + i);
}
16
5.5 Il costrutto switch
Se è necessario effettuare una scelta fra diversi alternativi percorsi di esecuzione, e tale scelta può essere basata
sul valore assunto a runtime da una variabile di un tipo intero o string, allora è possibile usare il costrutto
switch.
La sintassi dello switch è la seguente:
switch(espressione)
{
case costante1:
blocco_istruzioni
break;
case costante2:
blocco_istruzioni;
break;
default:
blocco_istruzioni;
break;
}
L’espressione all’interno delle parentesi viene valutata, e se il suo valore corrisponde ad una delle costanti
dei case, il flusso del programma salta al case corrispondente, del quale viene eseguito il blocco distruzioni,
fino a raggiungere una necessaria istruzione di salto. Se è presente una label default, e la valutazione
dell’espressione produce un risultato non contemplato dalle costanti dei case, il flusso va alla label di default,
se tale label invece non è presente il flusso va alla fine dello switch. Tipicamente l’istruzione di salto che
chiude un blocco case è l’istruzione break, che fa uscire il programma dal costrutto switch.
Esempio C#
switch(x)
{
case 0: Console.WriteLine(“x è nulla”);
break;
case 1: Console.WriteLine(“x è 0 oppure 1”);
break;
default: Console.WriteLine(“x è diverso da 0 e 1”);
break;
}
Tutti i case devono essere chiusi da un break;
int myValue=4;
switch(myValue)
{
case <= 0:
Console.WriteLine("Valore minore di 0");
break;
case > 0 and <= 10:
Console.WriteLine("Valore compreso 1 e 10 ");
break;
default:
Console.WriteLine("Valore maggiore di 10");
break;
}
switch (myValue)
{
case 1 or 2 or 3:
Console.WriteLine("Valore 1 o 2 o 3");
break;
case 4 or 5 or 6:
Console.WriteLine("Valore 4 o 5 o 6");
break;
default:
// Do stuff
break;
}
17
5.6 Casting
In C#, il casting è il processo di conversione di un tipo di dato in un altro. Ci sono due principali forme di
casting: casting esplicito e casting implicito.
- Casting Implicito: Il casting implicito avviene quando non c'è perdita di dati. Questo è comune con i
tipi numerici più piccoli a quelli più grandi, come da int a long.
Esempio:
int num = 10;
long bigNum = num; // Casting implicito, non c'è perdita di dati
- Casting Esplicito: Il casting esplicito è necessario quando si cerca di convertire un tipo di dato che
potrebbe comportare una perdita di dati. Si utilizza l'operatore di casting (ovvero il tipo di destinazione
tra parentesi).
Esempio
double pi = 3.14;
int intPi = (int)pi; // Casting esplicito, intPi sarà 3
5.7 Caratteri di escape
In C#, i caratteri di escape sono utilizzati per rappresentare caratteri speciali all'interno delle stringhe.
Questi caratteri iniziano con una barra inversa (\) e vengono utilizzati per rappresentare simboli o sequenze
che altrimenti non potrebbero essere scritti direttamente.
Esempio
//dopo aver stampato sullo schermo "OGGI E’ UNA BELLA GIORNATA" va a capo
Console.WriteLine("OGGI E\’ UNA BELLA GIORNATA");
//dopo aver stampato sullo schermo "OGGI E’ UNA BELLA GIORNATA" va a capo ANCHE QUESTA pur utilizzando write
Console.Write("OGGI E\’ UNA BELLA GIORNATA\n");
//in questo caso si riesce grazie ad \" a poter inserire degli apici doppi nel testo e non scambiarli per quelli di inizio e fine stringa
Console.WriteLine("Ciao \"Mondo\""); //-> Ciao "Mondo"
Sequenza di escape Nome carattere
\' Virgoletta singola
\\ Barra inversa (\)
\" Virgoletta doppia
\a Avviso
\b Backspace
\n Nuova riga(Line Feed LF )
\t Tabulazione orizzontale
18
5.8 Scope
Nell'ambito dell'informatica, il concetto di variabili e il loro "scope" (ambito) si riferisce alla
visibilità e alla durata delle variabili in un programma. In altre parole, lo scope determina da quale
parte del codice una variabile è accessibile e quando viene creata e distrutta. Questo concetto è
cruciale in molti linguaggi di programmazione per evitare errori e gestire efficientemente le risorse.
Esistono diversi tipi di scope per le variabili, che variano leggermente a seconda del linguaggio di
programmazione, ma i concetti principali sono generalmente comuni.
5.8.1 Tipi di Scope:
1. Scope globale (global scope):
o Una variabile globale è dichiarata al di fuori di qualsiasi funzione o blocco di codice e può
essere utilizzata in qualunque parte del programma, sia all'interno delle funzioni che al di
fuori.
o La sua vita dura per l'intero ciclo di esecuzione del programma.
o Pro: può essere comoda quando serve condividere dati tra diverse parti del programma.
o Contro: può portare a errori difficili da tracciare, in quanto le variabili globali possono
essere sovrascritte accidentalmente in diverse parti del codice.
2. Scope locale (local scope):
o Una variabile locale è dichiarata all'interno di una funzione o un blocco di codice,
come un ciclo for, e può essere utilizzata solo all'interno di quel contesto.
o La vita di una variabile locale inizia quando viene dichiarata e termina quando
l'esecuzione esce dalla funzione o dal blocco in cui è stata dichiarata.
o Pro: aiuta a evitare conflitti di nome e rende il codice più sicuro, poiché la variabile
è isolata.
o Contro: non è accessibile da altre parti del programma, quindi la condivisione di dati
deve essere fatta tramite il ritorno di valori o parametri passati a funzioni.
3. Scope di blocco (block scope):
o In alcuni linguaggi (come Java, C++, JavaScript,C#), le variabili dichiarate
all'interno di un blocco {} (come nei cicli o negli if) hanno uno scope limitato a
quel blocco.
19
6 Funzioni e Procedure
Una funzione è un blocco di codice che esegue un compito specifico e restituisce un valore come
risultato della sua esecuzione. Le funzioni prendono degli input sotto forma di parametri (o
argomenti), eseguono una serie di operazioni su di essi e restituiscono un output.
6.1 Caratteristiche principali delle funzioni:
1. Input: Una funzione può accettare uno o più parametri come input.
2. Output: Restituisce sempre un valore (anche null o void in alcuni linguaggi se non ha un risultato
significativo).
3. Modularità: Le funzioni sono utili per evitare la ripetizione di codice, permettendo di riutilizzare
blocchi di codice comuni.
4. Chiamata: Quando una funzione viene chiamata, il controllo del programma viene trasferito alla
funzione, e una volta terminata l'esecuzione, il controllo ritorna al punto di chiamata.
5. Purezza: Alcune funzioni possono essere considerate "pure" se non hanno effetti collaterali, cioè
non modificano lo stato esterno (come variabili globali o dati esterni) e restituiscono sempre lo
stesso output per lo stesso input. (Esempio in programmazione funzionale).
int Somma(int a, int b)
{
return a + b;
}
int Main()
{
int risultato = Somma(3, 4);
Console.WriteLine(risultato); // Output: 7
return 0;
}
6.2 Procedure
Una procedura è simile a una funzione, ma non restituisce un valore.
Esegue un insieme di istruzioni o operazioni, ma il suo scopo principale è eseguire un'azione piuttosto che
calcolare e restituire un risultato.
Le procedure possono comunque accettare parametri e modificare il loro stato o quello dell'ambiente
circostante.
- Caratteristiche principali delle procedure:
o Input: Come le funzioni, una procedura può accettare uno o più parametri.
o Output: Non restituisce un valore esplicito (sebbene possa modificare lo stato di variabili o
dati).
o Modularità: Le procedure, come le funzioni, consentono di scomporre il codice in blocchi
logici e riutilizzabili.
o Chiamata: Come nelle funzioni, il controllo del programma passa alla procedura quando viene
chiamata e ritorna alla fine della procedura.
using System;
class Program
{
static void Saluta(string nome)
{
Console.WriteLine("Ciao, " + nome + "!"); // Non restituisce nulla, ma esegue un'azione
}
static void Main()
{
Saluta("Marco"); // Output: Ciao, Marco!
}
}
20
Introduzione alla 00P
La Programmazione Orientata agli Oggetti (OOP, Object-Oriented Programming) è un paradigma
di programmazione che si basa sul concetto di "oggetti".
Gli oggetti sono istanze di classi e possono contenere dati e metodi che operano su quei dati. Questo approccio
è molto utile per progettare e gestire software complesso, favorendo la modularità e la riusabilità del
codice.
- Classi e Oggetti
o Classe: Una classe è una "blueprint" o un modello che definisce un tipo di oggetto. Contiene
attributi (dati) e metodi (funzioni) che determinano il comportamento degli oggetti creati da
essa.
o Oggetto: Un oggetto è un'istanza di una classe. Rappresenta un'entità concreta che ha uno
stato (valori degli attributi) e un comportamento (metodi).
6.2.1 In sintesi:
Classe: è una struttura che definisce proprietà e metodi.
Oggetto: è un'istanza concreta di una classe.
Istanza: è un termine che indica un oggetto specifico creato da una classe.
Un modo per visualizzare tutto questo è pensare a una classe come una "stampo" per creare oggetti:
lo stampo definisce la forma generale (classe), e ogni oggetto prodotto dallo stampo è un'istanza unica
(oggetto/istanza).
In programmazione orientata agli oggetti (OOP), i concetti di classe, oggetto e istanza sono
fondamentali. Vediamo come sono collegati e quali sono le loro differenze:
I linguaggi che consentono di attuare i principi della programmazione a oggetti hanno tre fattori in
comune: l'incapsulazione, il polimorfismo e l'ereditarietà.
6.2.2 Concetti Fondamentali dell'OOP
- Incapsulamento
L'incapsulamento è il principio di nascondere i dettagli interni di un oggetto e di esporre solo un'interfaccia
pubblica. In pratica, significa che i dati di un oggetto sono accessibili solo tramite metodi pubblici definiti
nella classe.
- Ereditarietà
L'ereditarietà consente di definire una nuova classe basata su una classe esistente.
La nuova classe (chiamata classe derivata o sottoclasse) eredita attributi e metodi dalla classe esistente
(chiamata classe base o superclasse), e può estendere o modificare il comportamento ereditato.
2. Polimorfismo
Il polimorfismo permette a metodi con lo stesso nome di comportarsi in modo diverso a seconda del contesto.
Ad esempio, un metodo può essere sovrascritto in una sottoclasse per fornire una nuova implementazione,
mentre in una classe base può avere una diversa.
21
7 Astrazione
L'astrazione implica la rappresentazione di concetti complessi attraverso modelli semplificati. In OOP, le classi
forniscono un'astrazione dei dati e dei comportamenti, permettendo di lavorare con oggetti e metodi senza
preoccuparsi dei dettagli implementativi.
7.1 Costruttori
Il costruttore di una classe è un metodo che permette di stabilire come un oggetto deve essere creato, cioè di
controllare la sua inizializzazione. E’ obbligatorio che il costruttore si chiami esattamente come la classe, e
che non venga specificato alcun tipo di ritorno, neanche void, in quanto quello che restituisce un costruttore
è proprio l’oggetto che esso deve costruire. Se per sbaglio o per distrazione scriviamo un costruttore con un
tipo di ritorno void o un altro tipo, il compilatore segnalerà comunque l’errore, in quanto esso verrà visto come
un membro qualsiasi della classe, ed un membro non può naturalmente avere lo stesso nome della classe che
lo contiene.
Un costruttore, come un qualsiasi altro metodo, può avere o meno dei parametri. Supponiamo di volere
costruire un oggetto della classe Veicolo specificando il numero di ruote, allora scriveremo un costruttore che
ha come parametro un intero:
public class Veicolo
{
int nRuote;
string targa;
float velocita;
//costruttore
public Veicolo(int ruote)
{
nRuote=ruote;
}
Non è necessario comunque implementare un costruttore, in quanto, in tal caso, viene di default fornito quello
senza parametri. Notate che se implementate almeno un costruttore nelle vostre classi, allora quello
di default non sarà più disponibile, cioè se la classe Veicolo è stata definita come sopra non è più possibile
istanziare un oggetto come abbiamo fatto finora con Veicolo auto=new Veicolo(); //non esiste il costruttore
ma è necessario usare quello che abbiamo definito sopra e che prende in ingresso un intero:
Veicolo auto=new Veicolo(4);
Per ogni classe possiamo implementare tutti i costruttori che vogliamo, naturalmente ognuno con un numero
o con un tipo differente di parametri, così come abbiamo visto parlando dell’overloading degli altri metodi
della classe.
7.2 Overloading dei metodi
In C# una classe può avere più metodi con lo stesso nome, ma con numero e/o tipo di parametri.
Ad esempio il metodo per ottenere il quadrato di un numero intero:
public int Quadrato(int a){
return a*a;
}
non può accettare un parametro a virgola mobile, ad esempio non è possibile invocare il metodo nella maniera
seguente:
double d=Quadrato(2.1);
22
Sia perché il metodo quadrato non accetta come parametro un double, ed inoltre il tipo di ritorno è ancora int,
mentre noi tentiamo di assegnare alla variabile d che è double. Magari si potrebbe risolvere questo semplice
caso con una conversione di tipo, ma il concetto che vogliamo esporre è che è possibile implementare diversi
metodi Quadrato(ognuno dei quali accetta diversi tipi di parametro, ad esempio:
public double Quadrato(double d) {
return d*d;
}
in tale maniera il compilatore sa che se invochiamo il metodo passando un parametro int deve utilizzare la
versione Quadrato(int a), mentre se invochiamo il metodo Quadrato con un parametro double deve utilizzare
la seconda versione. Questa tecnica viene chiamata overloading, cioè sovraccarico dei metodi, ed è una
tecnica fondamentale nella programmazione ad oggetti, ed è dunque assolutamente necessario imparare a
maneggiarla.
7.3 Modificatori di accesso
L’accesso alle informazioni è regolato per mezzo dei modificatori di accesso. Per default i membri di una
classe sono private, ma è consigliabile comunque specificare il tipo di accesso in modo esplicito per evitare
fraintendimenti.
Una dichiarazione di classe in generale può essere dichiarata con uno dei seguenti modificatori:
Modificatore
d’accesso
Descrizione
Per le Classi
internal La classe è accessibile solo a classi dello stesso assembly, questo è il livello di visibilità
predefinito.
public Classe visibile anche al di fuori dell ‘assembly contentente la classe stessa.
Per gli attributi e metodi
public I membri pubblici solo visibili a qualunque altra classe e relativi membri.
protected I membri protected sono accessibili oltre che dalla classe stessa, anche dalle classi derivate da
essa.
private I membri privati sono visibili solo all’interno della classe stessa.
internal I membri internal sono visibili all’interno dell’assembly in cui è definita la classe.
protected internal I mebri protected internal sono accessibili alle classi
La maggior parte delle classi viene dichiarata come public, quindi visibile anche in assembly diversi da quello
di appartenenza. Se si omette il modificatore di accesso, verrà sottinteso che esso sia internal, dunque che la
classe è visibile solo all’interno del suo stesso assembly.
Notate che non è possibile in genere utilizzare il modificatore private ad una classe o ad elementi che sono
contenuti direttamente in un namespace, perchè non avrebbe senso in quanto in questa maniera la classe non
sarebbe utilizzabile da nessuno, mentre è possibile definire una classe privata se innestata in un’altra come
vedremo fra qualche riga.
Il modificatore private applicato ad un membro di una classe, ad esempio ad un suo campo, indica che tale
membro è visibile ed utilizzabile solo da istanze della classe stessa, mentre il suo accesso sarà impedito ad
oggetti di classi diverse. Il modificatore public invece permette l’accesso da oggetti di qualunque altra classe.
e scrivessimo una classe come la seguente
23
7.4 L’incapsulazione
In un linguaggio a oggetti, il codice e i dati possono essere raggruppati in modo da creare una sorta di "scatola
nera". Quando il codice e i dati vengono raggruppati in questo modo, si crea un oggetto. In altre parole, un
oggetto è un "dispositivo" che supporta l'incapsulazione.
L'incapsulazione è il meccanismo che riunisce insieme il codice e i dati da esso manipolati e che mette entrambi
al sicuro da interferenze o errati utilizzi. All'interno di un oggetto, il codice, i dati o entrambi possono essere
definiti privati per tale oggetto oppure pubblici.
public class Persc {
private string nome;
private string cognome;
}
public void Salute() {
Console.WiteLine($"Ciao, sono {nome} {cognome}");
}
Il codice o i dati privati sono noti e accessibili solo da parte degli elementi dell'oggetto stesso.
Questo significa che il codice e i dati privati non risultano accessibili da parte di elementi del programma che
si trovano all'esterno dell'oggetto.
Se il codice o i dati sono pubblici, risulteranno accessibili anche da altre parti del programma che non
sono definite all'interno dell'oggetto. Generalmente, le parti pubbliche di un oggetto sono utilizzate per
fornire un'interfaccia controllata agli elementi privati dell'oggetto stesso.
Un oggetto è, in tutto e per tutto, una variabile di un tipo definito dall'utente. Può sembrare strano pensare a
un oggetto, che contiene codice e dati, come a una variabile; tuttavia, nella programmazione a oggetti avviene
proprio questo. Ogni volta che si definisce un nuovo tipo di oggetto, si crea implicitamente un nuovo tipo di
dato. Ogni specifica istanza di questo tipo è una variabile composta.
7.5 EREDITARIETA’
Il concetto di ereditarietà è molto importante nella programmazione ad oggetti. Esso ci consente di creare
nuove classi che riutilizzano, estendono e modificano il comportamento già definito in altre classi.
La classe i cui membri sono ereditati è chiamata classe di base, mentre la classe che eredita dalla classe base
viene chiamata classe derivata.
// Classe base
public class Animale
{
public string Nome { get; set;}
public Animale(string nome)
//Costruttore
public Animale(string nome)
{ Nome = nome;}
public virtual void Parla()
{
Console.WriteLine("L'animale fa un suono.");}
24
Concettualmente, una classe derivata è una specializzazione della classe base.
Ad esempio, nel grafico qui sopra riportato, possiamo notare di come la classe base Animale venga estesa da
due classi derivate: la classe Uccello e la classe Mammifero.
Per quanto sia i mammiferi che gli uccelli siano degli animali, le loro diversità li fanno specializzare in rami
differenti. In C#, per derivare da una classe base, usiamo una notazione molto sintetica, ovvero:
public class Cane: Animale{}
// Classe derivata
public class Cane : Animale
{
public Cane(string nome) : base(nome)
{
// Sovrascrittura del metodo
public override void Parla()
{
Console.WriteLine("Il cane abbaia.");
}
}
Qui sotto un altro esempio con la classe di base Person, rappresentante il concetto di persona, che, in questo
caso, viene derivata da due classi: Student e Teacher. Quest’ultime ereditano le info presenti in Person e ci
aggiungono ulteriori properties, ridefiniscono alcuni metodi e ne aggiungono di nuovi, specifici
dell’implementazione. Quest’ultimo esempio introduce un concetto molto importante e fondamentale, ovvero
la sovrascrittura dei metodi. Nella fattispecie, possiamo vedere come nella classe Person ci siano
// Definizione della classe Person
public class Person
{ // Propriete private
private string name;
private int age;
// Costruttore
public Person(string name, int age)
{
This.name = name;
this.age = age;
}
// Propriete pubblica per ottenere e impostare it nome
public string Name
{
get { return name; }
set { name = value; }
}
// Propriete pubblica per ottenere e impostare Age
public int Age
{
get { return age; }
set { age = value; }
}
// Metodo pubblico
public virtual void Introduce()
{
Console.WriteLine("Hello, my name is {name} and I am {age} years old");}}
Classe derivata Classe Person:
Proprietà Private: name e age sono dati privati che non possono essere accessibili direttamente dall'esterno
della classe. Questo è un esempio di incapsulamento.
Costruttore: Inizializza le proprietà name e age quando viene creato un oggetto Person.
Proprietà Pubbliche: Name e Age sono utilizzate per accedere e modificare le proprietà private.
Metodo Pubblico: Introduce è un metodo che stampa una presentazione dell'oggetto Person.
È dichiarato come virtual, il che significa che può essere sovrascritto nelle classi derivate (es. Student).
Ereditarietà: Student eredita da Person, il che significa che Student ha accesso ai membri della classe
Person.
25
Proprietà Aggiuntiva: StudentId è una proprietà aggiuntiva specifica per la classe Student.
Costruttore: Il costruttore di Student chiama il costruttore della classe base (Person) per inizializzare le
proprietà ereditate.
Metodo Sovrascritto: Introduce è sovrascritto per includere informazioni specifiche sullo studente, utilizzando
la parola chiave override.
Chiama anche il metodo della classe base con base.Introduce()
// Classe derivata Student the estende
Person public class Student : Person
{
// Propriety aggiuntiva
public string Studentld { get; set;
// Costruttore della classe derivata
public Student(string name, int age, string studentId) : base(name, age)
{
Studentld = studentld;
}
// Metodo sovrascritto
public override void Introduce()
{
base.Introduce(); // Chiamata al metodo della classe base
Console.WriteLine($"I am a student with ID: {Studentld}.");}}
Affinché un metodo possa essere sovrascritto da una classe, è necessario dichiararlo nella classe di base con
la keyword virtual.
Così facendo, la sottoclasse in questione avrà la possibilità di ridefinirlo a seconda delle necessità.
Se invece, al posto di virtual, una classe di base dichiara un metodo o una proprietà come abstract, allora quel
metodo o quella proprietà dovranno per forza essere sovrascritti dalla classe derivata.
L'ereditarietà è lo strumento che permette di costruire nuove classi utilizzando quelle già realizzate.
8 Le interfacce
In C#, un'interfaccia (interface) è un contratto che definisce un insieme di metodi, proprietà, eventi
o indicizzatori che una classe o una struttura deve implementare. Tuttavia, l'interfaccia non contiene
l'implementazione di questi membri; definisce solo la loro firma, lasciando il compito di fornire
l'implementazione a classi o strutture che la implementano.
8.1.1 Caratteristiche principali di un'interfaccia in C#:
Contratto: Un'interfaccia specifica ciò che una classe deve fare, ma non come farlo.
Membri: Può dichiarare metodi, proprietà, eventi e indicizzatori, ma senza implementazioni.
Implementazione obbligatoria: Una classe che implementa un'interfaccia deve fornire
l'implementazione di tutti i membri definiti nell'interfaccia.
Ereditarietà multipla: In C#, una classe può implementare più interfacce, anche se non può ereditare
da più classi.
8.1.2 Sintassi di un'interfaccia
La dichiarazione di un'interfaccia usa la parola chiave interface. Ecco un esempio di base:
public interface IVeicolo{
void Avvia();
void Ferma();
int Velocità { get; set; }
}
In questo esempio, l'interfaccia IVeicolo definisce due metodi (Avvia() e Ferma()) e una proprietà (Velocità), ma non fornisce
l'implementazione.
26
8.1.3 Implementazione di un'interfaccia
Una classe può implementare un'interfaccia fornendo l'implementazione dei membri definiti nell'interfaccia.
La sintassi prevede l'uso della parola chiave : , seguita dal nome dell'interfaccia.
public class Auto : IVeicolo{
public int Velocità { get; set; }
public void Avvia() {
Console.WriteLine("L'auto è stata avviata.");
}
public void Ferma()
{
Console.WriteLine("L'auto è stata fermata.");
}
}
In questo esempio, la classe Auto implementa l'interfaccia IVeicolo. Fornisce l'implementazione dei metodi
Avvia() e Ferma() e della proprietà Velocità.
public interface IVeicolo
{
void Avvia();
void Ferma();
int Velocità { get; set; }
}
public class Auto : IVeicolo
{ public int Velocità { get; set; }
public void Avvia()
{
Console.WriteLine("L'auto è stata avviata.");
}
public void Ferma() {
Console.WriteLine("L'auto è stata fermata.");
}
}
public class Moto : IVeicolo {
public int Velocità { get; set; }
public void Avvia() {
Console.WriteLine("La moto è stata avviata.");
}
public void Ferma() {
Console.WriteLine("La moto è stata fermata.");
}
}
class Program
{
static void Main(string[] args)
{
IVeicolo veicolo1 = new Auto();
IVeicolo veicolo2 = new Moto();
veicolo1.Avvia();
veicolo2.Avvia();
veicolo1.Ferma();
veicolo2.Ferma();
}
}
27
8.1.4 Ereditarietà multipla
Una classe può implementare più di un'interfaccia. Ecco un esempio di una classe che implementa due
interfacce:
public interface IVolante
{
void Vola();
}
public interface IVeicolo
{
void Avvia();
void Ferma();
}
public class Aereo : IVolante, IVeicolo
{
public void Vola()
{
Console.WriteLine("L'aereo sta volando.");
}
public void Avvia() {
Console.WriteLine("L'aereo è stato avviato.");
}
public void Ferma() {
Console.WriteLine("L'aereo è stato fermato.");
}
}
8.1.5 Interfacce predefinite in C#
C# fornisce alcune interfacce predefinite che sono molto utili, ad esempio:
IDisposable: Viene utilizzata per rilasciare le risorse non gestite. Il metodo principale è Dispose().
IComparable: Viene utilizzata per definire un metodo di confronto per ordinare oggetti.
IEnumerable: Viene utilizzata per fornire una funzionalità di enumerazione su una raccolta.
8.1.6 Differenze tra interfacce e classi astratte
Sia le interfacce che le classi astratte definiscono un contratto per altre classi, ma ci sono alcune
differenze:
Interfacce: Non possono avere implementazioni, ma solo la dichiarazione dei membri (a partire da
C# 8.0, possono avere implementazioni predefinite per i metodi tramite metodi predefiniti).
Classi astratte: Possono avere sia membri astratti (senza implementazione) che membri concreti
(con implementazione).
8.1.7 Conclusione
Le interfacce sono un potente strumento in C# per promuovere la flessibilità e il riuso del codice.
Consentono a diverse classi di implementare lo stesso insieme di funzionalità, indipendentemente dalla loro
posizione nella gerarchia di ereditarietà, e sono ampiamente utilizzate nella programmazione orientata agli
oggetti per favorire il principio dell'incapsulamento e l'uso di polimorfismo.
9 Overloading degli operatori
In C#, l'overloading degli operatori (sovraccarico degli operatori) permette di definire o ridefinire il
comportamento degli operatori predefiniti (come +, -, *, /, ==, etc.) quando vengono applicati a oggetti di una
classe o di una struttura. Questo rende il codice più intuitivo e leggibile, specialmente quando si lavora con
tipi di dati personalizzati, come la classe dei numeri complessi che abbiamo visto prima.
28
9.1 Come funziona l'overloading degli operatori
In C#, puoi sovraccaricare quasi tutti gli operatori, tranne alcuni (ad esempio, =, . e ?:).
Per fare ciò, devi definire un metodo statico all'interno della tua classe o struttura, utilizzando la
parola chiave operator seguita dall'operatore da sovraccaricare. Ogni operatore sovraccaricato deve
restituire un valore e accettare almeno uno o due parametri (uno per gli operatori unari e due per
quelli binari).
// Sovraccarico dell'operatore + (somma di matrici)
public static Matrice operator +(Matrice m1, Matrice m2)
public static Matrice operator -(Matrice m1, Matrice m2)
9.2 Indicizzatore
In C#, un indicizzatore (o indexer) è una caratteristica delle classi, strutture o interfacce che consente di
accedere agli oggetti utilizzando la stessa sintassi di accesso agli array.
In sostanza, permette di "indicizzare" un oggetto in base ad alcuni parametri, come se fosse un array, senza
definire esplicitamente dei metodi get o set.
Nel contesto delle matrici, un indicizzatore come this[int i, int j] permette di accedere agli elementi
di una matrice utilizzando due indici, esattamente come si farebbe con un array bidimensionale. È una sintassi
comoda per leggere o modificare gli elementi della matrice.
9.2.1 Sintassi dell'indicizzatore
Un indicizzatore viene definito come un metodo di proprietà che utilizza la parola chiave this, seguita da
parametri tra parentesi quadre. Ecco la struttura base di un indicizzatore:
public T this[int i, int j]
{
get { /* logica per restituire l'elemento alla posizione (i, j) */ }
set { /* logica per impostare l'elemento alla posizione (i, j) */ }
}
Esempio C#
// Array che rappresenta la classe Matrice
private double[,] elementi;
// Indicizzatore per accedere agli elementi della matrice.
public double this[int i, int j]
{
get { return elementi[i, j]; }
set { elementi[i, j] = value; }
}
10 File e directory
Il namespace System.IO fornisce due coppie fondamentali di classi per la gestione di file e directory, in
particolare per i primi abbiamo a disposizione le classi File e FileInfo, mentre per le directory le corrispondenti
Directory e DirectoryInfo.
La classe File contiene solo dei metodi statici, utili per la creazione, lo spostamento, la copia, l’eliminazione,
l’apertura di file o per ottenere generiche informazioni su di essi, come il tempo di creazione di un file o i suoi
atrributi.
La tabella seguente riassume i principali metodi della classe File.
Metodo Descrizione
Open Apre un file esistente e restituisce un oggetto FileStream.
Create Crea un file con nome e percorso specificati
Delete Elimina il file specificato
Copy Copia un file esistente in un’altra posizione
29
Move Sposta un file esistente in un’altra posizione
Exists Verifica se un determinato file esiste
OpenRead Apre un file esistente in lettura
OpenWrite Apre un file esistente in scrittura
CreateText Crea un file di testo con nome e percorso specificati
OpenText Apre un file di testo in lettura
AppendText Apre un file di testo per appendere del testo a quello esistente.
Tali metodi hanno diversi overload, quindi è possibile utilizzarli in diversi modi, di cui vedremo i principali
con qualche esempio, rinviando per ora i metodi che restituiscono un oggetto Stream..
Le seguenti righe mostrano come creare un file, copiarlo, spostarlo, ed eliminarlo:
File.Create(@"c:\temp\prova.txt");
File.Copy(@"c:\temp\prova.txt","c:\temp\prova2.txt");
File.Move(@"c:\temp\prova.txt","c:\temp\prova3.txt");
File.Delete(@"c:\temp\prova.txt");
È bene prestare attenzione alle possibili eccezioni che possono essere generate da tali metodi. Ad esempio
quando si tenta di eliminare un file inesistente o attualmente in uso da un altro processo sarà generata
un’eccezione IOException:
try{
File.Delete(@"c:\temp\prova.txt");
}
catch(IOException ioe)
{
//gestione dell’eccezione
}
Prima di eliminare il file sarebbe opportuno anche verificare che esso esista, con il metodo Exists:
if(!File.Exists(txtOrig.Text))
File.Delete(txtOrig.Text);
Analogamente alla classe File, la classe Directory fornisce i metodi per manipolare una directory ed ottenere
informazioni su di essa. La tabella seguente mostra i metodi principali della classe Directory:
Metodo Descrizione
CreateDirectory Crea una directory con nome e percorso specificati.
Delete Elimina lsa directory specificata.
Exists Copia una directory esistente in un’altra posizione.
Move Sposta una directory esistente in un’altra posizione.
GetFileSystemEntries
Restituisce i file e le directory contenuti nella directory specificata
GetFiles, GetDirectories Restituiscono i nomi dei file o delle sottodirectory di una directory specificata.
GetDirectoryRoot Restituisce la radice del percorso specificato
GetCurrentDirectory Restituisce la directory di lavoro corrente dell’applicazione.
GetLogicalDrives Restituisce i drive logici presenti nel sistema.
GetParent Restituisce la directory madre del percorso specificato
AppendText Apre un file di testo per appendere del testo a quello esistente
30
Sia la classe File che la classe Directory permettono di ottenere e impostare i tempi di creazione o di accesso
ad un dato elemento:
creazione=File.GetCreationTime(@"c:\temp\pippo.txt").ToString();
File.SetLastWriteTime(@"c:\temp\pippo.txt",new DateTime(2000,8,2);
string path=@"c:\temp\nuova cartella";
Directory.Create(path);
Directory.SetLastAccessTime(path,new DateTime(1981,11,25));
Le due classi FileInfo e DirectoryInfo, come detto, forniscono anche metodi che permettono di ottenere altre
informazioni sui file, come gli attributi, la data di creazione, la data di lettura e di modifica. A differenza delle
classi File e Directory, le classi FileInfo e DirectoryInfo possono essere istanziate.
Il metodo statico per creare una directory:
Directory.CreateDirectory(@"c:\temp");
10.1 Leggere e scrivere file
Nell’esempio seguente, vengono creati un oggetto StreamReader (adatto alla lettura di un file testo) e un
oggetto StreamWriter (adatto alla scrittura di un file testo).Il file che viene letto è “c:\temp\input.txt” e il
file che viene scritto è ‘c:\temp\output.txt’.
Quest’ultimo contiene il numero di byte di ogni riga del file di input.
StreamReader fileRead = File.OpenText(@"c:\temp\input.txt");
StreamWriter fileWrite = File.CreateText(@"c:\temp\output.txt");
string buffer;
while ((buffer = fileRead.ReadLine()) != null)
fileWrite.WriteLine(buffer.Length);
fileRead.Close();
fileWrite.Close();
Alternativa:
using (StreamReader fileRead = File.OpenText(@"c:\temp\input.txt"))
{
using (StreamWriter fileWrite = File.CreateText(@"c:\temp\output.txt"))
{
string buffer;
while ((buffer = fileRead.ReadLine()) != null)
fileWrite.WriteLine(buffer.Length);
}
}
31
10.2 Lista dei file di una directory
Per ottenere la lista dei file appartenenti ad una directory, utilizzare il codice seguente.
Le classi utilizzate appartengono al namespace "System.IO.DirecoryInfo".
Il metodo "GetFiles()", consente anche il filtraggio dei file.
Nell'esempio seguente, tutti i file con estensione "*.txt" sono inseriti in una lista.
string path = @"C:\Temp";
DirectoryInfo dInfo = new DirectoryInfo(path);
FileInfo[] files = dInfo.GetFiles("*.txt");
foreach (FileInfo file in files)
{
lstFiles.Items.Add(file.Name);
}
Determinare la directory corrente
string path = Directory.GetCurrentDirectory();
Console.WriteLine("The current directory is {0}", path);
Cambiare directory corrente
string dir = @"C:\test";
try
{
//Set the current directory.
Directory.SetCurrentDirectory(dir);
}
catch (DirectoryNotFoundException e)
{
Console.WriteLine("The specified directory does not exist. {0}", e);
}
11 Espressioni Lambda
In C#, le espressioni lambda possono essere utilizzate insieme a Func, Action e Predicate.
Ecco un approfondimento su questi concetti e come usarli insieme.
Le espressioni lambda sono funzioni anonime che possono essere utilizzate per creare metodi inline. La
sintassi è :
(parametri) => espressione
11.1 Func
Func è un delegato predefinito che rappresenta un metodo che restituisce un valore. Può avere fino a 16
parametri. Il tipo di valore restituito e indicato come ultimo valore della lista di parametri.
Func<int, int, int> somma = (x, y) => x + y;
int risultato = somma(5, 10); // risultato sarà 15
Console.WriteLine($"Somma: {risultato}");
11.2 Action
Action è un delegato che rappresenta un metodo che non restituisce un valore (void). Può avere fino a 16
parametri.
// Definire un Action per stampare due numeri
Action<int, int> stampaSomma = (x, y) => Console.WriteLine($"La somma di {x} e {y} è: {x
+ y}");
// Utilizzare l'Action
stampaSomma(5, 10); // Stampa "La somma di 5 e 10 è: 15"
32
11.3 Predicate
Predicate<T> è un delegato predefinito utilizzato per definire metodi che restituiscono un valore booleano.
Viene spesso utilizzato per filtrare dati.
// Definire un Predicate per verificare se un numero è pari
Predicate<int> isEven = x => x % 2 == 0;
Console.WriteLine(isEven(4)); // Stampa "True"
Console.WriteLine(isEven(5)); // Stampa "False"
class Program
{
static void Main()
{
// Func per sommare due numeri
Func<int, int, int> somma = (x, y) => x + y;
// Action per stampare il risultato
Action<int> stampaRisultato = risultato => Console.WriteLine($"Il risultato è:
{risultato}");
// Predicate per verificare se il risultato è pari
Predicate<int> isEven = x => x % 2 == 0;
// Calcolare e stampare il risultato
int risultatoSomma = somma(10, 20);
stampaRisultato(risultatoSomma); // Stampa "Il risultato è: 30"
// Verificare se il risultato è pari
if (isEven(risultatoSomma))
Console.WriteLine("Il risultato è un numero pari.");
else
Console.WriteLine("Il risultato è un numero dispari.");
}
}
11.3.1 Vantaggi di Lambda, Func, Action e Predicate
- Sintassi Concisa: Le espressioni lambda offrono un modo breve per scrivere metodi.
- Flessibilità: Puoi passare metodi come argomenti, il che rende il codice più modulare.
- Filtraggio Efficiente: Predicate è utile per filtrare e trovare elementi in collezioni. tempo stimato di studio per tutto questo materiale entro martedì oggi è sabato e sono le 4 del pomeriggio