Установка значений параметров анонимной функции в C#


Допустим, у меня есть следующий код

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"}););
}

В приведенном выше коде я хочу установить частную переменную в некоторую функцию, которая указывает на функцию anonymoust, и никогда не придется устанавливать ее снова. Затем я хотел бы иметь возможность вызвать ExecuteMethod(...) с различными параметрами. Этот метод должен обновить значения параметров переменной _method и затем вызвать метод. Я могу прочитать количество параметров и их значения отлично, я просто не уверен, как установить значения для этих параметров. параметр? Есть какие-нибудь мысли по этому поводу?

3 3

3 ответа:

Это не способ сделать это. Прямо сейчас ваше поле _method является делегатом типа Func<T>, и вы ожидаете, что его тело содержит еще один метод, который фактически выполняется. Это очень много, чтобы ожидать от ваших абонентов. Я бы забыл об этом подходе и стал искать что-то другое.

Одним из способов было бы предоставить метод, который принимает массив объектов в качестве своего параметра (Func<object[], T>), а затем вызвать его непосредственно с соответствующими параметрами (но никогда не метод в его тело). Даже это менее распространено для строго типизированного языка, такого как C#, поскольку вы теряете всю безопасность типов (но опять же, вы хотите быть довольно гибкими с этой структурой, которую вы разрабатываете).

Другой способ-получить экземпляр MethodInfo, а затем использовать его метод Invoke. В некотором смысле это может даже лучше выразить ваши намерения, потому что будет очевидно, что исполняемый метод способен практически на все.

Далее, вы можете использовать дженерики, чтобы получить некоторую безопасность типа, и требуется, чтобы все входные параметры были заключены в один класс параметров. В этом случае у вас может быть строго типизированный метод Func<Tparam, Tresult>, и ваш метод Execute будет принимать экземпляр Tparam в качестве своего параметра. Это отпустило бы необходимость в каких-либо размышлениях.

[Edit]

Как я уже писал, я старался избегать размышлений. Поскольку вы написали, что вам в основном нужен кэш результатов метода, простой подход может быть примерно таким:
  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);

Не знаю, почему это полезно, но здесь идет:

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"});
    }

Лично я думаю, что вы перегибаете палку, если только не существует перекрывающей архитектуру необходимости иметь дело с лямбдой как деревом выражений. Но я отвлекся.

Вместо работы с отражающими элементами (которые в основном предназначены для описания только в терминах дерева выражений), посмотрите на элемент аргументов вашего MethodCallExpression. Он будет содержать несколько объектов 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.метод в текущем экземпляре.

Однако, я думаю, что есть гораздо более простой способ:

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");
}