csharplang icon indicating copy to clipboard operation
csharplang copied to clipboard

Champion "Allow no-arg constructor and field initializers in struct declarations"

Open gafter opened this issue 7 years ago • 360 comments

  • [x] Proposal added: https://github.com/dotnet/csharplang/blob/main/proposals/csharp-10.0/parameterless-struct-constructors.md
  • [x] Discussed in LDM
  • [x] Decision in LDM
  • [x] Finalized: Done
  • [ ] Spec'ed

See also https://github.com/dotnet/roslyn/issues/13921 https://github.com/dotnet/roslyn/issues/1029

LDM history:

  • https://github.com/dotnet/csharplang/blob/master/meetings/2021/LDM-2021-01-27.md#field-initializers

gafter avatar Feb 14 '17 23:02 gafter

@jnm2 I don't think that it's about the benefit but more of a consistency so yes symmetry but dunno what it means for the run-time, I'm not sure what would happen when you define a parameterless constructor and set the fields, I voted because I hope that we could have default values but dunno.

iam3yal avatar Feb 15 '17 03:02 iam3yal

Downvoting this as per my comment for Roslyn #13921. For a struct, S, if default(S) and new S() leads to different values, then this feature has no real benefit and will cause serious bugs. This should be implemented properly in the CLR before it's considered for C#.

DavidArno avatar Feb 22 '17 12:02 DavidArno

I agree. If we're going to get non-nullable reference types and structs with parameterless constructors, then we should be able to override default for them,

orthoxerox avatar Feb 22 '17 13:02 orthoxerox

I really hope that they will design and implement this properly.

iam3yal avatar Feb 22 '17 13:02 iam3yal

To play Devil's advocate, why is it expected that new struct() and default(struct) produce the same value? That's never been the case with reference types.

Does C# support consuming structs with default constructors today? You could always define one using IL or some other language. How about the IL that C# produces for a generic method with a new constraint? Would that work today with such a type?

The concept of a "default" for struct types is interesting but even in the case of a blittable default you're talking about adding some serious overhead.

HaloFour avatar Feb 22 '17 13:02 HaloFour

@HaloFour Well, it might be possible to mitigate the overhead by an attribute? like opt-in/out?

iam3yal avatar Feb 22 '17 14:02 iam3yal

@HaloFour,

Your devil's advocate is a good question. To my mind, the answer is "and that is exactly what's wrong with reference types". It is the driver behind non-nullable reference types. SO just at the time when we are looking to address the "null problem", we'd be introducing the same problem in to structs.

DavidArno avatar Feb 22 '17 14:02 DavidArno

@eyalsk @DavidArno

I think we're wading into the territory of a different discussion here.

I just tested and answered my questions. C# does currently support consuming structs that define default constructors. It just doesn't support defining them. There is one notable exception and that's in a generic method where the type has a constructor constraint. I believe that this was already considered a bug in the JIT.

HaloFour avatar Feb 22 '17 14:02 HaloFour

We would need the core clr bug https://github.com/dotnet/coreclr/issues/6843 fixed before we could do this.

gafter avatar Feb 22 '17 17:02 gafter

What down-to-earth benefits would we gain from this proposal? I'm inclined to say this raises the complexity of the mental model with no benefits in sight, since no one in Codeplex, /roslyn or this thread has mentioned a single pragmatic benefit despite people repeatedly asking.

jnm2 avatar Feb 22 '17 17:02 jnm2

@jnm2 What do you mean? are you saying that it has zero benefit?

Just as a real world example, I have a text parser where the scanner returns a struct that looks like this:

public TextChar(char value, int index = 0, int lineNumber = 1, int columnNumber = 1)
{
	// ...
}

Now, when you create a new instance of the struct or use default lineNumber and columnNumber should default to 1 when there are no characters at all but atm it default to 0.

Update: Rephrased.

iam3yal avatar Feb 22 '17 18:02 iam3yal

@jnm2,

Let's say I have a type that holds information on a car gearbox, including the number of gear stick positions and the gearing ratios and there will always be at least two positions (forward and reverse).

