dotnet / docs

This repository contains .NET Documentation.
https://learn.microsoft.com/dotnet
Creative Commons Attribution 4.0 International
4.27k stars 5.9k forks source link

Static Constructor call guarantees #33704

Closed ohads-MSFT closed 1 year ago

ohads-MSFT commented 1 year ago

The docs state:

The user has no control on when the static constructor is executed in the program.

However one bullet below the above states:

It initializes the class before the first instance is created or any static members declared in that class (not its base classes) are referenced. A static constructor runs before an instance constructor. A type's static constructor is called when a static method assigned to an event or a delegate is invoked and not when it is assigned

The latter implies that the user does have some control over when the static constructor is executed, in that it would be executed immediately before accessing any property of the type, or invoking any of its static methods, but not before. Would that be a correct statement? If so, please clarify this in the docs.

In other words, the docs currently provide guarantees on the latest time a static ctor would be executed, but it's not clear whether there are any guarantees about the earliest time it would be executed (e.g. it will NOT be executed before any property/method is accessed/invoked).


Document Details

Do not edit this section. It is required for learn.microsoft.com ➟ GitHub issue linking.

BillWagner commented 1 year ago

The latter implies that the user does have some control over when the static constructor is executed, in that it would be executed immediately before accessing any property of the type, or invoking any of its static methods, but not before. Would that be a correct statement? If so, please clarify this in the docs.

Hi @ohads-MSFT

No, the user doesn't have any control over when the static constructor is executed.

§14.12 Static constructors, has the normative language for when a static constructor is invoked.

The key text is:

The static constructor for a closed class executes at most once in a given application domain. The execution of a static constructor is triggered by the first of the following events to occur within an application domain:

  • An instance of the class is created.
  • Any of the static members of the class are referenced.

The language doesn't restrict the compiler or runtime from invoking a static constructor in the absence of either of those events.

I'll add this to the backlog, and add the help-wanted label. The fix is:

  1. The sentence relating to delegates is an unnecessary complication. It's correct, in that assigning a static method to a delegate is not one of the triggering events. It isn't meant to imply that certain actions won't cause a static constructor to be invoked.
  2. This article should link to the previous reference to static constructors in the standard.

Also, related to #32809

ohads-MSFT commented 1 year ago

@BillWagner the standard text you quoted actually sounds like it does restrict the compiler/runtime from such an optimization, because it seems to provide a closed list.

In other words, The execution of a static constructor is triggered by the first of the following events for me sounds equivalent to The execution of a static constructor is triggered ONLY by the first of the following events.

The examples that follow also support this understanding in my view:

because the execution of A’s static constructor is triggered by the call to A.F, and the execution of B’s static constructor is triggered by the call to B.F.

Which implies these (and only these) are the triggers for static ctor execution.

BillWagner commented 1 year ago

@ohads-MSFT

You can't assume the word ONLY is in the standard. It's not there, and no such restriction is required. The standard says:

The static constructor for a closed class executes at most once in a given application domain. The execution of a static constructor is triggered by the first of the following events to occur within an application domain:

There is no rule that says a static constructor won't be called in the absence of those triggering events.

It may help resolve this to learn why that information is important to you.

Adding @jaredpar to this thread.

ohads-MSFT commented 1 year ago

@BillWagner When I see "triggered by" I read that as "only triggered by", otherwise it's not a trigger IMHO - but sure I could be wrong, let's wait for Jared's response... Note that the community seems to think as I do: https://stackoverflow.com/questions/1437352/when-is-a-static-constructor-called-in-c

As for my use case, we're using a library that reads some app settings from a static constructor. We want to inject some values to that configuration at runtime from a place we know for sure happens before the static ctor has been executed.

BillWagner commented 1 year ago

@ohads-MSFT

As for my use case,....

Would a ModuleInitializer work for that scenario?

ohads-MSFT commented 1 year ago

@BillWagner interesting, wasn't aware of those - are they guaranteed to happen before static constructors? Specifically, if my assembly references assembly X will my assembly's module initializer be executed before any static ctor of the referenced X assembly (assuming X's types aren't accessed / invoked)?

