Proprietà

Le proprietà sono elementi fondamentali in C#. Il linguaggio definisce la sintassi che consente agli sviluppatori di scrivere codice che esprime in modo preciso la finalità della progettazione.

Quando si accede alle proprietà, queste si comportano come i campi. Tuttavia, a differenza dei campi, le proprietà vengono implementate con funzioni di accesso che definiscono le istruzioni eseguite al momento dell'accesso e dell'assegnazione della proprietà.

Sintassi delle proprietà

La sintassi delle proprietà è un'estensione naturale dei campi. Un campo definisce una posizione di archiviazione:

public class Person
{
    public string? FirstName;

    // Omitted for brevity.
}

La definizione di una proprietà contiene le dichiarazioni di una funzione di accesso get e set che recupera e assegna il valore della proprietà:

public class Person
{
    public string? FirstName { get; set; }

    // Omitted for brevity.
}

La sintassi illustrata sopra è la sintassi della proprietà automatica. Il compilatore genera la posizione di archiviazione per il campo che esegue il backup della proprietà. Il compilatore implementa anche il corpo delle funzioni di accesso get e set.

In alcuni casi è necessario inizializzare una proprietà con un valore diverso da quello predefinito per il suo tipo. In C# questa operazione è possibile impostando un valore dopo la parentesi graffa chiusa della proprietà. Per la proprietà FirstName è preferibile usare come valore iniziale una stringa vuota anziché null. Ecco come eseguire questa operazione:

public class Person
{
    public string FirstName { get; set; } = string.Empty;

    // Omitted for brevity.
}

L'inizializzazione specifica è particolarmente utile per le proprietà di sola lettura, come si vedrà più avanti in questo articolo.

È anche possibile definire l'archiviazione manualmente, come illustrato di seguito:

public class Person
{
    public string? FirstName
    {
        get { return _firstName; }
        set { _firstName = value; }
    }
    private string? _firstName;

    // Omitted for brevity.
}

Se l'implementazione di una proprietà corrisponde a un'espressione singola, è possibile usare membri con corpo di espressione per il getter o il setter:

public class Person
{
    public string? FirstName
    {
        get => _firstName;
        set => _firstName = value;
    }
    private string? _firstName;

    // Omitted for brevity.
}

Questa sintassi semplificata verrà usata ovunque applicabile in questo articolo.

La definizione della proprietà illustrata sopra è una proprietà di lettura/scrittura. Si noti la parola chiave value nella funzione di accesso impostata. La funzione di accesso set ha sempre un singolo parametro denominato value. La funzione di accesso get deve restituire un valore che è convertibile nel tipo della proprietà (in questo esempio string).

Queste sono le nozioni di basi sulla sintassi. Esistono numerose varianti che supportano schemi di progettazione diversi. Di seguito sono descritte le opzioni di sintassi per ogni termine.

Convalida

Gli esempi precedenti hanno illustrato uno dei casi più semplici di definizione delle proprietà, ovvero una proprietà di lettura/scrittura senza convalida. Scrivendo il codice desiderato nelle funzioni di accesso get e set è possibile creare scenari diversi.

È possibile scrivere codice nella funzione di accesso set per assicurarsi che i valori rappresentati da una proprietà siano sempre validi. Si supponga, ad esempio, che una regola per la classe Person preveda che il nome non può essere vuoto o uno spazio vuoto. Sarà necessario scrivere:

public class Person
{
    public string? FirstName
    {
        get => _firstName;
        set
        {
            if (string.IsNullOrWhiteSpace(value))
                throw new ArgumentException("First name must not be blank");
            _firstName = value;
        }
    }
    private string? _firstName;

    // Omitted for brevity.
}

L'esempio precedente può essere semplificato usando un'espressione throw come parte della convalida del setter della proprietà:

public class Person
{
    public string? FirstName
    {
        get => _firstName;
        set => _firstName = (!string.IsNullOrWhiteSpace(value)) ? value : throw new ArgumentException("First name must not be blank");
    }
    private string? _firstName;

    // Omitted for brevity.
}

L'esempio precedente applica la regola che prevede che il nome non può essere vuoto o uno spazio vuoto. Se lo sviluppatore scrive

hero.FirstName = "";

L'assegnazione genera ArgumentException. Poiché la funzione di accesso di un insieme di proprietà deve avere un tipo restituito void, gli errori vengono segnalati nella funzione di accesso dell'insieme generando un'eccezione.

La stessa sintassi può essere estesa a ogni elemento necessario nello scenario. È possibile controllare le relazioni tra le diverse proprietà o eseguire la convalida in base a qualsiasi condizione esterna. Tutte le istruzioni C# valide sono valide nella funzione di accesso di una proprietà.

Controllo di accesso

