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 l'SDK .NET più recente dalla pagina dei download .NET.

Tipi di record

C# 9.0 introduce tipi di record. Si usa la record parola chiave per definire un tipo di riferimento che fornisce funzionalità predefinite per incapsulare i dati. È possibile creare tipi di record con proprietà non modificabili usando parametri posizionali o sintassi della 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!;
};

Anche se i record possono essere modificabili, sono principalmente destinati 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 nessun comportamento. Tuttavia, per modelli di dati relativamente grandi, i tipi di struttura presentano alcuni svantaggi:

  • Non supportano l'ereditarietà.
  • Sono meno efficienti per determinare l'uguaglianza dei valori. Per i tipi di valore, il ValueType.Equals 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 è molto più veloce.
  • Usano più memoria in alcuni scenari, poiché ogni istanza ha una copia completa di tutti i dati. I tipi di record sono tipi di 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à implementata automaticamente solo da init pubblica 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 di record.
  • Metodo Deconstruct con un out parametro per ogni parametro 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 modificabile. È possibile dichiarare proprietà con set funzioni di accesso e campi che non readonlysono . Ma mentre 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 un codice hash per rimanere 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 campo corrispondono. Per altri tipi di riferimento, l'uguaglianza significa identità. Vale a dire, due variabili di un tipo di riferimento sono uguali se fanno riferimento allo stesso oggetto.

Nell'esempio seguente viene illustrata 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 class tipi è possibile eseguire manualmente l'override dei metodi di uguaglianza e degli operatori per ottenere l'uguaglianza dei valori, ma lo sviluppo e il test del codice potrebbero richiedere tempo e soggette a errori. Se questa funzionalità predefinita impedisce ai bug di dimenticare di aggiornare il codice di override personalizzato quando vengono aggiunte o modificate proprietà o campi.

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

Mutazione non strutturativa

Se è necessario modificare le proprietà non modificabili di un'istanza di record, è possibile usare un'espressione per ottenere una withmutazione non strutturativa. Un'espressione with crea una nuova istanza di record che è una copia di un'istanza di record esistente, con proprietà e campi specificati modificati. Si usa la sintassi dell'inizializzatore a oggetti 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 strutturativa nell'articolo di riferimento del linguaggio C# sui record.

Formattazione predefinita per la visualizzazione

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

<nome> del tipo di record { <nome proprietà> = <valore>, nome> proprietà = <valore>, <...}

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

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

Per altre informazioni, vedere Formattazione predefinita nell'articolo di riferimento del 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.

Nell'esempio seguente viene illustrata 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 }
}

Affinché due variabili di record siano uguali, il tipo di runtime deve essere uguale. I tipi delle variabili contenenti 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 delle proprietà. Ma student == teacher restituisce anche se entrambe False sono Personvariabili di tipo -. Restituisce student == student2True anche se una è una variabile e una è una PersonStudent variabile.

Tutte le proprietà pubbliche e i campi dei tipi di base e derivati sono inclusi nell'output, come illustrato nell'esempio ToString 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 setters init forniscono una sintassi coerente per inizializzare i membri di un oggetto. Gli inizializzatori delle proprietà rendono chiaro quale valore è l'impostazione della proprietà. Lo svantaggio è che tali proprietà devono essere impostate. A partire da C# 9.0, è possibile creare init funzioni di accesso anziché funzioni di set accesso per proprietà e 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 dopo il completamento della costruzione. Solo i setter init forniscono una finestra per modificare lo stato. La finestra si chiude quando termina la fase di costruzione. La fase di costruzione termina efficacemente dopo tutto l'inizializzazione, inclusi gli inizializzatori di proprietà e le espressioni con il completamento.

È 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 comunque 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;

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 con espressioni. È possibile dichiarare solo setter init per qualsiasi class, structo record definito.

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

Istruzioni di primo livello

Le istruzioni di primo livello rimuovono la cerimonia non necessaria da molte applicazioni. Si consideri il programma canonico "Hello World!":

using System;

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

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

using System;

Console.WriteLine("Hello World!");

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

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. Si tratta anche di 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 trovano nel Main metodo di una Program classe.

Uno degli usi più comuni per questa funzionalità è la creazione di materiali didattici. Gli sviluppatori C# principianti possono scrivere il canonico "Hello World!" in una o due righe di codice. Non è necessaria alcuna cerimonia aggiuntiva. Tuttavia, gli sviluppatori con esperienza troveranno anche molti usi per questa funzionalità. Le istruzioni di primo livello consentono un'esperienza di tipo script per la sperimentazione simile a quella dei notebook di Jupyter. Le istruzioni di primo livello sono ideali per programmi e utilità 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 degli argomenti della riga di comando o dei valori restituiti. Le istruzioni di primo livello possono accedere a una matrice di stringhe denominate args. Se le istruzioni di livello superiore restituiscono un valore intero, tale valore diventa il codice restituito integer da un metodo sintetizzato Main . Le istruzioni di primo livello possono contenere espressioni asincrone. In tal caso, il punto di ingresso sintetizzato restituisce un Taskoggetto 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 per i criteri di ricerca:

  • I criteri di tipo corrispondono a un oggetto corrisponde a un tipo specifico
  • I modelli racchiusi tra parentesi applicano o enfatizzano la precedenza delle combinazioni di criteri
  • I modelli congiuntivi and richiedono che entrambi i modelli corrispondano
  • I modelli di disgiuntivi or richiedono uno dei criteri di corrispondenza
  • I modelli negati not richiedono che un criterio non corrisponda
  • I modelli relazionali richiedono che l'input sia minore, maggiore di, minore 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 le parentesi facoltative per chiarire che and ha una precedenza maggiore di 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 criteri: is espressioni di pattern, switch espressioni, modelli annidati e modello dell'etichetta di case un'istruzioneswitch.

