Novità in C# 9.0

C# 9.0 aggiunge le funzionalità e i miglioramenti seguenti al linguaggio C#:

C# 9.0 è supportato in .NET 5. Per altre informazioni, vedere Controllo delle versioni del linguaggio C#.

È possibile scaricare la versione più recente di .NET SDK dalla pagina dei download di .NET.

Tipi di record

C# 9.0 introduce i tipi di record. Usare la record parola chiave per definire un tipo riferimento che fornisce funzionalità incorporate per l'incapsulamento dei dati. È possibile creare tipi di record con proprietà non modificabili usando parametri posizionali o la sintassi delle proprietà standard:

public record Person(string FirstName, string LastName);
public record Person
{
    public string FirstName { get; init; } = default!;
    public string LastName { get; init; } = default!;
};

È anche possibile creare tipi di record con proprietà e campi modificabili:

public record Person
{
    public string FirstName { get; set; } = default!;
    public string LastName { get; set; } = default!;
};

Sebbene i record possano essere modificabili, sono destinati principalmente al supporto di modelli di dati non modificabili. Il tipo di record offre le funzionalità seguenti:

È possibile usare i tipi di struttura per progettare tipi incentrati sui dati che forniscono l'uguaglianza dei valori e un comportamento minimo o nullo. Per i modelli di dati relativamente grandi, tuttavia, i tipi di struttura hanno alcuni svantaggi:

  • Non supportano l'ereditarietà.
  • Sono meno efficienti nel determinare l'uguaglianza dei valori. Per i tipi valore, ValueType.Equals il metodo usa la reflection per trovare tutti i campi. Per i record, il compilatore genera il Equals metodo . In pratica, l'implementazione dell'uguaglianza dei valori nei record è notevolmente più veloce.
  • Usano più memoria in alcuni scenari, poiché ogni istanza dispone di una copia completa di tutti i dati. I tipi di record sono tipi riferimento,quindi un'istanza di record contiene solo un riferimento ai dati.

Sintassi posizionale per la definizione della proprietà

È possibile usare parametri posizionali per dichiarare le proprietà di un record e inizializzare i valori delle proprietà quando si crea un'istanza:

public record Person(string FirstName, string LastName);

public static void Main()
{
    Person person = new("Nancy", "Davolio");
    Console.WriteLine(person);
    // output: Person { FirstName = Nancy, LastName = Davolio }
}

Quando si usa la sintassi posizionale per la definizione della proprietà, il compilatore crea:

  • Proprietà pubblica implementata automaticamente solo init per ogni parametro posizionale fornito nella dichiarazione di record. Una proprietà init-only può essere impostata solo nel costruttore o usando un inizializzatore di proprietà.
  • Costruttore primario i cui parametri corrispondono ai parametri posizionali nella dichiarazione del record.
  • Metodo Deconstruct con un parametro per ogni parametro out posizionale fornito nella dichiarazione di record.

Per altre informazioni, vedere Sintassi posizionale nell'articolo di riferimento sul linguaggio C# sui record.

Immutabilità

Un tipo di record non è necessariamente non modificabile. È possibile dichiarare proprietà con set funzioni di accesso e campi diversi da readonly . Tuttavia, anche se i record possono essere modificabili, semplificano la creazione di modelli di dati non modificabili. Le proprietà create usando la sintassi posizionale non sono modificabili.

L'immutabilità può essere utile quando si vuole che un tipo incentrato sui dati sia thread-safe o che un codice hash rimanga invariato in una tabella hash. Può impedire bug che si verificano quando si passa un argomento per riferimento a un metodo e il metodo modifica in modo imprevisto il valore dell'argomento.

Le funzionalità univoche per i tipi di record vengono implementate dai metodi sintetizzati dal compilatore e nessuno di questi metodi compromette l'immutabilità modificando lo stato dell'oggetto.

Uguaglianza di valori

L'uguaglianza dei valori significa che due variabili di un tipo di record sono uguali se i tipi corrispondono e tutti i valori di proprietà e di campo corrispondono. Per altri tipi di riferimento, l'uguaglianza significa identità. In altre informazioni, due variabili di un tipo riferimento sono uguali se fanno riferimento allo stesso oggetto.