Currently, my options are to use a class (so I can control its construction) and suffer null ref problems, or have a struct. The latter then either suffers the equivalent of the null ref problem if created with default(Gearbox) or new Gearbox(), or I have to leave all the fields as mutable and, on all methods and properties of the struct, I have to put guard code that checks for a misconstruction and sets up a proper default set of values.

If this feature were correctly implemented, eg via an overridable default method in the struct, then I could set those default values (or even throw some "not properly constructed" exception at the point of creation) when default(Gearbox) or new Gearbox() is called.

As the proposal currently stands though, default(Gearbox) wouldn't call the constructor, so from my point of view it doesn't achieve anything useful: I'd still need that guard code or would still need to use a class.

DavidArno avatar Feb 23 '17 10:02 DavidArno

BTW, some people workaround this by doing something like the following:

struct S
{
	private int _lineNumber;
	
	public S(int lineNumber = 1)
	{
		_lineNumber = lineNumber;
	}

	public int LineNumber => _lineNumber == default(int) ? 1 : _lineNumber;
}

Now, this can work but doing this for every field is just boilerplate.

iam3yal avatar Feb 23 '17 10:02 iam3yal

@DavidArno default is not a call. Default means zero-fill. Default happens when you create an array. Default happens to all class fields before your initialization code runs. Default happens due to a number of things, and it's for safety so that you don't get junk. Treating it like a call makes no sense because it isn't a call. We need it to be what it is: zero-fill.

jnm2 avatar Feb 23 '17 14:02 jnm2

I would expect that new S() could be different from default(S).

That means that developers should be aware that their struct can be created without running any constructor.

paulomorgado avatar Feb 23 '17 14:02 paulomorgado

@eyalsk same thing about default. It means "I want this memory to be zero" and changing that semantic would be next to impossible, let alone desirable.

I'm not sure I follow you with the rest of what you showed. Those parameter defaults should only apply if you use that constructor. new TextChar() will not use the constructor because you did not specify the non-optional value, so you would not expect the other parameter defaults to apply since those parameters do not come into play since you chose not to use that constructor.

jnm2 avatar Feb 23 '17 14:02 jnm2

I'd like to reiterate that this proposal has nothing to do with changing how the C# compiler interprets new T() where T is a struct. That behavior is already means something different from default(T) and, as far as I'm aware, that has been the case since C# 1.0. To the CLR, and even to C#, new T() and default(T) have never meant the same thing.

Of course since neither C# nor VB.NET permitted defining a struct with a parameterless constructor the odds of running into one would be exceptionally low. So low that a bug made it into the BCL assuming that such a beast wouldn't exist. But it's quite possible for some other language to define such a struct, and C# already respects such structs by invoking their constructors.

So, in my opinion we need to move on from the conversation about whether structs should have parameterless constructors or whether C# should consider new T() and default(T) to be two different things. That ship has apparently sailed a long time ago. At this point the question should be only whether C# should permit defining new structs with parameterless constructors. Now, if you don't like the idea of parameterless constructors in structs and don't want Microsoft to encourage writing them I definitely think that's a fair argument.

HaloFour avatar Feb 23 '17 15:02 HaloFour

@jnm2

I'm not sure I follow you with the rest of what you showed. Those parameter defaults should only apply if you use that constructor. new TextChar() will not use the constructor because you did not specify the non-optional value, so you would not expect the other parameter defaults to apply since those parameters do not come into play since you chose not to use that constructor.

Let's disregard my previous post because it wasn't clear so let's focus on the following examples instead:

public struct Line
{
	public Line(int lineNumber = 1)
	{
		LineNumber = lineNumber;
	}

	public int LineNumber { get; }
}

Possible today but when you do new TextChar() then LineNumber results 0 instead of 1.

public struct Line
{
	private readonly int _lineNumber = 1;

	public Line(int lineNumber)
	{
		_lineNumber = lineNumber;
	}

	public int LineNumber => _lineNumber;
}

Not possible today but I'd like to have it so in the case of the following code:

public struct TextChar
{
	private readonly int _lineNumber = 1;
	
	private readonly int _columnNumber = 1;
	
