Udostępnij za pośrednictwem


Instrukcje najwyższego poziomu

Notatka

Ten artykuł jest specyfikacją funkcji. Specyfikacja służy jako dokument projektowy dla funkcji. Zawiera proponowane zmiany specyfikacji wraz z informacjami wymaganymi podczas projektowania i opracowywania funkcji. Te artykuły są publikowane do momentu sfinalizowania proponowanych zmian specyfikacji i włączenia ich do obecnej specyfikacji ECMA.

Mogą wystąpić pewne rozbieżności między specyfikacją funkcji a ukończoną implementacją. Te różnice są przechwytywane w odpowiednich spotkania projektowego języka (LDM).

Więcej informacji na temat procesu wdrażania specyfikacji funkcji można znaleźć w standardzie języka C# w artykule dotyczącym specyfikacji .

Kwestia mistrzowska: https://github.com/dotnet/csharplang/issues/2765

Streszczenie

Zezwalaj na sekwencję instrukcji ,, aby mogły pojawić się bezpośrednio przed namespace_member_declarationw compilation_unit (tj. pliku źródłowym).

Semantyka polega na tym, że jeśli taka sekwencja instrukcji jest obecna, następująca deklaracja typu, z pominięciem rzeczywistej nazwy metody, zostanie wygenerowana:

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

Zobacz również https://github.com/dotnet/csharplang/issues/3117.

Motywacja

Istnieje pewna ilość standardowego kodu nawet w najprostszych programach, ze względu na potrzebę jawnego zastosowania metody Main. Wydaje się, że to przeszkadza w nauce języka i przejrzystości programu. Podstawowym celem tej funkcji jest umożliwienie pisania programów w języku C# bez zbędnego kodu szablonowego, z myślą o uczniach i przejrzystości kodu.

Szczegółowy projekt

Składnia

Jedyną dodatkową składnią jest umożliwienie sekwencji instrukcji s w jednostce kompilacji tuż przed namespace_member_declarations:

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

Tylko jedna jednostka kompilacji może zawierać instrukcje s.

Przykład:

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);
}

Semantyka

Jeśli jakiekolwiek instrukcje najwyższego poziomu są obecne w dowolnej jednostce kompilacji programu, znaczenie jest tak, jakby zostały połączone w treści bloku metody Main klasy Program w globalnej przestrzeni nazw w następujący sposób:

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

Typ ma nazwę "Program", więc można odwoływać się do nazwy z kodu źródłowego. Jest to typ częściowy, więc typ o nazwie "Program" w kodzie źródłowym musi być również zadeklarowany jako częściowy.
Jednak nazwa metody "Main" jest używana tylko do celów ilustracyjnych, rzeczywista nazwa używana przez kompilator jest zależna od implementacji i nie można odwoływać się do metody według nazwy z kodu źródłowego.

Metoda jest wyznaczona jako punkt wejścia programu. Jawnie zadeklarowane metody, które zgodnie z konwencją można traktować jako kandydatów do punktu wejścia, są ignorowane. W takim przypadku zostanie zgłoszone ostrzeżenie. Podanie przełącznika kompilatora -main:<type> w przypadku występowania instrukcji najwyższego poziomu jest błędem.

Metoda punktu wejścia zawsze ma jeden parametr formalny, string[] args. Środowisko wykonywania tworzy i przekazuje argument string[] zawierający argumenty wiersza polecenia określone podczas uruchamiania aplikacji. Argument string[] nigdy nie ma wartości null, ale może mieć długość zero, jeśli nie określono argumentów wiersza polecenia. Parametr "args" znajduje się w zakresie w instrukcjach najwyższego poziomu i nie znajduje się poza nimi. Mają zastosowanie reguły regularnego konfliktu nazw/cieniowania.

Operacje asynchroniczne są dozwolone w instrukcjach na najwyższym poziomie w takim stopniu, w jakim są dozwolone w instrukcjach w ramach regularnej metody punktu wejścia asynchronicznego. Nie są one jednak wymagane, jeśli await wyrażenia i inne operacje asynchroniczne zostaną pominięte, żadne ostrzeżenie nie zostanie wygenerowane.

Podpis metody wygenerowanego punktu wejścia jest określany na podstawie operacji używanych przez instrukcje najwyższego poziomu w następujący sposób:

Asynchroniczne-operacje\Zwróć-z-wyrażeniem prezent nieobecny
obecny static Task<int> Main(string[] args) static Task Main(string[] args)
nieobecny static int Main(string[] args) static void Main(string[] args)

Powyższy przykład daje następującą deklarację metody $Main:

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);
        }
    }
}

W tym samym czasie przykład podobny do następującego:

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

dałoby:

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

Przykład podobny do następującego:

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

mogłoby dać:

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

I oto przykład:

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

dałoby:

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

Zakres zmiennych lokalnych najwyższego poziomu i funkcji lokalnych

Mimo że zmienne lokalne i funkcje najwyższego poziomu są "opakowane" w metodę wygenerowanego punktu wejścia, powinny one nadal znajdować się w widoczności w całym programie w każdej jednostce kompilacji. W celu oceny prostej nazwy po osiągnięciu globalnej przestrzeni nazw:

  • Najpierw podjęto próbę oceny nazwy w wygenerowanej metodzie punktu wejścia i tylko wtedy, gdy ta próba zakończy się niepowodzeniem
  • Wykonywana jest ocena "regularna" w globalnej deklaracji przestrzeni nazw.

Może to prowadzić do cieniowania nazw przestrzeni nazw i typów zadeklarowanych w globalnej przestrzeni nazw, a także do cieniowania importowanych nazw.

Jeśli ocena prostych nazw występuje poza instrukcjami najwyższego poziomu i skutkuje lokalną zmienną lub funkcją najwyższego poziomu, powinno to prowadzić do błędu.

W ten sposób chronimy naszą przyszłą zdolność do lepszego rozwiązywania problemów z "funkcjami najwyższego poziomu" (scenariusz 2 w https://github.com/dotnet/csharplang/issues/3117), i jesteśmy w stanie zapewnić przydatną diagnostykę użytkownikom, którzy błędnie uważają, że są one obsługiwane.