Puis-je modifier un champ privé en lecture seule en C # à l'aide de la réflexion?

115

Je me demande, étant donné que beaucoup de choses peuvent être faites en utilisant la réflexion, puis-je changer un champ privé en lecture seule après que le constructeur ait terminé son exécution?
(note: juste de la curiosité)

public class Foo
{
 private readonly int bar;

 public Foo(int num)
 {
  bar = num;
 }

 public int GetBar()
 {
  return bar;
 }
}

Foo foo = new Foo(123);
Console.WriteLine(foo.GetBar()); // display 123
// reflection code here...
Console.WriteLine(foo.GetBar()); // display 456
Ron Klein
la source

Réponses:

151

Vous pouvez:

typeof(Foo)
   .GetField("bar",BindingFlags.Instance|BindingFlags.NonPublic)
   .SetValue(foo,567);
Philippe Leybaert
la source
2
Vous avez tout à fait raison, bien sûr. Mes excuses. Et oui, j'ai essayé, mais j'ai essayé de définir une propriété en lecture seule directement, sans utiliser de champ de sauvegarde. Ce que j'ai tenté n'avait aucun sens. Votre solution fonctionne parfaitement bien (testée à nouveau, correctement cette fois)
Sage Pourpre
comment pouvons-nous faire cela avec moq?
l --''''''--------- '' '' '' '' '' ''
Dans dotnet core 3.0, ce n'est plus possible. Une exception System.FieldAccessException est lancée disant: "Impossible de définir le champ statique initonly 'bar' après l'initialisation du type 'Foo'."
David Perfors
54

La chose évidente est de l'essayer:

using System;
using System.Reflection;

public class Test
{
    private readonly string foo = "Foo";

    public static void Main()
    {
        Test test = new Test();
        FieldInfo field = typeof(Test).GetField
            ("foo", BindingFlags.Instance | BindingFlags.NonPublic);
        field.SetValue(test, "Hello");
        Console.WriteLine(test.foo);
    }        
}

Cela fonctionne très bien. (Java a des règles différentes, ce qui est intéressant - vous devez définir explicitement le Fieldpour qu'il soit accessible, et cela ne fonctionnera que pour les champs d'instance de toute façon.)

Jon Skeet
la source
4
Ahmed - mais nous n'utilisons pas la langue pour le faire, donc la spécification de la langue n'obtient pas de vote ...
Marc Gravell
4
Ouais - il y a beaucoup de choses à faire qui "cassent" ce que la langue veut. Vous pouvez exécuter des initialiseurs de type plusieurs fois, par exemple.
Jon Skeet
28
Je note également que ce n'est pas parce que vous pouvez dans une mise en œuvre aujourd'hui que vous pouvez sur chaque mise en œuvre pour tous les temps. Je ne connais aucun endroit où nous documentons que les champs en lecture seule doivent être modifiables par réflexion. Autant que je sache, une implémentation conforme de la CLI est parfaitement libre d'implémenter des champs en lecture seule de sorte qu'ils lancent des exceptions lorsqu'ils sont mutés via la réflexion une fois le constructeur terminé.
Eric Lippert
3
Ce n'est pas bon cependant, car il y a des cas où j'ai besoin d'étendre une classe plus qu'elle n'a été conçue à l'origine. Il devrait toujours y avoir un moyen de remplacer l'encapsulation lorsqu'elle est bien planifiée. Les seules alternatives sont sanglantes et parfois impossibles sans réécrire une partie du framework.
drifter
5
@drifter: À ce stade, vous vous ouvrez à un monde de douleur. Vous vous fiez aux détails d'implémentation actuels qui peuvent facilement changer dans une version future.
Jon Skeet
11

Je suis d'accord avec les autres réponses dans la mesure où cela fonctionne en général et en particulier avec le commentaire de E. Lippert selon lequel ce n'est pas un comportement documenté et donc pas un code à l'épreuve du temps.

Cependant, nous avons également remarqué un autre problème. Si vous exécutez votre code dans un environnement avec des autorisations restreintes, vous pouvez obtenir une exception.

Nous venons d'avoir un cas où notre code fonctionnait correctement sur nos machines, mais nous avons reçu un message VerificationExceptionlorsque le code fonctionnait dans un environnement restreint. Le coupable était un appel de réflexion au poseur d'un champ en lecture seule. Cela a fonctionné lorsque nous avons supprimé la restriction en lecture seule de ce champ.

Andreas
la source
2
Serait intéressé de savoir quels environnements lancent VerificationException
Sergey Zhukov
4

Vous avez demandé pourquoi vous voudriez rompre l'encapsulation comme ça.

