Classi e oggetti: la programmazione orientata agli oggetti in C#
C# è un linguaggio orientato agli oggetti fino al midollo, e la programmazione orientata agli oggetti è il paradigma attorno a cui ruota gran parte del codice reale. L'idea di fondo è modellare il dominio del problema tramite oggetti, entità che combinano dati e comportamenti. In questo articolo vediamo come definire una classe, quale ruolo hanno campi, proprietà e metodi, come si creano gli oggetti tramite i costruttori e come l'incapsulamento protegga lo stato interno.
Classi e oggetti
Una classe è un modello, un progetto che descrive quali dati e quali comportamenti avranno gli oggetti creati a partire da essa. Un oggetto è un'istanza concreta della classe, con i propri valori. La classe definisce la forma; l'oggetto è l'esemplare reale che vive in memoria.
// Definizione di una classe
class Person
{
// Campo: memorizza lo stato interno
public string Name;
public int Age;
// Metodo: definisce un comportamento
public void Introduce()
{
Console.WriteLine($"Mi chiamo {Name} e ho {Age} anni");
}
}
// Creazione di un oggetto con la parola chiave new
var person = new Person();
person.Name = "Gabriele";
person.Age = 40;
person.Introduce();
La parola chiave new crea una nuova istanza, allocando la memoria necessaria e restituendo un riferimento all'oggetto. Da quel momento si accede ai suoi membri tramite l'operatore punto.
Le proprietà
Esporre direttamente i campi, come nell'esempio precedente, è sconsigliato perché non permette di controllare l'accesso ai dati. La soluzione idiomatica in C# sono le proprietà, che offrono un accesso controllato attraverso gli accessori get e set. Nella forma più semplice, le proprietà auto-implementate generano automaticamente il campo sottostante.
class Product
{
// Proprieta auto-implementate
public string Name { get; set; }
public decimal Price { get; set; }
// Proprieta di sola lettura dall'esterno: set privato
public int Id { get; private set; }
}
Le proprietà permettono di aggiungere logica agli accessori. Ad esempio, si può validare un valore prima di assegnarlo, oppure calcolare un valore derivato senza memorizzarlo. Una proprietà che espone solo get con un'espressione è di sola lettura e calcolata al momento.
class Rectangle
{
public double Width { get; set; }
public double Height { get; set; }
// Proprieta calcolata: nessun campo dedicato
public double Area => Width * Height;
private decimal _price;
public decimal Price
{
get => _price;
set
{
// Validazione prima dell'assegnazione
if (value < 0)
{
throw new ArgumentException("Il prezzo non puo essere negativo");
}
_price = value;
}
}
}
Esiste anche l'accessore init, che consente di assegnare una proprietà solo durante l'inizializzazione dell'oggetto, rendendola immutabile in seguito. È il modo moderno di creare oggetti i cui valori non devono più cambiare una volta costruiti.
I costruttori
Un costruttore è un metodo speciale, con lo stesso nome della classe e senza tipo di ritorno, che viene eseguito automaticamente alla creazione di un oggetto. Serve a inizializzare lo stato dell'oggetto in modo che nasca già valido e coerente.
class Account
{
public string Owner { get; }
public decimal Balance { get; private set; }
// Costruttore: inizializza l'oggetto alla creazione
public Account(string owner, decimal initialBalance)
{
Owner = owner;
Balance = initialBalance;
}
public void Deposit(decimal amount)
{
Balance += amount;
}
}
// L'oggetto nasce gia inizializzato
var account = new Account("Gabriele", 100m);
account.Deposit(50m);
Console.WriteLine(account.Balance); // Stampa 150
Una classe può avere più costruttori con firme diverse, sfruttando l'overloading visto per i metodi. Un costruttore può inoltre richiamarne un altro della stessa classe con la parola chiave this, evitando di duplicare la logica di inizializzazione.
Incapsulamento e modificatori di accesso
L'incapsulamento è il principio secondo cui lo stato interno di un oggetto deve essere protetto e accessibile solo attraverso un'interfaccia controllata. I modificatori di accesso governano la visibilità di ciascun membro:
- public: accessibile da qualsiasi punto del codice.
- private: accessibile solo all'interno della stessa classe. È il livello predefinito per i membri di una classe.
- protected: accessibile dalla classe stessa e dalle classi derivate, argomento del prossimo articolo.
- internal: accessibile solo all'interno dello stesso assembly.
Nell'esempio del conto, il saldo ha un set privato: dall'esterno lo si può leggere ma non modificare arbitrariamente, e l'unico modo per alterarlo è passare per il metodo Deposit, che può applicare i controlli del caso. Questo è l'incapsulamento in azione.
Membri statici
Non tutto appartiene alle singole istanze. Un membro statico appartiene alla classe nel suo complesso, non a un oggetto specifico, ed è condiviso da tutte le istanze. Si usa la parola chiave static e vi si accede tramite il nome della classe.
class Counter
{
// Campo statico: condiviso da tutte le istanze
private static int _instanceCount = 0;
public Counter()
{
_instanceCount++;
}
// Metodo statico: richiamato sulla classe, non sull'oggetto
public static int GetInstanceCount()
{
return _instanceCount;
}
}
new Counter();
new Counter();
Console.WriteLine(Counter.GetInstanceCount()); // Stampa 2
I record
Le versioni recenti di C# introducono i record, un tipo pensato per modellare dati immutabili in modo conciso. Un record genera automaticamente costruttore, confronto basato sui valori e una rappresentazione testuale leggibile, riducendo drasticamente il codice ripetitivo.
// Un record con sintassi posizionale
record Point(int X, int Y);
var p1 = new Point(1, 2);
var p2 = new Point(1, 2);
// I record confrontano per valore, non per riferimento
Console.WriteLine(p1 == p2); // Stampa true
// L'espressione with crea una copia modificata mantenendo l'originale
var p3 = p1 with { X = 10 };
I record sono particolarmente adatti a rappresentare dati che, una volta creati, non devono cambiare, come i modelli trasferiti tra i vari strati di un'applicazione.
Conclusione
Abbiamo posto le fondamenta della programmazione orientata agli oggetti in C#: la distinzione tra classe e oggetto, l'uso delle proprietà per un accesso controllato ai dati, i costruttori per garantire oggetti sempre validi, l'incapsulamento tramite i modificatori di accesso e i membri statici condivisi dalla classe. Infine abbiamo introdotto i record per modellare dati immutabili con poche righe. Nel prossimo e ultimo articolo della serie completeremo il quadro dell'OOP con ereditarietà, interfacce e polimorfismo, i meccanismi che permettono di costruire gerarchie di tipi flessibili ed estensibili.