Zatronium Zatronium - 3 months ago 36
C# Question

C# params argument selection when used twice

I'm curious why neither of the following DoInvoke methods can be called with only one params:

public class foo {
private void bar(params object[] args) {
DoInvoke(args);
}

//Error: There is no argument given that corresponds to the required formal parameter 'args' of 'foo.DoInvoke(Delegate, object[])'
private void DoInvoke(Delegate d, object[] args) {
d.DynamicInvoke(args);
}

//Error: Argument 1: cannot convert from 'object[]' to 'System.Delegate'
private void DoInvoke(Delegate d, params object[] args) {
d.DynamicInvoke(args);
}
}


I already found a way that doesn't abuse params. I'm curious why params are not expanded here.

I was able to do something similar in Lua, hence my attempt. I know Lua is far less strict, but I'm not sure which C# rule I'm breaking by doing this.

Answer

I'm curious why neither of the following DoInvoke methods can be called with only one params:

Short version: the first can't, because it has two non-optional parameters and because you're passing a value of the wrong type for the first non-optional parameter. The second can't, but only because the value you are trying to pass for the single non-optional parameter is of the wrong type; the second parameter is optional and so may be omitted as you've done.


You seem to be under the impression that in your method declaration private void bar(params object[] args), the presence of the params keyword makes the args variable somehow different from any other variable. It's not. The params keyword affects only the call site, allowing (but not requiring) the caller to specify the array elements of the args variable to be specified as if they were individual parameters, rather than creating the array explicitly.

But even when you call bar() that way, what happens is that an array object is created and passed to bar() as any other array would be passed. The variable args inside the bar() method is just an array. It doesn't get any special handling, and the compiler won't (for example) implicitly expand it to a parameter list for use in passing to some other method.

I'm not familiar with Lua, but this is somewhat in contrast to variadic functions in C/C++ where the language provides a way to propagate the variable parameter list to callees further down. In C#, the only way you can directly propagate a params parameter list is if the callee can accept the exact type of array as declared in the caller (which, due to array type variance in C#, does not always have to be the exact same type, but is still limited).


If you're curious, the relevant C# language specification addresses this in a variety of places, but primarily in "7.5.1.1 Corresponding parameters". This reads (from the C# 5 specification…there is a draft C# 6 specification, but the C# 5 is basically the same and it's what I have a copy of):

For each argument in an argument list there has to be a corresponding parameter in the function member or delegate being invoked.

It goes on to describe what "parameter list" is used to validate the argument list, but in your simple example, overload resolution has already occurred at the point this rule is being applied, and so there's only one parameter list to worry about:

• For all other function members and delegates there is only a single parameter list, which is the one used.

It goes on to say:

The corresponding parameters for function member arguments are established as follows:
• Arguments in the argument-list of instance constructors, methods, indexers and delegates:
    o A positional argument where a fixed parameter occurs at the same position in the parameter list corresponds to that parameter. [emphasis mine]
    o A positional argument of a function member with a parameter array invoked in its normal form corresponds to the parameter array, which must occur at the same position in the parameter list.
    o A positional argument of a function member with a parameter array invoked in its expanded form, where no fixed parameter occurs at the same position in the parameter list, corresponds to an element in the parameter array.
    o A named argument corresponds to the parameter of the same name in the parameter list.
    o For indexers, when invoking the set accessor, the expression specified as the right operand of the assignment operator corresponds to the implicit value parameter of the set accessor declaration.

In other words, if you don't provide a parameter name in your argument list, arguments correspond to method parameters by position. And the parameter in the first position of both your called methods has the type Delegate.

When you try to call the first method, that method has zero optional parameters, but you haven't provided a second parameter. So you get an error telling you that your argument list, consisting of just a single argument (which by the above corresponds to the Delegate d parameter), does not include a second argument that would correspond to the object[] args parameter in the called method.

Even if you had provided a second argument, you would have run into the same error you get trying to call your second method example. I.e. while the params object[] args parameter is optional (the compiler will provide an empty array for the call), and so you can get away with providing just one argument in your call to the method, that one argument has the wrong type. Its positional correspondence is to the Delegate d parameter, but you are trying to pass a value of type object[]. There's no conversion from object[] to Delegate, so the call fails.


So, what's that all mean for real code? Well, that depends on what you are trying to do. What did you expect to happen when you tried to pass your args variable to a void DoInvoke(Delegate d, params object[] args) method?

One obvious possibility is that the args array contains as its first element a Delegate object, and the remainder of the array are the arguments to pass. In that case, you could do something like this:

private void bar(params object[] args) {
    DoInvoke((Delegate)args[0], args.Skip(1).ToArray());
}

That should be syntactically valid with either of the DoInvoke() methods you've shown. Of course, whether that's really what you want is unclear, since I don't know what the call was expected to do.