Passing delegate function with extra parameters

2019-02-09 00:13发布

问题:

I have a delegate which looks like the following:

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

I accept a delegate of this type as a parameter to the function I want to call. However, in one particular calling function, I want to pass some extra data to the function which matches this delegate.

Here's the signature of the implementing function:

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

and it's being called as follows:

PrepareReceipt(LogApprovalNeeded);

I'd like it to be:

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

which ideally would be used as follows:

PrepareReceipt(LogApprovalNeeded(myCustomer))

How can I accomplish such a thing? I'd rather not need to declare a field in the class just to hold the Customer parameter between one function and the callback...

回答1:

You can use a lambda to "curry" your function:

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

Currying a function is the formal term for storing a reference to a function but with one or more of the parameters "fixed", thus altering the signature of the method.

You can also use a lambda when the signature of your function doesn't need all of the arguments that the delegate is providing; you can effectively discard parameters by not passing forward all of the arguments in the lambda.



回答2:

You can use a lambda to achieve what you need.

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

Alternatively, change your LogApprovalNeeded signature to:

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

But it could get a bit confusing, considering that you already have a variable number of parameters defined after cust.

EDIT: As Servy rightfully pointed out, the change of signature won't let you call the method as you described. If you move the logic related to Customer to PrepareReceipt, though, you won't need to use the above approach (which basically generates a new anonymous method and wraps myCustomer in a closure.



回答3:

If you need generic solution for delegates partial application (parameters reduction) take a look to the NReco Commons open source library, it contains PartialDelegateAdapter that can do that for any delegate type:

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

in this example 1st parameter is fixed with myCustomer value. In addition it also tries to harmonize argument types in runtime.



回答4:

Lamba methods aren't perfect: they don't have attributes and they contribute for a messy code.
If you want to avoid that kind of methods, you can do it in an alternative way, which is just like JavaScript's .bind() function.
That function can adapted in C# as follow, using a static class with some extension method:

//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;
    }
}

Now you can create a delegate for your method (not lambda) and assign to it some fixed parameters:

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

So, the below code will produce a WaitCallback delegate:

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

Whereas the below code will produce a Func<object, object> delegate:

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


回答5:

You can change the PrepareReceipt function to take an additional parameter. The signature would look something like public void PrepareReceipt(Customer customer, ApprovalPrompt approvalPrompt) to accomplish this.



回答6:

You can't pass it to that delegate as the delegate does not declare an argument of type Customer. The "simple answer" would be to change the signature of the delegate to take the new argument.

That said, that would also require modification of all the consumers of the delegate.



标签: c# delegates