Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Define capture behavior when primary ctor bypassed #7354

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions proposals/primary-constructors.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ If a primary constructor parameter is referenced from within an instance member,

Capturing is not allowed for parameters that have ref-like type, and capturing is not allowed for `ref`, `in` or `out` parameters. This is similar to a limitation for capturing in lambdas.

Structs present a challenge for primary constructor parameter capture: there is no way to enforce the execution of constructors on structs. (For example, creating a single-element array of some struct type will produce an instance of that type without running any of its constructors.) Section [§9.2.5](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/variables#925-value-parameters) of the C# spec states that constructor parameters do not come into existence until the constructor is invoked. Since primary constructor parameters are in scope throughout the type, this can create the awkward situation in which a parameter is in scope, but does not actually exist. If a parameter of a struct's primary constructor is captured, this would mean that the member causing its capture would be using a variable that does not exist. To avoid this, we assert that in cases where an instance of a type with a primary constructor was created through a mechanism that bypasses that constructor, all captured parameters come into existence when the instance is created, and are all initialized with the default value for their type.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems ok to me, but I want @MadsTorgersen to give some input as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not have the "Structs present a challenge for primary constructor parameter capture:" part.

Similarly the " this can create the awkward situation in which a parameter is in scope, but does not actually exist."

I would simply state that constructor parameters for structs** are always in existence. And they either have the values provided when the constructor is invoked. Or they have the default value otherwise.

** We can also limit this to structs with primary constructors only. Or we can just state it's for any struct.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not have the...

That'll be my background as an instructor kicking in: always give a clear motivation so people understand why they should care. But it was probably a bit much for a spec, so I've taken out the negative language.

Regarding:

constructor parameters for structs are always in existence

and:

We can also limit this to structs with primary constructors only. Or we can just state it's for any struct.

I think that expanding it out to apply to all struct constructors (or even all struct primary constructors) would be problematic. Although I appreciate that a more broad-spectrum approach is simpler, and simpler is generally better in specs, I think there are likely to be additional problems with stating that all constructor arguments exist from the start of the lifetime of the class: it would disagree with §9.2.5 of the C# language spec about when the variable comes into existence. (That problem doesn't arise with my current wording, because it only applies in cases where §9.2.5 says the variable doesn't exist, meaning there can't be any disagreement about when it comes into existence: my addition would only define the moment of coming into existence for variables where §9.2.5 does not provide such a definition.) I'm not convinced it's possible to redefine when all struct constructor arguments come into existence without causing problems for other parts of the spec. This feels like it would be opening a can of worms.

And if the text only describes behaviour for captured parameters for primary constructors, it would then be odd to state that it applies to any struct, not just those with a primary constructor, because in a struct without a primary constructor, there won't be any captured primary constructor parameters. (So we'd be saying it applied in places where it has no effect.)

It really is just the captured primary constructor arguments for which this is relevant. (My wording doesn't even apply to primary constructor arguments which are used only for initialization, because the scenarios where those fail to come into existence are also the scenarios in which they are not used. It's only capture of primary ctor parameters that causes a problem. And it causes a problem because it defines a lexical scope for these parameters that is slightly incompatible with their dynamic lifetime.)

And making it all about structs would miss two other scenarios that occurred to me while writing this. First, binary serialization enables constructors to be bypassed. I know binary serialization has been deprecated for some time, and now generates runtime errors in .NET 8, but you can still use it if you set EnableUnsafeBinaryFormatterSerialization. So despite the deprecated status, it is absolutely possible to use it on current .NET 8 previews to create an instance of a class without running its primary constructor.

Second, MemberwiseClone also bypasses constructors for both struct and class types. There do not appear to be any public plans for MemberwiseClone to be deprecated.

There may also be other ways to bypass construction that I'm unaware of. But it's certainly true that there is at least one non-deprecated way to instantiate a class without running its primary constructor.

So the effect of saying that it applied to all constructors of structs would, paradoxically, be both too narrow (you can bypass constructors for non-structs too) and also too wide (this situation in which code can use a non-existent variable arises only for captured primary constructor arguments, so it's unnecessary to state it for any other kind of constructor argument).

This is why I scoped it very carefully to this:

in cases where an instance of a type with a primary constructor was created through a mechanism that bypasses that constructor, all captured parameters

The aim here is to characterise precisely the cases where the parameters would otherwise fail to exist, and only those cases.


If a primary constructor parameter is only referenced from within instance member initializers, those can directly reference the parameter of the generated constructor, as they are executed as part of it.

Primary Constructor will do the following sequence of operations:
Expand Down