C# (pronunciato come l’espressione inglese see sharp, vedere nitidamente) è un linguaggio di programmazione sviluppato da Microsoft all’interno del suo stack tecnologico, denominato .NET (dot net).

Si tratta di un linguaggio object-oriented, cioè implementa particolari convenzioni e modalità di scrittura ed organizzazione del codice che lo rendono particolarmente efficace per strutturare programmi che vanno dal mediamente all’altamente complesso. Le caratteristiche e i concetti fondamentali alla base della programmazione orientata agli oggetti verranno trattate più avanti nel corso di questo manuale.

I linguaggi di programmazione si dividono in linguaggi fortemente tipizzati o debolmente tipizzati, in base al loro approccio alla gestione dei tipi di dato di una variabile. Il tipo di dato di una variabile è definito come l’insieme di valori che la variabile può assumere (detto anche dominio) e le operazioni che su tali valori si possono effettuare. Un linguaggio debolmente tipizzato (come il JavaScript) accetta che una variabile contenga ad un certo punto del programma un certo tipo di dato (ad esempio una stringa) e in altre sezioni di codice dei tipi diversi (ad esempio un numero o un valore booleano). Al contrario i linguaggi fortemente tipizzati, come il C#, impongono la coerenza del tipo di dato di una variabile durante tutto il corso dell’esecuzione del programma: in pratica, è necessario che una variabile venga dichiarata sia esplicitandone il nome che il tipo di dato che potrà contenere, perché da quel momento in poi dovrà essere sempre utilizzata con dei dati appartenenti al tipo associato. Questa caratteristica è una piccola limitazione alle possibilità di utilizzo di una variabile da parte dello sviluppatore, ma porta l’enorme beneficio di eliminare tutti gli errori che possono derivare dall’esecuzione di operazioni incompatibili con il valore contenuto nella variabile.

Un’altra caratteristica fondamentale di questo linguaggio è la gestione automatica della memoria. I linguaggi che non gestiscono la memoria in modo autonomo richiedono un grande sforzo al programmatore, che deve occuparsi non solo di richiedere memoria quando necessario, ma anche di liberarla correttamente quando non è più utilizzata. Nel caso in cui quest’ultimo passaggio non venisse eseguito correttamente, si potrebbe andare incontro a gravi errori dovuti al fatto che la memoria utilizzata potrebbe crescere a tal punto da superare quella a disposizione del programma, mandandolo in crash e creando altri problemi al PC a cascata.

La gestione della memoria in C# viene affidata al garbage collector, un processo autonomo, interno al programma, che cerca periodicamente le variabili non più referenziate (utilizzate) e le elimina, liberando così la memoria che occupano.

PhaseInGarbageCollection

Il servizio di garbage collection, insieme a quello di gestione delle eccezioni e a numerose altre utilità per lo sviluppatore, è fornito dal CLR (Common Language Runtime), cioè un componente, formato dall’ambiente di esecuzione (o macchina virtuale) e dall’insieme di librerie standard di .NET, che permette di eseguire in tempo reale quello che il programmatore scrive tramite codice.

In qualsiasi programma, scritto in qualsiasi linguaggio, può capitare di leggere nel terminale (detto anche console o prompt dei comandi) una sequenza di chiamate, solitamente in rosso, che identificano un errore, tipicamente a run-time (avvenuto cioè mentre il programma era in esecuzione). Questi errori sono chiamati eccezioni, e nel caso del C# vengono generati dal CLR. Una delle eccezioni più comuni è la NullReferenceException, che segnala l’errato utilizzo di una variabile non inizializzata, cioè che è stata creata ma non ha ancora assunto: il suo contenuto è indefinito e non utilizzabile in modo sicuro, perciò il CLR ci impedisce di farlo.

Quando scrive del codice in C#, il programmatore utilizza una forma di linguaggio testuale, intelligibile dall’essere umano. Perché l’hardware possa eseguire questo codice, è necessario che venga tradotto in un formato ad esso comprensibile, denominato linguaggio (o codice) macchina. In C#, questa traduzione viene operata in due passaggi:

  •  il primo è affidato al compilatore, un componente del CLR che traduce il codice in un linguaggio intermedio comune (Common Intermediate Language, CIL), ancora di livello più alto rispetto al codice macchina, salvandolo in dei file binari (denominati assembly) insieme a dei metadati fondamentali per l’esecuzione del programma
  • il secondo è invece effettuato da una macchina virtuale e si chiama compilazione just-in-time (JIT), e consiste nell’effettiva traduzione del CIL in codice nativo, adatto quindi essere eseguito dall’hardware attualmente in uso; in pratica, gli assembly binari vengono letti ed interpretati per essere eseguiti correttamente.

