Delen via


Instructies op het hoogste niveau

Notitie

Dit artikel is een functiespecificatie. De specificatie fungeert als het ontwerpdocument voor de functie. Het bevat voorgestelde specificatiewijzigingen, samen met informatie die nodig is tijdens het ontwerp en de ontwikkeling van de functie. Deze artikelen worden gepubliceerd totdat de voorgestelde specificaties zijn voltooid en opgenomen in de huidige ECMA-specificatie.

Er kunnen enkele verschillen zijn tussen de functiespecificatie en de voltooide implementatie. Deze verschillen worden vastgelegd in de relevante notities van de Language Design Meeting (LDM) .

Meer informatie over het proces voor het aannemen van functiespeclets in de C#-taalstandaard vindt u in het artikel over de specificaties.

Kampioensprobleem: https://github.com/dotnet/csharplang/issues/2765

Samenvatting

Sta toe dat een reeks instructies direct vóór de namespace_member_declaratiesvan een compilation_unit (bijvoorbeeld bronbestand) optreedt.

De semantiek is dat als een dergelijke reeks instructies aanwezig is, de volgende typedeclaratie, modulo de werkelijke methodenaam, zou worden verzonden:

partial class Program
{
    static async Task Main(string[] args)
    {
        // statements
    }
}

Zie ook https://github.com/dotnet/csharplang/issues/3117.

Motivatie

Er zit een bepaalde hoeveelheid boilerplate in zelfs de eenvoudigste programma's, vanwege de noodzaak voor een expliciete Main-methode. Dit lijkt in de weg te staan van taalverwerving en de duidelijkheid van het programma. Het primaire doel van de functie is daarom om C#-programma's zonder onnodige code mogelijk te maken, ten behoeve van cursisten en de duidelijkheid van code.

Gedetailleerd ontwerp

Syntaxis

De enige extra syntaxis is het toestaan van een reeks instructies in een compilatie-eenheid, net vóór de namespace_member_declarations:

compilation_unit
    : extern_alias_directive* using_directive* global_attributes? statement* namespace_member_declaration*
    ;

Slechts één compilation_unit mag verklarings hebben.

Voorbeeld:

if (args.Length == 0
    || !int.TryParse(args[0], out int n)
    || n < 0) return;
Console.WriteLine(Fib(n).curr);

(int curr, int prev) Fib(int i)
{
    if (i == 0) return (1, 0);
    var (curr, prev) = Fib(i - 1);
    return (curr + prev, curr);
}

Semantiek

Als er instructies op het hoogste niveau aanwezig zijn in een compilatie-eenheid van het programma, is de betekenis alsof ze als volgt zijn gecombineerd in de bloktekst van een Main methode van een Program-klasse in de globale naamruimte:

partial class Program
{
    static async Task Main(string[] args)
    {
        // statements
    }
}

Het type heet 'Program', dus kan er vanuit de broncode naar worden verwezen met de naam. Het is een gedeeltelijk type, dus een type met de naam 'Programma' in broncode moet ook worden gedeclareerd als gedeeltelijk.
Maar de methodenaam 'Main' wordt alleen gebruikt voor illustratiedoeleinden, de werkelijke naam die door de compiler wordt gebruikt, is afhankelijk van de implementatie en de methode kan niet worden verwezen op naam uit de broncode.

De methode wordt aangewezen als het toegangspunt van het programma. Expliciet gedeclareerde methoden die volgens conventie als potentiële toegangspunten kunnen worden beschouwd, worden genegeerd. Er wordt een waarschuwing gerapporteerd wanneer dat gebeurt. Het is een fout om -main:<type> compilerswitch op te geven wanneer er instructies op het hoogste niveau zijn.

