Traduction

Ceci est la traduction la plus fidèle possible de l'article de Jon Skeet, Why Properties MatterWhy Properties Matter.

Quels choix allons-nous examiner ?

Il vaut mieux être clair sur le fait que cet article n'aborde pas la question de savoir si quelque chose doit être une méthode ou une propriété. Ce n'est pas toujours une décision évidente, et une discussion sur les mérites de l'une ou de l'autre serait intéressante mais détournerait l'attention de l'objet de cet article.

Le choix dont je parle dans cet article concerne la façon dont vous exposez les données : comme une propriété ou comme un champ non privé. C'est aussi simple que ça. Mon opinion personnelle est que presque aucun champ ne devrait être non privé. Je ferai une petite exception pour les champs statiques en lecture seule comme String.Empty, mais c'est (à peu près) tout.

Alors, si vous avez un champ non privé, pourquoi devriez-vous le rendre privé et exposer plutôt une propriété ? J'ai réparti mes arguments en trois catégories : problèmes de compatibilité, avantages pratiques de l'utilisation des propriétés, et des considérations plus philosophiques. Bien que les aspects philosophiques semblent moins importants, ce sont en fait ceux qui me causent le plus de préoccupation.

Problèmes de compatibilité

Les gens disent souvent que ça ne pose pas de problème d'utiliser des champs pour des données simples, et de les remplacer par des propriétés quand cela devient nécessaire. Mais...

Vous perdez la compatibilité binaire

Ce problème n'en est pas vraiment un si vous travaillez sur un projet pour lequel tous les binaires sont toujours livrés ensemble, ou si le champ est actuellement internal et que vous n'exposez pas les membres internes à d'autres assemblies. Cependant, si vous développez une bibliothèque de classes qui pourrait avoir besoin d'être mise à jour sans recompiler le code qui l'utilise, vous devriez comprendre que changer un champ en propriété est un breaking change. Cela inclut aussi la sérialisation.

Vous perdez la compatibilité des sources

On peut utiliser une propriété partout où on peut utiliser un champ, non ? Faux. On peut utiliser un champ pour un paramètre ref, alors qu'on ne peut pas (du moins en C#) utiliser une propriété de cette manière. Il y a d'autres cas subtils où il y a une différence entre les champs et les propriétés. Il est vrai que certains (la plupart ?) de ces cas concernent des structures mutables - une autre décision de conception à éviter dans presque toutes les situations - mais changer un champ en propriété peut parfois changer le comportement du code sans même générer un avertissement ou une erreur. Voici un exemple qui illustre cela, qui implique une structure mutable :

 
Sélectionnez
using System;

struct MutableStruct
{
    public int Value { get; set; }
    
    public void SetValue(int newValue)
    {
        Value = newValue;
    }
}

class MutableStructHolder
{
    public MutableStruct Field;
    public MutableStruct Property { get; set; }
}

class Test
{    
    static void Main(string[] args)
    {
        MutableStructHolder holder = new MutableStructHolder();
        // Affecte la valeur de  holder.Field
        holder.Field.SetValue(10);
        // Récupère holder.Property comme une copie et modifie la copie
        holder.Property.SetValue(10);
        
        Console.WriteLine(holder.Field.Value);
        Console.WriteLine(holder.Property.Value);
    }
} 

Les résultats sont différents : 10 pour la première ligne, ce qui montre que l'appel à holder.Field.SetValue a bien modifié la valeur dans holder.Field ; 0 pour la deuxième ligne, ce qui montre que l'appel à holder.Property.SetValue n'a pas changé la valeur dans holder.Property.

Si les lignes qui modifient les valeurs étaient simplement holder.Field.Value = 10 et holder.Property.Value = 10, le changement du champ en propriété serait un breaking change - la seconde ne compilerait pas, parce que l'expression holder.Property n'est pas classifiée comme une variable. C'est mieux que de changer le comportement silencieusement, mais ça reste un changement fâcheux.

Vous perdez la compatibilité avec la réflexion

La plupart du temps, on n'utilise pas la réflexion pour accéder à des membres auxquels on peut accéder directement. Cependant, si quelqu'un le fait, ce code ne fonctionnera plus s'il s'attend à trouver un champ et que celui-ci a été transformé en propriété de façon inattendue.

Avantages pratiques des propriétés

