
Dati, variabili e conversioni in C#: le fondamenta di ogni programma
Scritto da Marco Morello il 13 giugno 2025
Ciao a tutti, programmatori e aspiranti tali!
Nel nostro precedente articolo, abbiamo esplorato come dare ordine e struttura al nostro codice C# organizzando metodi e classi. È stato un po' come imparare a sistemare gli attrezzi nella nostra cassetta personale: tutto diventa più facile da trovare e da usare. Ma quali sono questi "attrezzi" fondamentali che maneggiamo ogni giorno quando scriviamo codice? Oggi scendiamo ancora più nel dettaglio, toccando il cuore pulsante di ogni applicazione: i tipi di dati, le variabili e le conversioni tra di essi.
📋 In questo articolo
Pensateci un attimo: ogni software, dal più semplice giochino sul telefono alla più complessa piattaforma enterprise, non fa altro che manipolare dati. Numeri, testi, date, scelte logiche... sono il pane quotidiano di noi sviluppatori. Capire come C# gestisce queste informazioni è come per un cuoco conoscere a menadito i propri ingredienti: è la base per creare piatti (o meglio, programmi) eccezionali!
L'obiettivo di questo articolo? Fornirvi una guida chiara e pratica, senza paroloni da accademico ma con la concretezza che serve nel lavoro di tutti i giorni. E chissà, magari questi concetti, se ben assimilati, vi aiuteranno a fare bella figura al prossimo colloquio o a sentirvi più sicuri nel vostro percorso di crescita. Pronti a mettere le mani in pasta (o meglio, nel codice)?
🗃️ Variabili: i contenitori dei nostri dati
Immaginate di dover organizzare la vostra dispensa. Probabilmente usereste dei barattoli o delle scatole, ognuna con un'etichetta: "Zucchero", "Farina", "Caffè". Le variabili in programmazione sono esattamente questo: dei contenitori nominati che riservano uno spazio in memoria per conservare un dato specifico.
Quando "dichiariamo" una variabile in C#, stiamo dicendo al computer: "Ehi, prepara uno spazio per conservare un certo tipo di informazione, e chiamalo con questo nome".
Ecco un esempio semplicissimo:
// ✅ ESEMPIO PRATICO: Dichiarazione e inizializzazione variabili
int etaPersona = 30;
string nomeCompleto = "Mario Rossi";
// Usando string interpolation (sintassi moderna)
Console.WriteLine($"{nomeCompleto} ha {etaPersona} anni.");
// Output: Mario Rossi ha 30 anni.
Semplice, no? etaPersona
è la nostra etichetta (il nome della variabile), e al suo interno conserviamo il numero 30. nomeCompleto
conserva la stringa di testo "Mario Rossi".
Un cenno alla parola chiave var
Forse avrete visto in alcuni esempi di codice C# l'uso della parola chiave var
per dichiarare le variabili, così:
// ✅ ESEMPIO: Inferenza di tipo con var
var miaEta = 35; // Il compilatore capisce che miaEta è di tipo int
var mioNome = "Luigi"; // Il compilatore capisce che mioNome è di tipo string
var oggi = DateTime.Now; // Tipo DateTime inferito automaticamente
💡 TIP: Quando usate
var
, il compilatore C# "inferisce" (capisce automaticamente) il tipo della variabile basandosi sul valore assegnato.var
non crea variabili dinamiche: una volta determinato il tipo, la variabile rimane di quel tipo per sempre!
Usatela con giudizio: può rendere il codice più conciso, ma a volte specificare esplicitamente il tipo (es. int eta = 35;
) può migliorare la leggibilità, specialmente per chi legge il codice per la prima volta o con tipi complessi.
🔢 Tipi di dati: l'identità degli ingredienti
Tornando all'esempio della dispensa: non useremmo lo stesso tipo di contenitore per la farina (che è una polvere) e per l'olio (che è un liquido). Allo stesso modo, in C#, ogni variabile deve avere un tipo di dato ben preciso. Il tipo di dato dice al computer che genere di informazione può contenere quella variabile e quanta memoria deve riservarle.
C# offre una ricca gamma di tipi di dati incorporati. Vediamo i più comuni, dividendoli in due grandi famiglie:
Tipi valore (Value Types)
I tipi valore contengono direttamente il loro dato. Pensateli come quei barattoli che contengono fisicamente lo zucchero o il caffè. Quando assegnate un tipo valore a un'altra variabile, il valore viene copiato.
🔢 Numeri interi: per i numeri senza la virgola
Ma non tutti i numeri sono interi. Per gestire valori con la virgola, C# ci offre un'altra famiglia di tipi, ognuno con un livello di precisione specifico per ogni esigenza.
// ✅ ESEMPI PRATICI: Tipi interi in scenari reali
int numeroStudenti = 25; // Contatori, quantità
long popolazioneMondiale = 8000000000L; // Numeri molto grandi
short numeroPianiEdificio = 10; // Valori limitati (risparmia memoria)
byte etaMassimaCane = 20; // Valori 0-255
🔸 Numeri in virgola mobile: per i numeri con la virgola
// ✅ ESEMPI PRATICI: Quando usare float, double, decimal
float velocitaMedia = 75.5f; // Grafica, quando serve meno precisione
double distanzaTerraLunaKm = 384400.56; // Calcoli scientifici generali
decimal prezzoArticolo = 19.99m; // 💰 SEMPRE per calcoli monetari!
⚠️ ATTENZIONE: Mai usare
float
odouble
per calcoli monetari! Gli errori di arrotondamento binario possono causare differenze nei centesimi. Usa sempredecimal
per soldi!
✅ Booleani e caratteri
Oltre a numeri e flag, C# gestisce ovviamente anche i singoli caratteri. Ma cosa succede se un valore non è semplicemente vero o falso, o se non ha ancora un valore assegnato? Per questi casi, C# introduce un concetto tanto semplice quanto potente.
// ✅ ESEMPI PRATICI: Bool e char in azione
bool utenteLoggato = true;
bool lampadinaAccesa = false;
char inizialeNome = 'L';
char simboloEuro = '€';
❓ Tipi valore nullable
A volte, per i tipi valore, abbiamo bisogno di rappresentare l'assenza di un valore (qualcosa di diverso da zero per i numeri o false per i booleani). Per questo esistono i tipi valore nullable. Si dichiarano aggiungendo un punto interrogativo (?
) dopo il tipo.
// ✅ ESEMPIO PRATICO: Nullable types in azione
int? punteggioPartita = null; // Il punteggio potrebbe non essere ancora registrato
bool? preferenzaUtente = null; // L'utente potrebbe non aver ancora scelto
// Controllo e utilizzo sicuro
int? quantitaOpzionale = null;
if (quantitaOpzionale.HasValue)
{
Console.WriteLine($"Quantità: {quantitaOpzionale.Value}");
}
else
{
Console.WriteLine("Quantità non specificata.");
}
Tipi riferimento (Reference Types)
A differenza dei tipi valore, che sono come scatole con dentro il loro contenuto, i tipi riferimento funzionano più come un indirizzo che punta a dove si trova il contenuto.
I tipi riferimento non contengono direttamente il dato, ma un "riferimento" (o indirizzo) alla locazione di memoria dove il dato risiede effettivamente. Immaginate di avere un bigliettino con su scritto "Zucchero - Scaffale 3, Ripiano A". Il bigliettino è il riferimento, lo zucchero è il dato.
📝 Stringhe
// ✅ ESEMPI PRATICI: Stringhe in scenari reali
string saluto = "Ciao mondo!";
string indirizzoEmail = "utente@esempio.com";
string pathFile = @"C:\Documents\file.txt"; // Stringa verbatim (utile per path)
💡 TIP: Le stringhe in C# sono immutabili: una volta create, il loro contenuto non può essere modificato. Ogni operazione che sembra modificare una stringa, in realtà ne crea una nuova in memoria.
📦 Oggetti (Object)
// ✅ ESEMPIO: object come contenitore universale
object datiMisti = 42; // Può contenere qualsiasi tipo
datiMisti = "Ora sono una stringa";
datiMisti = DateTime.Now; // Ora sono una data
⚠️ ATTENZIONE: Usa
object
con cautela! Si perde la type safety e servono conversioni esplicite (unboxing) per riutilizzare il valore nel suo tipo originale.
Visto il rischio di errori, affidarsi sempre al casting esplicito non è la strategia più robusta. Per fortuna, C# ci mette a disposizione una cassetta degli attrezzi molto più ricca e sicura per gestire le conversioni, specialmente quando si ha a che fare con input esterni come le stringhe.
📊 Tabella di riferimento rapido
Tipo | Range | Uso Consigliato | Memoria | Esempio |
---|---|---|---|---|
byte |
0-255 | Dati binari, età bambini | 1 byte | byte eta = 8; |
int |
±2.1 miliardi | Contatori, ID, quantità | 4 byte | int studenti = 150; |
long |
±9.2 quintilioni | Timestamp, numeri enormi | 8 byte | long ms = 1640995200000L; |
float |
±3.4×10³&sup8; | Grafica, precisione limitata | 4 byte | float speed = 60.5f; |
double |
±1.7×10³&sup0;&sup8; | Calcoli scientifici | 8 byte | double pi = 3.1415...; |
decimal |
±7.9×10²&sup8; | Calcoli monetari | 16 byte | decimal price = 29.99m; |
bool |
true/false | Flag, condizioni | 1 byte | bool isActive = true; |
char |
Carattere Unicode | Singoli caratteri | 2 byte | char grade = 'A'; |
string |
Testo vario | Testi, nomi, descrizioni | Variabile | string name = "Mario"; |
🧠 Un cenno alla memoria: stack vs heap
Senza entrare troppo nel tecnico, è utile sapere che:
- stack= I tipi valore vengono allocati qui. Area molto efficiente, gestita "Last-In, First-Out" (LIFO)
- heap= I tipi riferimento hanno il loro dato qui, mentre sullo Stack c'è solo il "riferimento" (indirizzo)
Questa distinzione ha implicazioni su performance e gestione della memoria, ma per ora teniamola come una curiosità che potremo approfondire in futuro.
🔄 Conversioni di tipo: quando i dati cambiano pelle
Capita spesso di dover "trasformare" un dato da un tipo a un altro. Magari abbiamo l'età di un utente come numero intero, ma dobbiamo mostrarla in un messaggio di testo. Oppure riceviamo un prezzo come stringa da un input utente e dobbiamo convertirlo in un decimal per farci dei calcoli.
1. Conversioni implicite (Sicure) ✅
Avvengono automaticamente quando si converte un valore da un tipo "più piccolo" a uno "più grande", senza rischio di perdita di dati.
// ✅ ESEMPI: Conversioni sicure automatiche
int mioIntero = 100;
long mioLungo = mioIntero; // int → long (sicura)
float mioFloat = mioIntero; // int → float (sicura)
double mioDouble = mioFloat; // float → double (sicura)
Console.WriteLine($"Lungo: {mioLungo}"); // Output: 100
Console.WriteLine($"Double: {mioDouble}"); // Output: 100
2. Conversioni esplicite (Casting) ⚠️
Quando la conversione potrebbe comportare perdita di dati, dobbiamo essere espliciti usando l'operatore di cast: (nuovoTipo)variabile
.
// ⚠️ ESEMPI: Conversioni che possono perdere dati
double mioDouble = 123.789;
int mioIntero = (int)mioDouble; // Perdiamo .789!
Console.WriteLine($"Intero da double: {mioIntero}"); // Output: 123
// PERICOLO: Overflow
long numeroGrande = 3000000000L; // 3 miliardi
// int numeroPiccolo = (int)numeroGrande; // OverflowException o valore sbagliato!
⚠️ PERICOLO: Le conversioni esplicite possono causare
OverflowException
o valori incorretti se il numero è troppo grande per il tipo di destinazione!
3. Metodi di conversione sicuri 🛡️
Classe Convert
// ✅ ESEMPIO PRATICO: Conversioni sicure con Convert
string numeroComeStringa = "42";
int numeroConvertito = Convert.ToInt32(numeroComeStringa);
Console.WriteLine($"Convertito: {numeroConvertito}"); // Output: 42
double valoreDecimale = 99.99;
string stringaDaDouble = Convert.ToString(valoreDecimale);
Console.WriteLine($"Stringa: {stringaDaDouble}"); // Output: "99.99"
bool valore = true;
string stringaDaBool = Convert.ToString(valore);
Console.WriteLine($"Bool come stringa: {stringaDaBool}"); // Output: "True"
Metodo ToString() Universale
// ✅ ESEMPIO: ToString() ovunque
int eta = 30;
DateTime oggi = DateTime.Now;
string etaStringa = eta.ToString();
string dataStringa = oggi.ToString("dd/MM/yyyy");
Console.WriteLine($"Età: {etaStringa}");
Console.WriteLine($"Data: {dataStringa}");
Parse() vs TryParse() - La battaglia per la sicurezza
// ✅ SCENARIO REALE: Validazione input utente
Console.WriteLine("Inserisci il prezzo del prodotto:");
string inputPrezzo = Console.ReadLine();
// Metodo 1: Parse (rischioso!)
try
{
decimal prezzo = decimal.Parse(inputPrezzo);
decimal iva = prezzo * 0.22m;
Console.WriteLine($"Prezzo finale con IVA: {prezzo + iva:C}");
}
catch (FormatException)
{
Console.WriteLine("❌ Formato prezzo non valido!");
}
// Metodo 2: TryParse (RACCOMANDATO!)
if (decimal.TryParse(inputPrezzo, out decimal prezzoSicuro) && prezzoSicuro > 0)
{
decimal iva = prezzoSicuro * 0.22m;
Console.WriteLine($"✅ Prezzo finale con IVA: {prezzoSicuro + iva:C}");
}
else
{
Console.WriteLine("❌ Inserisci un prezzo valido e positivo!");
}
💡 BEST PRACTICE: Usa sempre
TryParse()
invece diParse()
quando tratti input che potrebbero non essere nel formato corretto (utenti, file, API esterne). È più sicuro e professionale!
⚠️ Errori comuni da evitare
Conoscere la teoria è un conto, ma evitare le trappole pratiche è ciò che distingue uno sviluppatore affidabile. Vediamo tre degli errori più comuni in cui ti imbatterai quasi certamente e come gestirli con eleganza.
1. Overflow silenzioso
// ❌ SBAGLIATO - Può causare overflow silenzioso
int result = int.MaxValue + 1; // Diventa int.MinValue! (-2147483648)
Console.WriteLine($"Risultato: {result}"); // Output inaspettato!
// ✅ CORRETTO - Controllo esplicito
try
{
checked
{
int result = int.MaxValue + 1; // Lancia OverflowException
}
}
catch (OverflowException)
{
Console.WriteLine("❌ Overflow rilevato!");
}
L'uso del blocco checked
è una buona pratica di programmazione difensiva, che rende il codice più prevedibile e sicuro.
Un altro errore comune, altrettanto insidioso, riguarda la precisione matematica.
2. Arrotondamenti con Float/Double in calcoli monetari
// ❌ SBAGLIATO - Errori di arrotondamento!
double prezzo1 = 0.1;
double prezzo2 = 0.2;
double totale = prezzo1 + prezzo2;
Console.WriteLine($"Totale double: {totale}"); // Output: 0.30000000000000004
// ✅ CORRETTO - Usa decimal per i soldi!
decimal prezzoDecimal1 = 0.1m;
decimal prezzoDecimal2 = 0.2m;
decimal totaleDecimal = prezzoDecimal1 + prezzoDecimal2;
Console.WriteLine($"Totale decimal: {totaleDecimal}"); // Output: 0.3
Questo semplice esempio dimostra perché la scelta del tipo di dato corretto non è un dettaglio, ma una decisione fondamentale per la correttezza del software. Infine, affrontiamo un errore che ogni sviluppatore incontra prima o poi: il temuto riferimento nullo.
3. Null Reference con stringhe
// ❌ SBAGLIATO - Può causare NullReferenceException
string nome = null;
// int lunghezza = nome.Length; // 💥 NullReferenceException
// ✅ CORRETTO - Controllo null
string nome = null;
int lunghezza = nome?.Length ?? 0; // Operatore null-conditional
Console.WriteLine($"Lunghezza: {lunghezza}"); // Output: 0
🏃♂️ Suggerimenti per le performance
Oltre alla correttezza, scrivere codice efficiente è un'abilità fondamentale. Senza cadere nell'ottimizzazione prematura, ecco alcuni accorgimenti pratici che possono migliorare le performance delle tue applicazioni nei punti critici.
1. String vs StringBuilder
// ❌ INEFFICIENTE - Crea molte stringhe temporanee
string risultato = "";
for (int i = 0; i < 1000; i++)
{
risultato += i.ToString() + ", ";
}
// ✅ EFFICIENTE - Usa StringBuilder per concatenazioni multiple
var sb = new StringBuilder();
for (int i = 0; i < 1000; i++)
{
sb.Append(i).Append(", ");
}
string risultato = sb.ToString();
La regola è semplice: per poche concatenazioni, una stringa normale va bene. Per cicli o un numero elevato di unioni, StringBuilder
è quasi sempre la scelta giusta per non sprecare memoria e CPU.
L'ottimizzazione della memoria è cruciale anche in un altro ambito: la scelta tra classi e tipi più "leggeri".
2. Struct vs Class per tipi piccoli
// ✅ ESEMPIO: Struct per dati immutabili piccoli
public struct Punto
{
public int X { get; }
public int Y { get; }
public Punto(int x, int y)
{
X = x;
Y = y;
}
}
// Più efficiente per collezioni di punti
var punti = new Punto[1000];
3. Nullable Types - overhead minimo
// ✅ I nullable types hanno overhead minimo, usali quando necessario
int? valoreOpzionale = null;
DateTime? dataOpzionale = null;
// Pattern moderno con null-coalescing
int valore = valoreOpzionale ?? 0;
DateTime data = dataOpzionale ?? DateTime.Now;
🎯 Metti alla prova le tue conoscenze
Esercizio 1: qual è l'output?
double d = 3.7;
int i = (int)d;
Console.WriteLine(i); // ?
🔍 Soluzione
Output: 3
Il cast esplicito (int)d
tronca la parte decimale, non arrotonda. Quindi 3.7 diventa 3.
Esercizio 2: conversione sicura
Come convertiresti in modo sicuro "123.45" in decimal?
🔍 Soluzione
string input = "123.45";
if (decimal.TryParse(input, out decimal result))
{
Console.WriteLine($"Conversione riuscita: {result}");
}
else
{
Console.WriteLine("Conversione fallita");
}
Esercizio 3: scenario reale
Scrivi un programma che chieda all'utente di inserire la sua età e calcoli in quanti giorni compirà 100 anni.
🔍 Soluzione
Console.WriteLine("Inserisci la tua età:");
string input = Console.ReadLine();
if (int.TryParse(input, out int eta) && eta >= 0 && eta <= 120)
{
int anniRimasti = 100 - eta;
if (anniRimasti <= 0)
{
Console.WriteLine("Hai già superato i 100 anni! 🎉");
}
else
{
int giorniRimasti = anniRimasti * 365; // Semplificazione
Console.WriteLine($"Ti mancano circa {giorniRimasti} giorni per i 100 anni!");
}
}
else
{
Console.WriteLine("❌ Inserisci un'età valida (0-120)");
}
💡 Consigli pratici
🎯 Scelta del tipo giusto
- Contatori, ID, quantità normali:
int
- Calcoli monetari:
decimal
(SEMPRE!) - Calcoli scientifici generali:
double
- Numeri enormi (timestamp, popolazione):
long
- Flag, stati on/off:
bool
- Testi:
string
📝 Nomi parlanti per le variabili
// ❌ CATTIVI nomi
int x = 25;
string s = "Mario";
bool b = true;
// ✅ BUONI nomi
int numeroStudenti = 25;
string nomeCliente = "Mario";
bool isUtenteAttivo = true;
decimal prezzoUnitarioEuro = 15.99m;
🛡️ Gestione sicura delle conversioni
// ✅ PATTERN SICURO per input utente
public static bool LeggiEtaUtente(out int eta)
{
Console.WriteLine("Inserisci la tua età:");
string input = Console.ReadLine();
return int.TryParse(input, out eta) && eta >= 0 && eta <= 150;
}
// Uso
if (LeggiEtaUtente(out int eta))
{
Console.WriteLine($"✅ Età valida: {eta} anni");
}
else
{
Console.WriteLine("❌ Età non valida");
}
🐛 Debug & troubleshooting
Problema: "System.FormatException"
Causa: Conversione string → numero non valida
Soluzione: Usa TryParse()
invece di Parse()
// ❌ Può crashare
int numero = int.Parse(input);
// ✅ Gestisce l'errore
if (int.TryParse(input, out int numero))
{
// Conversione riuscita
}
Problema: "System.OverflowException"
Causa: Numero troppo grande per il tipo di destinazione
Soluzione: Usa tipi più grandi o controlli preventivi
// ✅ Controllo preventivo
if (numeroLong <= int.MaxValue && numeroLong >= int.MinValue)
{
int numeroInt = (int)numeroLong;
}
Problema: errori di arrotondamento nei calcoli
Causa: Uso di float
/double
per calcoli precisi
Soluzione: Usa decimal
per calcoli monetari/finanziari
// ❌ Impreciso
double prezzo = 0.1 + 0.2; // 0.30000000000000004
// ✅ Preciso
decimal prezzo = 0.1m + 0.2m; // 0.3
🎬 Conclusione: le basi per costruire alla grande
Abbiamo fatto un bel tuffo nei fondamentali di C#: variabili che custodiscono i nostri dati, tipi che ne definiscono la natura, e conversioni che permettono loro di interagire e trasformarsi. Potrebbe sembrare teoria di base, ma padroneggiare questi concetti è come avere fondamenta solide per un grattacielo: senza, tutto il resto rischia di crollare o di essere instabile.
"Qualsiasi sciocco può scrivere codice che un computer può capire. I bravi programmatori scrivono codice che gli umani possono capire."
Questa citazione di Martin Fowler, una figura di spicco nel mondo dell'ingegneria del software, ci ricorda che la chiarezza è fondamentale. E la chiarezza inizia proprio dalla scelta oculata dei tipi di dati, dei nomi delle variabili e da una gestione attenta delle loro trasformazioni.
Spero che questo articolo vi abbia chiarito le idee o vi abbia offerto qualche spunto di riflessione. La programmazione è un viaggio continuo di apprendimento, e anche per me scrivere questi articoli è un modo per consolidare e approfondire concetti che a volte diamo per scontati.
💬 E voi cosa ne pensate?
- Avete esempi particolari di conversioni "pericolose" che vi sono capitati?
- Magari trucchi del mestiere sulla scelta dei tipi di dato che volete condividere?
- Quali errori comuni avete incontrato nei vostri progetti?
Rimanete sintonizzati, perché il nostro viaggio nel mondo di C# è appena iniziato! Nel prossimo articolo faremo un passo fondamentale: vedremo come dare vita ai nostri oggetti nel modo corretto attraverso i costruttori, esplorando l'inizializzazione e l'overloading. 🚀
Articolo originale pubblicato su Il Viaggio del Programmatore