c# appelé - Comment utiliser la réflexion pour invoquer une méthode privée?




d'une être (9)

Il y a un groupe de méthodes privées dans ma classe, et j'ai besoin d'en appeler une dynamiquement en fonction d'une valeur d'entrée. Le code appelant et les méthodes cibles sont tous les deux dans la même instance. Le code ressemble à ceci:

MethodInfo dynMethod = this.GetType().GetMethod("Draw_" + itemType);
dynMethod.Invoke(this, new object[] { methodParams });

Dans ce cas, GetMethod() ne retournera pas les méthodes privées. Quel BindingFlags dois-je fournir à GetMethod() afin qu'il puisse localiser des méthodes privées?


Answers

Etes-vous absolument sûr que cela ne peut pas être fait par héritage? La réflexion est la toute dernière chose que vous devriez regarder quand vous résolvez un problème, cela rend le refactoring, la compréhension de votre code, et toute analyse automatisée plus difficile.

Il semble que vous devriez juste avoir une classe DrawItem1, DrawItem2, etc qui remplace votre dynMethod.


Microsoft a récemment modifié l'API de réflexion rendant la plupart de ces réponses obsolètes. Ce qui suit devrait fonctionner sur les plates-formes modernes (y compris Xamarin.Forms et UWP):

obj.GetType().GetTypeInfo().GetDeclaredMethod("MethodName").Invoke(obj, yourArgsHere);

Ou comme méthode d'extension:

public static object InvokeMethod<T>(this T obj, string methodName, params object[] args)
{
    var type = typeof(T);
    var method = type.GetTypeInfo().GetDeclaredMethod(methodName);
    return method.Invoke(obj, args);
}

Remarque:

  • Si la méthode désirée est dans une superclasse d' obj le générique T doit être explicitement défini sur le type de la superclasse.

  • Si la méthode est asynchrone, vous pouvez utiliser await (Task) obj.InvokeMethod(…) .


BindingFlags.NonPublic ne renverra aucun résultat par lui-même. Comme il s'avère, la combinaison avec BindingFlags.Instance fait l'affaire.

MethodInfo dynMethod = this.GetType().GetMethod("Draw_" + itemType, 
    BindingFlags.NonPublic | BindingFlags.Instance);

Ne pourriez-vous pas avoir une méthode Draw différente pour chaque type que vous voulez dessiner? Appelez ensuite la méthode Draw surchargée passant dans l'objet de type itemType à dessiner.

Votre question ne précise pas si itemType fait vraiment référence à des objets de types différents.


La réflexion surtout sur les membres privés est mauvaise

  • La réflexion brise la sécurité de type. Vous pouvez essayer d'invoquer une méthode qui n'existe plus (ou plus), ou avec les mauvais paramètres, ou avec trop de paramètres, ou pas assez ... ou même dans le mauvais ordre (celui-ci mon préféré :)). Par ailleurs, le type de retour pourrait également changer.
  • La réflexion est lente.

La réflexion des membres privés rompt le principe d' encapsulation et expose ainsi votre code à ce qui suit:

  • Augmentez la complexité de votre code car il doit gérer le comportement interne des classes. Ce qui est caché doit rester caché.
  • Rend votre code facile à casser car il compilera mais ne fonctionnera pas si la méthode a changé son nom.
  • Rend le code privé facile à rompre parce que s'il est privé, il n'est pas destiné à être appelé de cette façon. Peut-être que la méthode privée attend un état interne avant d'être appelée.

Et si je dois le faire quand même?

Il y a donc des cas, lorsque vous dépendez d'un tiers ou que vous avez besoin d'une API non exposée, vous devez réfléchir. Certains l'utilisent également pour tester certaines classes qu'ils possèdent mais qu'ils ne veulent pas changer d'interface pour donner accès aux membres internes juste pour les tests.

Si vous le faites, faites-le bien

  • Atténuer le facile à casser:

Pour atténuer le problème facile à résoudre, le mieux est de détecter toute rupture potentielle en testant dans des tests unitaires qui seraient exécutés dans une construction d'intégration continue ou similaire. Bien sûr, cela signifie que vous utilisez toujours le même assemblage (qui contient les membres privés). Si vous utilisez une charge dynamique et une réflexion, vous aimez jouer avec le feu, mais vous pouvez toujours attraper l'exception que l'appel peut produire.

  • Atténuer la lenteur de la réflexion:

Dans les versions récentes de .Net Framework, CreateDelegate a un facteur de 50 par rapport à l'appel MethodInfo:

// The following should be done once since this does some reflection
var method = this.GetType().GetMethod("Draw_" + itemType, 
  BindingFlags.NonPublic | BindingFlags.Instance);

// Here we create a Func that targets the instance of type which has the 
// Draw_ItemType method
var draw = (Func<TInput, Output[]>)_method.CreateDelegate(
                 typeof(Func<TInput, TOutput[]>), this);

draw appels sera environ 50 fois plus rapide que l'utilisation de MethodInfo.Invoke draw comme un Func standard comme ça:

var res = draw(methodParams);

Cochez ce post pour voir le benchmark sur différentes invocations de méthode


Changez simplement votre code pour utiliser la version surchargée de GetMethod qui accepte BindingFlags:

MethodInfo dynMethod = this.GetType().GetMethod("Draw_" + itemType, 
    BindingFlags.NonPublic | BindingFlags.Instance);
dynMethod.Invoke(this, new object[] { methodParams });

Voici la documentation de l'énumération BindingFlags .


Et si vous voulez vraiment vous mettre en difficulté, facilitez-le en écrivant une méthode d'extension:

static class AccessExtensions
{
    public static object call(this object o, string methodName, params object[] args)
    {
        var mi = o.GetType ().GetMethod (methodName, System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance );
        if (mi != null) {
            return mi.Invoke (o, args);
        }
        return null;
    }
}

Et utilisation:

    class Counter
    {
        public int count { get; private set; }
        void incr(int value) { count += value; }
    }

    [Test]
    public void making_questionable_life_choices()
    {
        Counter c = new Counter ();
        c.call ("incr", 2);             // "incr" is private !
        c.call ("incr", 3);
        Assert.AreEqual (5, c.count);
    }

Invoque n'importe quelle méthode malgré son niveau de protection sur l'instance d'objet. Prendre plaisir!

public static object InvokeMethod(object obj, string methodName, params object[] methodParams)
{
    var methodParamTypes = methodParams?.Select(p => p.GetType()).ToArray() ?? new Type[] { };
    var bindingFlags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static;
    MethodInfo method = null;
    var type = obj.GetType();
    while (method == null && type != null)
    {
        method = type.GetMethod(methodName, bindingFlags, Type.DefaultBinder, methodParamTypes, null);
        type = type.BaseType;
    }

    return method?.Invoke(obj, methodParams);
}

Quelle est la cause?

Bottom Line

Vous essayez d'utiliser quelque chose qui est null (ou Nothing dans VB.NET). Cela signifie que vous l'avez défini sur null ou que vous ne l'avez jamais défini.

Comme toute chose, null fait passer. Si elle est null dans la méthode "A", il se peut que la méthode "B" ait passé une valeur null à la méthode "A".

Le reste de cet article va plus en détail et montre les erreurs que beaucoup de programmeurs font souvent, ce qui peut conduire à une NullReferenceException .

Plus précisement