Il y a des circonstances où vous pourriez utiliser des champs non privés, parce que pour une raison ou une autre vous n'êtes pas concerné par les problèmes de compatibilité ci-dessus. Cependant, il y a quand même des avantages à utiliser des propriétés même pour des situations triviales :

  • les propriétés permettent une granularité d'accès plus fine. Vous voulez que la propriété soit publiquement accessible en lecture, mais que l'accès en écriture soit protégé ? Pas de problème (du moins à partir de C# 2) ;
  • vous voulez arrêter le débogueur à chaque fois que la valeur change ? Il suffit de mettre un point d'arrêt dans l'accesseur set ;
  • vous voulez journaliser tous les accès ? Il suffit de mettre le code de journalisation dans l'accesseur get ;
  • les propriétés sont utilisées dans la liaison de données , pas les champs.

Aucun des points ci-dessus ne correspond à l'usage traditionnel des propriétés qui consiste à ajouter de la logique, mais tous sont difficiles/impossibles à réaliser avec des simples champs. Vous pourriez utiliser des propriétés au cas par cas selon les besoins, mais pourquoi ne pas être cohérent dès le départ ? La question se pose encore moins avec les propriétés auto-implémentées de C# 3.

La raison philosophique de n'exposer que des propriétés

Pour chaque type que vous écrivez, vous devriez examiner son interface avec le reste du monde (y compris les classes dans le même assembly). C'est la description de ce qu'il rend disponible, son apparence externe. L'implémentation ne devrait pas faire partie de cette description au-delà de ce qui est absolument nécessaire (c'est pourquoi je préfère la composition à l'héritage, quand le choix a un sens - l'héritage expose ou limite généralement les implémentations possibles).

Une propriété communique l'idée que « je mettrai une valeur à votre disposition, ou j'accepterai une valeur que vous me fournirez ». Ce n'est pas un concept d'implémentation, c'est un concept d'interface. Un champ, au contraire, communique l'implémentation - il dit « ce type représente une valeur de cette façon très spécifique ». Il n'y a pas d'encapsulation, c'est le format de stockage brut. C'est une des raisons pour lesquelles les champs ne font pas partie des interfaces - ils n'y ont pas leur place, car ils indiquent comment quelque chose est réalisé plutôt que ce qui est réalisé.

Je suis plutôt d'accord sur le fait que dans beaucoup de cas, des champs pourraient être utilisés sans problème pendant la durée de vie d'une application. Il est juste difficile de savoir à l'avance quels sont ces cas, et cela viole toujours le principe de conception qui consiste à ne pas exposer l'implémentation

Cas vraiment exceptionnels

Chaque règle a bien sûr des exceptions. Rico Mariani est un type très malin, qui s'y connaît très bien en matière de performance, et qui connaît certainement les principes de conception aussi. Dans un billet de blog à propos des primitives graphiquesPerformance Quiz #11: Ten Questions on Value-Based Programming : Solution, il détaille pourquoi il a écrit une structure mutable de plus de 16 octets, qui expose tous ses champs publiquement - enfreignant trois importantes recommandations de conception dans le même type. N'utilisez pas cet exemple comme une raison pour exposer des champs publiquement - en tout cas, pas à moins que les mêmes justifications s'appliquent aussi. Les experts peuvent se permettre d'enfreindre les règles dans quelques rares cas, parce qu'ils savent exactement dans quoi ils s'engagent. Les simples mortels comme moi - et comme la plupart des lecteurs de cette page, probablement - devraient au moins passer une nuit blanche à réfléchir à leurs raisons avant d'enfreindre ces recommandations.

Il est intéressant de noter que l'une des raisons de Rico pour exposer des champs est en fait la même que ma raison précédente pour préférer des propriétés - cela expose l'implémentation. Occasionnellement, il est souhaitable de donner à l'appelant la garantie que l'implémentation restera toujours telle qu'elle est - et c'était le cas ici. Je dois dire que je n'ai jamais eu besoin de fournir ce genre de garanties dans le code que j'ai écrit, mais on peut trouver toutes sortes de situations.

Un autre cas exceptionnel que j'ai utilisé est de rendre des champs internal dans un type imbriqué privé. Puisque le type est privé, le fait qu'il s'agit d'un champ ne sera jamais exposé à d'autres types que celui dans lequel il est imbriqué - et qui « possède » effectivement le code du type imbriqué de toute façon. Avec C# 3, j'utiliserai à l'avenir des propriétés auto-implémentées de toute façon - avant c'était juste plus pratique d'utiliser des champs.

Conclusion

Dans presque tous les cas, les champs devraient être privés. Pas seulement non publics, mais privés. Avec les propriétés auto-implémentées en C# 3, ça ne coûte pratiquement plus rien en lisibilité ou en quantité de code d'utiliser une propriété plutôt qu'un champ. En de rares occasions, vous pourriez trouver une vraie bonne raison d'utiliser un champ, mais réfléchissez-y longuement et sérieusement avant de valider ce choix, surtout si vous allez l'exposer au-delà de l'accès internal.

Remerciements

Je tiens à remercier Jon Skeet pour son aimable autorisation de traduire cet article, ainsi que ClaudeLELOUP pour sa relecture attentive et ses corrections.