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 Pointe ogni Point3D istanza contiene tre proprietà, X, Ye 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.