Nella scorsa lezione abbiamo visto come creare, modificare, compilare ed eseguire un progetto C#. Ora proseguiremo il nostro percorso analizzando la sintassi e i costrutti principali del linguaggio, sempre prendendo d’esempio il progetto che abbiamo creato in precedenza.
Uno dei primi elementi che troviamo nel nostro file è un namespace (parola chiave namespace) chiamato “dotnet esempi”: si tratta di un oggetto fornito dal C# al programmatore per raggruppare elementi di codice sotto uno stesso insieme, identificandolo con un nome. Raggruppare funzionalità è fondamentale sia per mantenere ordine all’interno del progetto, sia per permettere di far accedere solo alcuni oggetti alle funzionalità racchiuse in un namespace. Ad esempio, se il nostro gioco avesse cinque classi che rappresentano cinque diversi tipi di armi del player (una classe pistola, una classe fucile, una classe mazza…) sarebbe opportuno metterle sotto lo stesso namespace: in questo modo, il programmatore avrebbe la possibilità di far utilizzare questo insieme solamente alla classe del player, andando a bloccare l’accesso a tutte le altre classi (se si provasse ad accedere ad un’arma via codice tramite una classe che non è il player il compilatore genererebbe infatti un errore).
Alla luce di quanto detto sui namespace, una best practice da seguire quando si struttura un progetto è quella di definire un primo namespace denominato come il progetto, utilizzando la notazione detta CamelCase, cioè scrivendo tutte le parole che compongono il nome senza spazi, e ciascuna con l’iniziale maiuscola.
Subito dopo il namespace, troviamo la dichiarazione e l’implementazione di una classe denominata Program (la identifichiamo grazie alla parola chiave class che la precede) che, a sua volta, contiene una funzione (o metodo) chiamata Main. Questa funzione è il punto di accesso dell’eseguibile, cioè il codice che viene eseguito dal .NET all’avvio, ed è il punto in cui il programmatore può andare ad inserire tutto il resto del codice necessario per l’implementazione delle funzionalità desiderate. All’interno della funzione possiamo osservare un’istruzione (cioè una riga di codice) che recita Console.WriteLine(“Hello World”): analizzandone le parole chiave, troviamo Console che si riferisce alla console in cui stiamo lavorando, e WriteLine che indica al programma di scrivere ciò che trova all’interno delle parentesi tonde e poi andare a capo (se avessimo usato l’istruzione Write, ad esempio, non saremmo andati a capo). Se proviamo a sostituire la scritta tra virgolette con qualcosa a nostro piacere, ad esempio scrivendo “Ciao”, ci basterebbe salvare (usando la scorciatoia da tastiera CTRL + S), ricompilare ed eseguire il programma (possiamo usare i tasti della tastiera freccia su ↑ e freccia giù ↓ per navigare tra gli ultimi comandi inseriti nella console, e Invio per eseguirli) per vedere che ne abbiamo modificato il funzionamento: adesso sulla console l’output non reciterà più Hello World ma Ciao.
Durante la scrittura di codice può capitare di compiere un errore di battitura, di dimenticare di inserire qualche simbolo necessario o di scrivere qualche carattere non riconosciuto dalla sintassi del C#. Fortunatamente, se ad esempio al posto che scrivere Console.WriteLine(“Ciao”) scrivessimo Console.WriteLine(“Ciao”, dimenticando quindi la chiusura della parentesi tonda, .NET ci avviserebbe con un popup vicino alla stringa che contiene l’errore. Se poi il programmatore non si accorgesse dell’errore e procedesse con un nuovo comando di build, la compilazione non andrebbe a buon fine e verrebbe generato un output di errore più preciso, riportante non solo la riga contenente l’errore ma anche una descrizione più dettagliata (in questo caso, ci avviserebbe del fatto che la nostra funzione non risulta chiusa parentesi tonda, con la frase “error CS1026: ) expected”).
Questo discorso è valido solo per gli errori sintattici, nei quali c’è un errore o di scrittura di qualche parola chiave oppure della sequenza nella quale vengono inseriti i simboli. Se invece l’errore è concettuale, cioè il programmatore non si accorge di aver inserito un errore logico nella sequenza di istruzioni, il compilatore non riuscirà ad individuarlo, e perciò porterà a compimento il processo di compilazione, lasciando l’errore all’interno del programma e generando così quello che viene chiamato bug.
Proviamo ora a scrivere un po’ di codice nel nostro programma, per vedere come possiamo implementare nuove funzionalità. Andiamo a collocarci nella riga tra static void Main (string [ ] args) e Console.Writeline(“Ciao!”); e scriviamo int i = 0: abbiamo appena effettuato la dichiarazione di una variabile, di tipo intero (il cui concetto verrà approfondito più avanti), di nome i ed inizializzata con il valore 0, perciò possiamo avere la certezza che sarà a nostra disposizione durante l’esecuzione del programma. Ad esempio, possiamo stampare a schermo il valore contenuto in questa variabile, modificando la riga Console.Writeline(“Ciao!”) in Console.Writeline(“Ciao! ” + i): compilando ed eseguendo il programma otterremo un output su console che recita Ciao! 0. L’operatore + che abbiamo usato viene interpretato dal C# come l’operazione di concatenamento di stringhe (cioè insiemi di caratteri), e può essere utilizzato anche con le variabili di tipo int perché queste ultime possono essere convertite automaticamente in caratteri.
Se vogliamo adesso modificare il valore contenuto all’interno della variabile i, possiamo procedere come per la dichiarazione effettuata precedentemente, ma senza indicare il tipo di variabile: scrivendo infatti solamente i = 10; effettueremo un’operazione di assegnamento di un valore ad una variabile. Vogliamo essere sicuri di aver effettivamente modificato il valore contenuto in i? Proviamo a scrivere su una nuova riga un’altra istruzione Console.Writeline(i); e compiliamo ed eseguiamo il programma: l’output reciterà ora:
Possiamo continuare in questo modo per inserire nel nostro programma tutte le variabili di cui abbiamo bisogno: proviamo ora con una di tipo double e scriviamo:
Il risultato è quello che già conosciamo, ma inizia a sorgere un problema: il nostro codice diventa via via più corposo e, leggendolo, possiamo notare che presenta una serie disordinata di operazioni, in cui dichiarazioni e assegnamenti si mischiano a istruzioni di stampa a schermo. Se continuassimo in questo modo, già dopo una decina di righe il nostro codice potrebbe diventare illeggibile, soprattutto se a doverlo analizzare fosse una persona diversa da chi lo ha scritto. È questo il motivo per cui si rende necessario raggruppare le funzionalità in quelle che vengono chiamate funzioni (o metodi), cioè segmenti di codice raggruppati sotto un certo nome (quello della funzione stessa) richiamabili al bisogno tramite quest’ultimo. Non solo il nostro codice sarà più ordinato e leggibile, ma eviteremo anche fastidiose ripetizioni e copia/incolla che renderebbero difficile una successiva manutenzione del progetto.
Proviamo a scrivere, ad esempio, un algoritmo che calcola il quadrato di un numero intero, ma questa volta lo facciamo all’interno di una funzione chiamata Quadrato che possa essere richiamata in seguito: ci posizioniamo fuori dal Main (cioè all’esterno della sua ultima parentesi graffa) ma sempre all’interno della classe Program e scriviamo:
Il codice appena inserito crea un metodo statico (vedremo più avanti cosa implica l’utilizzo della parola chiave static), chiamato Quadrato, che accetta un parametro di ingresso di tipo intero (int x tra parentesi tonde) e restituisce una variabile in uscita sempre di tipo intero (parola chiave int prima del nome della funzione, e istruzione di return all’interno del corpo della funzione stessa). Per utilizzare la nostra nuova funzione, dovremo quindi tornare nel nostro Main, dichiarare una variabile di tipo intero e assegnarle il risultato della funzione Quadrato a cui dovremo dare in ingresso un’altra variabile di tipo intero (in questo caso passeremo la variabile i creata in precedenza): tutto questo potrà essere eseguito in un’unica istruzione, cioè ci basterà scrivere:
come ultima riga del Main per poter osservare che, se stampassimo successivamente a schermo la nostra variabile q, questa conterrà effettivamente il quadrato della variabile i (in questo caso, stamperemmo a schermo 100).