Передача функции делегата с дополнительными параметрами

У меня есть делегат для моего кода, который выглядит как

public delegate bool ApprovalPrompt(ApprovalType type, int receipt, params string[] info);

Я принимаю делегат этого типа в качестве параметра функции, которую я хочу вызвать. Однако в одной конкретной вызывающей функции я хочу передать некоторые дополнительные данные функции, которая соответствует этому делегату.

В настоящее время сигнатура функции реализации

private static bool LogApprovalNeeded(FraudFilterUtilities.ApprovalType type, int receipt, params string[] info) { }

и называется

PrepareReceipt(LogApprovalNeeded);

Я хотел бы, чтобы это было

private static bool LogApprovalNeeded(Customer cust, FraudFilterUtilities.ApprovalType type, int receipt, params string[] info) { }

который, в идеале, будет использоваться примерно так:

PrepareReceipt(LogApprovalNeeded(myCustomer))

Есть ли способ сделать что-то подобное? Мне не нужно было бы объявлять поле в классе только для того, чтобы удерживать параметр Customer между одной функцией и обратным вызовом...

Ответ 1

Вы можете использовать лямбда для "curry" вашей функции:

PrepareReceipt((type, receipt, info) => 
    LogApprovalNeeded(myCustomer, type, receipt, info))

Currying function - это формальный термин для хранения ссылки на функцию, но с одним или несколькими параметрами "fixed", изменяя таким образом сигнатуру метода.

Вы также можете использовать лямбда, когда подпись вашей функции не нуждается во всех аргументах, предоставляемых делегатом; вы можете эффективно отбросить параметры, не передавая все аргументы в лямбда.

Ответ 2

Вы можете использовать лямбда для достижения того, что вам нужно.

PrepareReceipt((type, receipt, info) =>
               LogApprovalNeeded(myCustomer, type, receipt, info));

В качестве альтернативы измените свою подпись LogApprovalNeeded на:

static bool LogApprovalNeeded(ApprovalType type, int receipt, 
                              Customer cust = null, params string[] info)
{
}

Но это может немного запутать, учитывая, что у вас уже есть переменное количество параметров, определенных после cust.

РЕДАКТИРОВАТЬ: Как верно сказал Сервий, изменение подписи не позволит вам вызвать метод, как вы описали. Если вы переместите логику, связанную с Customer, на PrepareReceipt, вам не нужно будет использовать описанный выше подход (который в основном генерирует новый анонимный метод и обертывает myCustomer в закрытии.

Ответ 3

Если вам требуется общее решение для частичного приложения делегатов (уменьшение параметров), посмотрите в библиотеку с открытым исходным кодом NReco Commons, в которой содержится PartialDelegateAdapter, который может сделать это для любого типа делегата:

var logApprovalForCustomer = (new PartialDelegateAdapter(LogApprovalNeeded,
    new[] {myCustomer})).GetDelegate<Func<FraudFilterUtilities.ApprovalType,int,string[],bool>>();

в этом примере 1-й параметр фиксирован значением myCustomer. Кроме того, он также пытается согласовать типы аргументов во время выполнения.

Ответ 4

Методы Lamba не идеальны: у них нет атрибутов, и они вносят свой вклад в беспорядочный код.
Если вы хотите избежать подобных методов, вы можете сделать это альтернативным способом, который похож на функцию JavaScript .bind().
Эта функция может быть адаптирована в С# следующим образом, используя статический класс с некоторым методом расширения:

//This code requires the Nu-get plugin ValueTuple
using System.Diagnostics;

public static class Extensions
{

    [DebuggerHidden, DebuggerStepperBoundary]
    public static WaitCallback Bind(this Delegate @delegate, params object[] arguments)
    {
        return (@delegate, arguments).BoundVoid;
    }

    [DebuggerHidden, DebuggerStepperBoundary]
    public static Func<object, object> BindWithResult(this Delegate @delegate, params object[] arguments)
    {
        return (@delegate, arguments).BoundFunc;
    }

    [DebuggerHidden, DebuggerStepperBoundary]
    private static void BoundVoid(this object tuple, object argument)
    {
        tuple.BoundFunc(argument);
    }

    [DebuggerHidden, DebuggerStepperBoundary]
    private static object BoundFunc(this object tuple, object argument)
    {
        (Delegate @delegate, object[] arguments) = ((Delegate @delegate, object[] arguments))tuple;
        if (argument != null)
            if (!argument.GetType().IsArray)
                argument = new object[] { argument };
        object[] extraArguments = argument as object[];
        object[] newArgs = extraArguments == null ? arguments : new object[arguments.Length + extraArguments.Length];
        if (extraArguments != null)
        {
            extraArguments.CopyTo(newArgs, 0);
            arguments.CopyTo(newArgs, extraArguments.Length);
        }
        if (extraArguments == null)
            return @delegate.DynamicInvoke(newArgs);
        object result = null;
        Exception e = null;
        int argCount = newArgs.Length;
        do
        {
            try
            {
                if (argCount < newArgs.Length)
                {
                    object[] args = newArgs;
                    newArgs = new object[argCount];
                    Array.Copy(args, newArgs, argCount);
                }
                result = @delegate.DynamicInvoke(newArgs);
                e = null;
            } catch (TargetParameterCountException e2)
            {
                e = e2;
                argCount--;
            }
        } while (e != null);
        return result;
    }
}

Теперь вы можете создать делегат для своего метода (не лямбда) и назначить ему некоторые фиксированные параметры:

MessageBox.Show(new Func<double, double, double>(Math.Pow).BindWithResult(3, 2)(null).ToString()); //This shows you a message box with the operation 3 pow 2

Итак, в приведенном ниже коде будет представлен делегат WaitCallback:

new Func<double, double, double>(Math.Pow).Bind(3, 2)

В то время как в приведенном ниже коде будет представлен делегат Func<object, object>:

new Func<double, double, double>(Math.Pow).BindWithResult(3, 2)

Ответ 5

Вы можете изменить функцию PrepareReceipt, чтобы принять дополнительный параметр. Подпись выглядела бы как public void PrepareReceipt(Customer customer, ApprovalPrompt approvalPrompt), чтобы выполнить это.

Ответ 6

Вы не можете передать его этому делегату, поскольку делегат не объявляет аргумент типа Customer. "Простым ответом" было бы изменение подписи делегата для принятия нового аргумента.

Тем не менее, это также потребует изменения всех потребителей делегата.