Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can't I give a default value as optional parameter except null?

I want to have a optional parameter and set it to default value that I determine, when I do this:

private void Process(Foo f = new Foo())
{

}

I'm getting the following error (Foo is a class):

'f' is type of Foo, A default parameter of a reference type other than string can only be initialized with null.

If I change Foo to struct then it works but with only default parameterless constructor.

I read the documentation and it's clearly states that I cannot do this but it doesn't mention why?, Why is this restriction exists and why string is excluded from this? Why the value of an optional parameter has to be compile-time constant? If that wouldn't be a constant then what would be the side-effects ?

like image 824
Selman Genç Avatar asked Apr 12 '14 22:04

Selman Genç


People also ask

Which parameter Cannot have a default value?

An IN OUT parameter cannot have a default value. An IN OUT actual parameter or argument must be a variable.

How do you specify an optional parameter?

Optional parameters are defined at the end of the parameter list, after any required parameters. If the caller provides an argument for any one of a succession of optional parameters, it must provide arguments for all preceding optional parameters. Comma-separated gaps in the argument list aren't supported.

How do you make a function parameter optional?

Using the Logical OR operator ('||') In this method, the optional parameter is "Logically ORed" with the default value within the body of the function. In the example below, if the value of b is undefined, 2 is passed instead.


3 Answers

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.

like image 78
Hans Passant Avatar answered Oct 04 '22 23:10

Hans Passant


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:

  1. 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());  
    
  2. 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.

like image 43
Ondrej Tucny Avatar answered Oct 05 '22 01:10

Ondrej Tucny


It is just the way the language works, I can't say why they do it (and this site is not a site for discussions like that, if you want to discuss it take it to chat).

I can show you how to work around it, just make two methods and overload it (modified your example slightly to show how you would return results too).

private Bar Process()
{
    return Process(new Foo());
}

private Bar Process(Foo f)
{
    //Whatever.
}
like image 38
Scott Chamberlain Avatar answered Oct 05 '22 01:10

Scott Chamberlain