Introduzione all’uso dei record



Scaricare 0.73 Mb.
Pagina1/3
24.01.2018
Dimensione del file0.73 Mb.
  1   2   3

Dispense corso Informatica ABACUS, classe 4^ – versione Luglio 2010

La complessità del software
Sviluppare software è diventato sempre più difficile e possiamo distinguere due categorie di fattori che concorrono alla complessità di un progetto software: fattori esterni e fattori interni.
Fattori esterni


  • strumenti di sviluppo modesti: sviluppare un videogame tutto in linguaggio macchina ed usando il blocco note penso renda benissimo l'idea di strumento di sviluppo non adeguato! L'esistenza di un linguaggio di programmazione con le caratteristiche ottimali ed un ambiente di sviluppo sofisticato (editor intelligente, compilatore e linker integrati, potente strumento di debug, un framework di componenti pronti all'uso come quelli di .NET ecc.) rendono sicuramente il compito più semplice;

  • alto livello di qualità richiesto: gli utenti oggi si aspettano un software che sia affidabile (che dia sempre le risposte attese), robusto (che si comporti cioè in modo accettabile in situazioni anomale), performante (che svolga cioè il suo compito in poco tempo ed usando poche risorse hardware), offra un'interfaccia estremamente amichevole e sofisticata (grafica, controllo vocale, touch ecc.), in grado di comunicare o addirittura di sfruttare i servizi di altre piattaforme (sistemi operativi diversi e/o hardware diversi), di funzionare in rete ecc.



Fattori interni



  • grado di innovazione: sviluppare un software che non ha precedenti sul mercato è certamente più complicato che non potersi basare su prodotti simili;

  • grado di difficoltà: è la complessità oggettiva legata al tipo di progetto (ovvio che lo sviluppo di un nuovo sistema operativo sia palesemente ed oggettivamente più complesso rispetto allo sviluppo di un programma di conversione tra euro e dollaro.

Quali sono le 'armi' a disposizione di un programmatore per controbattere la complessità? Principalmente di due tipi: buone metodologie di progetto/sviluppo e strumenti linguistici adatti a metterle in atto sotto forma di codice. E iniziamo con il ricordare quelli che conoscete già.


Metodologie

Modularità (Top Down/bottom up)

Come sicuramente ricorderete il top down prevede la decomposizione di un complesso problema in sotto problemi; se necessario ogni sotto problema viene a sua volta decomposto in sotto problemi ancora più semplici fino al raggiungimento di compiti per i quali si sa quale sia l'algoritmo risolutivo. Con il bottom up si individuano e si risolvono senza un ordine particolare dei sotto problemi che concorreranno alla soluzione di un problema di grande.


Strumenti linguistici

  • possibilità di scrivere all'interno di uno stesso file sorgente funzioni che corrispondono alla soluzione di un sotto problema sufficientemente semplice;

  • possibilità di distribuire il codice su più file sorgente

  • information hiding: la possibilità per il programmatore di decidere cosa (quali costanti, variabili e funzioni) di un modulo (funzione o file sorgente) rendere visibile negli altri moduli; ad esempio mettere una variabile locale ad una funzione significa aver deciso di renderla invisibile all'esterno; oppure rendere non utilizzabile negli altri moduli una funzione che è usata solo internamente ad un altro; così si rendono più indipendenti tra loro i moduli

  • tipizzazione: la diamo talmente per scontata che rischiamo di non apprezzare la sua importanza; il fatto di dover dichiarare esplicitamente al compilatore i tipi (int, float, string ecc.) di costanti, variabili, valori restituiti dalle funzioni e parametri permette di intercettare appunto in fase di compilazione usi impropri che sarebbero causa di errori a volte molto difficili da scoprire; alcuni linguaggi molto diffusi consentono, a mio parere purtroppo, invece di introdurre oggetti senza essere costretti a dichiararne il tipo (Visual Basic, PHP, Javascript per citare i più noti)



Limiti dei linguaggi tradizionali – 1


struct bicicletta {

int x,y,z;

string modello;

bla; bla; …

};

struct b { … };



struct c { … };

float altezza;

void bla1bla1(…) {… };

int bla2bla2{ … };

void parti(bicicletta b) {…}

float bla3bla3{…};

int bla4bla4{…};

int posizioneBicicletta(…) {…};

...

DOV’E’ LA BICICLETTA ?



Nonostante gli strumenti ricordati cui sopra è ancora troppo difficile descrivere e modellare realtà complesse.

Tipicamente i programmi sono costituiti da molte dichiarazioni di variabili e da una lunga sequenza di sottoprogrammi; è difficile a colpo d’occhio riconoscere gli oggetti del mondo reale rappresentati nel codice.

Ad esempio, pensando ad un programma di simulazione di veicoli, osservate il riquadro qui a lato (scritto con un linguaggio tradizionale, il C): in mezzo alle numerose variabili e sottoprogrammi ci sono anche le informazioni con cui il programmatore ha voluto rappresentare una bicicletta; ma quali sono ? Di solito il codice, pur partendo ordinato, tende a diventare più confuso a causa di aggiunte e modifiche.


Limiti dei linguaggi tradizionali – 2: riutilizzo del codice
E’ difficile riutilizzare il codice. Immaginiamo di voler sfruttare almeno parte del codice della bicicletta cui si è accennato prima per realizzare un tandem. Con i linguaggi tradizionali l’unica via percorribile è il famigerato ‘copia/incolla’ cioè copiare il codice della bicicletta e modificarlo per il tandem.

Questo modo di procedere comporta molti rischi e inefficienze:




  • Dimenticanze di parte di codice

  • Duplicazione inutile di codice

  • Replica degli errori in tutti i sorgenti in cui metto il codice copiato

  • Manutenzione più pesante: deve essere apportata anche su tutte le copie


Sarebbe bello invece poter realmente ‘programmare per differenze’, dire cioè solo in cosa tandem differisce da una bicicletta e senza duplicazione di codice, derivare il tandem dalla bicicletta.
ADT: Abstract Data Type

A livello teorico (ma a livello pratico i linguaggi tradizionali non offrono meccanismi per implementarli) un grosso miglioramente è rappresentato da quelli che gli informatici chiamano ADT (Abstract Data Type = Tipi di dato astratto). Un ADT è un modello, quindi una rappresentazione semplificata della realtà; semplificata perchè nel modello rappresentiamo solo le caratteristiche che interessano che individuiamo con il meccanismo mentale dell'astrazione.


Nel concreto un ATD è una struttura dati per la quale il programmatore definisce un insieme di caratteristiche ed un insieme di operazioni. Ed è importante farlo mantenendo validi i principi dell'information hiding: chi userà l'ADT nei propri programmi non deve essere costretto a preoccuparsi della sua l’implementazione (come sia realizzato internamente un ADT) allo stesso modo in cui non siamo costretti a sapere com'è strutturato internamente un cellulare per usarlo con soddisfazione. L’ADT potrebbe sfruttare internamente dei vettori piuttosto che struct o addirittura files per memorizzare i dati ed usare un metodo di ordinamento piuttosto che un altro ecc.
Ed aggiungere un ADT al nostro codice dovrebbe corrispondere all'aggiunta di nuovo tipo: in questo modo potremmo dichiarare variabili ciascuna delle quali rappresenta un esemplare dell'oggetto definito dall'ADT. Interessa sapere solo come ottenere i suoi servizi cioè come dichiarare variabili di quel tipo, a quali caratteristiche è possibile accedere e quali comandi (messaggi) possono essere inviati ad un esemplare di un ADT e quali risultati si ottengono
Un esempio di ADT

Nella maggior parte dei linguaggi tradizionali non esiste il tipo data. E' abbastanza semplice descrivere un ADT, un tipo che dovrebbe comportarsi come una data comoda da usare (chiamiamo il tipo semplicemente data):


- dovrebbe rendere disponibili comandi per creare date in vari modi:

data d1 = nuova data(25,12,2010); //creazione data corrispondente a Natale 2010

data d2 = nuova data("25-12-2010"); //idem ma partendo da una stringa
- dovrebbe avere comandi per sapere se l'anno memorizzato è bisestile, per convertire la data dal formato italiano a quello anglosassone, per calcolare la differenza in giorni tra due date ... per aggiungere/togliere giorni ad una data ecc., per estrarre dalla data completa solo il giorno o l'anno ecc.

data d3 = d1.ConvertiInglese(); //d3 è d1 convertita

data attesa = d2 - d1; //giorni tra le due date d1 e d2

data d4 = d1 + 100; //aggiungiamo 100 giorni a d1 ottenendo d4

cout << "oggi è il giorno " << d4.Giorno() << " del mese"; ecc.
Nota: come sia rappresentata internamente la data e come vengano realizzati i comandi non interessa a livello di progetto dell'ADT. Tutto molto bello ma se il linguaggio non offre nulla di specifico per realizzare un ADT? Se alla fine tutto si riduce ancora a variabili e funzioni rimane solo un'idea non traducibile.
E' come se un linguaggio non mettesse a disposizione un meccanismo per implementare le funzioni: tutti sanno che sono utili ma rimarrebbero solo un'idea per quel linguaggio! Per anni si è rimasti proprio in questa situazione. A livello di progetto si era in grado di descrivere ADT utilissimi ma i linguaggi di programmazione non li supportavano. E questo non è l'unico punto debole dei linguaggi 'tradizionali'.
OOP: incapsulamento, classi, oggetti, stato interno e metodi
Come si diceva, per molto tempo i linguaggi di programmazione non hanno offerto strumenti linguistici per una rappresentazione semplice e naturale di un ADT. Questo fino agli anni 90, quando fecero il loro ingresso sulla scena i linguaggi OOP (Object Oriented Programming, OOP in sigla, cioè programmazione orientata agli oggetti).

Tre sono le parole chiave della OOP


incapsulamento ereditarietà polimorfismo

Con il termine incapsulamento si intende la possibilità di definire in un programma rappresentazioni di oggetti del mondo reale usando strutture (le classi) che contengono al loro interno (incapsulati, appunto) sia i dati che descrivono le caratteristiche degli oggetti sia le funzioni (metodi) che corrispondono al comportamento degli oggetti.


In pratica viene superata la storica separazione tra strutture dati e sottoprogrammi che le usano. Se ci pensate calza alla perfezione con la definizione di ADT.

E


class Cani

{

public string Razza;



public string Nome;
public string Verso()

{

return "bau bau!";



}

}
cco qui a lato come si potrebbe definire una classe per rappresentare il concetto di cane in C#.


All’inizio sono elencate le caratteristiche (Razza e Nome) che definiscono il cosiddetto stato interno di un oggetto) e di seguito i sottoprogrammi (li chiameremo metodi) che potranno essere usati con un oggetto di tipo Cani.
La classe introduce di fatto un nuovo tipo : il programmatore potrà dichiarare variabili di tipo Cani.
Public davanti alle dichiarazioni delle variabili e dei metodi li rende visibili all’esterno.

Il programmatore rispetto ai linguaggi non OOP può chiaramente distinguere nel codice la parte che corrisponde alla rappresentazione del cane. Il compilatore da parte sua potrà controllare ‘abusi’ come tentare di usare con un cane sottoprogrammi che non sono della classe dei cani (se un sottoprogramma non appare incluso nel blocco della classe, non potrà essere richiamato con un oggetto di tipo Cani):




Cani Trovatello = new Cani();

Trovatello.Razza=”bastardino”;

Trovatello.Nome=”Boby”;
La prima istruzione crea un nuovo oggetto (new) di tipo Cani: Trovatello; Trovatello ha una razza ed un nome; poiché queste caratteristiche sono public le possiamo manipolare direttamente. Allo stesso modo potremo invocare solo i metodi public:

Console.WriteLine( Trovatello.Verso() );

In questo caso abbiamo invocato il metodo Verso che restituirà la stringa “bau bau” (si può anche dire che abbiamo inviato all’oggetto il messaggio Verso; di nuovo, lo possiamo fare perché il metodo è public.


Non è possibile sbagliare ed invocare un metodo come in:

Console.WriteLine (Trovatello.Miagola() );
… perché semplicemente non esiste un metodo Miagola nei cani! Il compilatore rifiuta il codice e segnala un errore. Senza classi il compilatore potrebbe in tante situazioni accettare cose senza senso ma corrette da un punto di vista sintattico (per chiamare un sottoprogramma senza errori da parte del compilatore di un linguaggio non OOP è sufficiente rispettare il numero ed il tipo dei parametri previsti).
DEFINIZIONE. Una classe è un modello per creare oggetti descritti dalle stesse caratteristiche (che possono però assumere valori diversi) e che mostrano comportamenti identici se sollecitati con gli stessi stimoli a partire dalle stesse condizioni iniziali (cioè stato interno identico).
Il concetto non è del tutto estraneo ai programmatori tradizionali: voi stessi siete già abituati ad usare delle classi ‘predefinite’. Ad esempio potremmo dire che il tipo Integer è la classe per definire le variabili integer (gli oggetti) con cui rappresentiamo i numeri interi. Allo stesso modo sono già definiti dei ‘comportamenti’ per oggetti di questo tipo: le variabili intere ‘sanno’ sommarsi tra loro, sottrarsi ecc.
Una classe da sola non serve a niente, come del resto non servirebbe a niente il solo tipo integer. E’ necessario creare esemplari di oggetti di una classe, allo stesso modo in cui è necessario definire variabili integer per lavorare con numeri interi.
DEFINIZIONE. Un oggetto è un ben preciso esemplare (istanza) di una classe.
Il programmatore definisce una classe e crea sulla base di essa tutte le istanze che ritiene necessarie. Definisce ad esempio la classe ‘Cane’ e crea (istanzia) tanti oggetti di quella classe quanti sono i cani di cui ha bisogno nel suo programma. Quindi non confondete: la classe è il modello, gli oggetti sono gli esemplari che si basano su quel modello.
‘Razza’ e ‘Nome’ sono le caratteristiche che avranno tutti gli oggetti creati a partire da questo modello ‘Cani’ (ovviamente ciascun oggetto avrà il suo nome e la sua razza). L’insieme delle variabili che descrivono un oggetto è chiamato stato interno.
Nella terminologia OOP i sottoprogrammi di una classe sono chiamati metodi. La funzione ‘Verso’ è quindi l’unico metodo definito per la classe ‘Cani’. Metodi di classi diverse potrebbero avere lo stesso nome. Ad esempio potremmo aggiungere nel programma la classe dei Gatti ed anche per questi ultimi decidere di scrivere un metodo chiamato Verso. Il compilatore non farebbe confusione: dal tipo dell’oggetto saprebbe quale metodo mandare in esecuzione.
In generale quando ci si riferisce alle variabili dello stato interno od ai metodi si parla di membri della classe.
I metodi sono condivisi tra tutti gli oggetti di una classe: non viene cioè mantenuta una copia del codice per ogni oggetto. Ogni metodo invocato opera però sullo stato interno dell’oggetto per il quale è stato invocato.
Ad esempio, se cane1 e cane2 fossero due oggetti della classe Cani , e se vi fosse un metodo float Peso() che restituisce il peso di un cane, i comandi cane1.Peso() e cane2.Peso() attiverebbero lo stesso metodo (Peso) ma funzionante sugli stati interni rispettivamente di cane1 e cane2 e nel primo caso avremmo il peso di cane1 e nel secondo caso il peso di cane2.
Variabili reference
Cani ilMioCane=null;
Con la precedente istruzione viene solo dichiarata una variabile di tipo Cani. ATTENZIONE: l’oggetto non esiste ancora. La variabile ilMioCane non è l’oggetto ma un riferimento (reference) ad esso. Altri linguaggi (C, C++) chiamano queste variabili puntatori, nel senso che ‘puntano’ cioè individuano l’area in memoria dove VERRA’ creato l’oggetto.
L’istruzione di creazione è la seguente: ilMioCane = new Cani(); la parola chiave new chiarisce in modo inequivocabile che si vuole un nuovo oggetto richiamando il costruttore dei Cani. Il valore restituito da new è l’indirizzo dell’oggetto in memoria (l’indirizzo del suo primo byte): il punto in cui sarà memorizzata la coppia di valori Razza/Nome per questo oggetto:


PRIMA DEL COMANDO NEW


ilMioCane




i
DOPO IL COMANDO


ilMioCane = new Cani();

lMioCane




DOPO IL COMANDO
ilMioCane = new Cani (“Bichon Frisè”, “Tea”);
NOTA: in questo ultimo esempio sto immaginando che per la classe Cani esista un secondo costruttore (ricordate il discorso sugli overload??) che accetta da subito la razza ed il nome del cane.




ilMioCane


Questa gestione della memoria è detta dinamica in contrapposizione a quella statica che avete usato fino ad ora: con la dichiarazione int x; la variabile x è da subito utilizzabile perché l’area di memoria per contenere il suo valore è allocata dall’inizio alla fine dell’esecuzione del programma. La memoria dinamica viene occupata invece a comando (quando l’oggetto viene creato) e restituita a comando o in automatico quando l’oggetto viene distrutto. In automatico significa che periodicamente viene avviata una procedura (garbage collection, letteralmente ‘raccolta della spazzatura’) volta ad individuare gli oggetti non raggiunti (referenziati) da una variabile reference.


Ribadisco che è quindi un errore MOLTO grave tentare di usare un oggetto prima di averlo creato: ad esempio tentare di accedere al valore di una delle variabili dello stato interno o peggio tentare di modificarne una; allo stesso modo tentare di invocare un metodo. La creazione avviene invocando il cosiddetto costruttore, un metodo ‘speciale’.
Errori da evitare

Cani.Nome = “Diablo” è un uso sbagliato: infatti Cani è il nome della classe e non di un oggetto creato con new.
Nome = “Diablo” Anche usare il nome di una variabile dello stato interno senza mettere davanti l’identificatore dell’oggetto è sbagliato: non si saprebbe a che istanza (a quale cane) si vuole assegnare il nome.

Costruttori e distruttori definiti dal programmatore, overloading dei metodi

I


class Cani

{

string Razza="";



string Nome="";
public Cani(string Razza, string Nome)

{

this.Razza = Razza;

this.Nome = Nome;

}

public string FaiIlverso()

{

return "bau bau!";



}

}
l costruttore standard (quello non scritto da noi ma reso disponibile in automatico) si limita a fare spazio in memoria per le variabili dello stato interno ma non ci costringe ad indicare il loro valore. Il programmatore può per fortuna aggiungere altri costruttori che si preoccupano di dare un valore ad alcune (eventualmente tutte ma di solito solo quelle più importanti) le variabili dello stato interno. Naturalmente non siamo limitati a questo: possiamo far compiere al costruttore qualsiasi cosa. Ad esempio potremmo decidere che come ultima operazione il costruttore visualizzi sullo schermo l’immagine del cane.


H
NOTA

I costruttori devono essere dichiarati di pubblico accesso: public Cani().


Diversamente verrebbero considerati private (livello di accessibilità di default) e non potremmo richiamarli dall’esterno !
Il valore private di default è la scelta più aderente ai principi dell’information hiding …


o evidenziato in grassetto il costruttore. Il suo compito in questo programma è solamente quello, si diceva, di copiare i parametri inviati da chi sta chiedendo di creare l’oggetto nelle corrispondenti variabili dello stato interno. L’istruzione this.Razza = Razza richiede una spiegazione. Poiché è spontaneo dare al parametro lo stesso nome della variabile dello stato interno (rappresentano la stessa cosa, parametro Razza e variabile dello stato interno Razza) ci si ritroverebbe a comandareRazza=Razza per copiare il parametro. Ma è ovvio che la scrittura è ambigua: non si distingue il parametro dalla variabile interna. In questi casi si può ricorrere al reference speciale this che significa ‘di questa istanza della classe in cui si sta scrivendo il codice’. Quindi this.Razza significa la variabile Razza dello stato interno dell’istanza dell’oggetto che si sta usando, non il parametro. Ambiguità risolta.
Ovviamente sarebbe altrettanto valido usare un nome diverso per il parametro e non usare this:
public Cani(string _Razza, string _Nome)

{

Razza = _Razza;

Nome = _Nome;

}
NOTA: nel momento in cui si aggiunge anche un solo costruttore ad una classe, quello standard che non prevede parametri non è più disponibile. Se si vuole lasciare la possibilità di creare un oggetto senza parametri bisogna aggiungere esplicitamente un secondo costruttore senza parametri: public Cani() { }
Overloading

Notiamo innanzitutto che anche questo secondo costruttore si chiama Cani. Ho già discusso in precedenza questa possibilità nella sezione sulle funzioni con C# e la possiamo vedere in azione in tutta la sua convenienza proprio con i costruttori.




Metodi setter e getter
Come già accennato, tutti i membri di una classe sono automaticamente definiti privati (private). Questo aiuta il programmatore a rispettare la filosofia dell’information hiding: ci deve essere un buon motivo per rendere accessibile un membro di una classe. Per rendere accessibile una variabile dello stato interno o un metodo bisogna far precedere la sua dichiarazione dal modificatore di accessibilità public, come abbiamo visto per il costruttore.
Poter leggere il valore di una variabile dello stato interno o modificarne il valore NON è un buon motivo per renderla pubblica. Meglio rendere possibili queste azioni in modo controllato attraverso dei metodi (questi ovviamente pubblici altrimenti non potremmo invocarli e saremmo punto a capo).
Vediamo ad esempio come leggere/modificare correttamente il valore della variabile Razza di un cane: per leggere è stato aggiunto il metodo pubblico getRazza; il nome del metodo è composto dal nome della variabile il cui valore viene restituito e dal prefisso get (prendere); questa è una convenzione abbastanza diffusa ma non è un obbligo ed avremmo potuto scegliere un qualsiasi alto nome; simmetricamente per modificare il valore della stessa variabile è stato aggiunto un metodo setRazza(string Razza), prefisso set (cambiare); ovviamente il parametro è il valore da assegnare alla variabile dello stato interno:
… parte iniziale classe omessa per brevità …

public string getRazza()

{ return Razza; }
public void setRazza(string Razza)

{ this.Razza = Razza; }
Ed ecco un esempio d’uso: (parte iniziale programma omessa per brevità)

Cani ilMioCane = new Cani("Bichon Frisè", "Tea");

Console.WriteLine(ilMioCane.getRazza()); //scrive 'Bichon Frisè'

Console.WriteLine("Inserire nuova razza: ");

string nuovaRazza = Console.ReadLine();

ilMioCane.setRazza(nuovaRazza);

Console.WriteLine("Razza modificata: " + ilMioCane.getRazza());
Si potrebbe obiettare che sembra un meccanismo che ha complicato un’operazione che, definendo public la variabile Razza, avrebbe potuto essere codificata semplicemente con:

Console.WriteLine(ilMioCane.Razza); e ilMioCane.Razza = nuovaRazza
Certamente *è* più semplice procedere in questo modo ma anche decisamente più pericoloso. I metodi set e get, infatti, possono far ben di più che semplicemente restituire/modificare il valore della variabile dello stato interno: ad esempio controllare se l’accesso è consentito a chi lo sta richiedendo (pensate ai valori di un conto corrente bancario!), presentare all’esterno i dati in modo diverso da quello di memorizzazione interno (ad esempio internamente una data potrebbe essere mantenuta in forma anglosassone ma se richiesta venire restituita in formato italiano), compiere altre operazioni necessarie dopo una modifica (se in un gioco di ruolo viene aumentata la ‘forza’ di un giocatore anche la sua immagine a video deve essere ‘irrobustita’).



Ereditarieta’, generalizzazione e specializzazione


E’ il meccanismo sintattico che consente di definire nuove classi più specializzate (dette classi derivate) a partire da una classe preesistente (detta classe base) condividendone lo stato interno ed i metodi (ma con la possibilità di adattarne o sostituirne alcuni).


L’idea di fondo è quella di non ripetere in tutte le classi derivate le stesse variabili e le stesse funzioni ma di sfruttare quelle presenti nelle classi base.
Se tutti i sotto tipi di animali hanno un nome scientifico perché ripeterlo come variabile per ogni specie? Conviene derivare tutte le specie da una classe base ‘Animali’ dove metteremo una volta sola tutte le variabili comuni a Insetti, Mammiferi ecc. Stessa cosa per i metodi.

Generalizzazione e specializzazione

Nel progettare la gerarchia delle classi a volte viene spontaneo partire dal fondo, dalle classi derivate, ed individuare le classe antenate. Questo modo di procedere implica un processo mentale di generalizzazione (identifico le parti che accomunano le classi derivate e le concentro in una classe antenata). Si procede dal particolare al generale (dall’insetto all’animale).

Altre volte è più spontaneo partire dalla cima, dalle classi antenate e individuare le classi derivate. In questo caso si procede invece per specializzazione, dal generale al particolare (dall’animale al cane).
Programmare per differenze. Adottare il meccanismo dell’ereditarietà per la scrittura del codice significa programmare per differenze, una prospettiva molto efficace!! Ad esempio, per definire un insetto non si parte da zero, ma si dice in cosa esso si distingue da un animale generico. Oppure dopo aver realizzato una complessa simulazione di una partita di basket derivo quella per una partita di pallamano (si tratta sempre di gestire un certo numero di giocatori, un campo di gioco e regole). Meglio: se in prospettiva dovrò realizzare simulazioni di altri sport, metto a fattore comune tutto ciò che nei diversi giochi si assomiglia:

NOTA IMPORTANTE: a differenza di quello che accade nella programmazione tradizionale, per realizzare il codice per il basket e la pallamano, NON copio/incollo il codice che voglio usare della classe dei giochi! Tutto ciò che si decide di ereditare dai giochi è disponibile nel basket e nella pallamano grazie al meccanismo dell’ereditarietà.



Derivazione da una classe base

Vediamo come concretamente derivare in C# una classe da un’altra. Immaginiamo di avere definito in un sorgente la classe degli animali. Tutti gli animali hanno una razza ed un nome scientifico, per cui questi attributi sono solo in questa classe e non ripetuti inutilmente in ogni classe derivata. La classe dei leoni viene derivata indicando dopo il suo nome quello della classe antenata separando con due punti i due nomi:



class Animali //classe base (madre)

{
string Razza = "NON SPECIFICATA";

string NomeScientifico = "NON SPECIFICATO";
public Animali() { }

public Animali(string Razza, string NomeScientifico)

{

this.Razza = Razza;



this.NomeScientifico = NomeScientifico;

}
public string getRazza()

{ return Razza; }

}


class Leoni : Animali //classe derivata (figlia)

{

string Criniera = "NON DEFINITA";



public Leoni(string Criniera) : base("Leone", "Micius Magnum")

{

this.Criniera = Criniera;



}

}



NOTA BENE. Il costruttore della classe figlia di solito invoca uno dei costruttori della classe madre ovviamente fornendo i parametri previsti: public Leoni(Criniera) : base("Leone", "Micius Magnum")
Significa che prima di tutto sarà invocato questo costruttore e solo poi si procederà con l’esecuzione delle istruzioni del costruttore della classe figlia. Questo dovrebbe essere sempre fatto e per un motivo molto logico: chiedo a chi lo sa fare (il costruttore della classe madre) di inizializzare correttamente la parte ereditata. Diversamente ci ritroveremmo con un oggetto ‘mal formato’ (senza un valore di partenza per la razza ed il nome scientifico).
NOTA IMPORTANTE: cosa accade se per la classe figlia non viene definito alcun costruttore (cosa perfettamente lecita) ?


class Leoni : Animali //classe figlia

{

string Criniera = "NON DEFINITA";



}

RISPOSTA: nulla a patto che nella classe madre sia stato definito anche il costruttore senza parametri. Infatti nella logica di inizializzare correttamente lo stato interno ereditato il compilatore tenterà di richiamare in automatico almeno il costruttore vuoto.




Differenziare le classi derivate
La classe derivata non può essere un semplice clone di quella base ma deve potersi differenziare. Le principali possibilità sono: aggiungere nuove variabili e/o metodi (ovvio), sostituire in toto variabili e/o metodi non adatti per la classe derivata.


Il linguaggio C#

Perché??
Perché un altro linguaggio? Perché non proseguire con il C++? Lo avremmo certamente potuto fare ed anche con lo stesso ambiente di lavoro con cui useremo C# (il Visual Studio). Ecco alcune considerazioni che ci hanno portato a fare questa scelta:


  • In C++ è più facile commettere errori per evitare i quali i progettisti del C# hanno previsto meccanismi più sicuri.



  • Il C# è il linguaggio con cui Microsoft ha sviluppa .NET (il cuore di Vista e Seven); è per così dire il linguaggio oggi ‘preferito’ da Microsoft. I progettisti di Microsoft stanno inoltre continuamente aggiungendo nuove interessantissime caratteristiche (senza compromettere la validità di quelle esistenti, però!)



  • E’ progettato per la OOP fin dalle origini: C++ è un C a cui sono state aggiunte le classi ma in modo non del tutto naturale.



  • Microsoft ha scelto un approccio aperto per il linguaggio: specifiche standardizzate e libere (standard ECMA); esistono ambienti visuali per lo sviluppo con C# anche per Linux e Mac (progetto Mono; SharpDevelop); Mono non è un giochetto: sta diventando l’ambiente più utilizzato per Linux …



  • Microsoft distribuisce un ambiente di sviluppo free (Visual Studio Express)



  • E’ molto simile a Java, un altro linguaggio molto importante per i programmatori! Imparate C# e sarete più pronti anche per Java!



  • La sintassi è comunque C like!! Ho lasciato per ultimo questo punto che forse è quello che nell’immediato interessa voi tutti e che risponde al domandone ‘Devo allora buttare via tutto quello che ho duramente imparato sul C++???’. La risposta è un grosso NO! Gran parte di quello che avete imparato rimane valido, tantè che troverete nelle dispense solo quelle cose importanti che in C# si fanno in modo diverso



Hello World!
Prima di affrontare gli aspetti più innovativi ritroviamo quelli 'standard' di ogni linguaggio iniziando con l’immancabile e scontatissimo primo programma: l'hello world. Fate partire Visual Studio e scegliete File / Nuovo progetto (io sto usando la versione professional per cui aspettatevi qualche leggera differenza in alcune parti dell’interfaccia ma non dovreste avere comunque problemi). Nel pannello che appare scegliete di creare una ‘applicazione console’; nella sezione inferiore troverete:
Nome: quello da dare al progetto. Percorso: dove si trova o verrà creata la cartella del progetto.
Nome soluzione: il progetto è parte di un contenitore più grande chiamato soluzione (infatti su disco verrà creato un nome con estension .sln ed è su quello che dovrete fare doppio clic per riaprire un progetto dopo averlo chiuso); una soluzione potrebbe contenere più progetti (ad esempio anche un sito web oppure una versione dell’applicazione per palmari). In partenza viene dato alla soluzione lo stesso nome del progetto. Barrando la casellina ‘crea directory per soluzione’ la cartella del progetto verrà creata dentro una cartella che diventa quella della soluzione, diversamente verrà creata la sola cartella del progetto. Una volta confermati i valori inseriti partirà il cosiddetto IDE (Integrated Development Environment):

Se visibile, la casella degli strumenti , di solito sulla sinistra, sarà vuota perché in ambiente a carattere non avrebbe senso accedere a controlli grafici. Il codice viene scritto nell’ampia zona bianca centrale. Sulla destra il pannello ‘Esplora soluzioni’ con i file che fanno parte della soluzione/progetto (che con il dev c++ stava sulla sinistra).


Lo scheletro del programma è stato interamente creato in automatico. All’inizio troviamo delle direttive ‘using’ che corrispondono agli #include del C++. Anche i namespace sono una vecchia conoscenza del C++ …
In fondo l’unica differenza consiste nel fatto che il Main (notate la M maiuscola!) è a sua volta contenuto nel blocco class program { … }. Per il momento ignoreremo le classi (che vi anticipo essere il modo in cui con i linguaggi OOP vengono realizzati gli ADT). I più attenti/e tra voi avranno forse notato come il vettore args sia dichiarato mettendo le parentesi quadre dopo il nome del tipo e non dopo l’identificatore: string[] args; ma ritorneremo sui vettori… Anche la parola chiave static ha a che fare con le classi e la ignoreremo come tutti gli altri particolari non indispensabili a creare il programma. Limitatevi quindi a scrivere questo codice dentro il Main: Console.WriteLine("Hello World!");
E poi:

CTRL F5: compilazione + link + esecuzione; si aprirà la classica finestra nera di esecuzione a carattere (se non avrete commesso errori di sintassi, naturalmente!). A programma terminato o forzando la chiusura della finestra si ritorna all’editor.


F5: esecuzione con debug; torneremo presto ad esaminare questa possibilità.

Array (da www.morpheusweb.it con adattamenti del prof. Camuso)
Array monodimensionali
Per dichiarare un array utilizziamo una sintassi con le parentesi quadre come nell’esempio.
int[] v = new int[3]; //novità
v[0] = 1; v[1] = 2; v[2] = 3; //nulla di nuovo qui…
(inserto del prof. Camuso)
Non ci sconvolge certamente il fatto che rispetto al C++ nella dichiarazione le parentesi quadre cambino posto (dal C++ con ‘int v[]’ al C# con ‘int[] v’). E’ invece FONDAMENTALE capire che in C# gli array sono classi e che prima di poter usare un array ne dobbiamo comandare la creazione usando il comando new come in: new int[3].
Possiamo anche popolare l'array in fase di dichiarazione; in questo caso non dobbiamo usare il new in quanto il compilatore capisce quanto spazio usare dall’inizializzazione (l’elenco dei valori da memorizzare separati da virgole ed il tutto racchiuso tra parentesi graffe). Da notare che l’indice dell’array parte come al solito da zero: Esempio: int[] v = {1, 2, 3};
        

Tra le proprietà più utili di un vettore V troviamo V.Length, il numero di elementi del vettore.


Array bidimensionali

Esempio: int[,] matrix = new int[2,3]; //matrice di 2 righe e tre colonne


Anche in questo caso possiamo creare un array assegnando i valori agli elementi:



int[,] matrix = {{1, 2, 3}, {4, 5, 6}};

Per scorrere l’array possiamo usare dei cicli for:


        int[,] matrix = {{1, 2, 3}, {4, 5, 6}};
        for(int i = 0; i < matrix.GetLength(0); i++)
           for (int j = 0; j < matrix.GetLength(1); j++)




Inserto del prof. Camuso
Notare l’uso del metodo GetLength(dimensione): restituisce il numero di elementi in quella dimensione; la prima dimensione è la 0 (numero di righe della matrice) e la seconda è la 1 (numero di colonne).
GetLengh() può essere usato anche con i vettori ma essendoci una sola dimensione è più comodo usare la proprietà Length.


NB: il fatto di poter ‘chiedere’ agli array il loro numero di elementi elimina la necessità di prevedere questo dato come parametro in ingresso per funzioni che elaborano array.

Enumerazioni (prof. Camuso )
Una enumerazione è una struttura contenente un elenco di valori numerici (interi) rappresentati però da identificatori semplici da ricordare:


Dal punto di vista numerico il primo elemento valo 0, il secondo 1 e così via. E’ però possibile scegliere da che valore partire:
public enum GiorniSettimana
{  Lunedì=1, …}

oppure dei valori scelti uno ad uno:

public enum CodiciColoriInItaliano

{

Nero = 0x0,



Bianco = 0xFFFFFF

}




public enum GiorniSettimana
{
  Lunedì,
  Martedì,
  Mercoledì,
  Giovedì,
  Venerdì,
  Sabato,
  Domenica
}

Molti degli elenchi di valori usati da .NET sono stati resi disponibili sotto forma di enumerazioni (ricordate Console.Color ?).



Funzioni, anzi metodi! (prof. Camuso )
La sintassi generale è: [visibilità] [tipo] nomeMetodo(elenco parametri formali)
La visibilità (essenzialmente private, protected o public) determina chi può usare il metodo (public = tutti, private = solo i metodi della stessa classe, protected = anche i metodi delle classi ‘figlie’). I dettagli verranno forniti in seguito e per il momento basti sapere che sperimentando all’interno della classe program possiamo anche non mettere nulla.
Il tipo: non cambia nulla rispetto al C++, trattandosi semplicemente del tipo restituito dal metodo.
Parametri: Questa parte è importante e dovete prestare la massima attenzione. Distinguiamo come in C++ tra quelli per valore e quelli per indirizzo; le variabili di tipi standard (int, float, double, string ecc.) vengono automaticamente passate per valore; gli oggetti, cioè gli esemplari delle classi e che sapete essere creati con new sono invece automaticamente passati per indirizzo. Se volete passare una variabile di tipi standard per indirizzo dovete mettere sia davanti al nome del parametro formale sia davanti al parametro attuale la parola ref:

static void modifica(ref string s)

{ s = "ooooh!"; }
nel main …
string nome=”Mario”;

modifica(ref nome);

Console.WriteLine(nome); //stampa ‘ooooh!’

Gestione delle eccezioni – try catch finally (da www.morpheusweb.it )

Output:

Unhandled Exception: System.DivideByZeroException: Attempted to divide by zero. at Sample.Sample.Main() in c:\documents and settings\scuderi\my documents\visual studio project\\consoleapplication1\class1.cs:line 12


e terminerebbe l'esecuzione anche se vi fosse dell'altro codice da eseguire.
Un'eccezione è un comportamento indesiderato e non previsto, che fa andare in errore un programma. Un classico esempio è il tentativo di divisione per zero, ma potrebbe anche essere una conversione esplicita de una stringa ad un intero. Senza la gestione delle eccezioni, quando un programma esegue un'istruzione che porta ad un errore a runtime, viene interrotto e mostrato un messaggio di errore. Per evitare ciò possiamo utilizzare la gestione delle eccezioni.

Possiamo includere il blocco di codice che pensiamo possa portare ad un'eccezione, in un blocco try … catch … finally …

Esempio

Consideriamo il seguente codice

    

  int numero = 10, divisore = 0, risultato = 0;


      risultato = (numero / divisore);
      … altro codice …
    



c
Output:

Errore: Attempted to divide by zero.


Fine elaborazione

La divisione per zero "solleva" un'eccezione, che viene "gestita" nel blocco catch.


In più il programma continua l'esecuzione ed il blocco finally viene eseguito..


Da notare che il codice nel blocco finally, viene eseguito comunque, è utile quindi per far eseguire al sistema operazioni di pulizia come la chiusura di recordset o di connessioni a database, perchè siamo sicuri che verranno eseguite.




on try catch finally


      int numero = 10;
      int divisore = 0;
      int risultato = 0;
      try
      {
        risultato = (numero / divisore);
      }
      catch (System.DivideByZeroException e)
      {
        Console.WriteLine("Errore: " + e.Message);
      }
      finally
      {
        Console.WriteLine("Fine elaborazione");
      }
    

Sviluppo di applicazioni GUI .NET / C#
Per una rassegna più approfondita dei principali controlli grafici utilizzabili e la programmazione dei loro principali eventi vi rimando alle mie videolezioni che trovate all’indirizzo Internet www.camuso.it.
Creare un progetto Windows Form (prof. Camuso)

La procedura per creare una applicazione Windows inizia con File/Nuovo Progetto o clic sull’equivalente bottone proprio sotto il menu ‘File’:



Figura 2: La nuova finestra di dialogo New Project




Condividi con i tuoi amici:
  1   2   3


©astratto.info 2017
invia messaggio

    Pagina principale