diff --git a/proposals/top-level-members.md b/proposals/top-level-members.md new file mode 100644 index 0000000000..4a0c0bea68 --- /dev/null +++ b/proposals/top-level-members.md @@ -0,0 +1,185 @@ +# Top-Level Members + +Champion issue: https://github.com/dotnet/csharplang/issues/9803 + +## 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(IEnumerable e) +{ + public IEnumerable AsEnumerable() => e; +} +``` + +## Motivation + +- Avoid boilerplate utility static classes. +- Evolve top-level statements from C# 9. + +## Detailed design + +- 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. + +- Top-level members in a namespace are semantically members of an "implicit" class which: + - is `static` and `partial`, + - has accessibility either `internal` (by default) or `public` (if any member is also `public`), + - is named `TopLevel` (can be addressed even from C# which is useful for extension member disambiguation or source-generated `partial` implementations), + - is synthesized per each compilation unit (so having top-level members in the same namespace across assemblies can lead to [ambiguities](#drawbacks)). + + For top-level members, this means: + - The `static` modifier is disallowed (the members are implicitly static). + - The default accessibility is `internal`. + `public` and `private` is also allowed. + `protected` and `file` is disallowed. + - Overloading is supported. + - `extern` and `partial` are supported. + - XML doc comments work. + +- Metadata: + - The implicit class is recognized only if it has name `TopLevel` and an attribute `[TopLevel]` (full attribute name is TBD), + otherwise it is considered a plain old type. This prevents a breaking change + (where new members are in scope which can lead to ambiguity overload resolution errors). + +- Usage (if there is an appropriately-shaped `NS.TopLevel` type): + - `using NS;` implies `using static NS.TopLevel;`. + - Lookup for `NS.Member` can find `NS.TopLevel.Member`. + - Nothing really changes for extension member lookup (the class name is already not used for that). + +- Entry points: + - Top-level `Main` methods can be entry points. + - Top-level statements are generated into `Program.Main` (speakable function; previously it was unspeakable). + This is a breaking change: there could be a conflict with an existing `Program.Main` method declared by the user. + - Simplify the logic: TLS entry-points are normal candidates (previously they were not considered to be candidates and for example `-main` could not be used to point to them). + This is a breaking change: if the user has custom `Main` methods and top-level statements, they will get an error now because the compiler doesn't know which entrypoint to choose + (to fix that, they can specify `-main`). + +## Drawbacks + +- 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. + +- There might be ambiguities if two assemblies have top-level members in the same namespace: + ```cs + // A.dll + namespace X; + public void M1() { } // ok + ``` + ```cs + // B.dll + namespace X; + public void M2() { } // ok + + // generated code + namespace X + { + partial class TopLevel // ambiguity error + { + // ... + } + } + ``` + ```cs + // C.dll + using X; + M1(); M2(); // ok + X.TopLevel.M1(); // ambiguity error for `TopLevel` type + ``` + + Since such top-level members would work until one would reference the type, + that could lead to people declaring such conflicting APIs and realizing they are blocked when it's too late (e.g., they have shipped a public API). + The compiler could report a warning but that would still not work if the other DLL reference is added later. + +## Alternatives + +- 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`. + - Or they could be in scope only in the current assembly. +- Allow declaring top-level statements inside namespaces as well. + - Top-level local functions would introduce ambiguities with top-level methods. Wouldn't be a breaking change though, just need to decide which one wins. +- If we ever allow the `file` modifier on members (methods, fields, etc.), that would be naturally useful for top-level members, too. + `file` members would be scoped to the current file. + Compare that with `private` members which are scoped to the current _namespace_. + +## Open questions + +- Which member kinds? Methods, fields, properties, indexers, events, constructors, operators. +- Accessibility: what should be the default and which modifiers should be allowed? +- Clustering: currently each namespace per assembly gets its `TopLevel` class. +- Shape of the synthesized static class (currently `[TopLevel] TopLevel`). +- 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)? +- Should we disallow mixing top-level members and existing declarations in one file? + - Or we could limit their relative ordering, like top-level statements vs. other declarations are limited today. + - Allowing such mixing might be surprising, for example: + ```cs + namespace N; + int s_field; + int M() => s_field; // ok + static class C + { + static int M() => s_field; // error, `s_field` is not visible here + } + ``` + - Disallowing such mixing might be surprising too, for example, consider there is an existing code: + ```cs + namespace N; + class C; + ``` + and I just want to add a new declaration to it which fails and forces me to create a new file or namespace block: + ```cs + namespace N; + extension(object) {} // error + class C; + ``` +- Do we need new name conflict rules for declarations and/or usages? + For example, should the following be an error when declared (and/or when used)? + ```cs + namespace NS; + int Foo; + class Foo { } + ```