【问题标题】:Setting parameter values of anonymous function in C#在C#中设置匿名函数的参数值
【发布时间】:2011-10-11 20:51:55
【问题描述】:

假设我有以下代码

private Func<T> _method;

public void SetExecutableMethod<T>(Func<T> methodParam)
{
    _method = methodParam;
}

public T ExecuteMethod(object[] parameterValues)
{
    //get the number of parameters _method has;
    var methodCallExpression = _method.Body as MethodCallExpression;
    var method = methodCallExpression.Method;
    ParameterInfo[] methodParams = method.GetParameters();

    //So i now have a list of parameters for the method call,
    //How can i update the parameter values for each of these?
    for (int i = 0; i < parameters.Count(); i++ )
    {
        methodParams[i] = ???''
    }

    return _method.Compile()();
}

public void InitAndTest()
{
    SetExecutableMethod( () => _service.SomeMethod1("param1 placeholder", "param2 placeholder") );

    T result1 = ExecuteMethod(new object[]{"Test1", "Test2"});
    T result2 = ExecuteMethod(new object[]{"Test3", "Test4"}););
}

在上面的代码中,我想为某个指向匿名函数的 Func 设置一个私有变量,并且永远不必再次设置它。 然后我希望能够使用不同的参数调用 ExecuteMethod(...) 。此方法应更新变量_method 的参数值,然后调用该方法。 我可以很好地读取参数的数量及其值,我只是不确定如何设置这些参数的值?对此有什么想法吗?

【问题讨论】:

  • 你能解释一下最终目标是什么吗?您打算如何使用可以接受任意数量参数的对象,并以任意方式处理它们?调用代码如何知道将什么传递给该对象?如果你的意图更清楚一点,可能会有更好的方法来完成它。
  • 我正在为服务调用实现一个通用缓存处理程序。缓存键将是方法本身 + 参数的哈希码。该值将是方法调用的结果。这基本上允许我实现一个不知道或不关心传递给方法的东西的类,只需要执行方法并缓存结果以供以后检索。
  • 我已经用一个基本示例更新了我的答案。请记住,哈希码不能是 actual 键。你的缓存(字典)可能会计算一个哈希码来加速搜索,但你应该确保每个参数都实现了一个IEqualityComparer或覆盖了它的Equals方法,然后比较它们。

标签: c# expression-trees anonymous-methods


【解决方案1】:

这不是这样做的方法。现在,您的_method 字段是Func&lt;T&gt; 类型的委托,并且您希望它的body 包含另一个实际执行的方法。对您的来电者有很多期望。我会忘记这种方法并寻找不同的东西。

一种方法是提供一个方法,该方法将对象数组作为其参数 (Func&lt;object[], T&gt;),然后使用适当的参数直接调用它(但绝不是其主体中的方法)。即使这对于像 C# 这样的强类型语言来说也不太常见,因为您失去了所有类型安全性(但话又说回来,您确实希望在设计这个框架时非常灵活)。

另一种方法是获取MethodInfo 实例,然后使用其Invoke 方法。在某种程度上,这甚至可以更好地表达你的意图,因为很明显可执行方法几乎可以做任何事情。

接下来,您可以使用泛型来获得某种类型安全性,并要求将所有输入参数包装在单个参数类中。在这种情况下,您可能有一个强类型的 Func&lt;Tparam, Tresult&gt; 方法,并且您的 Execute 方法将接受一个 Tparam 实例作为其参数。这样就不需要任何反思了。

[编辑]