L'esempio seguente illustra l'uguaglianza dei valori dei tipi di record:

public record Person(string FirstName, string LastName, string[] PhoneNumbers);

public static void Main()
{
    var phoneNumbers = new string[2];
    Person person1 = new("Nancy", "Davolio", phoneNumbers);
    Person person2 = new("Nancy", "Davolio", phoneNumbers);
    Console.WriteLine(person1 == person2); // output: True

    person1.PhoneNumbers[0] = "555-1234";
    Console.WriteLine(person1 == person2); // output: True

    Console.WriteLine(ReferenceEquals(person1, person2)); // output: False
}

Nei tipi è possibile eseguire manualmente l'override di metodi e operatori di uguaglianza per ottenere l'uguaglianza dei valori, ma lo sviluppo e il test di tale codice richiederebbe molto tempo e sarebbe class rischioso per gli errori. La presenza di questa funzionalità incorporata impedisce ai bug che verrebbero dimenticati di aggiornare il codice di override personalizzato quando le proprietà o i campi vengono aggiunti o modificati.

Per altre informazioni, vedere Uguaglianza dei valori nell'articolo di riferimento sul linguaggio C# sui record.

Mutazione non distruttiva

Se è necessario modificare le proprietà non modificabili di un'istanza di record, è possibile usare un'espressione per ottenere with una mutazione non distruttiva. withUn'espressione crea una nuova istanza di record che è una copia di un'istanza di record esistente, con le proprietà e i campi specificati modificati. Usare la sintassi dell'inizializzatore di oggetto per specificare i valori da modificare, come illustrato nell'esempio seguente:

public record Person(string FirstName, string LastName)
{
    public string[] PhoneNumbers { get; init; }
}

public static void Main()
{
    Person person1 = new("Nancy", "Davolio") { PhoneNumbers = new string[1] };
    Console.WriteLine(person1);
    // output: Person { FirstName = Nancy, LastName = Davolio, PhoneNumbers = System.String[] }

    Person person2 = person1 with { FirstName = "John" };
    Console.WriteLine(person2);
    // output: Person { FirstName = John, LastName = Davolio, PhoneNumbers = System.String[] }
    Console.WriteLine(person1 == person2); // output: False

    person2 = person1 with { PhoneNumbers = new string[1] };
    Console.WriteLine(person2);
    // output: Person { FirstName = Nancy, LastName = Davolio, PhoneNumbers = System.String[] }
    Console.WriteLine(person1 == person2); // output: False

    person2 = person1 with { };
    Console.WriteLine(person1 == person2); // output: True
}

Per altre informazioni, vedere Mutazione non distruttiva nell'articolo di riferimento sul linguaggio C# sui record.

Formattazione predefinita per la visualizzazione

I tipi di record hanno un metodo generato dal ToString compilatore che visualizza i nomi e i valori delle proprietà e dei campi pubblici. Il ToString metodo restituisce una stringa nel formato seguente:

<record type name> { <property name> = <value>, <property name> = <value>, ...}

Per i tipi riferimento, viene visualizzato il nome del tipo dell'oggetto a cui fa riferimento la proprietà anziché il valore della proprietà. Nell'esempio seguente la matrice è un tipo riferimento, pertanto viene visualizzata al System.String[] posto dei valori effettivi degli elementi della matrice:

Person { FirstName = Nancy, LastName = Davolio, ChildNames = System.String[] }

Per altre informazioni, vedere Formattazione predefinita nell'articolo di riferimento sul linguaggio C# sui record.

Ereditarietà

Un record può ereditare da un altro record. Tuttavia, un record non può ereditare da una classe e una classe non può ereditare da un record.

L'esempio seguente illustra l'ereditarietà con la sintassi della proprietà posizionale:

public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);
public static void Main()
{
    Person teacher = new Teacher("Nancy", "Davolio", 3);
    Console.WriteLine(teacher);
    // output: Teacher { FirstName = Nancy, LastName = Davolio, Grade = 3 }
}

