56 votes

Comment exprimer un appel de méthode void comme résultat de DynamicMetaObject.BindInvokeMember ?

J'essaie de donner un court exemple de IDynamicMetaObjectProvider pour la deuxième édition de C# in Depth, et je rencontre des problèmes.

Je veux être capable d'exprimer un appel au vide, et j'échoue. Je suis sûr que c'est possible, car si j'appelle dynamiquement une méthode void en utilisant le binder de réflexion, tout va bien. Voici un exemple court mais complet :

using System;
using System.Dynamic;
using System.Linq.Expressions;

class DynamicDemo : IDynamicMetaObjectProvider
{
    public DynamicMetaObject GetMetaObject(Expression expression)
    {
        return new MetaDemo(expression, this);
    }

    public void TestMethod(string name)
    {
        Console.WriteLine(name);
    }

}

class MetaDemo : DynamicMetaObject
{
    internal MetaDemo(Expression expression, DynamicDemo demo)
        : base(expression, BindingRestrictions.Empty, demo)
    {
    }

    public override DynamicMetaObject BindInvokeMember
        (InvokeMemberBinder binder, DynamicMetaObject[] args)
    {
        Expression self = this.Expression;

        Expression target = Expression.Call
            (Expression.Convert(self, typeof(DynamicDemo)),
             typeof(DynamicDemo).GetMethod("TestMethod"),
             Expression.Constant(binder.Name));

        var restrictions = BindingRestrictions.GetTypeRestriction
            (self, typeof(DynamicDemo));

        return new DynamicMetaObject(target, restrictions);
    }
}

class Test
{
    public void Foo()
    {
    }

    static void Main()
    {
        dynamic x = new Test();
        x.Foo(); // Works fine!

        x = new DynamicDemo();
        x.Foo(); // Throws
    }
}

Une exception est alors levée :

Exception non gérée : System.InvalidCastException : L'adresse type de résultat 'System.Void' de la fonction liaison dynamique produite par l'objet avec le type 'DynamicDemo' pour le liant. Microsoft.CSharp.RuntimeBinder.CSharpInvokeMemberBinder'. n'est pas compatible avec le type de résultat 'System.Object' attendu par le site d'appel. site d'appel.

Si je modifie la méthode pour qu'elle renvoie un objet et retourne null, cela fonctionne bien... mais je ne veux pas que le résultat soit null, je veux qu'il soit void. Cela fonctionne bien pour le binder de réflexion (voir le premier appel dans Main) mais cela échoue pour mon objet dynamique. Je veux que cela fonctionne comme le liant par réflexion - il est possible d'appeler la méthode, tant que vous n'essayez pas d'utiliser le résultat.

Ai-je manqué un type d'expression particulier que je peux utiliser comme cible ?

26voto

Dino Viehland Points 5414

C'est similaire à :

Type de retour DLR

Vous devez faire correspondre le type de retour spécifié par l'option ReturnType propriété. Pour tous les binaires standards, cette propriété est fixée à object pour presque tout ou void (pour les opérations de suppression). Si vous savez que vous faites un appel void, je vous suggère de l'envelopper dans :

Expression.Block(
    call,
    Expression.Default(typeof(object))
);

Le DLR était auparavant assez laxiste quant à ce qu'il autorisait et fournissait automatiquement un minimum de coercition. Nous nous sommes débarrassés de cela parce que nous ne voulions pas fournir un ensemble de convensions qui auraient pu ou non avoir un sens pour chaque langage.

On dirait que vous voulez prévenir :

dynamic x = obj.SomeMember();

Il n'y a aucun moyen de le faire, il y aura toujours une valeur renvoyée avec laquelle l'utilisateur pourra tenter de continuer à interagir de manière dynamique.

11voto

Marc Gravell Points 482669

Je n'aime pas cela, mais cela semble fonctionner ; le vrai problème semble être la binder.ReturnType arrive bizarrement (et n'est pas déposé ("pop") automatiquement), mais :

if (target.Type != binder.ReturnType) {
    if (target.Type == typeof(void)) {
        target = Expression.Block(target, Expression.Default(binder.ReturnType));
    } else if (binder.ReturnType == typeof(void)) {
        target = Expression.Block(target, Expression.Empty());
    } else {
        target = Expression.Convert(target, binder.ReturnType);
    }
}
return new DynamicMetaObject(target, restrictions);

6voto

x0n Points 26002

Peut-être que le site d'appel s'attend à ce que null soit retourné mais rejette le résultat - Cet enum semble intéressant, en particulier l'indicateur "ResultDiscarded"...

[Flags, EditorBrowsable(EditorBrowsableState.Never)]
public enum CSharpBinderFlags
{
    BinaryOperationLogical = 8,
    CheckedContext = 1,
    ConvertArrayIndex = 0x20,
    ConvertExplicit = 0x10,
    InvokeSimpleName = 2,
    InvokeSpecialName = 4,
    None = 0,
    ResultDiscarded = 0x100,
    ResultIndexed = 0x40,
    ValueFromCompoundAssignment = 0x80
}

De quoi réfléchir...

UPDATE :

D'autres indices peuvent être glanés dans Microsoft / CSharp / RuntimeBinder / DynamicMetaObjectProviderDebugView qui est utilisé (je présume) comme visualisateur pour les débogueurs. La méthode TryEvalMethodVarArgs examine le délégué et crée un binder avec le drapeau discarded result ( ???)

 Type delegateType = Expression.GetDelegateType(list.ToArray());
    if (string.IsNullOrEmpty(name))
    {
        binder = new CSharpInvokeBinder(CSharpCallFlags.ResultDiscarded, AccessibilityContext, list2.ToArray());
    }
    else
    {
        binder = new CSharpInvokeMemberBinder(CSharpCallFlags.ResultDiscarded, name, AccessibilityContext, types, list2.ToArray());
    }
    CallSite site = CallSite.Create(delegateType, binder);

... Je suis à la fin de mon Reflector-foo ici, mais l'encadrement de ce code semble un peu étrange puisque la méthode TryEvalMethodVarArgs elle-même attend un objet comme type de retour, et la dernière ligne retourne le résultat de l'invocation dynamique. Je suis probablement en train d'écorcer le mauvais arbre [expression].

-Oisin

5voto

Barry Kelly Points 30330

Le classeur C# (dans Microsoft.CSharp.dll) sait si le résultat est utilisé ou non ; comme le dit x0n (+1), il en garde la trace dans un drapeau. Malheureusement, l'indicateur est enterré dans un fichier CSharpInvokeMemberBinder instance, qui est un type privé.

Il semble que le mécanisme de liaison C# utilise ICSharpInvokeOrInvokeMemberBinder.ResultDiscarded (une propriété sur une interface interne) pour le lire ; CSharpInvokeMemberBinder implémente l'interface (et la propriété). Le travail semble être fait dans Microsoft.CSharp.RuntimeBinder.BinderHelper.ConvertResult() . Cette méthode contient un code qui lance la procédure si l'erreur susmentionnée ResultDiscarded ne renvoie pas true si le type de l'expression est void.

Il me semble donc qu'il n'y a pas de moyen facile d'extraire le fait que le résultat de l'expression est supprimé du liant C#, du moins dans la version bêta 2.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X