9

A question to all of you C# wizards. I have a method, call it myFunc, and it takes variable length/type argument lists. The argument signature of myFunc itself is myFunc(params object[] args) and I use reflection on the lists (think of this a bit like printf, for example).

I want to treat myFunc(1, 2, 3) differently from myFunc(new int[] { 1, 2, 3 }). That is, within the body of myFunc, I would like to enumerate the types of my arguments, and would like to end up with { int, int, int} rather than int[]. Right now I get the latter: in effect, I can't distinguish the two cases, and they both come in as int[].

I had wished the former would show up as obs[].Length=3, with obs[0]=1, etc.

And I had expected the latter to show up as obs[].Length=1, with obs[0]={ int[3] }

Can this be done, or am I asking the impossible?

svick
  • 236,525
  • 50
  • 385
  • 514
Ken Birman
  • 1,088
  • 8
  • 25
  • 9
    Why on earth would you ever need something like that – Jan Jongboom Mar 13 '12 at 13:48
  • How can you expect `obs[0]` to be an array? `obs` is `int[]`, its type is known at compile time, and its elements can only be of type `int`. If you had `params object[]` then you might argue that `object[]` is also an `object`, but this doesn't make sense. – vgru Mar 13 '12 at 13:56
  • Have a look at isis2.codeplex.com. I need to go teach a class and will be back in 90 minutes will revisit this thread then. the question really relates to calls to "Reply", for a "Query". Documentation has many examples. Jongboom, sorry you found this confusing. But if you see the goal perhaps that will help. – Ken Birman Mar 13 '12 at 13:57
  • @KenBirman: It should be fine, if you've got `params object[]` as the parameter type. See my short but complete programs... – Jon Skeet Mar 13 '12 at 13:59

6 Answers6

9

Well this will do it:

using System;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("First call");
        Foo(1, 2, 3);
        Console.WriteLine("Second call");
        Foo(new int[] { 1, 2, 3 });
    }

    static void Foo(params object[] values)
    {
        foreach (object x in values)
        {
            Console.WriteLine(x.GetType().Name);
        }
    }
}

Alternatively, if you use DynamicObject you can use dynamic typing to achieve a similar result:

using System;
using System.Dynamic;

class Program
{
    static void Main(string[] args)
    {
        dynamic d = new ArgumentDumper();
        Console.WriteLine("First call");
        d.Foo(1, 2, 3);
        Console.WriteLine("Second call");
        d.Bar(new int[] { 1, 2, 3 });
    }
}

class ArgumentDumper : DynamicObject
{
    public override bool TryInvokeMember
        (InvokeMemberBinder binder,
         Object[] args,
         out Object result)
    {
        result = null;
        foreach (object x in args)
        {
            Console.WriteLine(x.GetType().Name);
        }
        return true;
    }
}

Output of both programs:

First call
Int32
Int32
Int32
Second call
Int32[]

Now given the output above, it's not clear where your question has really come from... although if you'd given Foo("1", "2", "3") vs Foo(new string[] { "1", "2", "3" }) then that would be a different matter - because string[] is compatible with object[], but int[] isn't. If that's the real situation which has been giving you problems, then look at the dynamic version - which will work in both cases.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • DOn't know that one. I'll look it up – Ken Birman Mar 13 '12 at 13:56
  • This is what I too originally posted, but I don't think this is what the OP wants. – Ani Mar 13 '12 at 13:59
  • @Ani: Well the output seems to meet the requirements. It's not clear what the OP was doing differently which didn't work, but this *does* work... – Jon Skeet Mar 13 '12 at 14:00
  • Based on comments to the answer by BasBrekelmans, I think the second solution is what the OP wants. This requires .Net 4.0, which isn't really a surprise: one of the goals in .Net 4.0 was to provide better support for interop scenarios. – Brian Mar 13 '12 at 14:17
  • Interesting! If that works, I know where I went wrong, but not why. In fact I have several versions of my Reply method, and they call pass the "objects" vector to a single main Reply handler. The problem I see is occuring in that second procedure, not the first one (e.g. Reply calls doReply(obs) where obs was the params vector). Somehow the compiler must be losing the distinction in this second call since, if your example really works, it had the distinction right in Reply itself. So I can easily fix my code. No idea why the issue arises on the nested call, but what the heck... – Ken Birman Mar 13 '12 at 15:39
  • @KenBirman: It's hard to follow what you mean (particularly talking about vectors - do you mean arrays) - but if you could come up with a short but complete example which demonstrates the problem, I could probably explain it... Note the part at the bottom of my answer - you may just be bumping into array covariance. – Jon Skeet Mar 13 '12 at 15:41
  • Jon,you are quite right. Turns out that the issue was just a bug in my logic. Sorry for wasting time for those who helped! – Ken Birman Mar 13 '12 at 16:00
  • @KenBirman: Can I assume that something's gone wrong, given the unacceptance? Let me know if I can help... – Jon Skeet Mar 14 '12 at 20:08
  • OK, I unchecked the box. Here's why. Your example works flawlessly for int. But if I do the IDENTICAL thing but now I use a user-defined class... the same code reports the incoming objects incorrectly! – Ken Birman Mar 14 '12 at 20:09
  • @Ken: Yes, that's exactly as I've explained in the bottom paragraph - it's due to reference type array covariance. However, the dynamic version (the second piece of code) works fine. – Jon Skeet Mar 14 '12 at 20:11
9

