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 auxquelles le code accède se comportent comme des champs. 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 beaucoup de variantes de cette syntaxe, qui sont adaptées aux divers 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, définissez une règle pour la classe Person qui spécifie que le nom ne peut pas être vide, ni contenir d’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é en utilisant une expression throw dans le cadre de la validation de la méthode 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. En revanche, vous ne pouvez pas déclarer une propriété private avec un accesseur public. Les propriétés peuvent également être déclarées comme protected, internal, protected internal ou même private.

Placer le modificateur le plus restrictif sur l’accesseur get est également autorisé. 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 limiter les modifications apportées à une propriété pour qu’elle puisse uniquement être définie 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 uniquement

L’exemple précédent exige que les appelants utilisent le constructeur qui inclut le paramètre FirstName. 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 faire de l’accesseur set un 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 à l’aide du constructeur par défaut, même lorsque ce code ne définit pas la propriété FirstName. À compter de C# 11, vous pouvez demander aux appelants de définir 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 classe Person. Tout d’abord, la déclaration de propriété FirstName inclut le modificateur required. Cela signifie que tout code qui crée un nouveau Person doit définir cette propriété. Deuxièmement, le constructeur qui prend un paramètre firstName a l’attribut System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute. Cet attribut informe le compilateur que ce constructeur définit tousrequired les membres.

Important

Ne confondez pas required avec non-nullable. Définir une required propriété sur null ou default est valide. Si le type est non-nullable, comme string dans ces exemples, le compilateur émet un avertissement.

Les appelants doivent soit utiliser le constructeur avec SetsRequiredMembers, soit 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é peut faire plus que simplement retourner la valeur d’un champ de 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 peuvent utiliser cette classe sans 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

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 propriété Id en utilisant une propriété implémentée automatiquement, mais votre conception n’effectue pas d’appel pour le stockage de la propriété Id. 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 de nameof peut vous éviter des erreurs dues à la saisie incorrecte du nom de 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.