web-dev-qa-db-fra.com

Les pointeurs peuvent-ils être utilisés pour modifier le champ en lecture seule? Mais pourquoi?

Je viens de C++ et trouve un comportement très différent entre les pointeurs en C++ et C #.

Je trouve étonnamment que ce code compile ... Et même ... Fonctionne parfaitement.

class C
{
    private readonly int x = 0;
    unsafe public void Edit()
    {
        fixed (int* p = &x)
        {
           *p = x + 1;
        }
        Console.WriteLine($"Value: {x}");
    }
}

Cela me rend très perplexe, même en C++, nous avons un mécanisme pour protéger les objets const (const en C++ presque la même chose que readonly en C #, pas const en C #), car nous n'avons pas assez de raisons de modifier arbitrairement les valeurs const via des pointeurs.

En explorant plus loin, je trouve qu'il n'y a pas d'équivalent aux pointeurs const de bas niveau de C++ en C #, qui seront quelque chose comme:

readonly int* p

en C # s'il en avait un.

Alors p ne peut lire que l'objet pointé et ne peut pas y écrire.

Et pour les objets const, C # interdit les tentatives de récupération de leur adresse.

En C++, toute tentative de modification de const est soit une erreur de compilation, soit un comportement indéfini. En C #, je ne sais pas s'il y a une possibilité que nous puissions utiliser cela.

Ma question est donc:

  1. Ce comportement est-il vraiment bien défini? Bien que je sache en C #, il n'y a pas de concept comme UB en C++
  2. Si oui, comment dois-je l'utiliser? Ou ne jamais l'utiliser?

Remarque: Dans la section des commentaires: rejeter const en C++ n'est pas lié à cette question, car il n'est valide que lorsque l'objet pointé lui-même n'est pas const, sinon c'est UB. De plus, je parle essentiellement de C # et du comportement au moment de la compilation.

Vous pouvez me demander de fournir plus de détails si vous ne comprenez pas entièrement la question. J'ai trouvé que la plupart des gens ne peuvent pas comprendre cela correctement, c'est peut-être ma faute de ne pas le dire clairement.

8
John Ding

J'aimerais pouvoir dire que vous n'obtenez ce comportement inattendu qu'à cause du mot-clé unsafe. Malheureusement, il existe au moins deux autres façons de modifier ce champ en lecture seule, qui ne nécessitent même pas de non-sécurité. Vous pouvez trouver plus d'informations sur ces méthodes dans le blog de Joe Duffy 'When is a readonly field not readonly' .

En superposant un champ avec une structure non en lecture seule:

class C
{
    private readonly int x = 0;

    class other_C
    {
        public int x;
    }

    [StructLayout(LayoutKind.Explicit)]
    class Overlap_them
    {
        [FieldOffset(0)] public C actual;
        [FieldOffset(0)] public other_C sneaky;
    }

    public void Edit()
    {
        Overlap_them overlapper = new Overlap_them();
        overlapper.actual = this;
        overlapper.sneaky.x = 1;
        Console.WriteLine($"Value: {x}");
    }
}

Et en aliasant accidentellement this avec un paramètre ref (celui-ci se fait de l'extérieur):

class C
{
    private readonly int x = 0;

    public C(int X)
    {
        x = X;
    }

    public void Edit(ref C foo)
    {
        foo = new C(1);
        Console.WriteLine($"Value: {x}");
    }
}

private static void Main()
{
    var c = new C(0);
    c.Edit(ref c);
}

Les trois méthodes sont parfaitement définies en C # que vous devriez éviter comme la peste. Imaginez simplement un code de débogage avec des problèmes d'alias complexes provenant de l'extérieur de votre classe. Malheureusement, il n'existe toujours pas de solution satisfaisante pour arrêter les problèmes d'alias en C #.

2
TamaMcGlinn