	public TextChar(char value, int index = 0, int lineNumber = 1, int columnNumber = 1)
	{
		Value = value;

		Index = index;

		_lineNumber = lineNumber;

		_columnNumber = columnNumber;
	}

	public int ColumnNumber => _columnNumber;

	public int Index { get; }

	public int LineNumber => _lineNumber;

	public char Value { get; }
}

When I'd do new TextChar() then LineNumber and ColumnNumber would result 1 as opposed to 0.

Now, it's a not a deal breaker but I think that having the ability to do it will fix the slight disparity with classes and people won't need to circumvent or fight the language and do something like this:

public int LineNumber => _lineNumber == default(int) ? 1 : _lineNumber;

iam3yal avatar Feb 23 '17 16:02 iam3yal

@HaloFour,

Currently, New S() and default(S) do the same thing in all .NET languages, due to the optimization introduced in Activator.CreateInstance. That comes about because the dominant .NET languages didn't allow custom parameterless constructors, so there was no point in calling it for structs. Because of that, lots of code will have been written that uses default(S) instead of new S(), because they currently do exactly the same thing. As far as I can see therefore, "fixing" that optimisation, and allowing the two struct creation options to behave differently could prove a huge breaking change for many folk.

@jmn2, If non-nullable reference types are introduced, then for a non-nullable class, C, what would you expect the result of default(S) to be? "Zero-filled" null? That seems broken to me as suddenly my non-nullable class as null reference, which just broke my code. Likewise with structs that implement parameterless constructors. Those constructors are being implemented because a zero-filled default doesn't make sense for that type, therefore a zero-filled default(S) would be an invalid value for that struct.

DavidArno avatar Feb 23 '17 17:02 DavidArno

@DavidArno

Currently, new S() and default(S) do the same thing in all .NET languages, due to the optimization introduced in Activator.CreateInstance.

That only affects specific uses with generics. When using a struct directly the C# compiler will always invoke the parameterless constructor if it exists. That is also being regarded as a bug in the CLR which at the time of implementation was itself a breaking change.

Even though I do largely use the generic type argument syntax, I am referring to all uses of structs in my argument. If you were handed an assembly the contained a struct called FooStruct and you used new FooStruct() that never implied the same thing as default(FooStruct).

void Test1() {
    var s = new FooStruct(); // invokes parameterless constructor
}

void Test2<T>() where T : struct {
    var s = new T(); // invokes parameterless constructor
}

void Test3<T>() where T : new() {
    var s = new T(); // does not invoke parameterless constructor
}

As mentioned in https://github.com/dotnet/coreclr/issues/6843, structs with parameterless constructors are a part of the CLI spec. As far as I can tell they also meet CLS requirements. C# is required to at least respect them. So is the CLR.

HaloFour avatar Feb 23 '17 17:02 HaloFour

@eyalsk So all you are really asking for is struct field initializers. (Field initializers would be possible if you call new Struct() but impossible if you use default(Struct) because, as previously mentioned, default(T) is defined as zeroing the containing memory space.)

I suppose it's a matter of perspective. Since default(Struct) is not going away, nor should it, you'll still have to deal with (new Struct[1])[0] zero-initializing all the fields and you'll still have to harden your struct against zero-inited fields. Since you'll still always have to handle zero-init fields, are field initializers that much of a win? Field initializers can only work if you call a constructor. Creating an array does not call constructors for each element.

jnm2 avatar Feb 23 '17 18:02 jnm2

@jnm2 I can relate to what @DavidArno says but at the same time I wouldn't mind that new S() and default(S) would result a different value.

I wasn't speaking about default and as @HaloFour said we shouldn't derail the discussion as it should be discussed in a new issue so I didn't mention it.

are field initializers that much of a win? Field initializers can only work if you call a constructor. Creating an array does not call constructors for each element.

You're right but at the same time it depends: in my case I'm not returning an array but an Enumerable<TextChar> and all the instance of TextChar are being properly initialized and it's only happens in one place this is the reason I wrote that for me, it isn't a deal breaker, it's just nice to have.

Beyond that I think that it might be nice to fix the slight disparity between classes and structs that might be a surprise to new comers.