The docs you linked to don't seem to explicit guarantee that...

BillWagner commented 1 year ago

are they (module initialzers) guaranteed to happen before static constructors?

Yes, other than in truly pathological scenarios (like another module initializer creating an instance of the type with the static constructor).

Adding @RikkiGibson to be sure my answer is correct.

ohads-MSFT commented 1 year ago

In case we get validation, it would just translates into yet another docs clarification request (this time for module initializers) 😎

RikkiGibson commented 1 year ago

I would refer to https://github.com/dotnet/runtime/blob/main/docs/design/specs/Ecma-335-Augments.md#module-initializer.

A module initializer is executed at, or sometime before, first access to any static field or first invocation of any method defined in the module.

If you control the startup of the app, you can probably ensure your module initializer runs before the static constructor in the library in question.

Do take care that the above document refers to the platform concept of the module initializer. A module initializer defined in C# is a static method on a type. If that particular type has a static constructor, it will run before the module initializer. For example, in a scenario like this.

Also, when a compilation has multiple module initializers in C#, the order in which they run is implementation-defined. (We really do this by emitting the platform-level module initializer during compilation, and calling each of the user's module initializers.)

BillWagner commented 1 year ago

reopening this based on https://github.com/dotnet/docs/issues/33704#issuecomment-1409226234

ohads-MSFT commented 1 year ago

@RikkiGibson thank you for clarifying! Any thoughts about the original question regarding static constructor execution guarantees ?

ohads-MSFT commented 1 year ago

@BillWagner apologies but I don't follow, the associated fix (https://github.com/dotnet/docs/pull/37691) only seems to relate to module initializers. As far as I can tell, my original question about static constructors hasn't been officially answered...

Recall that AFAIK most of the community agrees with my understanding (including Jon Skeet / @jskeet for example), who quoted the exact same line in the spec about static ctor triggering, interpreting it as I did: https://stackoverflow.com/questions/32525628/why-static-constructor-not-called-before-first-call-to-class-method/32525769#comment52909611_32525769

BillWagner commented 1 year ago

Hi @ohads-MSFT

You can rely on the standard language. I don't want to add a stronger guarantee in the less formal language reference. That just adds two sources of truth that might drift over time.

Note that there is a little wiggle room in the standard language, but I really doubt the runtime would make any changes here.

ohads-MSFT commented 1 year ago

Hi @ohads-MSFT

You can rely on the standard language. I don't want to add a stronger guarantee in the less formal language reference. That just adds two sources of truth that might drift over time.

My claim is that adding "only triggered by" would actually not be a stronger guarantee than the standard provides... surely, we could involve some C# standard authority who could settle this?

BillWagner commented 1 year ago

My claim is that adding "only triggered by" would actually not be a stronger guarantee than the standard provides... surely, we could involve some C# standard authority who could settle this?

Sure. You can write an issue in the dotnet/csharpstandard repo and the committee as a whole will take it up.

KathleenDollard commented 1 year ago

@ohads-MSFT

I suspect we all agree that docs should not codify current behavior as standard.

The ECMA standard is the standard. Sometimes it is written in spec language that doesn't follow all the implications we may expect. In this case, adding an implication to the docs, when the spec would have said "only" had that been intended. If the spec needs to change that is a separate and careful process for good reason.

If I understand, you want to set some settings before the static initializer runs. Running things first is the purpose of module initializers. Is that working out for you?

ohads-MSFT commented 1 year ago

If I understand, you want to set some settings before the static initializer runs. Running things first is the purpose of module initializers. Is that working out for you?

To be honest I don't even remember why I started this whole discussion :) But it definitely sounds like module initializers would do the trick.

The ECMA standard is the standard. Sometimes it is written in spec language that doesn't follow all the implications we may expect. In this case, adding an implication to the docs, when the spec would have said "only" had that been intended. If the spec needs to change that is a separate and careful process for good reason.

Fair enough, I'll comment in those StackOverflow threads to try and let them know they are all (even @jskeet who modified said standard page multiple times) apparently mistaken...