-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Add top-level members proposal #9719
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
base: main
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| # Top-Level Members | ||
|
|
||
| Champion issue: (TODO) | ||
|
|
||
| ## Summary | ||
|
|
||
| Allow some members (methods, operators, extension blocks, and fields) to be declared in namespaces | ||
| and make them available when the corresponding namespace is imported. | ||
|
|
||
| ```cs | ||
| // util.cs | ||
| namespace MyApp; | ||
|
|
||
| void Print(string s) => Console.WriteLine(s); | ||
|
|
||
| string Capitalize(this string input) => | ||
| input.Length == 0 ? input : char.ToUpper(input[0]) + input[1..]; | ||
| ``` | ||
|
|
||
| ```cs | ||
| // app.cs | ||
| #!/usr/bin/env dotnet | ||
|
|
||
| using MyApp; | ||
|
|
||
| Print($"Hello, {args[0].Capitalize()}!"); | ||
| ``` | ||
|
|
||
| ```cs | ||
| // Fields are useful: | ||
| namespace MyUtils; | ||
|
|
||
| string? cache; | ||
|
|
||
| string GetValue() => cache ??= Compute(); | ||
| ``` | ||
|
|
||
| ```cs | ||
| // Simplifies extensions: | ||
| namespace System.Linq; | ||
|
|
||
| extension<T>(IEnumerable<T> e) | ||
| { | ||
| public IEnumerable<T> AsEnumerable() => e; | ||
| } | ||
| ``` | ||
|
|
||
| ## Motivation | ||
|
|
||
| TODO: Why are we doing this? What use cases does it support? What is the expected outcome? | ||
|
|
||
| - Avoid boilerplate utility static classes. | ||
| - Evolve top-level statements from C# 9. | ||
|
|
||
| ## Detailed design | ||
|
|
||
| TODO: This is the bulk of the proposal. Explain the design in enough detail for somebody familiar with the language to understand, and for somebody familiar with the compiler to implement, and include examples of how the feature is used. This section can start out light before the prototyping phase but should get into specifics and corner-cases as the feature is iteratively designed and implemented. | ||
|
|
||
| - Some members can be declared directly in a namespace (file-scoped or block-scoped). | ||
| - Allowed kinds currently are: methods, operators, extension blocks, and fields. | ||
| - Existing declarations like classes still work the same, there shouldn't be any ambiguity. | ||
| - There is no ambiguity with top-level statements because those are not allowed inside namespaces. | ||
jjonescz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| - It is as if the members were in an "implicit" `static` class | ||
| whose accessibility is either `internal` (by default) or `public` (if any member is also `public`). | ||
| For top-level members, this means: | ||
| - The `static` modifier is disallowed (the members are implicitly static). | ||
| - The default accessibility is `internal`. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I definitely don't like this or expect this. I would expect members without modifiers to have the narrowest accessibility (so only visible within the file. To be visible outside, you'd need to be explicit about internal/public. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it is expected because it's the default visibility for everything that language currently allows to be top-level (classes/structs/delegates/enums). For example, I think this is pretty unsatisfactory: // util.cs
namespace MyApp;
// Implicitly `internal`
class MyThing(string _) {}
// Implicitly `private` or `file`
MyThing CreateThing(string s) => new MyThing(s);Come to think of it, this raises another question: what happens to types declared inside a file that also contains top-level members? Using the example above, we could take it to mean either (a): // util.cs
namespace MyApp;
static class <>__Generated
{
class MyThing(string _) {}
// Implicitly `private` or `file`
MyThing CreateThing(string s) => new MyThing(s);
}or (b) // util.cs
namespace MyApp;
// Implicitly `internal`
class MyThing(string _) {}
static class <>__Generated
{
// Implicitly `private` or `file`
MyThing CreateThing(string s) => new MyThing(s);
}Of the two I think (b) makes more sense†, in which case my initial reaction to visibility stands. † I think (b) is preferrable to (a) because, otherwise, adding a top-level member to a file would suddenly nest all the classes declared in that file, potentially hiding them.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Agreed, I said that as well in another discussion above (#9719 (comment)). Although it might just be disallowed to mix existing declarations with the new top-level ones to avoid these kinds of issues. |
||
| `public` and `private` is also allowed. | ||
| `protected` and `file` is disallowed. | ||
| - Overloading is supported. | ||
| - `extern` and `partial` are supported. | ||
| - XML doc comments work. | ||
| - Metadata: | ||
| - A type synthesized per namespace and file. That means `private` members are only visible in the file. | ||
| - Cannot be addressed from C#, but has speakable name `TopLevel` so it is callable from other languages. | ||
| This means that custom types named `TopLevel` become disallowed in a namespace where top-level members are used. | ||
| - Usage: | ||
| - `using NS;` implies `using static NS.TopLevel;`. | ||
jjonescz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| - Lookup for `NS.Method()` can find `NS.TopLevel.Method()`. | ||
| - Nothing really changes for extensions. | ||
| - Entry points: | ||
| - Top-level `Main` methods can be entry points. | ||
| - Top-level statements are generated into `Program.Main` (speakable function). | ||
| This is a breaking change (previously the main method was unspeakable). | ||
| - Simplify the logic: TLS entry-points are normal candidates. | ||
| This is a breaking change (previously they were not considered to be candidates and for example `-main` could not be used to point to them). | ||
|
|
||
| ## Drawbacks | ||
|
|
||
| TODO: Why should we *not* do this? | ||
|
|
||
| - Polluting namespaces with loosely organized helpers. | ||
| - Requires tooling updates to properly surface and organize top-level methods in IntelliSense, refactorings, etc. | ||
| - Entry point resolution breaking changes. | ||
|
|
||
| ## Alternatives | ||
|
|
||
| TODO: What other designs have been considered? What is the impact of not doing this? | ||
|
|
||
| - Support `args` keyword in top-level members (just like it can be accessed in top-level statements). But we have `System.Environment.GetCommandLineArgs()`. | ||
| - Allow capturing variables from top-level statements inside non-`static` top-level members. | ||
| Could be used to refactor a single-file program into multi-file program just by extracting functions to separate files. | ||
| But it would mean that a method's implementation (top-level statements) can influence what other methods see (which variables are available in top-level members). | ||
| - Allow declaring top-level members outside namespaces as well. | ||
| - Would introduce ambiguities with top-level statements. | ||
| - Could be brought to scope via `extern alias`. | ||
| - To avoid needing to specify those in project files (e.g., so file-based apps also work), | ||
| there could be a syntax for that like `extern alias Util = Util.dll`. | ||
|
|
||
| ## Open questions | ||
|
|
||
| TODO: What parts of the design are still undecided? | ||
jjonescz marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| - Which member kinds? Methods, fields, properties, indexers, events, constructors, operators. | ||
| - Allow `file` or `private` or both? What should `private` really mean? Visible to file, namespace, or something else? | ||
| - Name for the speakable static class (currently `TopLevel`)? Should it be speakable at all? | ||
| - Should we simplify the TLS entry point logic? Should it be a breaking change? | ||
| - Should we require the `static` modifier (and keep our doors open if we want to introduce some non-`static` top-level members in the future)? | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Top-level statements can co-exist with type declarations, but must come first. Is there a similar rule for top-level members?
Would such types be in the namespace or nested in the
TopLeveltype? From the spec it should be in the namespace (since types are not allowed as top-level members), but that's surprising. So it may be better to just disallow them...There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good question, I will add it to the list. But I don't find the behavior surprising, consider that you have an existing code like
and you decide to add a top-level member, for example
that could be disallowed but if it's allowed it seems natural that the class remains directly in the namespace as it was before.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's arguable, but not obvious. What if I sandwich the type?
It doesn't seem obvious why
M()andM2()are inN.TopLevel, butCis directly inNUh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My $0.02: There's been many a request to "reduce the indentation" with file-scoped classes (like file-scoped namespaces), and the team has rejected them for a variety of reasons — I would too. Combining top-level members with file-scoped types sounds like a recipe for disaster.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jcouv
Aren't you taking on a little too much implementation detail here? As far as the user is concerned
MandM2are just functions in the namespaceN. The fact that a containing class is generated for them would be--and should be--entirely hidden from the user.You could even nominally think of each top-level member as being given its own, generated, containing class. As long as the members can still see each other it makes no difference from a usability standpoint.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To put it another way, my mental model of how this should work goes like this:
Rather than thinking "If this file contains top-level members then lift the contents of the file into a generated class" I think "For each declaration in this file, if the declaration is a type then leave it as-is, if it's a member then lift it into a generated class".
So
becomes