Partilhar via


Declarações de nível superior

Observação

Este artigo é uma especificação de recurso. A especificação serve como o documento de design para o recurso. Ele inclui mudanças de especificação propostas, juntamente com as informações necessárias durante o design e desenvolvimento do recurso. Estes artigos são publicados até que as alterações de especificações 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 linguagem (LDM) .

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

Questão campeã: https://github.com/dotnet/csharplang/issues/2765

Resumo

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

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

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

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

Motivação

Há uma certa quantidade de clichê em torno até mesmo dos programas mais simples, por causa da necessidade de um método de Main explícito. Isso parece atrapalhar a aprendizagem de idiomas e a clareza do programa. O principal objetivo do recurso, portanto, é permitir programas em C# sem clichês desnecessários em torno deles, para o bem dos alunos e da clareza do código.

Projeto detalhado

Sintaxe

A única sintaxe adicional é permitir uma sequência de instruções em uma unidade de compilação, imediatamente antes das declarações de membros de namespace .

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

Apenas um compilation_unit pode ter declaraçãos.

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 elas 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 é chamado de "Programa", por isso 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 de ilustração, o nome real usado pelo compilador é dependente 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 explicitamente declarados que, por convenção, poderiam ser considerados candidatos a ponto de entrada são ignorados. Um aviso é comunicado quando isso acontece. É possível especificar um ponto de entrada diferente através do switch do -main:<type> compilador.

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[] contendo os argumentos de linha de comando que foram especificados quando o aplicativo foi iniciado. O argumento string[] nunca é nulo, mas pode ter um comprimento de zero se nenhum argumento de linha de comando for especificado. O parâmetro 'args' está no escopo dentro das declarações de nível superior e não está no escopo fora delas. Aplicam-se regras regulares de conflito de nome/sombreamento.

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

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

Async-operations\Return-with-expression presente Ausente
presente static Task<int> Main(string[] args) static Task Main(string[] args)
Ausente 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!");

produziria:

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;

produziria:

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;

produziria:

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

Âmbito das variáveis locais de nível superior e das funções locais

Mesmo que as 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 de 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, tenta-se avaliar o nome no método de ponto de entrada gerado e, somente se essa tentativa falhar,
  • A avaliação "regular" dentro da declaração de namespace global é executada.

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

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

Desta forma, protegemos a nossa capacidade futura de abordar melhor as "funções de nível superior" (cenário 2 em https://github.com/dotnet/csharplang/issues/3117), e somos capazes de fornecer diagnósticos úteis aos utilizadores que erroneamente acreditam que são suportados.