Compartilhar via


Instruções de nível superior

Nota

Este artigo é uma especificação de recurso. A especificação serve como o documento de design para o recurso. Ele inclui alterações de especificação propostas, juntamente com as informações necessárias durante o design e o desenvolvimento do recurso. Esses artigos são publicados até que as alterações de especificação propostas sejam finalizadas e incorporadas na especificação ECMA atual.

Pode haver algumas discrepâncias entre a especificação do recurso e a implementação concluída. Essas diferenças são capturadas nas notas pertinentes da reunião de design de idioma (LDM).

Você pode saber mais sobre o processo de adoção de speclets de recursos no padrão de linguagem C# no artigo sobre as especificações de .

Problema do especialista: https://github.com/dotnet/csharplang/issues/2765

Resumo

Permitir que uma sequência de instruções ocorra exatamente antes das namespace_member_declarations de uma compilation_unit (ou seja, arquivo de origem).

A semântica é que, se essa sequência de instruções estiver presente, a seguinte declaração de tipo, modulo o nome do método real, será emitida:

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

Consulte também https://github.com/dotnet/csharplang/issues/3117.

Motivação

Há uma certa quantidade de clichês em torno até mesmo do mais simples dos programas, devido à necessidade de um método Main explícito. Isso parece atrapalhar o aprendizado de idiomas e a clareza do programa. O objetivo principal do recurso, portanto, é permitir programas C# sem clichês desnecessários ao seu redor, para fins de aprendizado e clareza do código.

Design detalhado

Sintaxe

A única sintaxe adicional é permitir uma sequência de instruções em uma unidade de compilação, pouco antes das namespace_member_declarations:

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

Apenas uma compilation_unit pode conter instruções.

Exemplo:

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

Semântica

Se quaisquer instruções de nível superior estiverem presentes em qualquer unidade de compilação do programa, o significado será como se fossem combinadas no corpo do bloco de um método Main de uma classe Program no namespace global, da seguinte maneira:

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

O tipo é denominado "Programa", portanto, pode ser referenciado pelo nome do código-fonte. É um tipo parcial, portanto, um tipo chamado "Programa" no código-fonte também deve ser declarado como parcial.
Mas o nome do método "Main" é usado apenas para fins ilustrativos, o nome real usado pelo compilador depende da implementação e o método não pode ser referenciado pelo nome do código-fonte.

O método é designado como o ponto de entrada do programa. Métodos declarados explicitamente que, por convenção, podem ser considerados candidatos a ponto de entrada são ignorados. Um aviso é emitido quando isso acontece. É um erro especificar a opção de compilador -main:<type> quando há instruções de nível superior.

O método de ponto de entrada sempre tem um parâmetro formal, string[] args. O ambiente de execução cria e passa um argumento string[] que contém os argumentos de linha de comando que foram especificados quando o aplicativo foi iniciado. O argumento string[] nunca é nulo, mas pode ter um comprimento igual a zero se nenhum argumento de linha de comando tiver sido especificado. O parâmetro 'args' está no escopo quando se trata de instruções de nível superior, e não está no escopo fora dessas instruções. Regras comuns de conflito/sombreamento de nomes são aplicáveis.

As operações assíncronas são permitidas em instruções de nível superior até o grau em que são permitidas em instruções dentro de um método de ponto de entrada assíncrono regular. No entanto, eles não serão necessários, se expressões await e outras operações assíncronas forem omitidas, nenhum aviso será produzido.

A assinatura do método de ponto de entrada gerado é determinada com base nas operações usadas pelas instruções de nível superior da seguinte maneira:

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

O exemplo acima produziria a seguinte declaração de método $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);
        }
    }
}

Ao mesmo tempo, um exemplo como este:

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

resultaria em:

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

Um exemplo como este:

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

resultaria em:

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

E um exemplo como este:

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

resultaria em:

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

Escopo de variáveis locais de nível superior e funções locais

Embora variáveis e funções locais de nível superior sejam "encapsuladas" no método de ponto de entrada gerado, elas ainda devem estar no escopo em todo o programa em cada unidade de compilação. Para fins de avaliação de nome simples, uma vez que o namespace global é alcançado:

  • Primeiro, é feita uma tentativa de avaliar o nome no método de ponto de entrada gerado e somente se a tentativa não for bem-sucedida
  • A avaliação "regular" na declaração de namespace global é executada.

Isso pode levar ao sombreamento de nomes de namespaces e tipos declarados dentro do namespace global, bem como à sombreamento de nomes importados.

Se a avaliação de um nome simples ocorrer fora das instruções de nível superior e resultar em uma variável ou função local de nível superior, isso levará a um erro.

Dessa forma, protegemos nossa capacidade futura de lidar melhor com "funções de nível superior" (cenário 2 em https://github.com/dotnet/csharplang/issues/3117) e podemos fornecer diagnósticos úteis aos usuários que acreditam erroneamente que eles têm suporte.