Perché due variabili di record siano uguali, il tipo di run-time deve essere uguale. I tipi delle variabili contenitore potrebbero essere diversi. Questo è illustrato nell'esempio di codice seguente:

public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);
public static void Main()
{
    Person teacher = new Teacher("Nancy", "Davolio", 3);
    Person student = new Student("Nancy", "Davolio", 3);
    Console.WriteLine(teacher == student); // output: False

    Student student2 = new Student("Nancy", "Davolio", 3);
    Console.WriteLine(student2 == student); // output: True
}

Nell'esempio tutte le istanze hanno le stesse proprietà e gli stessi valori di proprietà. Ma student == teacher restituisce anche se False entrambe sono variabili di tipo Person . E student == student2 restituisce anche se una è una variabile e una è una True Person Student variabile.

Tutte le proprietà e i campi pubblici dei tipi derivati e di base sono inclusi ToString nell'output, come illustrato nell'esempio seguente:

public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);

public static void Main()
{
    Person teacher = new Teacher("Nancy", "Davolio", 3);
    Console.WriteLine(teacher);
    // output: Teacher { FirstName = Nancy, LastName = Davolio, Grade = 3 }
}

Per altre informazioni, vedere Ereditarietà nell'articolo di riferimento sul linguaggio C# sui record.

Setter di sola inizializzazione

Solo i setter init forniscono una sintassi coerente per inizializzare i membri di un oggetto. Gli inizializzatori di proprietà chiarino quale valore sta impostando la proprietà. Lo svantaggio è che tali proprietà devono essere impostate. A partire da C# 9.0, è possibile creare funzioni di accesso anziché init funzioni di accesso per proprietà e set indicizzatori. I chiamanti possono usare la sintassi dell'inizializzatore di proprietà per impostare questi valori nelle espressioni di creazione, ma tali proprietà sono di sola lettura al termine della costruzione. Solo i setter init forniscono una finestra per modificare lo stato. Tale finestra si chiude al termine della fase di costruzione. La fase di costruzione termina in modo efficace dopo il completamento di tutte le inizializzazioni, inclusi gli inizializzatori di proprietà e le espressioni with.

È possibile dichiarare init solo setter in qualsiasi tipo scritto. Ad esempio, lo struct seguente definisce una struttura di osservazione meteo:

public struct WeatherObservation
{
    public DateTime RecordedAt { get; init; }
    public decimal TemperatureInCelsius { get; init; }
    public decimal PressureInMillibars { get; init; }

    public override string ToString() =>
        $"At {RecordedAt:h:mm tt} on {RecordedAt:M/d/yyyy}: " +
        $"Temp = {TemperatureInCelsius}, with {PressureInMillibars} pressure";
}

I chiamanti possono usare la sintassi dell'inizializzatore di proprietà per impostare i valori, mantenendo al tempo stesso l'immutabilità:

var now = new WeatherObservation 
{ 
    RecordedAt = DateTime.Now, 
    TemperatureInCelsius = 20, 
    PressureInMillibars = 998.0m 
};

Un tentativo di modificare un'osservazione dopo l'inizializzazione genera un errore del compilatore:

// Error! CS8852.
now.TemperatureInCelsius = 18;

Solo i setter init possono essere utili per impostare le proprietà della classe di base dalle classi derivate. Possono anche impostare proprietà derivate tramite helper in una classe di base. I record posizionali dichiarano le proprietà usando solo setter init. Questi setter vengono usati in with-expressions. È possibile dichiarare solo setter init per qualsiasi class struct , o definito record dall'utente.