Le definizioni di proprietà descritte fino a questo punto si riferiscono a proprietà di lettura/scrittura con funzioni di accesso pubbliche. Non si tratta tuttavia dell'unica accessibilità valida per le proprietà. È possibile creare proprietà di sola lettura o assegnare un'accessibilità diversa alle funzioni di accesso set e get. Si supponga che la classe Person debba consentire soltanto la modifica del valore della proprietà FirstName da altri metodi della classe. È possibile assegnare alla funzione di accesso set l'accessibilità private anziché public:

public class Person
{
    public string? FirstName { get; private set; }

    // Omitted for brevity.
}

L'accesso alla proprietà FirstName potrà quindi essere eseguito da qualsiasi codice, ma la proprietà potrà essere assegnata soltanto da altro codice della classe Person.

È possibile aggiungere qualsiasi modificatore di accesso restrittivo alle funzioni di accesso set o get. Il modificatore di accesso inserito nella singola funzione di accesso deve essere più restrittivo del modificatore di accesso della definizione della proprietà. Il codice precedente è valido poiché la proprietà FirstName è public e la funzione di accesso set è private. Non è possibile dichiarare una proprietà private con una funzione di accesso public. Le dichiarazioni di proprietà possono anche essere dichiarate protected, internal, protected internal o anche private.

È anche consentito inserire il modificatore più restrittivo nella funzione di accesso get. Ad esempio, è possibile avere una proprietà public e limitare la funzione di accesso get a private. Questo scenario viene usato raramente.

Sola lettura

È inoltre possibile limitare le modifiche a una proprietà in modo che possa essere impostata solo in un costruttore. È possibile modificare in tal senso la classe Person come segue:

public class Person
{
    public Person(string firstName) => FirstName = firstName;

    public string FirstName { get; }

    // Omitted for brevity.
}

Sola inizializzazione

L'esempio precedente richiede ai chiamanti di usare il costruttore che include il parametro FirstName. I chiamanti non possono usare inizializzatori di oggetti per assegnare un valore alla proprietà. Per supportare gli inizializzatori, è possibile sostituire la funzione di accesso set con init, come illustrato nel codice seguente:

public class Person
{
    public Person() { }
    public Person(string firstName) => FirstName = firstName;

    public string? FirstName { get; init; }

    // Omitted for brevity.
}

L'esempio precedente consente a un chiamante di creare un oggetto Person usando il costruttore predefinito, anche quando tale codice non imposta la proprietà FirstName. A partire da C# 11, è possibile richiedere ai chiamanti di impostare tale proprietà:

public class Person
{
    public Person() { }

    [SetsRequiredMembers]
    public Person(string firstName) => FirstName = firstName;

    public required string FirstName { get; init; }

    // Omitted for brevity.
}

Il codice precedente apporta due aggiunte alla classe Person. In primo luogo, la dichiarazione di proprietà FirstName include il modificatore required. Ciò significa che qualsiasi codice che crea un nuovo oggetto Person deve impostare questa proprietà. In secondo luogo, il costruttore che accetta un parametro firstName ha l'attributo System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute. Quest'ultimo informa il compilatore che questo costruttore imposta tutti i membri required.

Importante

Non confondere un tipo required con uno che non ammette i valori Null. Una proprietà required può essere impostata su null o default. Se il tipo non ammette i valori Null, come string in questi esempi, il compilatore genera un avviso.

I chiamanti devono usare il costruttore con SetsRequiredMembers o impostare la proprietà FirstName usando un inizializzatore di oggetto, come illustrato nel codice seguente:

var person = new VersionNinePoint2.Person("John");
person = new VersionNinePoint2.Person{ FirstName = "John"};
// Error CS9035: Required member `Person.FirstName` must be set:
//person = new VersionNinePoint2.Person();

Proprietà calcolate

Non è necessario che una proprietà restituisca semplicemente il valore di un campo membro. È possibile creare proprietà che restituiscono un valore calcolato. Espandere l'oggetto Person per restituire il nome completo, calcolato concatenando il nome e il cognome:

public class Person
{
    public string? FirstName { get; set; }

    public string? LastName { get; set; }

    public string FullName { get { return $"{FirstName} {LastName}"; } }
}

L'esempio precedente usa la funzionalità di interpolazione delle stringhe per creare la stringa formattata per il nome completo.

È anche possibile usare un membro con corpo di espressione che consente di creare la proprietà calcolata FullName in modo più conciso:

public class Person
{
    public string? FirstName { get; set; }

    public string? LastName { get; set; }

    public string FullName => $"{FirstName} {LastName}";
}

I membri con corpo di espressione usano la sintassi delle espressioni lambda per definire i metodi che contengono una singola espressione. In questo caso, l'espressione restituisce il nome completo per l'oggetto person.

Proprietà con valutazione memorizzata nella cache

È possibile unire il concetto di proprietà calcolata al concetto di archiviazione e creare una proprietà con valutazione memorizzata nella cache. Ad esempio, è possibile aggiornare la proprietà FullName in modo che venga eseguita soltanto la formattazione della stringa al primo accesso:

public class Person
{
    public string? FirstName { get; set; }

    public string? LastName { get; set; }

    private string? _fullName;
    public string FullName
    {
        get
        {
            if (_fullName is null)
                _fullName = $"{FirstName} {LastName}";
            return _fullName;
        }
    }
}

Il codice riportato sopra tuttavia contiene un bug. Se il codice aggiorna il valore della proprietà FirstName o LastName, il campo fullName valutato in precedenza non è valido. Modificare le funzioni di accesso set della proprietà FirstName e LastName in modo che il campo fullName venga calcolato nuovamente:

public class Person
{
    private string? _firstName;
    public string? FirstName
    {
        get => _firstName;
        set
        {
            _firstName = value;
            _fullName = null;
        }
    }

    private string? _lastName;
    public string? LastName
    {
        get => _lastName;
        set
        {
            _lastName = value;
            _fullName = null;
        }
    }

    private string? _fullName;
    public string FullName
    {
        get
        {
            if (_fullName is null)
                _fullName = $"{FirstName} {LastName}";
            return _fullName;
        }
    }
}

La versione finale valuta la proprietà FullName solo quando necessario. Se è valida, viene usata la versione calcolata in precedenza. Se un'altra modifica dello stato annulla la validità della versione calcolata in precedenza, la versione verrà ricalcolata. Non è necessario che gli sviluppatori che usano questa classe siano a conoscenza dei dettagli dell'implementazione. Nessuna di queste modifiche interne ha effetto sull'uso dell'oggetto Person. Questo è il motivo principale dell'uso delle proprietà per l'esposizione dei membri dati di un oggetto.

Collegamento di attributi a proprietà implementate automaticamente

Gli attributi di campo possono essere associati al campo sottostante generato dal compilatore nelle proprietà implementate automaticamente. Si consideri ad esempio una revisione della classe Person che aggiunge una proprietà Id al valore intero univoco. Si scrive la proprietà Id usando una proprietà implementata automaticamente, ma la progettazione non esegue la chiamata per salvare in modo permanente la proprietà Id. La classe NonSerializedAttribute può essere collegata soltanto a campi, non a proprietà. È possibile collegare la classe NonSerializedAttribute al campo sottostante per la proprietà Id usando l'identificatore field: dell'attributo, come illustrato nell'esempio seguente:

public class Person
{
    public string? FirstName { get; set; }

    public string? LastName { get; set; }

    [field:NonSerialized]
    public int Id { get; set; }

    public string FullName => $"{FirstName} {LastName}";
}

Questa tecnica funziona con qualsiasi attributo collegato al campo sottostante nella proprietà implementate automaticamente.

Implementazione di NotifyPropertyChanged

Uno scenario finale in cui è necessario scrivere codice nella funzione di accesso di una proprietà è quello finalizzato al supporto dell'interfaccia INotifyPropertyChanged usata per inviare ai client di data binding la notifica della modifica di un valore. Quando viene modificato il valore di una proprietà, l'oggetto genera l'evento INotifyPropertyChanged.PropertyChanged per indicare la modifica. Le librerie di data binding aggiornano a loro volta gli elementi visualizzati in base alla modifica. Il codice seguente illustra come implementare INotifyPropertyChanged per la proprietà FirstName della classe person.

public class Person : INotifyPropertyChanged
{
    public string? FirstName
    {
        get => _firstName;
        set
        {
            if (string.IsNullOrWhiteSpace(value))
                throw new ArgumentException("First name must not be blank");
            if (value != _firstName)
            {
                _firstName = value;
                PropertyChanged?.Invoke(this,
                    new PropertyChangedEventArgs(nameof(FirstName)));
            }
        }
    }
    private string? _firstName;

    public event PropertyChangedEventHandler? PropertyChanged;
}

L'operatore ?. è chiamato operatore condizionale Null. L'operatore cerca un riferimento Null prima di eseguire la valutazione della parte destra dell'operatore. Se non vengono trovati sottoscrittori dell'evento PropertyChanged, il codice che genera l'evento non viene eseguito. In questo caso, verrà generato NullReferenceException senza eseguire il controllo. Per ulteriori informazioni, vedere events. Questo esempio usa anche il nuovo operatore nameof per convertire il simbolo del nome della proprietà nella rappresentazione di testo. L'uso di nameof può ridurre gli errori nel caso in cui il nome della proprietà sia stato digitato erroneamente.

L'implementazione di INotifyPropertyChanged è quindi un esempio di caso in cui è possibile scrivere codice nelle funzioni di accesso per supportare gli scenari necessari.

Riassumendo

Le proprietà sono una forma di campi intelligenti in una classe o un oggetto. All'esterno dell'oggetto, vengono visualizzate come campi dell'oggetto. Tuttavia, le proprietà possono essere implementate usando l'intera gamma di funzionalità di C#. È possibile specificare la convalida, un'accessibilità diversa, la valutazione lazy o tutti i requisiti necessari negli scenari.