iam3yal avatar Feb 24 '17 02:02 iam3yal

@eyalsk default is a necessary part of this conversation. I asked what the benefit of this proposal is; you answered that the benefit is not having to "fight the language" by worrying about zero-inited fields.

But because default will happen to every struct, therefore you must harden your field logic against zero-init. Because you must harden your field logic against zero-init, this proposal does not alleviate any worry about zero-inited fields. Therefore, that's not a benefit. Unless some other benefit is proposed, this proposal is at -100. :')

jnm2 avatar Feb 24 '17 03:02 jnm2

@jnm2 I've updated my last post with some more info, I don't know why default is necessary part of this conversation, it's a different thing and as such should be discussed in a new issue.

The discussion about default just "hammer" this proposal down for no reason at all, features should stand on their own and as such discussed separately.

iam3yal avatar Feb 24 '17 03:02 iam3yal

default and new() for class is difference

So we can assume the same for struct. new Struct() is explicit construct, default(Struct) will be zero memory. And that's normal behaviour

Thaina avatar Feb 24 '17 04:02 Thaina

If non-nullable reference types are introduced, then for a non-nullable class, C, what would you expect the result of default(S) to be? "Zero-filled" null? That seems broken to me as suddenly my non-nullable class as null reference, which just broke my code.

I presume you meant to use default(C) there. And well, are you talking about the current thing that really only warns against potential null-derefs at design/compile-time, or are you holding out for some brand new CLR + language version where you could state a class can't be null at its declaration rather than at the individual use-sites?

Joe4evr avatar Feb 24 '17 08:02 Joe4evr

@Joe4evr,

Yes, I did mean default(C). Thanks for the correction.

Like with this feature, only providing compile-time checks would make it a leaky feature, prone to bugs. So yes, I'd want to see it properly implemented in the CLR, not just in the language.

However, it's clear that I'm in the minority here when it comes to structs, so I'm likely in the minority on that feature too. I can't win them all...

DavidArno avatar Feb 24 '17 08:02 DavidArno

Even then, all other languages I heard about that have non-nullable classes have it so you specify whether or not null is an accepted value at the use-site, not at its declaration. And I guess that would be because the latter option would be inhibiting composability.

Joe4evr avatar Feb 24 '17 08:02 Joe4evr

@DavidArno If we have non-nullable type. I would prefer that it will be compile time feature you will never be able to create collection of it from new array (at compile time)

But instead you need to convert from another collection (such as by linq) and make sure it will never be null

string![] NonNullable = new string![10]; // ERROR

string![] NonNullable = Enumerable.Repeat((string!)"",10).ToArray(); // Pass

string![] NonNullable = new string![] { "","","","","","","","","","" }; // Pass

I wish all feature like that should only be compile time. We just need to make sure we protect everywhere, such as array of non nullable object like you concern, to avoid bug like that

Thaina avatar Feb 24 '17 08:02 Thaina

This is a real pain point when trying to use struct as non-nullable wrappers for more complex types. In my functional library I have a ton of immutable collection types (Lists, Maps, Sets, etc.). They are all structs that contain a single reference to a class with the full implementation. It means I can have:

    Map<string, int> x = default(Map<string, int>); 

But that then requires a null check every time the type is used:

    public struct Map<A, B>
    {
        readonly MapInternal<A, B> value;

        // This property is used throughout the type to get a valid MapInternal
        MapInternal<A, B> Value => value ?? MapInternal<A, B>.Empty;

        // .. impl ...
    }

Obviously this is still better than forcing the programmer to do it manually, but less efficient overall.

I seem to remember this being brought up for C#6 and was dropped, so I'd be interested to know why?

Right now no struct will compile with a default constructor, and existing structs can continue working as they are, with the same semantics for default instantiation and copying. There shouldn't be any need to change any layout behaviours, array layout behaviour, default zeroing behaviour for structs that have no default constructor.

For types with a default constructor then I expect it only to be called when the type is instantiated via new or default(T), any copying semantics should stay the same and not call the default constructor.

louthy avatar Apr 05 '17 19:04 louthy