Tipi e membri C#
Essendo un linguaggio orientato a oggetti, C# supporta i concetti di incapsulamento, ereditarietà e polimorfismo. Una classe può ereditare direttamente da una classe padre e può implementare un numero qualsiasi di interfacce. Per evitare una ridefinizione accidentale, i metodi che eseguono l'override di metodi virtuali in una classe padre richiedono la parola chiave override
. In C#, uno struct è simile a una classe leggera; è un tipo allocato da stack che può implementare le interfacce, ma non supporta l'ereditarietà. C# fornisce record class
tipi e record struct
, che sono tipi il cui scopo consiste principalmente nell'archiviare i valori dei dati.
Tutti i tipi vengono inizializzati tramite un costruttore, un metodo responsabile dell'inizializzazione di un'istanza. Due dichiarazioni di costruttore hanno un comportamento univoco:
- Costruttore senza parametri, che inizializza tutti i campi nel valore predefinito.
- Costruttore primario, che dichiara i parametri obbligatori per un'istanza di tale tipo.
Classi e oggetti
Le classi sono le più fondamentali dei tipi C#. Una classe è una struttura di dati che combina in una singola unità lo stato (campi) e le azioni (metodi e altri membri di funzione). Una classe fornisce una definizione per le istanze della classe , note anche come oggetti . Le classi supportano l'ereditarietà e il polimorfismo, meccanismi in base ai quali le classi derivate possono estendere e specializzare le classi di base.
Le nuove classi vengono create tramite dichiarazioni di classe. Una dichiarazione di classe inizia con un'intestazione. L'intestazione specifica:
- Attributi e modificatori della classe
- Nome della classe
- Classe base (quando eredita da una classe di base)
- Interfacce implementate dalla classe .
L'intestazione è seguita dal corpo della classe, costituito da un elenco di dichiarazioni di membro scritte tra i delimitatori {
e }
.
Il codice seguente mostra una dichiarazione di una classe semplice denominata Point
:
public class Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) => (X, Y) = (x, y);
}
Le istanze delle classi vengono create usando l'operatore new
, che alloca memoria per una nuova istanza, richiama un costruttore per inizializzare l'istanza e restituisce un riferimento all'istanza. Le istruzioni seguenti creano due Point
oggetti e archiviano i riferimenti a tali oggetti in due variabili:
var p1 = new Point(0, 0);
var p2 = new Point(10, 20);
La memoria occupata da un oggetto viene automaticamente recuperata nel momento in cui l'oggetto non è più raggiungibile. Non è necessario o possibile deallocare in modo esplicito gli oggetti in C#.
var p1 = new Point(0, 0);
var p2 = new Point(10, 20);
Le applicazioni o i test per gli algoritmi potrebbero dover creare più Point
oggetti. La classe seguente genera una sequenza di punti casuali. Il numero di punti viene impostato dal parametro del costruttore primario . Il parametro numPoints
del costruttore primario è nell'ambito di tutti i membri della classe :
public class PointFactory(int numberOfPoints)
{
public IEnumerable<Point> CreatePoints()
{
var generator = new Random();
for (int i = 0; i < numberOfPoints; i++)
{
yield return new Point(generator.Next(), generator.Next());
}
}
}
È possibile usare la classe come illustrato nel codice seguente:
var factory = new PointFactory(10);
foreach (var point in factory.CreatePoints())
{
Console.WriteLine($"({point.X}, {point.Y})");
}
Parametri di tipo
Le classi generiche definiscono i parametri di tipo. I parametri di tipo sono un elenco di nomi di parametri di tipo racchiusi tra parentesi angolari. I parametri di tipo seguono il nome della classe. I parametri di tipo possono essere quindi usati nel corpo delle dichiarazioni di classe per definire i membri della classe. Nell'esempio seguente i parametri di tipo di Pair
sono TFirst
e TSecond
:
public class Pair<TFirst, TSecond>
{
public TFirst First { get; }
public TSecond Second { get; }
public Pair(TFirst first, TSecond second) =>
(First, Second) = (first, second);
}
Un tipo di classe dichiarato per accettare parametri di tipo prende il nome di tipo di classe generico. Anche i tipi struct, interfaccia e delegato possono essere generici. Quando si usa la classe generica, è necessario specificare argomenti di tipo per ogni parametro di tipo:
var pair = new Pair<int, string>(1, "two");
int i = pair.First; //TFirst int
string s = pair.Second; //TSecond string
Un tipo generico per il quale sono stati specificati argomenti di tipo, come Pair<int,string>
nell'esempio precedente, prende il nome di tipo costruito.
Classi di base
Una dichiarazione di classe può specificare una classe di base. Seguire il nome della classe e i parametri di tipo con due punti e il nome della classe di base. L'omissione della specifica della classe di base equivale alla derivazione dal tipo object
. Nell'esempio seguente la classe base di Point3D
è Point
. Dal primo esempio, la classe di base di Point
è object
:
public class Point3D : Point
{
public int Z { get; set; }
public Point3D(int x, int y, int z) : base(x, y)
{
Z = z;
}
}
Una classe eredita i membri della relativa classe di base. L'ereditarietà indica che una classe contiene in modo implicito quasi tutti i membri della relativa classe di base. Una classe non eredita l'istanza e i costruttori statici e il finalizzatore. Una classe derivata può aggiungere nuovi membri a tali membri che eredita, ma non può rimuovere la definizione di un membro ereditato. Nell'esempio precedente eredita Point3D
i X
membri e Y
da Point
e ogni Point3D
istanza contiene tre proprietà, X
, Y
e Z
.
Un tipo di classe viene implicitamente convertito in uno dei relativi tipi di classe di base. Una variabile di un tipo di classe può fare riferimento a un'istanza di tale classe o a un'istanza di qualsiasi classe derivata. Nel caso delle dichiarazioni di classe precedenti, ad esempio, una variabile di tipo Point
può fare riferimento a Point
o Point3D
:
Point a = new(10, 20);
Point b = new Point3D(10, 20, 30);
Struct
Le classi definiscono tipi che supportano l'ereditarietà e il polimorfismo. Consentono di creare comportamenti sofisticati in base alle gerarchie delle classi derivate. Al contrario, i tipi di struct sono tipi più semplici il cui scopo principale è archiviare i valori dei dati. Gli struct non possono dichiarare un tipo di base; derivano in modo implicito da System.ValueType. Non è possibile derivare altri struct
tipi da un struct
tipo. Sono sigillati in modo implicito.
public struct Point
{
public double X { get; }
public double Y { get; }
public Point(double x, double y) => (X, Y) = (x, y);
}
Interfacce
Un'interfaccia definisce un contratto che può essere implementato da classi e struct. Si definisce un'interfaccia per dichiarare le funzionalità condivise tra tipi distinti. Ad esempio, l'interfaccia System.Collections.Generic.IEnumerable<T> definisce un modo coerente per attraversare tutti gli elementi di una raccolta, ad esempio una matrice. Può contenere metodi, proprietà, eventi e indicizzatori. Un'interfaccia in genere non fornisce implementazioni dei membri definiti, ma specifica semplicemente i membri che devono essere forniti da classi o struct che implementano l'interfaccia.
Le interfacce possono usare più ereditarietà. Nell'esempio seguente l'interfaccia IComboBox
eredita da ITextBox
e IListBox
.
interface IControl
{
void Paint();
}
interface ITextBox : IControl
{
void SetText(string text);
}
interface IListBox : IControl
{
void SetItems(string[] items);
}
interface IComboBox : ITextBox, IListBox { }
Classi e struct possono implementare più interfacce. Nell'esempio seguente la classe EditBox
implementa IControl
e IDataBound
.
interface IDataBound
{
void Bind(Binder b);
}
public class EditBox : IControl, IDataBound
{
public void Paint() { }
public void Bind(Binder b) { }
}
Quando una classe o un tipo struct implementa un'interfaccia specifica, le istanze di tale classe o struct possono essere convertite in modo implicito in quel tipo di interfaccia. Ad esempio:
EditBox editBox = new();
IControl control = editBox;
IDataBound dataBound = editBox;
Enumerazioni
Un tipo Enum definisce un set di valori costanti. Di seguito enum
vengono dichiarate costanti che definiscono diverse verdure radice:
public enum SomeRootVegetable
{
HorseRadish,
Radish,
Turnip
}
È anche possibile definire un enum
oggetto da usare in combinazione come flag. La dichiarazione seguente dichiara un set di flag per le quattro stagioni. È possibile applicare qualsiasi combinazione delle stagioni, incluso un All
valore che include tutte le stagioni:
[Flags]
public enum Seasons
{
None = 0,
Summer = 1,
Autumn = 2,
Winter = 4,
Spring = 8,
All = Summer | Autumn | Winter | Spring
}
L'esempio seguente mostra le dichiarazioni di entrambe le enumerazioni precedenti:
var turnip = SomeRootVegetable.Turnip;
var spring = Seasons.Spring;
var startingOnEquinox = Seasons.Spring | Seasons.Autumn;
var theYear = Seasons.All;
Tipi nullable
Le variabili di qualsiasi tipo possono essere dichiarate come non nullable o nullable. Una variabile nullable può contenere un valore aggiuntivo null
, a indicare che non è disponibile alcun valore. I tipi Valore nullable (struct o enum) sono rappresentati da System.Nullable<T>. I tipi Reference non nullable e Nullable sono entrambi rappresentati dal tipo riferimento sottostante. La distinzione è rappresentata dai metadati letti dal compilatore e da alcune librerie. Il compilatore fornisce avvisi quando i riferimenti nullable vengono dereferenziati senza prima controllare il valore rispetto a null
. Il compilatore fornisce anche avvisi quando ai riferimenti non nullable viene assegnato un valore che può essere null
. Nell'esempio seguente viene dichiarato un valore int nullable, inizializzandolo in null
. Imposta quindi il valore su 5
. Viene illustrato lo stesso concetto con una stringa nullable. Per altre informazioni, vedere Tipi valore nullable e tipi riferimento nullable.
int? optionalInt = default;
optionalInt = 5;
string? optionalText = default;
optionalText = "Hello World.";
Tuple
C# supporta le tuple, che fornisce una sintassi concisa per raggruppare più elementi dati in una struttura di dati leggera. È possibile creare un'istanza di una tupla dichiarando i tipi e i nomi dei membri tra (
e )
, come illustrato nell'esempio seguente:
(double Sum, int Count) t2 = (4.5, 3);
Console.WriteLine($"Sum of {t2.Count} elements is {t2.Sum}.");
//Output:
//Sum of 3 elements is 4.5.
Le tuple offrono un'alternativa per la struttura dei dati con più membri, senza usare i blocchi predefiniti descritti nell'articolo successivo.