Per altre informazioni, vedere Patterns (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.

I numeri interi nint con dimensioni native e nuint, sono tipi integer. Sono espressi dai tipi System.IntPtr sottostanti e System.UIntPtr. Il compilatore espone conversioni e operazioni aggiuntive per questi tipi come int nativi. I numeri 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 numero intero nel computer di destinazione. Questi valori sono di sola lettura in fase di esecuzione. È possibile usare i valori costanti per nint nell'intervallo [int.MinValue .. int.MaxValue]. È possibile usare i valori costanti per nuint nell'intervallo [uint.MinValue .. uint.MaxValue]. Il compilatore esegue la riduzione costante per tutti gli operatori unari e binari usando i System.Int32 tipi e System.UInt32 . Se il risultato non rientra in 32 bit, l'operazione viene eseguita in fase di esecuzione e non viene considerata una costante. Gli interi con dimensioni native possono aumentare le prestazioni negli scenari in cui la matematica integer viene usata ampiamente e deve avere le prestazioni più veloci possibili. Per altre informazioni, vedere nint e nuint tipi

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

Infine, è possibile aggiungere per System.Runtime.CompilerServices.SkipLocalsInitAttribute indicare al compilatore di non generare il localsinit flag. Questo flag indica a CLR di inizializzare zero tutte le variabili locali. Il localsinit flag è stato 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à oppure a un classmodulo , , interfacestructo anche a un modulo. Questo attributo non influisce sui abstract metodi, ma influisce sul codice generato per l'implementazione. Per altre informazioni, vedere SkipLocalsInit Attributo.

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

Caratteristiche di adattabilità e finitura

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

private List<WeatherObservation> _observations = new();

Il tipo di new destinazione può essere usato anche quando è necessario creare un nuovo oggetto da passare come argomento a un metodo. Si consideri un ForecastFor() 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 interessante per questa funzionalità consiste nel combinarlo con solo le proprietà init per inizializzare un nuovo oggetto:

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

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

Una funzionalità simile migliora la risoluzione dei tipi di destinazione delle espressioni condizionali. Con questa modifica, le due espressioni non devono avere una conversione implicita da una all'altra, ma possono avere entrambe conversioni implicite in un tipo di destinazione. Probabilmente non si noterà questa modifica. Ciò che si noterà è che alcune espressioni condizionali che in precedenza richiedevano cast o non compilavano ora solo il lavoro.

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

I tipi restituiti Covariant offrono flessibilità per i tipi restituiti di metodi di override. Un metodo di override può restituire un tipo derivato dal tipo restituito del metodo base sottoposto a override. Ciò può essere utile per i record e per altri tipi che supportano metodi di clone o factory virtuali.

Inoltre, il foreach ciclo riconoscerà e userà un metodo GetEnumerator di estensione che soddisfa in caso contrario il foreach modello. Questo cambiamento significa foreach che è coerente con altre costruzioni basate su modelli, ad esempio il modello asincrono e la deconstrutturazione basata su modelli. In pratica, questa modifica significa che è possibile aggiungere foreach supporto a qualsiasi tipo. È consigliabile limitare l'uso a quando si enumera un oggetto nella progettazione.

È quindi possibile usare le eliminazioni come parametri per espressioni lambda. Questa praticità consente di evitare di assegnare un nome all'argomento e il compilatore potrebbe evitare di usarlo. Usare l'oggetto _ 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 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 nell'ambito del processo di compilazione. Un generatore di codice tipico cerca codice per attributi o altre convenzioni.

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

Le due funzionalità aggiunte per i generatori di codice sono estensioni alla sintassi del metodo parziale e agli inizializzatori del modulo. Prima di tutto, le modifiche ai metodi parziali. Prima di C# 9.0, i metodi parziali sono private ma non possono specificare un modificatore di accesso, avere una void restituzione e non possono avere out parametri. Queste restrizioni significano 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 parziali del metodo 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 per seguire le regole precedenti. Se il metodo parziale include il private modificatore di accesso, le nuove regole regolano il metodo parziale. Per altre informazioni, vedere Metodo parziale (Riferimento C#).

La seconda nuova funzionalità per i generatori di codice è inizializzatori di moduli. Gli inizializzatori del modulo sono metodi che hanno l'attributo ModuleInitializerAttribute collegato a loro. Questi metodi verranno chiamati dal runtime prima di qualsiasi altra chiamata di campo o di metodo all'interno dell'intero modulo. Metodo di inizializzazione del 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 contenente

L'ultimo punto puntato indica in modo efficace il metodo e la classe contenente deve essere interna o pubblica. Il metodo non può essere una funzione locale. Per altre informazioni, vedere ModuleInitializer attributo.