Compartir a través de


Declaraciones de alto nivel

Nota

Este artículo es una especificación de características. La especificación actúa como documento de diseño de la característica. Incluye cambios de especificación propuestos, junto con la información necesaria durante el diseño y el desarrollo de la característica. Estos artículos se publican hasta que se finalizan los cambios de especificación propuestos e se incorporan en la especificación ECMA actual.

Puede haber algunas discrepancias entre la especificación de características y la implementación completada. Esas diferencias se recogen en las notas de la reunión de diseño de lenguaje (LDM) correspondientes.

Puede obtener más información sobre el proceso de adopción de especificaciones de características en el estándar del lenguaje C# en el artículo sobre las especificaciones de .

Problema planteado por experto: https://github.com/dotnet/csharplang/issues/2765

Resumen

Permitir que una secuencia de instrucciones se produzca justo antes de las namespace_member_declaration de una compilation_unit (es decir, archivo fuente).

La semántica es que si existe una secuencia de instrucciones , se emitiría la siguiente declaración de tipo, módulo del nombre del método real:

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

Consulte también https://github.com/dotnet/csharplang/issues/3117.

Motivación

Incluso los programas más sencillos están rodeados de una cierta cantidad de burocracia, debido a la necesidad de un método explícito Main. Esto parece obstaculizar el aprendizaje de lenguas y la claridad en la programación. Por lo tanto, el objetivo principal de esta función es permitir que los programas de C# no contengan elementos innecesarios, en beneficio de los alumnos y de la claridad del código.

Diseño detallado

Sintaxis

La única sintaxis adicional es permitir una secuencia de instrucciones en una unidad de compilación, justo antes de la namespace_member_declaration:

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

Solo se permite que un compilation_unit tenga instrucción.

Ejemplo:

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

Si hay instrucciones de nivel superior presentes en cualquier unidad de compilación del programa, el significado es como si se combinaran en el cuerpo del bloque de un método Main de una clase Program en el espacio de nombres global, como se indica a continuación:

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

El tipo se denomina "Program", por lo que se puede hacer referencia por nombre desde el código fuente. Es un tipo parcial, por lo que un tipo denominado "Program" en el código fuente también debe declararse como parcial.
Sin embargo, el nombre de método "Main" solo se usa con fines ilustrativos, el nombre real usado por el compilador depende de la implementación y el método no puede hacer referencia a él por su nombre desde el código fuente.

El método se designa como punto de entrada del programa. Se ignoran los métodos declarados explícitamente que, por convención, podrían considerarse candidatos a punto de entrada. Se notifica una advertencia cuando esto sucede. Es un error especificar el conmutador del compilador -main:<type> cuando hay instrucciones de nivel superior.

El método de punto de entrada siempre tiene un parámetro formal, string[] args. El entorno de ejecución crea y pasa un argumento string[] que contiene los argumentos de la línea de comandos que se especificaron cuando se inició la aplicación. El argumento string[] nunca es NULL, pero puede tener una longitud de cero si no se especificó ningún argumento de línea de comandos. El parámetro "args" está dentro del ámbito de las instrucciones de nivel superior y no lo está fuera de ellas. Se aplican las reglas habituales de conflicto de nombres/sombreado.

Las operaciones asincrónicas se permiten en instrucciones de nivel superior en el grado en que se permiten en instrucciones dentro de un método de punto de entrada asincrónico normal. Cuando se omiten las expresiones await y otras operaciones asincrónicas, no son necesarios y no se genera ninguna advertencia.

La firma del método de punto de entrada generado se determina en función de las operaciones usadas por las instrucciones de nivel superior de la siguiente manera:

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)

El ejemplo anterior produciría la siguiente declaración 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);
        }
    }
}

Al mismo tiempo, un ejemplo como este:

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

produciría:

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

Un ejemplo similar al siguiente:

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

produciría:

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

Y un ejemplo similar al siguiente:

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

produciría:

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

Ámbito de las variables locales de nivel superior y las funciones locales

Aunque las variables y funciones locales de nivel superior estén "encapsuladas" en el método de punto de entrada generado, deberían seguir estando en el ámbito de todo el programa en cada unidad de compilación. Para el propósito de la evaluación de nombres simples, una vez que se alcanza el espacio de nombres global:

  • En primer lugar, se intenta evaluar el nombre dentro del método de punto de entrada generado y solo si este intento falla
  • Se realiza la evaluación "regular" dentro de la declaración del espacio de nombres global.

Esto podría dar lugar al ensombrecimiento de nombres de espacios de nombres y tipos declarados dentro del espacio de nombres global, así como al ensombrecimiento de nombres importados.

Si la evaluación de nombres simple se produce fuera de las instrucciones de nivel superior y la evaluación produce una variable o función local de nivel superior, esto debería provocar un error.

De este modo protegemos nuestra capacidad futura para abordar mejor las "funciones de nivel superior" (escenario 2 en https://github.com/dotnet/csharplang/issues/3117), y podemos ofrecer diagnósticos útiles a los usuarios que creen erróneamente que son compatibles.