Le runtime lançant une NullReferenceException signifie toujours la même chose: vous essayez d'utiliser une référence, et la référence n'est pas initialisée (ou une fois initialisée, mais n'est plus initialisée).

Cela signifie que la référence est null et que vous ne pouvez pas accéder aux membres (comme les méthodes) via une référence null . Le cas le plus simple:

string foo = null;
foo.ToUpper();

Cela lancera une NullReferenceException à la deuxième ligne car vous ne pouvez pas appeler la méthode d'instance ToUpper() sur une référence de string pointant vers null .

Débogage

Comment trouvez-vous la source d'une NullReferenceException ? En plus de regarder l'exception elle-même, qui sera lancée exactement à l'endroit où elle se produit, les règles générales de débogage dans Visual Studio s'appliquent: placer des points d'arrêt stratégiques et inspecter vos variables , soit en plaçant la souris sur leurs noms, Quick) Regarder la fenêtre ou utiliser les différents panneaux de mise au point comme Locals et Autos.

Si vous voulez savoir où la référence est ou n'est pas définie, faites un clic droit sur son nom et sélectionnez "Trouver toutes les références". Vous pouvez ensuite placer un point d'arrêt à chaque emplacement trouvé et exécuter votre programme avec le débogueur joint. Chaque fois que le débogueur interrompt un tel point d'arrêt, vous devez déterminer si vous pensez que la référence est non nulle, inspecter la variable et vérifier qu'elle pointe vers une instance lorsque vous le souhaitez.

En suivant le flux du programme de cette façon, vous pouvez trouver l'emplacement où l'instance ne doit pas être nulle, et pourquoi elle n'est pas correctement définie.

Exemples

Quelques scénarios courants où l'exception peut être levée:

Générique

ref1.ref2.ref3.member

Si ref1 ou ref2 ou ref3 est nul, vous obtiendrez une NullReferenceException . Si vous voulez résoudre le problème, trouvez celui qui est nul en réécrivant l'expression à son équivalent plus simple:

var r1 = ref1;
var r2 = r1.ref2;
var r3 = r2.ref3;
r3.member

Plus précisément, dans HttpContext.Current.User.Identity.Name , le HttpContext.Current pourrait être null, ou la propriété User pourrait être nulle, ou la propriété Identity pourrait être nulle.

Indirect

public class Person {
    public int Age { get; set; }
}
public class Book {
    public Person Author { get; set; }
}
public class Example {
    public void Foo() {
        Book b1 = new Book();
        int authorAge = b1.Author.Age; // You never initialized the Author property.
                                       // there is no Person to get an Age from.
    }
}

Si vous voulez éviter la référence null enfant (Person), vous pouvez l'initialiser dans le constructeur de l'objet parent (Book).

Initialiseurs d'objets imbriqués

La même chose s'applique aux initialiseurs d'objets imbriqués:

Book b1 = new Book { Author = { Age = 45 } };

Cela se traduit par

Book b1 = new Book();
b1.Author.Age = 45;

Lorsque le new mot-clé est utilisé, il crée uniquement une nouvelle instance de Book , mais pas une nouvelle instance de Person , de sorte que la propriété Author la propriété est toujours null .

Initialiseurs de collection imbriqués

public class Person {
    public ICollection<Book> Books { get; set; }
}
public class Book {
    public string Title { get; set; }
}

Les initialiseurs de collection imbriqués se comportent de la même manière:

Person p1 = new Person {
    Books = {
        new Book { Title = "Title1" },
        new Book { Title = "Title2" },
    }
};

Cela se traduit par

Person p1 = new Person();
p1.Books.Add(new Book { Title = "Title1" });
p1.Books.Add(new Book { Title = "Title2" });

La new Person crée uniquement une instance de Person , mais la collection Books est toujours null . La syntaxe d'initialisation de collection ne crée pas de collection pour p1.Books , elle se traduit uniquement par les p1.Books.Add(...) .

Array

int[] numbers = null;
int n = numbers[0]; // numbers is null. There is no array to index.

Éléments de tableau

Person[] people = new Person[5];
people[0].Age = 20 // people[0] is null. The array was allocated but not
                   // initialized. There is no Person to set the Age for.

Jagged Arrays

long[][] array = new long[1][];
array[0][0] = 3; // is null because only the first dimension is yet initialized.
                 // Use array[0] = new long[2]; first.

Collection / Liste / Dictionnaire

Dictionary<string, int> agesForNames = null;
int age = agesForNames["Bob"]; // agesForNames is null.
                               // There is no Dictionary to perform the lookup.

Variable de plage (indirecte / différée)

public class Person {
    public string Name { get; set; }
}
var people = new List<Person>();
people.Add(null);
var names = from p in people select p.Name;
string firstName = names.First(); // Exception is thrown here, but actually occurs
                                  // on the line above.  "p" is null because the
                                  // first element we added to the list is null.

Événements

public class Demo
{
    public event EventHandler StateChanged;

    protected virtual void OnStateChanged(EventArgs e)
    {        
        StateChanged(this, e); // Exception is thrown here 
                               // if no event handlers have been attached
                               // to StateChanged event
    }
}

Mauvaises conventions de dénomination:

Si vous avez nommé les champs différemment des locaux, vous avez peut-être réalisé que vous n'avez jamais initialisé le champ.

public class Form1 {
    private Customer customer;

    private void Form1_Load(object sender, EventArgs e) {
        Customer customer = new Customer();
        customer.Name = "John";
    }

    private void Button_Click(object sender, EventArgs e) {
        MessageBox.Show(customer.Name);
    }
}

Cela peut être résolu en suivant la convention pour préfixer les champs avec un trait de soulignement:

private Customer _customer;

Cycle de vie de la page ASP.NET:

public partial class Issues_Edit : System.Web.UI.Page
{
    protected TestIssue myIssue;

    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            // Only called on first load, not when button clicked
            myIssue = new TestIssue(); 
        }
    }

    protected void SaveButton_Click(object sender, EventArgs e)
    {
        myIssue.Entry = "NullReferenceException here!";
    }
}