OK, so let's say that we abandon the other question where you incorrectly believe that any of this is a compiler bug and actually address your real question.

First off, let's try to state the real question. Here's my shot at it:


The preamble:

A "variadic" method is a method which takes an unspecified-ahead-of-time number of parameters.

The standard way to implement variadic methods in C# is:

void M(T1 t1, T2 t2, params P[] p)

that is, zero or more required parameters followed by an array marked as "params".

When calling such a method, the method is either applicable in its normal form (without params) or its expanded form (with params). That is, a call to

void M(params object[] x){}

of the form

M(1, 2, 3)

is generated as

M(new object[] { 1, 2, 3 });

because it is applicable only in its expanded form. But a call

M(new object[] { 4, 5, 6 });

is generated as

M(new object[] { 4, 5, 6 });

and not

M(new object[] { new object[] { 4, 5, 6 } });

because it is applicable in its normal form.

C# supports unsafe array covariance on arrays of reference type elements. That is, a string[] may be implicitly converted to object[] even though attempting to change the first element of such an array to a non-string will produce a runtime error.

The question:

I wish to make a call of the form:

M(new string[] { "hello" });

and have this act like the method was applicable only in expanded form:

M(new object[] { new string[] { "hello" }});

and not the normal form:

M((object[])(new string[] { "hello" }));

Is there a way in C# to implement variadic methods that does not fall victim to the combination of unsafe array covariance and methods being applicable preferentially in their normal form?


The Answer

Yes, there is a way, but you're not going to like it. You are better off making the method non-variadic if you intend to be passing single arrays to it.

The Microsoft implementation of C# supports an undocumented extension that allows for C-style variadic methods that do not use params arrays. This mechanism is not intended for general use and is included only for the CLR team and others authoring interop libraries so that they can write interop code that bridges between C# and languages that expect C-style variadic methods. I strongly recommend against attempting to do so yourself.

The mechanism for doing so involves using the undocumented __arglist keyword. A basic sketch is:

public static void M(__arglist) 
{
    var argumentIterator = new ArgIterator(__arglist);
    object argument = TypedReference.ToObject(argumentIterator.GetNextArg());

You can use the methods of the argument iterator to walk over the argument structure and obtain all the arguments. And you can use the super-magical typed reference object to obtain the types of the arguments. It is even possible using this technique to pass references to variables as arguments, but again I do not recommend doing so.

What is particularly awful about this technique is that the caller is required to then say:

M(__arglist(new string[] { "hello" }));

which frankly looks pretty gross in C#. Now you see why you are better off simply abandoning variadic methods entirely; just make the caller pass an array and be done with it.

Again, my advice is (1) under no circumstances should you attempt to use these undocumented extensions to the C# language that are intended as conveniences for the CLR implementation team and interop library authors, and (2) you should simply abandon variadic methods; they do not appear to be a good match for your problem space. Do not fight against the tool; choose a different tool.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
3

Yes, you can, checking the params length and checking the argument type, see the following working code sample:

class Program
{
    static void Main(string[] args)
    {
        myFunc(1, 2, 3);
        myFunc(new int[] { 1, 2, 3 });
    }

    static void myFunc(params object[] args)
    {
        if (args.Length == 1 && (args[0] is int[]))
        {
            // called using myFunc(new int[] { 1, 2, 3 });
        }
        else
        {
            //called using myFunc(1, 2, 3), or other
        }
    }
}
Daniel Peñalba
  • 30,507
  • 32
  • 137
  • 219
2

You can achieve something like this by breaking the first element out of the list and providing an extra overload, for example:

class Foo
{
   public int Sum()
   {
      // the trivial case
      return 0;
   }
   public int Sum(int i)
   {
      // the singleton case
      return i;
   }
   public int Sum(int i, params int[] others)
   {
      // e.g. Sum(1, 2, 3, 4)
      return i + Sum(others);
   }
   public int Sum(int[] items)
   {
      // e.g. Sum(new int[] { 1, 2, 3, 4 });
      int i = 0;
      foreach(int q in items)
          i += q;
      return i;
   }
}
SecurityMatt
  • 6,593
  • 1
  • 22
  • 28
0

This is not possible in C#. C# will replace your first call by your second call at compile time.

One possibility is to create an overload without params and make it a ref parameter. This probably won't make sense. If you want to change behavior based on the input, perhaps give the second myFunc another name.

Update

I understand your issue now. What you want is not possible. If the only argument is anything that can resolve to object[] it's impossible to distinguish from this.

You need an alternative solution, maybe have a dictionary or array created by the caller to build up the parameters.

Bas
  • 26,772
  • 8
  • 53
  • 86
  • You aren't getting the picture. The idea here is that my users are replying to a multicast and the arguments they supply will be marshalled (serialized) by me. So I need to know the types used in calling to my reply function, argument by argument, to reconstruct the signature correctly for the guy at the other end who will extract the data. The number of arguments and the types both matter, and I need to have just one function that can take pretty much arbitrary arguments... – Ken Birman Mar 13 '12 at 13:53
  • Well, if that other code works (and I plan to check it now), apparently I can do it in C#... I'll uncheck the answer button if it doesn't actually work as advertised. Otherwise, I'm out of here and a happy camper! Thanks, everyone... – Ken Birman Mar 13 '12 at 15:41
0

Turns out that there was a real issue, and it comes down to the way that C# does type inference. See the discussion on this other thread

Community
  • 1
  • 1
Ken Birman
  • 1,088
  • 8
  • 25