proposals/top-level-members.md
Champion issue: https://github.com/dotnet/csharplang/issues/9803
Allow some members (methods, operators, extension blocks, fields, constants) to be declared in namespaces and make them available when the corresponding namespace is imported (this is a similar concept to instance extension members which are also usable without referencing the container class).
// 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..];
// app.cs
#!/usr/bin/env dotnet
using MyApp;
Print($"Hello, {args[0].Capitalize()}!");
// Fields are useful:
namespace MyUtils;
string? cache;
string GetValue() => cache ??= Compute();
// Simplifies extensions:
namespace System.Linq;
extension<T>(IEnumerable<T> e)
{
public IEnumerable<T> AsEnumerable() => e;
}
Some members can be declared directly in a namespace (file-scoped or block-scoped).
Top-level members in a namespace are semantically members of an "implicit" class which:
static and partial,internal (by default) or public (if any member is also public),<>TopLevel,For top-level members, this means:
static modifier is disallowed (the members are implicitly static).internal.
public and private is also allowed.
protected and file is disallowed.extern and partial are supported.Metadata:
[TopLevel] (full attribute name is TBD).Usage (if there is an appropriately-shaped [TopLevel] type in namespace NS):
using NS; implies using static NS.<>TopLevel;.NS.Member can find NS.<>TopLevel.Member (useful for disambiguation).Entry points:
Main methods can be entry points.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.-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).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.
extern alias.
extern alias Util = Util.dll.Allow declaring top-level statements inside namespaces as well.
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.
Indentation concerns about current utility/extension methods could be resolved with
file-scoped types instead, i.e., allowing something like class MyNamespace.MyClass;
(although beware that class MyClass; has already a valid meaning today).
That wouldn't solve the use-site though, where you'd still need using static MyNamespace.MyClass; instead of just using MyNamespace; as with this proposal.
We could have something similar to VB's modules which are mostly like static classes but their members don't need to be qualified with the module name if they are brought to scope via an import:
Imports N
Namespace N
Module M
Sub F()
End Sub
End Module
End Namespace
Class C
Sub Main()
F()
End Sub
End Class
F# has something similar, too:
module Utilities =
let M() = ()
open Utilities
M()
For example, C# could have implicit classes like:
public implicit static class Utilities
{
public static void M() { }
}
Combined with top-level classes feature mentioned above, this could look like:
public implicit static class Utilities;
public static void M() { }
extension(int) { /* ... */ }
This makes the declaration side a bit more complicated to write, but it avoids problems with naming the implicit static class.
Open questions for this alternative:
static members?<>TopLevel class.[TopLevel] <>TopLevel).
[file: TopLevel("MyTopLevelClassName")]).static modifier (and keep our doors open if we want to introduce some non-static top-level members in the future)?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
}
namespace N;
class C;
namespace N;
extension(object) {} // error
class C;
namespace NS;
int Foo;
class Foo { }