web-dev-qa-db-fra.com

Est-ce que l'utilisation de champs publics en lecture seule pour des structures immuables fonctionne?

Est-ce une bonne façon de déclarer des structures immuables?

public struct Pair
{
    public readonly int x;
    public readonly int y;

    // Constructor and stuff
}

Je ne peux pas penser à la raison pour laquelle cela poserait des problèmes, mais je voulais juste demander à être sûr.

Dans cet exemple, j'ai utilisé des ints. Et si j'utilisais une classe à la place, mais cette classe est également immuable, comme ça? Cela devrait bien fonctionner aussi, non?

public struct Pair
{
    public readonly (immutableClass) x;
    public readonly (immutableClass) y;

    // Constructor and stuff
}

(En plus: je comprends que l'utilisation de Propriétés est plus généralisable et permet de changer, mais cette structure est destinée à ne stocker que deux valeurs. Je suis juste intéressé par la question d'immuabilité ici.)

58
Mike

Si vous allez utiliser des structures, il est recommandé de les rendre immuables.

Rendre tous les champs en lecture seule est un excellent moyen d'aider (1) à documenter que la structure est immuable et (2) à prévenir les mutations accidentelles.

Cependant, il y a une ride, qui en fait dans une étrange coïncidence, je prévoyais de bloguer la semaine prochaine. C'est-à-dire: en lecture seule sur un champ struct est un mensonge . On s'attend à ce qu'un champ en lecture seule ne puisse pas changer, mais bien sûr il peut. "en lecture seule" sur un champ struct est la déclaration qui écrit des chèques sans argent dans son compte. Une structure ne possède pas son stockage, et c'est ce stockage qui peut muter.

Par exemple, prenons votre structure:

public struct Pair
{
    public readonly int x;
    public readonly int y;
    public Pair(int x, int y)
    {
        this.x = x;
        this.y = y;
    }
    public void M(ref Pair p)
    {
        int oldX = x;
        int oldY = y;
        // Something happens here
        Debug.Assert(x == oldX);
        Debug.Assert(y == oldY);
    }
}

Y a-t-il quelque chose qui peut se produire à "quelque chose se passe ici" qui provoque la violation des assertions de débogage? Sûr.

    public void M(ref Pair p)
    {
        int oldX = this.x;
        int oldY = this.y;
        p = new Pair(0, 0);
        Debug.Assert(this.x == oldX);
        Debug.Assert(this.y == oldY);
    }
...
    Pair myPair = new Pair(10, 20);
    myPair.M(ref myPair);

Et maintenant que se passe-t-il? L'affirmation est violée! "ceci" et "p" se réfèrent au même emplacement de stockage. L'emplacement de stockage est muté, et donc le contenu de "this" est muté car c'est la même chose. La structure n'est pas en mesure d'appliquer la lecture seule de x et y car la structure n'est pas propriétaire du stockage; le stockage est une variable locale qui est libre de muter autant qu'il le souhaite.

Vous ne pouvez pas compter sur l'invariant qu'un champ en lecture seule dans une structure ne change jamais; la seule chose sur laquelle vous pouvez compter est que vous ne pouvez pas écrire de code qui le modifie directement. Mais avec un petit travail sournois comme celui-ci, vous pouvez indirectement changer tout ce que vous voulez.

Voir également l'excellent article de blog de Joe Duffy sur cette question:

http://joeduffyblog.com/2010/07/01/when-is-a-readonly-field-not-readonly/

113
Eric Lippert

Cela le rendrait en effet immuable. Je suppose que vous feriez mieux d'ajouter un constructeur.
Si tous ses membres sont également immuables, cela le rendrait entièrement immuable. Il peut s'agir de classes ou de valeurs simples.

5
Mr47

Depuis C # 7.2, vous pouvez maintenant déclarer une structure entière immuable:

public readonly struct Pair
{
    public int x;
    public int y;

    // Constructor and stuff
}

Cela aura le même effet que de marquer tous les champs comme readonly, et documentera également au compilateur lui-même que la structure est immuable. Cela augmentera les performances des zones où la structure est utilisée en réduisant le nombre de copies défensives effectuées par le compilateur.

Comme indiqué dans réponse d'Eric Lippert , cela n'empêche pas la structure elle-même d'être entièrement réaffectée, et donc de faire en sorte que ses champs changent sous vous. Soit en passant par valeur, soit en utilisant le nouveau modificateur de paramètre in, vous pouvez éviter cela:

public void DoSomething(in Pair p) {
    p.x = 0; // illegal
    p = new Pair(0, 0); // also illegal
}
4
TheHansinator

Le compilateur interdira l'attribution aux champs readonly ainsi qu'aux propriétés en lecture seule.

Je recommande d'utiliser des propriétés en lecture seule principalement pour des raisons d'interface publique et de liaison de données (qui ne fonctionnera pas sur les champs). Si c'était mon projet, j'exigerais que si la structure/classe soit publique. Si elle doit être interne à un assembly ou privée à une classe, je pourrais la négliger au début et la refactoriser en propriétés en lecture seule plus tard.

1
Joel B Fant