How to pass a Func<T> with a variable number of parameters
Asked Answered
S

5

6

So I'm attempting to be able to pass a Func with a variable number of parameters.

Something like:

public object GetValue<T>(string name, Func<object> func) { 
    var result = func.DynamicInvoke();
}

The above function/signature works great when the number of arguments to func is know. But it breaks down quickly when you want the number of arguments to be unknown until runtime.

I'd like to change the method signature to allow for the following scenarios, without using method overloading:

// No arguments
var result = GetValue("Bob", () => { return "Bob Smith"; });

// 1 argument
var result = GetValue("Joe", (i) => { return "Joe " + i.ToString(); });

// 2 arguments
var result = GetValue("Henry", (i,e) => { 
    return $"i: {i.ToString()}, e: {e.ToString()}"; 
});

Beyond 2 arguments is not necessary right now.. but may be in the future. The calling syntax is the most important bit to me. I'd prefer to not have the caller cast anything.

I've taken a look at this question and the answers but they all seem to require some calling syntax that I would prefer not to use.

Any ideas how this can be accomplished?

Ski answered 29/5, 2018 at 22:6 Comment(10)
I'm not aware of any way of doing this. Given that at the moment you'd only need three overloads (which could all delegate their implementation to a private method) I suspect that's the best way of going. Note that you haven't shown how you'd end up in a context where "the number of arguments [would be] unknown until runtime". You're still using lambda expressions in source code, right? At the moment this is a bit of an XY problem. If you can give us more context, we're more likely to be able to help you.Guanase
@DaisyShipton: I appreciate your comments. Unfortunately it's difficult for me to describe the use case as I'm still trying to architect it. I have a situation where I need the caller to provide a callback. I'm going to provide some arguments to the callback. Think of it like the jQuery .Each method. I want the caller to be able to specify whether they are interested in none of the args, 1 of the args, or both. I suppose I could just require a 2-arg signature.. but that would not do exactly what I was hoping for :)Ski
It seems you are concerned with how to write the Func so it will accept a variable number of arguments. But to me the real question is-- how do you intend to write the code that invokes that function? Won't that code have to populate the parameters? Where does it get the arguments from and how does it know how many to get?Hussite
What is the reasoning behind not wanting to use overloads? That's what most APIs do in this scenario. For example, LINQ's Select method has a similar scenario. One overload provides only the item. Another overload provides the index and the item.Mortie
@JohnWu: the arguments get generated by the called method - in the example it would be GetValue.Ski
Possible duplicate of Can you pass generic delegate without the Type parameterHussite
OP, I understand the arguments are generated by the called method. My question is how. How does it know to pass 1, 2, or 3 arguments? How does it know what is supposed to go into them? There's no point in making the receiver generic if the caller can't be.Hussite
@mikez: I suppose method overloading would not be the end of the world.Ski
Will all the arguments always be the same type? (say the first one will always be int and the second one will always be string) Are all the arguments the same type?Granddaughter
@JoshPart: Yes. The first arg will always be an int, and the second will always be a System.Windows.Forms.Control.Ski
N
6

The answer is don't. First of all, you are attempting to call a method that takes no parameters and returns some object. You can't just "make" a function that requires parameters this type, otherwise how else are you going to invoke it with the required parameters.

You're already creating lambdas, you'll need to close over the "parameters" you want that way you can effectively add additional parameters.

// No arguments
var result = GetValue("Bob", () => { return "Bob Smith"; });

// 1 argument
var i = ...;
var result = GetValue("Joe", () => { return "Joe " + i.ToString(); });

// 2 arguments
var i = ...;
var e = ...;
var result = GetValue("Henry", () => { 
    return $"i: {i.ToString()}, e: {e.ToString()}"; 
});

Otherwise if you truly want to pass in any delegate with any number of parameters, make the parameter Delegate, but you must provide the exact type of the delegate and you must provide the arguments to the call.

public object GetValue<T>(string name, Delegate func, params object[] args) { 
    var result = func.DynamicInvoke(args);
}
var result = GetValue("Bob", new Func<object>(() => { return "Bob Smith"; }));

// 1 argument
var result = GetValue("Joe", new Func<T, object>((i) => { return "Joe " + i.ToString(); }), argI);

// 2 arguments
var result = GetValue("Henry", new Func<T1, T2, object>((i,e) => { 
    return $"i: {i.ToString()}, e: {e.ToString()}"; 
}), argI, argE);
Noenoel answered 29/5, 2018 at 22:41 Comment(0)
K
3

First of all, you are defeating the purpose of static typing: In static typing, the whole point is that at compile time you can tell exactly what type a certain object has and as such the compiler will allow you to do certain things. What you are expecting here is something that dynamic typing usually offers, where you can just pass “something” and then you can figure out at run-time how to use it.

You should really rethink this requirement of yours, and see if you cannot solve this in a better, statically typed way.


That being said, there is a somewhat ugly way to make this statically sane, using a custom type and some implicit type conversions (which happen at compile time!):

public object GetValue(string name, DynamicFunc func)
{
    return func.DynamicInvoke("a", "b");
}

public class DynamicFunc
{
    public Func<object> None { get; private set; }
    public Func<object, object> One {get; private set;}
    public Func<object, object, object> Two { get; private set; }

    public object DynamicInvoke(object param1 = null, object param2 = null)
    {
        if (Two != null)
            return Two(param1, param2);
        else if (One != null)
            return One(param1 ?? param2);
        else if (None != null)
            return None();
        return null;
    }