Per altre informazioni, vedere init (Riferimenti per C#).

Istruzioni di primo livello

Le istruzioni di primo livello rimuovono le operazioni non necessarie da molte applicazioni. Si consideri il "Hello World!" canonico Programma:

using System;

namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

Esiste una sola riga di codice che esegue qualsiasi operazione. Con le istruzioni di primo livello, è possibile sostituire tutto il boilerplate con la direttiva e la singola using riga che esegue il lavoro:

using System;

Console.WriteLine("Hello World!");

Se si vuole un programma a una riga, è possibile rimuovere la using direttiva e usare il nome di tipo completo:

System.Console.WriteLine("Hello World!");

Solo un file nell'applicazione può usare istruzioni di primo livello. Se il compilatore trova istruzioni di primo livello in più file di origine, si tratta di un errore. È anche un errore se si combinano istruzioni di primo livello con un metodo del punto di ingresso del programma dichiarato, in genere un Main metodo . In un certo senso, si può pensare che un file contenga le istruzioni che normalmente si trovarebbero nel Main metodo di una Program classe.

Uno degli usi più comuni per questa funzionalità è la creazione di materiali didattici. Gli sviluppatori C# per principianti possono scrivere il codice canonico "Hello World!" in una o due righe di codice. Non è necessaria nessuna delle spese aggiuntive. Tuttavia, anche gli sviluppatori esperti troveranno molti usi per questa funzionalità. Le istruzioni di primo livello consentono un'esperienza simile a uno script per la sperimentazione simile a quella dei notebook di Jupyter. Le istruzioni di primo livello sono molto grandi per utilità e programmi console di piccole dimensioni. Funzioni di Azure è un caso d'uso ideale per le istruzioni di primo livello.

Soprattutto, le istruzioni di primo livello non limitano l'ambito o la complessità dell'applicazione. Tali istruzioni possono accedere o usare qualsiasi classe .NET. Non limitano inoltre l'uso di argomenti della riga di comando o valori restituiti. Le istruzioni di primo livello possono accedere a una matrice di stringhe denominata args . Se le istruzioni di primo livello restituiscono un valore intero, tale valore diventa il codice restituito integer da un metodo Main sintetizzato. Le istruzioni di primo livello possono contenere espressioni asincrone. In tal caso, il punto di ingresso sintetizzato restituisce Task , o Task<int> .

Per altre informazioni, vedere Istruzioni di primo livello nella Guida per programmatori C#.

Miglioramenti dei criteri di ricerca

C# 9 include nuovi miglioramenti dei criteri di ricerca:

  • I criteri di tipo corrispondono a una variabile è un tipo
  • I criteri tra parentesi applicano o evidenziano la precedenza delle combinazioni di modelli
  • I modelli di congiunzione and richiedono che entrambi i modelli corrispondano
  • I modelli disgiuntivi or richiedono che uno dei due criteri corrisponda
  • I criteri not negati richiedono che un criterio non corrisponda
  • I modelli relazionali richiedono che l'input sia minore di, maggiore di, minore o uguale a oppure maggiore o uguale a una determinata costante.

Questi modelli arricchiscono la sintassi per i modelli. Considerare i seguenti esempi:

public static bool IsLetter(this char c) =>
    c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';

Con parentesi facoltative per chiarire che ha and una precedenza maggiore rispetto a or :

public static bool IsLetterOrSeparator(this char c) =>
    c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or '.' or ',';

Uno degli usi più comuni è una nuova sintassi per un controllo Null:

if (e is not null)
{
    // ...
}

Uno di questi modelli può essere usato in qualsiasi contesto in cui sono consentiti i modelli: espressioni di criteri, espressioni, modelli annidati e modello is switch switch dell'etichetta di case un'istruzione.

Per altre informazioni, vedere Modelli (Riferimenti per C#).

Per altre informazioni, vedere le sezioni Modelli relazionali e Modelli logici dell'articolo Modelli.

Prestazioni e interoperabilità

Tre nuove funzionalità migliorano il supporto per l'interoperabilità nativa e le librerie di basso livello che richiedono prestazioni elevate: numeri interi di dimensioni native, puntatori a funzione e omissione del localsinit flag.

Gli interi di dimensioni nint native, e nuint , sono tipi Integer. Sono espressi dai tipi sottostanti System.IntPtr e System.UIntPtr . Il compilatore consente di visualizzare conversioni e operazioni aggiuntive per questi tipi come tipi nativi. Gli interi con dimensioni native definiscono le proprietà per MaxValue o MinValue . Questi valori non possono essere espressi come costanti in fase di compilazione perché dipendono dalle dimensioni native di un intero nel computer di destinazione. Questi valori sono di sola lettura in fase di esecuzione. È possibile usare valori costanti per nint nell'intervallo [ int.MinValue .. int.MaxValue]. È possibile usare valori costanti per nuint nell'intervallo [ uint.MinValue .. uint.MaxValue]. Il compilatore esegue la conversione costante per tutti gli operatori unari e binari usando i System.Int32 tipi System.UInt32 e . Se il risultato non rientra nei 32 bit, l'operazione viene eseguita in fase di esecuzione e non viene considerata una costante. I numeri interi di dimensioni native possono migliorare le prestazioni negli scenari in cui i calcoli matematici su interi vengono ampiamente usati e devono avere le prestazioni più veloci possibili. Per altre informazioni, vedere nint i tipi nuint e

I puntatori a funzione forniscono una sintassi semplice per accedere ai codici operativo IL ldftn e calli . È possibile dichiarare puntatori a funzione usando una nuova delegate* sintassi. Un delegate* tipo è un tipo puntatore. La chiamata delegate* al tipo usa , a differenza di un delegato che usa nel metodo calli callvirt Invoke() . Sintatticamente, le chiamate sono identiche. La chiamata al puntatore a funzione usa la managed convenzione di chiamata . Aggiungere la parola unmanaged chiave dopo la delegate* sintassi per dichiarare che si vuole usare la unmanaged convenzione di chiamata. È possibile specificare altre convenzioni di chiamata usando gli attributi nella delegate* dichiarazione. Per altre informazioni, vedere Codice unsafe e tipi puntatore.

Infine, è possibile aggiungere per System.Runtime.CompilerServices.SkipLocalsInitAttribute indicare al compilatore di non generare il localsinit flag . Questo flag indica a CLR di inizializzare con zero tutte le variabili locali. Il localsinit flag è il comportamento predefinito per C# dalla versione 1.0. Tuttavia, l'inizializzazione zero aggiuntiva può avere un impatto misurabile sulle prestazioni in alcuni scenari. In particolare, quando si usa stackalloc . In questi casi, è possibile aggiungere SkipLocalsInitAttribute . È possibile aggiungerlo a un singolo metodo o proprietà o a class un modulo , , o anche struct interface . Questo attributo non influisce sui abstract metodi, ma influisce sul codice generato per l'implementazione. Per altre informazioni, vedere SkipLocalsInit l'attributo.

Queste funzionalità possono migliorare le prestazioni in alcuni scenari. Devono essere usati solo dopo un attento benchmarking sia prima che dopo l'adozione. Il codice che interessa numeri interi di dimensioni native deve essere testato su più piattaforme di destinazione con dimensioni intere diverse. Le altre funzionalità richiedono codice unsafe.

Adattare e completare le funzionalità

Molte delle altre funzionalità consentono di scrivere codice in modo più efficiente. In C# 9.0 è possibile omettere il tipo in un'espressione new quando il tipo dell'oggetto creato è già noto. L'uso più comune è nelle dichiarazioni di campo:

private List<WeatherObservation> _observations = new();

I tipi di destinazione possono essere usati anche quando è necessario creare new un nuovo oggetto da passare come argomento a un metodo. Si ForecastFor() consideri un metodo con la firma seguente:

public WeatherForecast ForecastFor(DateTime forecastDate, WeatherForecastOptions options)

È possibile chiamarlo come segue:

var forecast = station.ForecastFor(DateTime.Now.AddDays(2), new());

Un altro uso utile per questa funzionalità è combinarlo con le proprietà solo init per inizializzare un nuovo oggetto:

WeatherStation station = new() { Location = "Seattle, WA" };

È possibile restituire un'istanza creata dal costruttore predefinito usando return new(); un'istruzione .

Una funzionalità simile migliora la risoluzione del tipo di destinazione delle espressioni condizionali. Con questa modifica, le due espressioni non devono avere una conversione implicita da una all'altra, ma entrambe possono avere conversioni implicite in un tipo di destinazione. Probabilmente non si noterà questa modifica. Si noterà che alcune espressioni condizionali che in precedenza richiedeva cast o non avrebbero eseguito la compilazione ora funzionano.

A partire da C# 9.0, è possibile aggiungere il static modificatore alle espressioni lambda o ai metodi anonimi. Le espressioni lambda statiche sono analoghe alle funzioni locali: un metodo lambda statico o anonimo non può acquisire static variabili locali o stato dell'istanza. Il static modificatore impedisce l'acquisizione accidentale di altre variabili.

I tipi restituiti covarianti offrono flessibilità per i tipi restituiti dei metodi di override. Un metodo di override può restituire un tipo derivato dal tipo restituito del metodo di base sottoposto a override. Può essere utile per i record e per altri tipi che supportano i metodi di clonazione virtuale o factory.

Inoltre, il foreach ciclo riconoscerà e userà un metodo di estensione che in caso contrario GetEnumerator soddisfa il foreach modello. Questa modifica significa che è coerente con altre strutture basate su modelli, ad esempio il modello asincrono e la decostruzione basata foreach su modelli. In pratica, questa modifica significa che è possibile aggiungere foreach il supporto a qualsiasi tipo. È consigliabile limitarne l'uso a durante l'enumerazione di un oggetto sensato nella progettazione.

Successivamente, è possibile usare discards come parametri per le espressioni lambda. Questa praticità consente di evitare di denominare l'argomento e il compilatore può evitare di usarlo. Usare per _ qualsiasi argomento. Per altre informazioni, vedere la sezione Parametri di input di un'espressione lambda dell'articolo Espressioni lambda.

Infine, è ora possibile applicare attributi alle funzioni locali. Ad esempio, è possibile applicare annotazioni di attributi nullable alle funzioni locali.

Supporto per i generatori di codice

Due funzionalità finali supportano i generatori di codice C#. I generatori di codice C# sono un componente che è possibile scrivere simile a un analizzatore roslyn o a una correzione del codice. La differenza è che i generatori di codice analizzano il codice e scrivono nuovi file di codice sorgente come parte del processo di compilazione. Un generatore di codice tipico cerca attributi o altre convenzioni nel codice.

Un generatore di codice legge attributi o altri elementi di codice usando le API di analisi di Roslyn. Da queste informazioni, aggiunge nuovo codice alla compilazione. I generatori di origine possono aggiungere solo codice. non è consentito modificare il codice esistente nella compilazione.

Le due funzionalità aggiunte per i generatori di codice sono estensioni a * sintassi del metodo parziale _, e _* inizializzatori di modulo**. In primo luogo, le modifiche ai metodi parziali. Prima di C# 9.0, i metodi parziali sono ma non possono specificare un modificatore di accesso, hanno un private void valore restituito e non possono avere out parametri. Queste restrizioni significavano che se non viene fornita alcuna implementazione del metodo, il compilatore rimuove tutte le chiamate al metodo parziale. C# 9.0 rimuove queste restrizioni, ma richiede che le dichiarazioni di metodo parziali abbiano un'implementazione. I generatori di codice possono fornire tale implementazione. Per evitare di introdurre una modifica di rilievo, il compilatore considera qualsiasi metodo parziale senza un modificatore di accesso che segua le regole meno esistenti. Se il metodo parziale include il private modificatore di accesso, le nuove regole governano tale metodo parziale. Per altre informazioni, vedere metodo parziale (Riferimenti per C#).

La seconda nuova funzionalità per i generatori di codice è l'inizializzatori di modulo. Gli inizializzatori di modulo sono metodi a cui ModuleInitializerAttribute è associato l'attributo . Questi metodi verranno chiamati dal runtime prima di qualsiasi altra chiamata di metodo o di accesso al campo all'interno dell'intero modulo. Metodo dell'inizializzatore di modulo:

  • Deve essere statico
  • Deve essere senza parametri
  • Deve restituire void
  • Non deve essere un metodo generico
  • Non deve essere contenuto in una classe generica
  • Deve essere accessibile dal modulo contenitore

L'ultimo punto elenco indica in modo efficace che il metodo e la relativa classe contenitore devono essere interni o pubblici. Il metodo non può essere una funzione locale. Per altre informazioni, vedere ModuleInitializer attributo.