J'utilise une classe d'assistance d'entité pour hydrater les entités. Cela utilise la réflexion pour obtenir toutes les propriétés d'une nouvelle entité vide, fait correspondre le nom de la propriété / du champ à la colonne dans le jeu de résultats, et le définit à l'aide de propertyinfo.setvalue ().

Je ne veux pas que quelqu'un d'autre puisse changer la valeur, mais je ne veux pas non plus consacrer tous les efforts nécessaires pour personnaliser les méthodes d'hydratation du code pour chaque entité.

Mes nombreux processus stockés renvoient des ensembles de résultats qui ne correspondent pas directement aux tables ou aux vues, donc les ORM de génération de code ne font rien pour moi.

Nécroposter
la source
1
Je l'ai également utilisé pour dépasser certaines limitations de l'API où la valeur était soit codée en dur, soit nécessitait un fichier de configuration que je ne pourrais pas fournir. (Taille de fichier WSE 2.0 pour les pièces jointes DIME lorsque les assemblys ont été chargés via la réflexion, par exemple)
StingyJack
3

Un autre moyen simple de le faire en utilisant unsafe (ou vous pouvez passer le champ à une méthode C via DLLImport et le définir ici).

using System;

namespace TestReadOnly
{
    class Program
    {
        private readonly int i;

        public Program()
        {
            i = 66;
        }

        private unsafe void ForceSet()
        {
            fixed (int* ptr = &i) *ptr = 123;
        }

        static void Main(string[] args)
        {
            var program = new Program();
            Console.WriteLine("Contructed Value: " + program.i);
            program.ForceSet();
            Console.WriteLine("Forced Value: " + program.i);
        }
    }
}
zezba9000
la source
2

La réponse est oui, mais plus important encore:

Pourquoi voudriez-vous? Briser intentionnellement l'encapsulation me semble une idée horriblement mauvaise.

Utiliser la réflexion pour changer un champ en lecture seule ou constant revient à combiner la loi des conséquences imprévues avec la loi de Murphy .

Powerlord
la source
1
la réponse est «juste de la curiosité», comme mentionné ci-dessus.
Ron Klein
Il y a des moments où je me retrouve à devoir faire juste cette astuce pour écrire le meilleur code possible. Affaire au point - elegantcode.com/2008/04/17/testing-a-membership-provider
sparker
3
J'utilise aussi cette astuce dans les projets de test unitaire pour remplacer les valeurs par défaut qui ne devraient être modifiées dans aucun code métier ...
Koen
Et j'essayais de définir des propriétés internes privées dans les bibliothèques de classes de base à des fins de test, en particulier l'API Membership, où MS marquait tout comme privé, interne et sans setters sur les propriétés. Il y a des cas pour cela, mais vous avez raison si la question s'applique à une API sous votre contrôle
Chad Grant
3
Il y a des situations où cela a du sens. Les mappeurs O / R comme NHibernate le font tout le temps pour l'hydratation, car c'est la seule façon de mettre en œuvre l'encapsulation de données pour les entités persistantes en premier lieu.
chris
2

Ne fais pas ça.

Je viens de passer une journée à corriger un bug surréaliste où les objets pourraient ne pas être de leur propre type déclaré.

La modification du champ en lecture seule a fonctionné une fois. Mais si vous essayez de le modifier à nouveau, vous obtiendrez des situations comme celle-ci:

SoundDef mySound = Reflection_Modified_Readonly_SoundDef_Field;
if( !(mySound is SoundDef) )
    Log("Welcome to impossible-land!"); //This would run

Alors ne le fais pas.

C'était sur le runtime Mono (moteur de jeu Unity).

Tynan Sylvester
la source
2
FYI - Le moteur Unity ne peut pas être utilisé pour répondre efficacement à des questions spécifiques au langage C # approfondies comme celle-ci car Unity effectue sa propre compilation de C # comme si les .cs étaient des scripts, dans un sens. Je ne dis pas que votre argument n'est pas valide, mais il est certainement spécifique à Unity Engine et C # ensemble.
Dave Jellison
0

Je veux juste ajouter que si vous devez faire cela pour les tests unitaires, vous pouvez utiliser:

A) La classe PrivateObject

B) Vous aurez toujours besoin d'une instance PrivateObject, mais vous pouvez générer des objets "Accessor" avec Visual Studio. Guide pratique: régénérer les accesseurs privés

Si vous définissez des champs privés d'un objet dans votre code en dehors des tests unitaires, ce serait une instance de "code sentir" Je pense que la seule autre raison pour laquelle vous voudriez faire cela est peut-être si vous traitez avec un tiers bibliothèque et vous ne pouvez pas modifier le code de la classe cible. Même dans ce cas, vous voudrez probablement contacter le tiers, expliquer votre situation et voir s'il ne va pas de l'avant et changer son code pour répondre à vos besoins.

Dudeman3000
la source