Valeurs de session ASP.NET

// if the "FirstName" session value has not yet been set,
// then this line will throw a NullReferenceException
string firstName = Session["FirstName"].ToString();

Modèles à vue vide ASP.NET MVC

Si l'exception se produit lors du référencement d'une propriété de @Model dans une vue ASP.NET MVC, vous devez comprendre que le Model est défini dans votre méthode d'action lorsque vous return une vue. Lorsque vous renvoyez un modèle vide (ou une propriété de modèle) à partir de votre contrôleur, l'exception se produit lorsque les vues y accèdent:

// Controller
public class Restaurant:Controller
{
    public ActionResult Search()
    {
         return View();  // Forgot the provide a Model here.
    }
}

// Razor view 
@foreach (var restaurantSearch in Model.RestaurantSearch)  // Throws.
{
}

<p>@Model.somePropertyName</p> <!-- Also throws -->

Commande de création de contrôle WPF et événements

Les contrôles WPF sont créés lors de l'appel à InitializeComponent dans l'ordre dans InitializeComponent ils apparaissent dans l'arborescence visuelle. Une NullReferenceException sera NullReferenceException dans le cas de contrôles créés au début avec des gestionnaires d'événements, etc., qui se déclenchent pendant InitializeComponent qui référence les contrôles créés en retard.

Par exemple :

<Grid>
    <!-- Combobox declared first -->
    <ComboBox Name="comboBox1" 
              Margin="10"
              SelectedIndex="0" 
              SelectionChanged="comboBox1_SelectionChanged">
        <ComboBoxItem Content="Item 1" />
        <ComboBoxItem Content="Item 2" />
        <ComboBoxItem Content="Item 3" />
    </ComboBox>

    <!-- Label declared later -->
    <Label Name="label1" 
           Content="Label"
           Margin="10" />
</Grid>

