Unintuitive behaviour with struct initialization and default arguments

1k views Asked by At
public struct Test 
{
    public double Val;
    public Test(double val = double.NaN) { Val = val; }
    public bool IsValid { get { return !double.IsNaN(Val); } }
}

Test myTest = new Test();
bool valid = myTest.IsValid;

The above gives valid==true because the constructor with default arg is NOT called and the object is created with the standard default val = 0.0.
If the struct is a class the behaviour is valid==false which is what I would expect.

I find this difference in behaviour and particularly the behaviour in the struct case suprising and unintuitive - what is going on? What does the default arg on the stuct construct serve? If its useless why let this compile?

Update: To clarify the focus here is not on what the behaviour is - but rather why does this compile without warning and behave unintuitively. I.e If the default arg is not applied because in the new Test() case the constructor is not called then why let it compile?

5

There are 5 answers

2
Jean Hominal On BEST ANSWER

In C# (at least until C# 6 - see blog post), invoking new Test() is equivalent to writing default(Test) - no constructor is actually called, the default value is provided.

The default arg serves no purpose, what happens is that it is likely the result of an oversight in the implementation of the compiler, due to the fact that optional arguments were only added in C# 4:

  • The code that checks that optional arguments do not conflict with already existing overloads is unaware of a possible conflict with the initializer in the case of structs;
  • The code that translates what new Test() means is probably unaware of the existence of optional arguments;

    • After digging into comments, I noticed the following gem by Mads Torgersen:

      It is true that the compiler implementation has so far "optimized" 'new T()' to mean essentially default(T) when T is a struct. That was actually a bug - it was always supposed to call an actual parameterless constructor if there is one - which there could have been all along, since it is allowed in IL.

      For your example, it means that new Test() is effectively replaced by the compiler to default(Test) - so that is a bug, which will be fixed in the next version of Visual Studio.

In other words, you have a corner case. That would probably be a good time to look at how that behaves in the next version of Visual Studio, as that behavior is changing.

0
Kell On

I suspect this is because a default constructor with no parameters is not allowed in c# so when you call the constructor Test without parameters it just initialises them as usual. Check this post for more details (not quite a duplicate): Why can't I define a default constructor for a struct in .NET?

0
AudioBubble On

For all value types T, new T() and default(T) are equivalent. They do not call any constructor, they merely set all fields to zero. This is also why C# does not let you write a parameterless constructor: public Test() { Val = double.NaN; } would not compile, because there would be no way for that constructor to be used.

You've found a corner case. Your constructor looks like it would be used for new T(). Since your type is still a value type, it isn't used. Since your constructor can be called, no error is issued.

1
CodeCaster On

Because a struct can't have a user-defined parameterless constructor.

Test(double val = double.NaN) looks like one, but it's actually compiled as Test(double val) with some metadata about the default value.

0
Yuval Itzchakov On

I find this difference in behaviour and particularly the behaviour in the struct case suprising and unintuitive - what is going on? What does the default arg on the stuct construct serve? If its useless why let this compile?

It serves nothing. The emitted IL code wont generate a call to the constructor with the default parameter, but will call default(Test). It seems totally reasonable that the compiler would emit a warning saying the constructor will not be invoked (although that is an implementation detail). I file an issue on http://connect.microsoft.com

If we look at the generated IL code for:

Test myTest = new Test();
bool valid = myTest.IsValid;

We'll see:

IL_0000:  ldloca.s    00 // myTest
IL_0002:  initobj     UserQuery.Test // default(Test);
IL_0008:  ldloca.s    00 // myTest
IL_000A:  call        UserQuery+Test.get_IsValid

Note the call made in IL isn't a method call to the constructor (which would look like: call Test..ctor) , it generated a call to initobj:

Initializes each field of the value type at a specified address to a null reference or a 0 of the appropriate primitive type. Unlike Newobj, initobj does not call the constructor method. Initobj is intended for initializing value types, while newobj is used to allocate and initialize objects.

Which means the compiler is merely disregarding the constructor with default parameters, as until C#-6.0 it is forbidden to declare such a constructor.

@JonSkeet takes this to great depth in his answer to Does using "new" on a struct allocate it on the heap or stack?

Edit:

I actually asked Mads Torgerson a question regarding the new use of the parameterless constructor in C#-6.0 which i think is related, and he said:

@Yuval and others, regarding parameterless constructors on structs: the thing to realize is that, before and now, constructors don't necessarily run on structs. All we did was add the ability to have an parameterless constructor that also cannot be guaranteed to run. There is no reasonable way to have structs that are guaranteed to have been initialized, and parameterless constructors don't help with that.

The thing parameterless constructors help with is allowing you to have a parameterless constructor.

I think a main source of confusion is that 'new S()' is allowed to mean 'default(S)'. That is a historical mistake in the language and I dearly wish I could take it away. I would strongly discourage anyone from using 'new S()' on a struct that doesn't have a parameterless constructor. As far as I can tell, this is in because the default(S) syntax didn't exist in C# 1.0, so this was just the syntax used for getting a default value of a struct.

It is true that the compiler implementation has so far "optimized" 'new T()' to mean essentially default(T) when T is a struct. That was actually a bug - it was always supposed to call an actual parameterless constructor if there is one - which there could have been all along, since it is allowed in IL. We are fixing this, so that we will call the constructor even in the generic case.

The semantics therefore is clean: new S() is the only way to run a parameterless constructor on a struct, and it always runs that constructor - even through generics.