Propriétés

Les propriétés sont des éléments de première classe dans C#. Le langage définit la syntaxe que les développeurs utilisent pour écrire du code qui exprime leur intention de conception avec précision.

Les propriétés se comportent comme des champs lorsqu’ils sont accessibles. Toutefois, contrairement aux champs, les propriétés sont implémentées avec des accesseurs qui définissent quelles instructions sont exécutées au moment de l’accès à une propriété ou de son assignation.

Syntaxe des propriétés

La syntaxe des propriétés est une extension naturelle des champs. Un champ définit un emplacement de stockage :

public class Person
{
    public string FirstName;

    // Omitted for brevity.
}

Une définition de propriété contient les déclarations de l’accesseur get, qui récupère la valeur de cette propriété, et de l’accesseur set, qui assigne cette valeur :

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

    // Omitted for brevity.
}

La syntaxe illustrée ci-dessus est la syntaxe auto property. Le compilateur génère l’emplacement de stockage pour le champ qui enregistre la propriété. Le compilateur implémente également le corps des accesseurs get et set.

Parfois, vous devez initialiser une propriété sur une valeur autre que la valeur par défaut pour son type. C# permet cette opération en définissant une valeur après l’accolade fermante de la propriété. Vous pouvez choisir comme valeur initiale pour la propriété FirstName une chaîne vide au lieu de null. Vous pouvez le spécifier comme indiqué ci-dessous :

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

    // Omitted for brevity.
}

Une initialisation spécifique est pratique surtout pour les propriétés en lecture seule, comme vous le verrez plus loin dans cet article.

Vous pouvez aussi définir le stockage vous-même, de la manière suivante :

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

    // Omitted for brevity.
}

Quand une implémentation de propriété est une expression unique, vous pouvez utiliser des membres expression-bodied pour l’accesseur Get ou Set :

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

    // Omitted for brevity.
}

Cette syntaxe simplifiée est utilisée partout où elle est applicable dans cet article.

La définition de propriété présentée ci-dessus est une propriété en lecture-écriture. Notez la présence du mot clé value dans l’accesseur set. L’accesseur set a toujours un seul paramètre nommé value. L’accesseur get doit retourner une valeur convertible dans le type de la propriété (string, dans cet exemple).

Nous venons de voir les éléments de base de la syntaxe. Il existe de nombreuses variantes différentes qui prennent en charge différentes idiomes de conception. Nous allons les explorer et découvrir les options syntaxiques de chacune.

Validation

Les exemples ci-dessus ont montré un des cas les plus simples de définition de propriété, à savoir une propriété en lecture-écriture sans validation. En écrivant le code souhaité dans les accesseurs get et set, vous pouvez créer de nombreux scénarios différents.

Vous pouvez écrire du code dans l’accesseur set pour garantir que les valeurs représentées par une propriété sont toujours valides. Par exemple, supposons qu’une règle pour la Person classe soit que le nom ne peut pas être vide ou espace blanc. Le code à écrire est le suivant :

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’exemple précédent peut être simplifié à l’aide d’une throw expression dans le cadre de la validation setter de propriété :

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’exemple ci-dessus applique la règle selon laquelle le nom ne doit pas être vide, ni contenir d’espace blanc. Supposons qu’un développeur écrive cette ligne de code :

hero.FirstName = "";

Cette assignation lève une exception ArgumentException. Étant donné qu’un accesseur set de propriété doit avoir un type de retour void, vous signalez les erreurs dans l’accesseur set en levant une exception.

Vous pouvez employer cette même syntaxe pour valider d’autres éléments dans votre scénario. Vous pouvez notamment vérifier les relations entre plusieurs propriétés ou effectuer une validation par rapport à des conditions externes. Toute instruction C# valide peut être utilisée dans un accesseur de propriété.

Contrôle d’accès

