Because there's no other compile-time constant than null. For strings, string literals are such compile-time constants.
I think that some of the design decisions behind it may have been:
- Simplicity of implementation
- Elimination of hidden / unexpected behavior
- Clarity of method contract, esp. in cross-assembly scenarios
Lets elaborate on these three a bit more to get some insight under the hood of the problem:
1. Simplicity of implementation
When limited to constant values, both the compiler's and CLR's jobs are pretty easy. Constant values can be easily stored in assembly metadata, and the compiler can easily . How this is done was outlined in Hans Passant's answer.
But what could the CLR and compiler do to implement non-constant default values? There are two options:
Store the initialization expressions themselves, and compile them there:
// seen by the developer in the source code Process(); // actually done by the compiler Process(new Foo());
Generate thunks:
// seen by the developer in the source code Process(); … void Process(Foo arg = new Foo()) { … } // actually done by the compiler Process_Thunk(); … void Process_Thunk() { Process(new Foo()); } void Process() { … }
Both solutions introduce a lot more new metadata into assemblies and require complex handling by the compiler. Also, while solution (2) can be seen as a hidden technicality (as well as (1)), it has consequences in respect to the perceived behavior. The developer expects that arguments are evaluated at call site, not somewhere else. This may impose extra problems to be solved (see part related to method contract).
2. Elimination of hidden / unexpected behavior
The initialization expression could have been arbitrarily complex. Hence a simple call like this:
Process();
would unroll into a complex calculation performed at call site. For example:
Process(new Foo(HorriblyComplexCalculation(SomeStaticVar) * Math.Power(GetCoefficient, 17)));
That can be rather unexpected from the point of view of reader that does not inspect ´Process´'s declaration thoroughly. It clutters the code, makes it less readable.
3. Clarity of method contract, esp. in cross-assembly scenarios
The signature of a method together with default values imposes a contract. This contract lives in a particular context. If the initialization expression required bindings to some other assemblies, what would that require from the caller? How about this example, where the method 'CalculateInput' is from 'Other.Assembly':
void Process(Foo arg = new Foo(Other.Assembly.Namespace.CalculateInput()))
Here's the point where the way this would be implemented plays critical role in thinking whether this is a problem or note. In the “simplicity” section I've outlined implementation methods (1) and (2). So if (1) were chosen, it would require the caller to bind to 'Other.Assembly'. On the other hand, if (2) were chosen, there's far less a need—from the implemenetation point of view—for such rule, because the compiler-generated Process_Thunk is declared at the same place as Process and hence naturally has a reference to Other.Aseembly. However, a sane language designer would even though impose such a rule, because multiple implementations of the same thing are possible, and for the sake of stability and clarity of method contract.
Nevertheless, there cross-assembly scenarios would impose assembly references that are not clearly seen from the plain source code at call site. And that's a usability and readability problem, again.