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 [..arrayindexvalue] and replaces the value in array at index with value
2 comments:
Good post.
I have a question: what do you use to prepare code samples in the blogpost?
I use windows live writer, you should give it a spin with the paste from Visual Studio plugin. In addition to that I use css to give pre tags a border and a white background.....
Post a Comment