Sometime back I posted about variable parameters in Ruby. C# also supports methods that accepts variable number of arguments (e.g. Console.Writeline). In this post I'll try to cover what happens in the background. This is a long one and so bear with me :)
Consider the following two methods. Both prints out each argument passed to it. However, the first accepts variable arguments using the params keyword.
static void Print1(params int[] args) { foreach (int arg in args) { Console.WriteLine(arg); } } static void Print2(int[] args) { foreach (int arg in args) { Console.WriteLine(arg); } }
The above methods can be called as follows
Print1(42, 84, 126); // variable argument passing int[] a = new int[] { 42, 84, 126 }; Print2(a); // called with an array
Obviously in the case above, using variable number of parameters is easier.
If we see the generated IL for Print1 and Print2 using ILDASM or Reflector and then do a diff, we will get the following diff
.method private hidebysig static void Print2(object[] args) cil managed .method private hidebysig static void Print1(object[] args) cil managed { .param [1] .custom instance void [mscorlib]System.ParamArrayAttribute::.ctor() .maxstack 2 .locals init ( [0] object arg, [1] object[] CS$6$0000, [2] int32 CS$7$0001, [3] bool CS$4$0002) L_0000: nop L_0001: nop L_0002: ldarg.0 L_0003: stloc.1 L_0004: ldc.i4.0 L_0005: stloc.2 L_0006: br.s L_0019 L_0008: ldloc.1 L_0009: ldloc.2 L_000a: ldelem.ref L_000b: stloc.0 L_000c: nop L_000d: ldloc.0 L_000e: call void [mscorlib]System.Console::WriteLine(object) L_0013: nop L_0014: nop L_0015: ldloc.2 L_0016: ldc.i4.1 L_0017: add L_0018: stloc.2 L_0019: ldloc.2 L_001a: ldloc.1 L_001b: ldlen L_001c: conv.i4 L_001d: clt L_001f: stloc.3 L_0020: ldloc.3 L_0021: brtrue.s L_0008 L_0023: ret }
Only the lines in Green are additional in Print1 (which takes variable arguments) and otherwise both methods looks identical. In this context .param[1*] indicates that the first parameter of Print1 (args) is the variable argument. The ParamArrayAttribute is applied to the method to indicate that the method allows variable number of arguments.
Effectively all of the above means that the callee is not really bothered with being invoked with variable number of arguments. It receives an array parameter as it would even without the param keyword usage. The only difference is that the method is decorated with the some directive and attribute when param is used. Now it's the caller-code compiler's duty to read this attribute and generate the correct code so that variable number of parameters are put into a array and Print1 is called with that.
The generated IL for the call Print1(42, 84, 126); is as follows...
.method private hidebysig static void Main(string[] args) cil managed { .entrypoint .maxstack 3 .locals init ( [0] int32[] CS$0$0000) L_0000: nop L_0001: ldc.i4.3 ; <= Array of size 3 is created, int32[3] L_0002: newarr int32 ; <= L_0007: stloc.0 ; <= the array is stored in the var CS$0$0000 L_0008: ldloc.0 L_0009: ldc.i4.0 ; push 0 L_000a: ldc.i4.s 0x2a ; push 42 L_000c: stelem.i4 ; this makes 42 to be stored at index 0 ** L_000d: ldloc.0 L_000e: ldc.i4.1 L_000f: ldc.i4.s 0x54 L_0011: stelem.i4 ; similarly as above stores 84 at index 1 L_0012: ldloc.0 L_0013: ldc.i4.2 L_0014: ldc.i4.s 0x7e L_0016: stelem.i4 ; stores 126 at index 2 L_0017: ldloc.0 L_0018: call void VariableArgs.Program::Print1(int32[]) ; call Print1 with array L_001d: nop L_001e: ret }
This shows that for the call an array is created and all the parameters are placed in it. Then Print1 is called with that array.
Footnote:*interestingly it starts at 1 and not 0 because 0 is used for the return value.**stelem takes the stack [..|array|index|value] and replaces the value in array at index with value
Cross posted here