Ici, comboBox1 est créé avant label1 . Si comboBox1_SelectionChanged tente de référencer `label1, il n'a pas encore été créé.

private void comboBox1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    label1.Content = comboBox1.SelectedIndex.ToString(); // NullReference here!!
}

Changer l'ordre des déclarations dans le XAML (c'est-à-dire, listing label1 avant comboBox1 , en ignorant les problèmes de philosophie de conception, résoudrait au moins le NullReferenceException ici.

Cast avec as

var myThing = someObject as Thing;

Cela ne lance pas InvalidCastException mais renvoie une valeur null lorsque la distribution échoue (et lorsque someObject est lui-même null). Alors soyez conscient de cela.

LINQ FirstOrDefault () et SingleOrDefault ()

Les versions simples First() et Single() lancent des exceptions quand il n'y a rien. Les versions "OrDefault" renvoient null dans ce cas. Alors soyez conscient de cela.

pour chaque

foreach lance lorsque vous essayez d'itérer une collection nulle. Habituellement causé par un résultat null inattendu provenant de méthodes qui retournent des collections.

 List<int> list = null;    
 foreach(var v in list) { } // exception

Exemple plus réaliste - sélectionnez les noeuds du document XML. Va lancer si les nœuds ne sont pas trouvés mais le débogage initial montre que toutes les propriétés sont valides:

 foreach (var node in myData.MyXml.DocumentNode.SelectNodes("//Data"))

Façons d'éviter

Vérifiez explicitement null et ignorez les valeurs null.

Si vous pensez que la référence est parfois nulle, vous pouvez vérifier qu'elle est null avant d'accéder aux membres de l'instance:

void PrintName(Person p) {
    if (p != null) {
        Console.WriteLine(p.Name);
    }
}

Vérifiez explicitement null et fournissez une valeur par défaut.

Les méthodes appelées à retourner une instance peuvent renvoyer une valeur null , par exemple lorsque l'objet recherché ne peut pas être trouvé. Vous pouvez choisir de renvoyer une valeur par défaut lorsque c'est le cas:

string GetCategory(Book b) {
    if (b == null)
        return "Unknown";
    return b.Category;
}

Vérifiez explicitement null pour null appels de méthode et lancez une exception personnalisée.

Vous pouvez également lancer une exception personnalisée, uniquement pour l'attraper dans le code appelant:

string GetCategory(string bookTitle) {
    var book = library.FindBook(bookTitle);  // This may return null
    if (book == null)
        throw new BookNotFoundException(bookTitle);  // Your custom exception
    return book.Category;
}

Utilisez Debug.Assert si une valeur ne doit jamais être null , pour intercepter le problème plus tôt que l'exception ne se produit.

Lorsque vous savez au cours du développement qu'une méthode peut le faire, mais ne doit jamais renvoyer de valeur null , vous pouvez utiliser Debug.Assert() pour rompre le plus tôt possible quand cela se produit:

string GetTitle(int knownBookID) {
    // You know this should never return null.
    var book = library.GetBook(knownBookID);  

    // Exception will occur on the next line instead of at the end of this method.
    Debug.Assert(book != null, "Library didn't return a book for known book ID.");

    // Some other code

    return book.Title; // Will never throw NullReferenceException in Debug mode.
}

Bien que cette vérification ne finisse pas dans votre version release , elle provoque à nouveau l' NullReferenceException lorsque book == null à l'exécution en mode release.

Utilisez GetValueOrDefault() pour les types de valeur Nullable pour fournir une valeur par défaut lorsqu'ils sont null .

DateTime? appointment = null;
Console.WriteLine(appointment.GetValueOrDefault(DateTime.Now));
// Will display the default value provided (DateTime.Now), because appointment is null.

appointment = new DateTime(2022, 10, 20);
Console.WriteLine(appointment.GetValueOrDefault(DateTime.Now));
// Will display the appointment date, not the default

Utilisez l'opérateur de coalescence nulle: ?? [C #] ou If() [VB].

Le raccourci pour fournir une valeur par défaut lorsqu'une valeur null est rencontrée:

IService CreateService(ILogger log, Int32? frobPowerLevel)
{
    var serviceImpl = new MyService(log ?? NullLog.Instance);

    // Note that the above "GetValueOrDefault()" can also be rewritten to use
    // the coalesce operator:
    serviceImpl.FrobPowerLevel = frobPowerLevel ?? 5;
}

Utilisez l'opérateur de condition null: ?. ou ?[x] pour les tableaux (disponibles en C # 6 et VB.NET 14):

Ceci est également parfois appelé la navigation de sécurité ou Elvis (après sa forme) opérateur. Si l'expression sur le côté gauche de l'opérateur est null, alors le côté droit ne sera pas évalué, et null est retourné à la place. Cela signifie des cas comme celui-ci:

var title = person.Title.ToUpper();

Si la personne n'a pas de titre, une exception est ToUpper car elle tente d'appeler ToUpper sur une propriété avec une valeur nulle.

Dans C # 5 et ci-dessous, ceci peut être gardé avec:

var title = person.Title == null ? null : person.Title.ToUpper();

Maintenant, la variable title sera null au lieu de lancer une exception. C # 6 introduit une syntaxe plus courte pour ceci:

var title = person.Title?.ToUpper();

Cela entraînera null variable de titre et l'appel à ToUpper n'est pas effectué si person.Title est null .

Bien sûr, vous devez toujours vérifier title pour null ou utiliser l'opérateur de condition null avec l'opérateur de coalescence null ( ?? ) pour fournir une valeur par défaut:

// regular null check
int titleLength = 0;
if (title != null)
    titleLength = title.Length; // If title is null, this would throw NullReferenceException

// combining the `?` and the `??` operator
int titleLength = title?.Length ?? 0;

De même, pour les tableaux, vous pouvez utiliser ?[i] comme suit:

int[] myIntArray=null;
var i=5;
int? elem = myIntArray?[i];
if (!elem.HasValue) Console.WriteLine("No value");

Cela fera ce qui suit: Si myIntArray est null, l'expression renvoie null et vous pouvez le vérifier en toute sécurité. S'il contient un tableau, il fera la même chose que: elem = myIntArray[i]; et renvoie le i ème élément.







c# .net reflection