A starting point is that the CLR has no support for this. It must be implemented by the compiler. Something you can see from a little test program:
class Program {
static void Main(string[] args) {
Test();
Test(42);
}
static void Test(int value = 42) {
}
}
Which decompiles to:
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 15 (0xf)
.maxstack 8
IL_0000: ldc.i4.s 42
IL_0002: call void Program::Test(int32)
IL_0007: ldc.i4.s 42
IL_0009: call void Program::Test(int32)
IL_000e: ret
} // end of method Program::Main
.method private hidebysig static void Test([opt] int32 'value') cil managed
{
.param [1] = int32(0x0000002A)
// Code size 1 (0x1)
.maxstack 8
IL_0000: ret
} // end of method Program::Test
Note how there is no difference whatsoever between the two call statements after the compiler is done with it. It was the compiler that applied the default value and did so at the call site.
Also note that this still needs to work when the Test() method actually lives in another assembly. Which implies that the default value needs to be encoded in the metadata. Note how the .param
directive did this. The CLI spec (Ecma-335) documents it in section II.15.4.1.4
This directive stores in the metadata a constant value associated with method parameter number Int32,
see §II.22.9. While the CLI requires that a value be supplied for the parameter, some tools can use the
presence of this attribute to indicate that the tool rather than the user is intended to supply the value of
the parameter. Unlike CIL instructions, .param uses index 0 to specify the return value of the method,
index 1 to specify the first parameter of the method, index 2 to specify the second parameter of the
method, and so on.
[Note: The CLI attaches no semantic whatsoever to these values—it is entirely up to compilers to
implement any semantic they wish (e.g., so-called default argument values). end note]
The quoted section II.22.9 goes into the detail of what a constant value means. The most relevant part:
Type shall be exactly one of: ELEMENT_TYPE_BOOLEAN, ELEMENT_TYPE_CHAR,
ELEMENT_TYPE_I1, ELEMENT_TYPE_U1, ELEMENT_TYPE_I2, ELEMENT_TYPE_U2,
ELEMENT_TYPE_I4, ELEMENT_TYPE_U4, ELEMENT_TYPE_I8, ELEMENT_TYPE_U8,
ELEMENT_TYPE_R4, ELEMENT_TYPE_R8, or ELEMENT_TYPE_STRING; or
ELEMENT_TYPE_CLASS with a Value of zero
So that's where the buck stops, no good way to even reference an anonymous helper method so some kind of code hoisting trick cannot work either.
Notable is that it just isn't a problem, you can always implement an arbitrary default value for an argument of a reference type. For example:
private void Process(Foo f = null)
{
if (f == null) f = new Foo();
}
Which is quite reasonable. And the kind of code you want in the method instead of the call site.