Un file binario è un file in cui le informazioni vengono codificate in codice binario, cioè basato sui soli caratteri 0 e 1; ogni carattere viene denominato bit (binary digit, cifra binaria), ed è l’unità elementare dell’informazione che un elaboratore elettronico può gestire. All’interno di un file binario possono essere salvati dati di qualsiasi tipo, che vengono solitamente interpretati in sequenze di byte (1 byte = 8 bit). Se invece è necessario che il programma che deve utilizzare questi file binari segua altre regole per interpretarne il contenuto, possono essere anche aggiunti al suo interno dei metadati con queste direttive.

Il CLR è fondamentale per il funzionamento dei framework di .NET, che sono dei contenitori di funzionalità di alto livello specifiche dei domini ai quali si riferiscono. In pratica, in base al tipo di applicazione che si vorrà creare creare, sarà necessario scegliere il framework che espone delle interfacce software, chiamate API (Application Programming Interface), pronte per essere sfruttate dal programmatore per implementare le funzionalità desiderate.

Tra il CLR e i framework troviamo un altro layer applicativo indispensabile, denominato BCL (Base Class Library). Questo fornisce altre funzionalità di basso livello, come il threading, l’implementazione delle collections (liste di elementi), o il networking.

Quando scarichiamo una delle versioni di .NET dal sito di Microsoft, viene scaricato un pacchetto che contiene i framework e i rispettivi CLR e BCL, sotto forma di librerie che vengono installate nel sistema operativo. Al momento della pubblicazione di un software, il BCL e il CLR relativi al framework che si andrà ad utilizzare durante l’esecuzione vengono inclusi nell’eseguibile, in modo da renderli disponibili anche sulla macchina dell’utente finale. Un processo aggiuntivo che può essere eseguito, piuttosto esigente in termini di tempo e competenze, prevede di tradurre tutte le dipendenze del programma in linguaggio C++ prima dell’effettiva compilazione, per poter aggiungere ulteriori ottimizzazioni sia in termini di dimensione che di performance dell’applicazione.

La prima versione del .NET è stata diffusa nel 2002, e nel corso degli anni ha subito numerose modifiche. Attualmente, i suoi framework più utilizzati sono:

  • .NET Core: framework per applicazioni web, console ed eseguibili su Windows, Linux e macOS
  • UWP (Universal Windows Platform): framework relativo allo sviluppo di applicazioni client su Windows, perfetto per eseguibili che devono funzionare non solo su PC o laptop ma anche su tablet e dispositivi dell’Internet of Things (IoT)
  • Mono e Xamarin: framework per lo sviluppo di applicazioni mobile, eseguibili su Android e iOS
  • Framework .NET: vecchio framework ormai soppiantato da .NET Core

Storicamente, ogni framework di .NET usava una sua versione del BCL e del CLR, e questo impediva la corretta portabilità del codice di un programma su piattaforme diverse. Col passare del tempo, l’esigenza di poter riutilizzare il codice per creare un programma cross-platform si è fatta sempre più pressante, tanto che ha portato all’introduzione del .NET Standard, cioè una specifica formale, definita da un set uniformato di API che devono essere obbligatoriamente supportate dai framework che vogliono aderirvi. In pratica, se al momento dello sviluppo di un programma si utilizza del codice C# compatibile con una certa versione dello standard .NET, si potrà essere certi della presenza delle API relative.

Esaminando più in dettaglio l’argomento dello standard dal punto di vista di Unity, durante il processo di build di un gioco una delle opzioni da valutare con attenzione è la versione dello scripting runtime. La scelta può ricadere su .NET 4.x oppure su .NET Standard 2.0, e la seconda è da preferire perché caratterizzata da API più piccole, che garantiscono quindi una dimensione minore dell’applicazione, e perché è cross-platform, perciò è eseguibile su piattaforme eterogenee. Inoltre, .NET Standard ha il grosso vantaggio di identificare e segnalare numerosi errori durante la fase di compilazione, mentre con .NET 4.x c’è una buona probabilità di trovarli solo durante l’esecuzione del programma, creando problemi visibili dall’utente o, nei casi più gravi, crash dell’applicazione. Quest’ultima funzionalità di .NET Standard viene sfruttata largamente da Unity perché quando si passa dalla finestra di modifica del codice (ad esempio quella di Visual Studio) a quella dell’editor di Unity avviene una compilazione automatica del codice, attraverso il compilatore C#, generando gli assembly.

A riprova di quanto detto finora, le stesse linee guida di Unity consigliano l’utilizzo di .NET Standard 2.0, ma in alcuni casi non è possibile farlo perché, nel caso in cui si utilizzino dei plugin o librerie esterne, non è detto che siano conformi allo standard. È quindi sempre necessario analizzare le dipendenze dei componenti esterni utilizzati nel nostro progetto, per evitare di introdurre vincoli non desiderati, e scegliere in una fase preliminare se accettare il compromesso di dover utilizzare .NET 4.x (sconsigliato) oppure cercare un altro plugin.

Il materiale è tratto dalle lezioni IPID svolte da Marco Pirruccio di Heartwood Labs/Operaludica.