De invoerpuntmethode heeft altijd één formele parameter, string[] args. De uitvoeringsomgeving maakt en geeft een string[] argument door met de opdrachtregelargumenten die zijn opgegeven toen de toepassing werd gestart. Het argument string[] is nooit null, maar heeft mogelijk een lengte van nul als er geen opdrachtregelargumenten zijn opgegeven. De parameter 'args' bevindt zich in het bereik bij instructies op het hoogste niveau en bevindt zich daarbuiten niet in het bereik. Standaardregels voor naamconflict/schaduwwerking zijn van toepassing.

Asynchrone bewerkingen zijn toegestaan in instructies op het hoogste niveau tot de mate waarin ze zijn toegestaan in instructies binnen een reguliere asynchrone invoerpuntmethode. Ze zijn echter niet vereist, als await expressies en andere asynchrone bewerkingen worden weggelaten, wordt er geen waarschuwing gegenereerd.

De signatuur van de gegenereerde invoerpuntmethode wordt als volgt bepaald op basis van bewerkingen die door de topniveaustatements worden gebruikt:

Async-operations\Return-with-expression Presenteren Afwezige
Heden static Task<int> Main(string[] args) static Task Main(string[] args)
Afwezige static int Main(string[] args) static void Main(string[] args)

In het bovenstaande voorbeeld zou de volgende $Main methodedeclaratie worden verkregen:

partial class Program
{
    static void $Main(string[] args)
    {
        if (args.Length == 0
            || !int.TryParse(args[0], out int n)
            || n < 0) return;
        Console.WriteLine(Fib(n).curr);
        
        (int curr, int prev) Fib(int i)
        {
            if (i == 0) return (1, 0);
            var (curr, prev) = Fib(i - 1);
            return (curr + prev, curr);
        }
    }
}

Tegelijkertijd een voorbeeld als dit:

await System.Threading.Tasks.Task.Delay(1000);
System.Console.WriteLine("Hi!");

zou opleveren:

partial class Program
{
    static async Task $Main(string[] args)
    {
        await System.Threading.Tasks.Task.Delay(1000);
        System.Console.WriteLine("Hi!");
    }
}

Een voorbeeld als volgt:

await System.Threading.Tasks.Task.Delay(1000);
System.Console.WriteLine("Hi!");
return 0;

zou opleveren:

partial class Program
{
    static async Task<int> $Main(string[] args)
    {
        await System.Threading.Tasks.Task.Delay(1000);
        System.Console.WriteLine("Hi!");
        return 0;
    }
}

En een voorbeeld als volgt:

System.Console.WriteLine("Hi!");
return 2;

zou opleveren:

partial class Program
{
    static int $Main(string[] args)
    {
        System.Console.WriteLine("Hi!");
        return 2;
    }
}

Bereik van lokale variabelen op het hoogste niveau en lokale functies

Hoewel lokale variabelen en functies op het hoogste niveau 'verpakt' zijn in de gegenereerde invoerpuntmethode, moeten ze in elke compilatie-eenheid nog steeds binnen het bereik van het programma zijn. Voor de evaluatie van eenvoudige namen, zodra de algemene naamruimte is bereikt:

  • Eerst wordt geprobeerd de naam binnen de gegenereerde invoerpuntmethode te evalueren en alleen als deze poging mislukt
  • De 'reguliere' evaluatie binnen de globale naamruimtedeclaratie wordt uitgevoerd.

Dit kan leiden tot naamschaduwing van naamruimten en typen die zijn gedeclareerd in de globale naamruimte en tot schaduw van geïmporteerde namen.

Als de eenvoudige naamevaluatie buiten de instructies op het hoogste niveau plaatsvindt en de evaluatie een lokale variabele of functie op het hoogste niveau oplevert, kan dit leiden tot een fout.

Op deze manier beschermen we ons toekomstige vermogen om "Functies op het hoogste niveau" (scenario 2 in https://github.com/dotnet/csharplang/issues/3117) beter aan te pakken en kunnen we nuttige diagnostische gegevens geven aan gebruikers die per ongeluk geloven dat ze worden ondersteund.