Jusqu’ici, nous avons vu uniquement des définitions de propriétés qui sont en lecture-écriture dans des accesseurs publics. Ce n’est pas la seule accessibilité valide pour les propriétés. Vous pouvez créer des propriétés en lecture seule, ou assigner une accessibilité différente aux accesseurs set et get. Supposons que votre classe Person doit uniquement autoriser la modification de la valeur de la propriété FirstName à partir des autres méthodes de cette classe. Vous pouvez alors assigner l’accessibilité private au lieu de public à l’accesseur set :

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

    // Omitted for brevity.
}

À présent, la propriété FirstName est accessible à partir de n’importe quel code, mais elle peut uniquement être assignée à partir de code dans la classe Person.

Vous pouvez ajouter n’importe quel modificateur d’accès restrictif à l’accesseur set ou get. Le modificateur d’accès que vous ajoutez à un accesseur doit être plus restrictif que le modificateur d’accès spécifié dans la définition de propriété. Le code ci-dessus est autorisé, car la propriété FirstName est public, mais l’accesseur set est private. Vous n’avez pas pu déclarer de private propriété avec un public accesseur. Les propriétés peuvent également être déclarées comme protected, internal, protected internal ou même private.

Il est également légal de placer le modificateur plus restrictif sur l’accesseur get . Par exemple, vous pouvez avoir une propriété public, mais restreindre l’accesseur get à private. Ce scénario s’observe rarement dans la pratique.

Lecture seule

Vous pouvez également restreindre les modifications apportées à une propriété afin qu’elle ne puisse être définie que dans un constructeur. Vous pouvez modifier la classe Person, comme suit :

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

    public string FirstName { get; }

    // Omitted for brevity.
}

Init-only

L’exemple précédent nécessite que les appelants utilisent le constructeur qui inclut le FirstName paramètre. Les appelants ne peuvent pas utiliser d’initialiseurs d’objet pour affecter une valeur à la propriété. Pour prendre en charge les initialiseurs, vous pouvez rendre l’accesseur set accesseur accesseur init , comme indiqué dans le code suivant :

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

    public string FirstName { get; init; }

    // Omitted for brevity.
}

L’exemple précédent permet à un appelant de créer un Person constructeur à l’aide du constructeur par défaut, même si ce code ne définit pas la FirstName propriété. À compter de C# 11, vous pouvez exiger que les appelants définissent cette propriété :

public class Person
{
    public Person() { }

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

    public required string FirstName { get; init; }

    // Omitted for brevity.
}

Le code précédent ajoute deux ajouts à la Person classe. Tout d’abord, la déclaration de FirstName propriété inclut le required modificateur. Cela signifie que tout code qui crée un nouveau Person doit définir cette propriété. Deuxièmement, le constructeur qui prend un firstName paramètre a l’attribut System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute . Cet attribut informe le compilateur que ce constructeur définit tous lesrequired membres.

Important

Ne confondez required pas avec les valeurs non nullables. Il est valide pour définir une required propriété null sur ou default. Si le type n’est pas nullable, comme string dans ces exemples, le compilateur émet un avertissement.

Les appelants doivent utiliser le constructeur avec SetsRequiredMembers ou définir la propriété à l’aide FirstName d’un initialiseur d’objet, comme indiqué dans le code suivant :

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();

Propriétés calculées

Une propriété n’a pas besoin de retourner simplement la valeur d’un champ membre. Vous pouvez créer des propriétés qui retournent une valeur calculée. L’objet Person est étendu pour retourner le nom complet, calculé en concaténant le nom et le prénom :

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

    public string LastName { get; set; }

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

L’exemple ci-dessus utilise la fonctionnalité d’interpolation de chaîne pour créer la chaîne mise en forme du nom complet.

Vous pouvez également utiliser un membre expression-bodied, qui constitue un moyen plus succinct de créer la propriété FullName calculée :

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

    public string LastName { get; set; }

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

Les membres expression-bodied utilisent la syntaxe des expressions lambda pour définir des méthodes qui contiennent une seule expression. Ici, cette expression retourne le nom complet de l’objet person.

