Func<> with unknown number of parameters
Asked Answered
G

6

25

Consider the following pseudo code:

TResult Foo<TResult>(Func<T1, T2,...,Tn, TResult> f, params object[] args)
{
    TResult result = f(args);
    return result;
}

The function accepts Func<> with unknown number of generic parameters and a list of the corresponding arguments. Is it possible to write it in C#? How to define and call Foo? How do I pass args to f?

Gangway answered 3/4, 2014 at 9:59 Comment(1)
Unfortunately not possible :( You can do it via Delegate and using reflection, but I am sure that is not what you want.Handily
T
18

That's not possible. At best, you could have a delegate that also takes a variable number of arguments, and then have the delegate parse the arguments

TResult Foo<TResult>(Func<object[], TResult> f, params object[] args)
{
    TResult result = f(args);
    return result;
}


Foo<int>(args =>
{
    var name = args[0] as string;
    var age = (int) args[1];

    //...

    return age;
}, arg1, arg2, arg3);
Thermography answered 3/4, 2014 at 10:4 Comment(0)
H
22

You can use Delegate with DynamicInvoke.

With that, you don't need to handle with object[] in f.

TResult Foo<TResult>(Delegate f, params object[] args)
{
    var result = f.DynamicInvoke(args);
    return (TResult)Convert.ChangeType(result, typeof(TResult));
}

Usage:

Func<string, int, bool, bool> f = (name, age, active) =>
{
    if (name == "Jon" && age == 40 && active)
    {
        return true;
    }
    return false;
}; 

Foo<bool>(f,"Jon", 40, true);

I created a fiddle showing some examples: https://dotnetfiddle.net/LdmOqo


Note:

If you want to use a method group, you need to use an explict casting to Func:

public static bool Method(string name, int age)
{
    ...
}
var method = (Func<string, int, bool>)Method;
Foo<bool>(method, "Jon", 40);

Fiddle: https://dotnetfiddle.net/3ZPLsY

Harewood answered 18/2, 2016 at 16:51 Comment(8)
Does this actually work? Does the DynamicInvoke reflect the Array as comma-delimited parameters into the methods execution?Whallon
Yes @GoldBishop. DynamicInvoke does exactly that. In this fiddle I'm showing some examples. Check msdn doc DynamicInvoke for more information.Nevermore
hmmmm...the mischief that can be had with such knowledge :)Whallon
This works, which is cool. Unfortunately you can't directly pass a method that takes arbitrary arguments, you must first create a Func. So a method bool F(string name, int age, bool active) {...} cannot be passed directly to Foo<bool>(F, "Jon",40,true).Wolcott
Nice point @yoyo. To use this function with method group you need to use explict casting. I created another fiddle showing how you can do this: dotnetfiddle.net/qVlt47 I'll update my awnser explaining it. Thanks for comment!Nevermore
Thanks for the update, and the fiddle (dotNetFiddle is great!) I've been trying to find a way of handling arbitrary parameters without needing to state the parameter types at the call site, but I've decided to find another way to solve my problem. (BTW, Method("yoyo", 22) definitely returns false! ;-)Wolcott
@Wolcott I'm glad to hear that. :)Nevermore
With c# 7.3 you can define R Foo<D,R>(D f, params object[] args) where D:Delegate (learn.microsoft.com/en-us/dotnet/csharp/programming-guide/…)Obau
T
18

That's not possible. At best, you could have a delegate that also takes a variable number of arguments, and then have the delegate parse the arguments

TResult Foo<TResult>(Func<object[], TResult> f, params object[] args)
{
    TResult result = f(args);
    return result;
}


Foo<int>(args =>
{
    var name = args[0] as string;
    var age = (int) args[1];

    //...

    return age;
}, arg1, arg2, arg3);
Thermography answered 3/4, 2014 at 10:4 Comment(0)
B
17

This could become easy with lambda expressions:

TResult Foo<TResult>(Func<TResult> f)
{
    return f();
}

Then usage could be like:

var result = Foo<int>(() => method(arg1, arg2, arg3));

Where method can be arbitrary method returning int.

This way you can pass any number of any erguments directly through lambda.

To support asynchoronous code we can define:

Task<TResult> Foo<TResult>(Func<Task<TResult>> f)
{
    return f();
}

// or with cancellation token
Task<TResult> Foo<TResult>(Func<CancellationToken, Task<TResult>> f, CancellationToken cancellationToken)
{
    return f(cancellationToken);
}

and use it like:

var asyncResult = await Foo(async () => await asyncMethod(arg1, arg2, arg3));
// With cancellation token
var asyncResult = await Foo(
    async (ct) => await asyncMethod(arg1, arg2, arg3, ct), 
    cancellationToken);
Biscay answered 19/6, 2019 at 7:5 Comment(0)
O
2

You could try something similar to what I posted here: https://mcmap.net/q/338688/-can-i-use-params-in-action-or-func-delegates

It will allow for any number of arguments, and enforces their types.

public delegate T ParamsAction<T>(params object[] args);

TResult Foo<TResult>(ParamsAction<TResult> f)
{
    TResult result = f();
    return result;
}

to call it, simply......

Foo(args => MethodToCallback("Bar", 123));
Obverse answered 29/11, 2017 at 15:59 Comment(1)
I think it should be called ParamsFunc<> instead, but otherwise I agree. I would make it covariant ("out") in T. For example public delegate TResult ParamsFunc<out TResult>(params object[] args);Motheaten
S
0

In some cases you may be able to get away with a trick like this:

public static class MyClass
{
    private static T CommonWorkMethod<T>(Func<T> wishMultipleArgsFunc)
    {
        // ... do common preparation
        T returnValue = wishMultipleArgsFunc();
        // ... do common cleanup
        return returnValue;
    }

    public static int DoCommonWorkNoParams() => CommonWorkMethod<int>(ProduceIntWithNoParams);
    public static long DoCommonWorkWithLong(long p1) => CommonWorkMethod<long>(() => ProcessOneLong(p1));
    public static string DoCommonWorkWith2Params(int p1, long p2) => CommonWorkMethod<string>(() => ConvertToCollatedString(p1, p2));

    private static int ProduceIntWithNoParams() { return 5; }
}
Sculley answered 27/1, 2017 at 15:42 Comment(0)
R
-1

Although it is not really what asked, a simple workaround would be to define several Foo method with different number of type arguments. It is uncommon to have function with more than 6 parameters, so one could define the following method and get away with almost every use case, while staying type safe. Renan's solution could then be used for the remaining cases.

public TResult Foo<TResult> (Func<TResult> f)
{
    return f();
}

public TResult Foo<T1, TResult>(Func<T1, TResult> f, T1 t1)
{
    return f(t1);
}

public TResult Foo<T1, T2, TResult>(Func<T1, T2, TResult> f, T1 t1, T2 t2)
{
    return f(t1, t2);
}

...
Raguelragweed answered 10/2, 2020 at 7:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.