正如我所写,我会尽量避免反思。既然你写了你基本上需要一个方法结果的缓存,一个简单的方法可能是这样的:

  1. 为您的参数列表创建一个包装器,以便您可以“按值”比较它们。我添加了一个示例类,但您甚至可能希望允许显式传递 IEqualityComparer,这样您就不必为每个部分参数覆盖 Equals

    // implements `IEquatable` for a list of parameters
    class Parameters : IEquatable<Parameters>
    {
        private readonly object[] _parameters;
        public Parameters(object[] parms)
        {
            _parameters = parms;
        }
    
        #region IEquatable<Parameters> Members
    
        public bool Equals(Parameters other)
        {
            if (other == null)
                return false;
    
            if (_parameters.Length != other._parameters.Length)
                return false;
    
            // check each parameter to see if it's equal
            // ...     
        }
    
        public override bool Equals(object obj)
        {
            return Equals(obj as Parameters);
        }
    
        public override int GetHashCode()
        { ... }
    
        #endregion
    }
    
  2. 为单个服务创建缓存。使用上面的包装类,它应该简单地检查缓存结果是否存在:

    // contains cached results for a single service
    class CachedCallInfo
    {
        private readonly Func<object[], object> _method;
        private readonly Dictionary<Parameters, object> _cache
            = new Dictionary<Parameters, object>();
    
        public CachedCallInfo(Func<object[], object> method)
        {
            _method = method;
        }
    
        public T GetResult<T>(params object[] parameters)
        {
            // use out Parameters class to ensure comparison
            // by value
            var key = new Parameters(parameters);
            object result = null;
    
            // result exists?
            if (!_cache.TryGetValue(key, out result))
            {
                // do the actual service call
                result = _method(parameters);
    
                // add to cache
                _cache.Add(key, result);
            }
            return (T)result;
        }
    }
    
  3. 创建将按名称引用服务的最终类:

    public class ServiceCache
    {
        private readonly Dictionary<string, CachedCallInfo> _services =
            new Dictionary<string, CachedCallInfo>();
    
        public void RegisterService(string name, Func<object[], object> method)
        {
            _services[name] = new CachedCallInfo(method);
        }
    
        // "params" keyword is used to simplify method calls
        public T GetResult<T>(string serviceName, params object[] parameters)
        {
            return _services[serviceName].GetResult<T>(parameters);
        }
    }
    

您的缓存设置将如下所示:

serviceCache.RegisterService("ServiceA", @params => DoSomething(@params));
serviceCache.RegisterService("ServiceB", @params => SomethingElse(@params));

你可以这样称呼它:

var result = serviceCache.GetResult("ServiceA", paramA, paramB, paramC);

【讨论】:

  • 感谢 groo,这里的所有建议都很好,但你的建议让我想到了稍微不同的方法。我将方法定义为 Func 的唯一问题是,这要求我要缓​​存的服务调用与这样的签名匹配。这并不总是可能的,因为我可能正在缓存不属于我的服务调用。这是我认为的指向匿名委托作品的 Func
【解决方案2】:

不知道为什么这是有用的,但这里是:

public class SomeCrazyClass<T>
{
    private Expression<Func<T>> _method;

    public void SetExecutableMethod(Expression<Func<T>> methodParam)
    {
        _method = methodParam;
    }

    public object ExecuteMethod(SomeService someService, object[] parameterValues)
    {
        var methodCallExpression = _method.Body as MethodCallExpression;
        var method = methodCallExpression.Method;
        var methodCall = Expression.Call(Expression.Constant(someService), method,
                                parameterValues.Select(Expression.Constant));

        return Expression.Lambda(methodCall).Compile().DynamicInvoke();
    }
}

这样称呼它:

    public static void InitAndTest()
    {
        var something = new SomeCrazyClass<int>(); //or whatever type your method returns
        var _service = new SomeService();
        something.SetExecutableMethod(() => _service.SomeMethod1("param1 placeholder", "param2 placeholder"));

        var result1 = something.ExecuteMethod(_service,new object[] {"Test1", "Test2"});
        var result2 = something.ExecuteMethod(_service, new object[] {"Test3", "Test4"});
    }

【讨论】:

  • 不是将_service作为参数传递给ExecuteMethod,有没有办法确定该方法的类实例?例如,如果我查看 MemberExpression.Object,我可以看出它指的是类实例,但由于某种原因我无法使用它,因为它也返回一个表达式,我无法将其转换为实际服务的实例。
【解决方案3】:

就个人而言,我认为你太过分了,除非有一个覆盖性的架构需要将 lambda 作为表达式树来处理。但是,我离题了。

不要使用反射元素(基本上仅用于根据表达式树进行描述),而是查看 MethodCallExpression 的 Arguments 成员。它将包含几个 ContantExpression 对象,您可以将其替换为您自己的包含要传入的字符串值的 ConstantExpressions。但是,表达式是只读的;你必须为这个调用重建一个等效的树。

public class FuncManipulator<T>
{
    private Func<T> _method;

    public void SetExecutableMethod(Func<T> methodParam)
    {
        _method = methodParam;
    }

    //you forgot the "params" keyword
    public T ExecuteMethod(params object[] parameterValues)
    {
        //get the number of parameters _method has;
        var methodCallExpression = _method.Body as MethodCallExpression;
        var arguments = methodCallExpression.Arguments;

        var newArguments = new List<Expression>();        
        for (int i = 0; i < arguments.Count(); i++ )
        {
            newArguments.Add(Expression.Constant(parameterValues[i]));
        }

        //"Clone" the expression, specifying the new parameters instead of the old.
        var newMethodExpression = Expression.Call(methodCallExpression.Object, 
                                                  methodCallExpression.Method, 
                                                  newArguments)

        return newMethodExpression.Compile()();
    }

}

...

public void InitAndTest()
{
    SetExecutableMethod( () => _service.SomeMethod1("param1 placeholder", "param2 placeholder") );

    T result1 = ExecuteMethod("Test1", "Test2");
    T result2 = ExecuteMethod("Test3", "Test4");
    T result3 = ExecuteMethod("Test6", "Test5");
}

只要表达式树可以在当前实例中找到 MethodCallExpression.method 引用的 Func,这将起作用。

不过,我认为有一个更简单的方法:

public class FuncManipulator<T>
{
    private Func<T> _method;

    public void SetExecutableMethod(Func<T> methodParam)
    {
        _method = methodParam;
    }

    //you must pass the actual array; we are creating a closure reference that will live
    //as long as the delegate
    public void SetMethodParams(object[] param)
    {
        _param = param;
    } 

    public T ExecuteMethod(params object[] passedParam)
    {
       //We have to re-initialize _param based on passedParam
       //instead of simply reassigning the reference, because the lambda
       //requires we don't change the reference.
       for(int i=0; i<_param.Length; i++)
          _param[i] = passedParam.Length <= i ? null : passedParam[i];

       //notice we don't pass _param; the lambda already knows about it
       //via the reference set up when declaring the lambda.
       return _method(); 
    }

}

...

public void InitAndTest()
{
    //this is an "external closure" we must keep in memory
    object[] param = new object[2];
    SetExecutableMethod( () => _service.SomeMethod1(param[0], param[1]) );
    //We do so by passing the reference to our object
    SetMethodParams(param);

    //now, don't ever reassign the entire array.
    //the ExecuteMethod function will replace indices without redefining the array.
    T result1 = ExecuteMethod("Test1", "Test2");
    T result2 = ExecuteMethod("Test3", "Test4");
    T result3 = ExecuteMethod("Test6", "Test5");
}

【讨论】:

  • 相信我,我同意你的意见。但是那些想要更多抽象和减少冗余的权力。我会把它交给他们,他们会看到,抽象程度很高,可读性也很差。 :)
  • 您能否获取您要指向的方法的 MethodInfo,然后将其调用方法与您的对象数组一起使用?
  • @Keith,Arguements 属性似乎是只读集合
  • @mike:是的。您必须使用 Expression.Call() 方法来“克隆”您拥有的表达式,使用现有的对象和方法,但使用新的参数。检查编辑。
  • @KeithS:很好的解决方案。唯一的说明是,您的实现不是线程安全的。不确定这是否是 OP 的问题....
猜你喜欢
  • 2021-02-14
  • 1970-01-01
  • 1970-01-01
  • 2021-02-19
  • 2021-09-20
  • 2011-12-04
  • 2011-03-03
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多