Propriétés évaluées avec mise en cache

Vous pouvez combiner le concept d’une propriété calculée avec le stockage et créer une propriété évaluée avec mise en cache. Par exemple, vous pouvez mettre à jour la propriété FullName pour que la chaîne soit mise en forme uniquement lors du premier accès à cette propriété :

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;
        }
    }
}

Le code ci-dessus contient toutefois un bogue. Si le code met à jour la valeur de la propriété FirstName ou LastName, le champ fullName qui a été précédemment évalué n’est plus valide. Vous modifiez les accesseurs set des propriétés FirstName et LastName pour que le champ fullName soit recalculé :

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;
        }
    }
}

Dans cette version finale du code, la propriété FullName est évaluée uniquement si cela est nécessaire. Si la version précédemment calculée est valide, elle est utilisée. Si elle n’est plus valide en raison d’un changement d’état, la version est recalculée. Les développeurs qui utilisent cette classe n’ont pas besoin de connaître les détails de l’implémentation. Ces modifications internes n’ont pas d’impact sur l’utilisation de l’objet Person. C’est l’un des principaux avantages d’utiliser des propriétés pour exposer les membres de données d’un objet.

Attachement d’attributs à des propriétés implémentées automatiquement

À compter de C# 7.3, les attributs de champ peuvent être attachés au champ de stockage généré par le compilateur dans les propriétés implémentées automatiquement. Par exemple, considérez une révision de la classe Person qui ajoute une propriété Id unique de type entier. Vous écrivez la Id propriété à l’aide d’une propriété implémentée automatiquement, mais votre conception n’appelle pas la persistance de la Id propriété. NonSerializedAttribute peut être attaché seulement à des champs, et pas à des propriétés. Vous pouvez attacher NonSerializedAttribute au champ de stockage pour la propriété Id en utilisant le spécificateur field: sur l’attribut, comme illustré dans l’exemple suivant :

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}";
}

Cette technique fonctionne pour tout attribut que vous voulez attacher au champ de stockage sur la propriété implémentée automatiquement.

Implémentation de INotifyPropertyChanged

Il existe un dernier scénario où vous devrez écrire du code dans un accesseur de propriété : pour prendre en charge l’interface INotifyPropertyChanged, qui notifie les changements de valeurs aux clients de liaison de données. Quand la valeur d’une propriété change, l’objet déclenche l’événement INotifyPropertyChanged.PropertyChanged pour signaler le changement. Les bibliothèques de liaison de données mettent ensuite à jour les éléments d’affichage en fonction de cette modification. Le code ci-dessous montre comment implémenter INotifyPropertyChanged pour la propriété FirstName de la 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’opérateur ?. est appelé opérateur conditionnel Null. Il recherche une référence null avant d’évaluer le côté droit de l’opérateur. Au final, s’il n’y a pas d’abonné à l’événement PropertyChanged, le code devant déclencher l’événement n’est pas exécuté. Dans ce cas précis, il lèverait une exception NullReferenceException sans cette vérification. Pour plus d’informations, consultez events. Cet exemple utilise également le nouvel opérateur nameof pour convertir le symbole de nom de propriété en sa représentation textuelle. L’utilisation nameof peut réduire les erreurs dans lesquelles vous avez mal tapé le nom de la propriété.

L’implémentation de INotifyPropertyChanged est un autre exemple de cas où vous pouvez écrire du code dans vos accesseurs pour prendre en charge les scénarios souhaités.

Résumé

Les propriétés sont une forme de champs intelligents dans une classe ou un objet. De l’extérieur de l’objet, elles apparaissent sous la forme de champs dans l’objet. Toutefois, les propriétés peuvent être implémentées avec toutes les fonctionnalités C#. Vous pouvez écrire du code qui remplit les exigences de validation, d’accessibilité, d’évaluation différée ou toute autre exigence requise dans vos scénarios.