    public static implicit operator DynamicFunc(Func<object> func)
        => new DynamicFunc { None = func };
    public static implicit operator DynamicFunc(Func<object, object> func)
        => new DynamicFunc { One = func };
    public static implicit operator DynamicFunc(Func<object, object, object> func)
        => new DynamicFunc { Two = func };
}

And then you can use it like this:

var result0 = GetValue("Bob", (Func<object>)(() => { return "Bob Smith"; }));
var result1 = GetValue("Joe", (Func<object, object>)((i) => { return "Joe " + i.ToString(); }));
var result2 = GetValue("Henry", (Func<object, object, object>)((i, e) =>
{
    return $"i: {i.ToString()}, e: {e.ToString()}";
}));

Note that you need to give the lambda expressions an explicit type since the compiler won’t be able to figure out the type otherwise.

Does this look good or make it easy to understand? I don’t think so. If you want proper static typing, just use method overloading here. That way, you also won’t need to invoke the function dynamically which also makes calling it actually easier.

Kneecap answered 29/5, 2018 at 22:55 Comment(0)
C
2

From what I understand, you want to be able to ignore part of the parameters in case the caller doesn't need them. However, I think you should really start by deciding how the delegate is going to be called. Note that the number of arguments of DynamicInvoke must match the number of parameters of the actual method, so it won't work for all delegates you pass if you specify more arguments than they accept.

Unless you use reflection and emit to call the delegate, you have to use overloads. The way you make them depends on whether the arguments are supposed to be "lazy" or not. If so, the body of each overload will be substantially different from the others to justify its presence. If not, it goes like this:

object GetValue(Func<int, object> f)
{
    return GetValue((i,s) => f(i));
}

object GetValue(Func<int, string, object> f)
{
    return f(1, "0");
}

I don't think this a good solution, since the first overload suggests that only the first argument is produced, while under the hood, all of them are passed. In this case, it might be better to wrap all the information you intend to get from the method in a class and pass an instance of it instead. This is applicable for the first case too if you use Lazy<T>, but the code will get more complex.

Another thing relevant to your problem: there is the delegate{ ... } syntax which can be cast to a delegate type with any parameters, which are then ignored. This happens at the call site though.

Cyril answered 29/5, 2018 at 22:51 Comment(0)
T
1

First, my answer is not about whether you should do this or not; other answers on this post have given well explained advice/opinions about this.

My answer is only about the technical C# part: how to pass a variable number of arguments to a Func.

It can be done via a delegate, to which a variable number of arguments can be passed via a params array and which is 'compatible' with a Func.

Because of the params array, the Func must be the first argument. Notice that I moved the 'name' argument as in your question to be passed via the params array in order to be accessible from within the Func.

All strings below get assigned the value 'Bob Smith', being constructed with 0 or 2 arguments.

public delegate T ParamsDelegate<T>(params Object[] args);

public T GetValue<T>(ParamsDelegate<T> f, params Object[] args)
{
    return f(args);            
}

// 0 passed argument values.  
String s0 = GetValue(args => "Bob Smith");

// 1 argument.
String s1 = GetValue(
    args => String.Format("{0} Smith", args),
    "Bob"
    );

// 2 arguments.            
String s2 = GetValue(
    args => String.Format("{0} {1}", args),
    "Bob", "Smith"
    );

Edit:

Starting from C# 7 you can make the intend of not passing any arguments clearer via discards.

Discards are local variables which you can assign but cannot read from. i.e. they are “write-only” local variables. They don’t have names, instead, they are represented as a _ (underscore.) _ is a contextual keyword, it is very similar to var, and _ cannot be read (i.e. cannot appear on the right side of an assignment.)

So the following:

String s0 = GetValue(args => "Bob Smith");

can be rewritten as:

String s0 = GetValue(_ => "Bob Smith");
Tradesfolk answered 30/5, 2018 at 9:32 Comment(2)
I really appreciate the comments. Correct me if I'm wrong, but your proposal appears to have the caller provide the args to GetValue(...). My question is about having GetValue(...) provide the args to the passed in Func or delegate. (e.g: string value = GetValue<string>((a,b) => { return $"arg-a: {a}; arg-b: {b}"; });)Ski
I understand and did this by design, because otherwise there would be no means to assign values to a and b in string value = GetValue<string>((a,b) => { ...});. Argument values always get passed to a function/method from the outermost one.Tradesfolk
H
0

Provide only one parameter, but make it a complex type

If you have a laundry list of variables that may or may not be needed by the delegate that is being passed in, you can supply them as a single argument, perhaps named CallingContext.

class CallingContext
{
    private CallingContext(string name, int index)
    {
        Name = name;
        Index = index;
    }
    public string Name { get; private set; }
    public int Index { get; private set; }
}

public TOut GetValue<TOut>(string name, Func<CallingContext,TOut> func) { 
    var i = LookupIndex(name);
    var context = new CallingContext(name, i);        
    return func(context);
}

This way, the caller's Func can pull whatever it needs, and you don't have to worry about pushing the right things, or the generic parameters of the Func, at all.

var result = x.GetValue("Bob", ctxt => ctxt.Name + ctxt.Id.ToString());

In the future, if you have to add additional parameters, just add them to the CallingContext class. If you expect the class to become pretty rich, consider exposing it as a interface, and using a delegate signature of Func<ICallingContext,TOut>, so that you can shim and stub it for unit tests. Also, if any of the arguments are expensive to compute, you can expose them as lazy properties, and only fetch them if called.

Hussite answered 30/5, 2018 at 9:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.