En programación informática , el alcance de un enlace de nombre (una asociación de un nombre a una entidad, como una variable ) es la parte de un programa donde el enlace de nombre es válido; es decir, donde el nombre se puede usar para hacer referencia a la entidad. En otras partes del programa, el nombre puede hacer referencia a una entidad diferente (puede tener un enlace diferente) o a nada en absoluto (puede no estar enlazado). El alcance ayuda a prevenir colisiones de nombres al permitir que el mismo nombre haga referencia a diferentes objetos, siempre que los nombres tengan alcances separados. El alcance de un enlace de nombre también se conoce como la visibilidad de una entidad, particularmente en la literatura más antigua o más técnica; esto es en relación con la entidad referenciada, no con el nombre de referencia.
El término "alcance" también se utiliza para referirse al conjunto de todos los enlaces de nombres que son válidos dentro de una parte de un programa o en un punto dado de un programa, al que se denomina más correctamente contexto o entorno . [a]
Estrictamente hablando [b] y en la práctica para la mayoría de los lenguajes de programación , "parte de un programa" se refiere a una porción del código fuente (área de texto), y se conoce como ámbito léxico . En algunos lenguajes, sin embargo, "parte de un programa" se refiere a una porción del tiempo de ejecución (período durante la ejecución ), y se conoce como ámbito dinámico . Ambos términos son algo engañosos (utilizan incorrectamente términos técnicos, como se analiza en la definición), pero la distinción en sí es precisa y exacta, y estos son los términos estándar respectivos. El ámbito léxico es el foco principal de este artículo, y el ámbito dinámico se entiende por contraste con el ámbito léxico.
En la mayoría de los casos, la resolución de nombres basada en el alcance léxico es relativamente sencilla de usar e implementar, ya que en el uso se puede leer hacia atrás en el código fuente para determinar a qué entidad se refiere un nombre, y en la implementación se puede mantener una lista de nombres y contextos al compilar o interpretar un programa. Surgen dificultades en el enmascaramiento de nombres , las declaraciones hacia adelante y el izado , mientras que surgen otras considerablemente más sutiles con las variables no locales , particularmente en los cierres .
La definición estricta del "alcance" (léxico) de un nombre ( identificador ) es inequívoca: el alcance léxico es "la parte del código fuente en la que se aplica una vinculación de un nombre con una entidad". Esto prácticamente no ha cambiado desde su definición de 1960 en la especificación de ALGOL 60. A continuación, se presentan especificaciones representativas del lenguaje:
Más comúnmente, "alcance" se refiere a cuando un nombre dado puede referirse a una variable dada (cuando una declaración tiene efecto), pero también puede aplicarse a otras entidades, como funciones, tipos, clases, etiquetas , constantes y enumeraciones.
Una distinción fundamental en el ámbito es lo que significa "parte de un programa". En lenguajes con ámbito léxico (también llamado ámbito estático ), la resolución de nombres depende de la ubicación en el código fuente y del contexto léxico (también llamado contexto estático ), que se define por el lugar donde se define la variable o función nombrada. Por el contrario, en lenguajes con ámbito dinámico, la resolución de nombres depende del estado del programa cuando se encuentra el nombre, que está determinado por el contexto de ejecución (también llamado contexto de tiempo de ejecución , contexto de llamada o contexto dinámico ). En la práctica, con ámbito léxico un nombre se resuelve buscando en el contexto léxico local, luego, si eso falla, buscando en el contexto léxico externo, y así sucesivamente; mientras que con ámbito dinámico, un nombre se resuelve buscando en el contexto de ejecución local, luego, si eso falla, buscando en el contexto de ejecución externo, y así sucesivamente, progresando hacia arriba en la pila de llamadas. [4]
La mayoría de los lenguajes modernos utilizan un ámbito léxico para las variables y funciones, aunque en algunos lenguajes se utiliza un ámbito dinámico, en particular en algunos dialectos de Lisp, algunos lenguajes de "scripting" y algunos lenguajes de plantillas . [c] Perl 5 ofrece tanto un ámbito léxico como dinámico. Incluso en lenguajes con ámbito léxico, el ámbito de los cierres puede resultar confuso para los no iniciados, [ cita requerida ] ya que estos dependen del contexto léxico donde se define el cierre, no donde se lo llama.
La resolución léxica se puede determinar en tiempo de compilación y también se conoce como enlace temprano , mientras que la resolución dinámica, en general, solo se puede determinar en tiempo de ejecución y, por lo tanto, se conoce como enlace tardío .
En la programación orientada a objetos , el envío dinámico selecciona un método de objeto en tiempo de ejecución, aunque el hecho de que la vinculación de nombres real se realice en tiempo de compilación o de ejecución depende del lenguaje. El alcance dinámico de facto es común en los lenguajes de macros , que no realizan directamente la resolución de nombres, sino que se expanden en el lugar.
Algunos frameworks de programación como AngularJS usan el término "ámbito" para significar algo completamente diferente de cómo se usa en este artículo. En esos frameworks, el ámbito es simplemente un objeto del lenguaje de programación que usan ( JavaScript en el caso de AngularJS) que el framework usa de ciertas maneras para emular el ámbito dinámico en un lenguaje que usa ámbito léxico para sus variables. Esos ámbitos de AngularJS pueden estar en contexto o no (usando el significado habitual del término) en cualquier parte dada del programa, siguiendo las reglas habituales de ámbito de variable del lenguaje como cualquier otro objeto, y usando sus propias reglas de herencia y transclusión . En el contexto de AngularJS, a veces se usa el término "$ámbito" (con un signo de dólar) para evitar confusiones, pero las guías de estilo a menudo desaconsejan usar el signo de dólar en los nombres de las variables. [5]
El alcance es un componente importante de la resolución de nombres , [d] que a su vez es fundamental para la semántica del lenguaje . La resolución de nombres (incluido el alcance) varía entre lenguajes de programación y, dentro de un lenguaje de programación, varía según el tipo de entidad; las reglas para el alcance se denominan reglas de alcance (o reglas de alcance ). Junto con los espacios de nombres , las reglas de alcance son cruciales en la programación modular , por lo que un cambio en una parte del programa no interrumpe una parte no relacionada.
Cuando se habla de alcance, hay tres conceptos básicos: alcance, extensión y contexto. "Alcance" y "contexto" en particular se confunden con frecuencia: alcance es una propiedad de un enlace de nombre, mientras que contexto es una propiedad de una parte de un programa, que es una porción de código fuente ( contexto léxico o contexto estático ) o una porción de tiempo de ejecución ( contexto de ejecución, contexto de tiempo de ejecución, contexto de llamada o contexto dinámico ). El contexto de ejecución consiste en contexto léxico (en el punto de ejecución actual) más estado de tiempo de ejecución adicional como la pila de llamadas . [e] Estrictamente hablando, durante la ejecución un programa entra y sale de varios alcances de enlaces de nombre, y en un punto en la ejecución los enlaces de nombre están "en contexto" o "no en contexto", por lo tanto, los enlaces de nombre "entran en contexto" o "salen de contexto" a medida que la ejecución del programa entra o sale del alcance. [f] Sin embargo, en la práctica el uso es mucho más laxo.
El alcance es un concepto de nivel de código fuente y una propiedad de los enlaces de nombres, en particular los enlaces de nombres de variables o funciones (los nombres en el código fuente son referencias a entidades en el programa) y es parte del comportamiento de un compilador o intérprete de un lenguaje. Como tal, las cuestiones de alcance son similares a los punteros , que son un tipo de referencia utilizado en programas de manera más general. Usar el valor de una variable cuando el nombre está en contexto pero la variable no está inicializada es análogo a desreferenciar (acceder al valor de) un puntero salvaje , ya que no está definido. Sin embargo, como las variables no se destruyen hasta que salen de contexto, el análogo de un puntero colgante no existe.
Para entidades como las variables, el ámbito es un subconjunto del tiempo de vida (también conocido como extensión ): un nombre solo puede hacer referencia a una variable que existe (posiblemente con un valor indefinido), pero las variables que existen no son necesariamente visibles: una variable puede existir pero ser inaccesible (el valor se almacena pero no se hace referencia a él dentro de un contexto determinado), o accesible pero no a través del nombre dado, en cuyo caso no está en contexto (el programa está "fuera del alcance del nombre"). En otros casos, el "tiempo de vida" es irrelevante: una etiqueta (posición nombrada en el código fuente) tiene un tiempo de vida idéntico al programa (para lenguajes compilados estáticamente), pero puede estar en contexto o no en un punto dado en el programa, y lo mismo para las variables estáticas : una variable global estática está en contexto para todo el programa, mientras que una variable local estática solo está en contexto dentro de una función u otro contexto local, pero ambas tienen un tiempo de vida de toda la ejecución del programa.
Determinar a qué entidad se refiere un nombre se conoce como resolución de nombres o vinculación de nombres (particularmente en programación orientada a objetos ), y varía entre lenguajes. Dado un nombre, el lenguaje (adecuadamente, el compilador o intérprete) verifica todas las entidades que están en contexto para encontrar coincidencias; en caso de ambigüedad (dos entidades con el mismo nombre, como una variable global y local con el mismo nombre), se utilizan las reglas de resolución de nombres para distinguirlas. Con mayor frecuencia, la resolución de nombres se basa en una regla de "contexto interno a externo", como la regla LEGB (Local, Enclosing, Global, Built-in) de Python: los nombres se resuelven implícitamente en el contexto relevante más estrecho. En algunos casos, la resolución de nombres se puede especificar explícitamente, como por las palabras clave global
and nonlocal
en Python; en otros casos, las reglas predeterminadas no se pueden anular.
Cuando dos nombres idénticos se encuentran en contexto al mismo tiempo, haciendo referencia a entidades diferentes, se dice que se está produciendo un enmascaramiento de nombres , en el que el nombre de mayor prioridad (normalmente el más interno) está "enmascarando" al nombre de menor prioridad. A nivel de variables, esto se conoce como " enmascaramiento de variables " . Debido a la posibilidad de que se produzcan errores lógicos a causa del enmascaramiento, algunos lenguajes lo desautorizan o desalientan, lo que genera un error o una advertencia en tiempo de compilación o de ejecución.
Varios lenguajes de programación tienen distintas reglas de alcance para distintos tipos de declaraciones y nombres. Dichas reglas de alcance tienen un gran efecto en la semántica del lenguaje y, en consecuencia, en el comportamiento y la corrección de los programas. En lenguajes como C++ , acceder a una variable no vinculada no tiene una semántica bien definida y puede resultar en un comportamiento indefinido , similar a hacer referencia a un puntero colgante ; y las declaraciones o nombres utilizados fuera de su alcance generarán errores de sintaxis .
Los alcances frecuentemente están vinculados a otras construcciones del lenguaje y se determinan implícitamente, pero muchos lenguajes también ofrecen construcciones específicamente para controlar el alcance.
El alcance puede variar desde una sola expresión hasta todo el programa, con muchas gradaciones posibles entre ambas. La regla de alcance más simple es el alcance global: todas las entidades son visibles en todo el programa. La regla de alcance modular más básica es el alcance de dos niveles, con un alcance global en cualquier parte del programa y un alcance local dentro de una función. La programación modular más sofisticada permite un alcance de módulo separado, donde los nombres son visibles dentro del módulo (privados para el módulo) pero no visibles fuera de él. Dentro de una función, algunos lenguajes, como C, permiten el alcance de bloque para restringir el alcance a un subconjunto de una función; otros, en particular los lenguajes funcionales, permiten el alcance de expresión, para restringir el alcance a una sola expresión. Otros ámbitos incluyen el alcance de archivo (en particular en C), que se comporta de manera similar al alcance de módulo, y el alcance de bloque fuera de las funciones (en particular en Perl).
Un problema sutil es exactamente cuándo comienza y termina un ámbito. En algunos lenguajes, como C, el ámbito de un nombre comienza en la declaración del nombre y, por lo tanto, los diferentes nombres declarados dentro de un bloque determinado pueden tener diferentes ámbitos. Esto requiere declarar funciones antes de su uso, aunque no necesariamente definirlas, y requiere una declaración hacia adelante en algunos casos, en particular para la recursión mutua. En otros lenguajes, como Python, el ámbito de un nombre comienza al comienzo del bloque relevante donde se declara el nombre (como el comienzo de una función), independientemente de dónde se defina, por lo que todos los nombres dentro de un bloque determinado tienen el mismo ámbito. En JavaScript, el ámbito de un nombre declarado con let
o const
comienza en la declaración del nombre, y el ámbito de un nombre declarado con var
comienza al comienzo de la función donde se declara el nombre, lo que se conoce como elevación de variable . El comportamiento de los nombres en contexto que tienen un valor indefinido difiere: en Python, el uso de nombres indefinidos produce un error de tiempo de ejecución, mientras que en JavaScript los nombres indefinidos declarados con var
se pueden usar en toda la función porque están implícitamente vinculados al valor undefined
.
El alcance de un enlace de nombre es una expresión , que se conoce como alcance de expresión . El alcance de expresión está disponible en muchos lenguajes, especialmente en lenguajes funcionales que ofrecen una característica llamada expresiones let que permiten que el alcance de una declaración sea una sola expresión. Esto es conveniente si, por ejemplo, se necesita un valor intermedio para un cálculo. Por ejemplo, en Standard ML , si f()
devuelve 12
, entonces es una expresión que evalúa a , utilizando una variable temporal nombrada para evitar llamar dos veces. Algunos lenguajes con alcance de bloque se aproximan a esta funcionalidad al ofrecer una sintaxis para que un bloque se incruste en una expresión; por ejemplo, la expresión de Standard ML mencionada anteriormente podría escribirse en Perl como , o en GNU C como .let val x = f() in x * x end
144
x
f()
do { my $x = f(); $x * $x }
({ int x = f(); x * x; })
En Python, las variables auxiliares en expresiones generadoras y comprensiones de listas (en Python 3) tienen alcance de expresión.
En C, los nombres de las variables en un prototipo de función tienen un ámbito de expresión, conocido en este contexto como ámbito de protocolo de función . Como no se hace referencia a los nombres de las variables en el prototipo (pueden ser diferentes en la definición real), son solo ficticios, por lo que a menudo se omiten, aunque se pueden usar para generar documentación, por ejemplo.
El alcance de un enlace de nombre es un bloque , que se conoce como alcance de bloque . El alcance de bloque está disponible en muchos lenguajes de programación estructurados en bloques, pero no en todos. Esto comenzó con ALGOL 60 , donde "[c]ada declaración... es válida solo para ese bloque", [6] y hoy en día se asocia particularmente con lenguajes de las familias y tradiciones Pascal y C. La mayoría de las veces, este bloque está contenido dentro de una función, lo que restringe el alcance a una parte de una función, pero en algunos casos, como Perl, el bloque puede no estar dentro de una función.
entero sin signo suma_de_cuadrados ( const entero sin signo N ) { entero sin signo ret = 0 ; para ( entero sin signo n = 1 ; n <= N ; n ++ ) { constante entero sin signo n_cuadrado = n * n ; ret += n_cuadrado ; } return ret ; }
Un ejemplo representativo del uso del ámbito de bloque es el código C que se muestra aquí, donde dos variables están limitadas al ámbito del bucle: la variable de bucle n , que se inicializa una vez y se incrementa en cada iteración del bucle, y la variable auxiliar n_squared , que se inicializa en cada iteración. El propósito es evitar agregar variables al ámbito de la función que solo son relevantes para un bloque en particular; por ejemplo, esto evita errores donde la variable genérica de bucle i ya se ha establecido accidentalmente en otro valor. En este ejemplo, la expresión n * n
generalmente no se asignaría a una variable auxiliar, y el cuerpo del bucle simplemente se escribiría, ret += n * n
pero en ejemplos más complicados, las variables auxiliares son útiles.
Los bloques se utilizan principalmente para el control del flujo, como en los bucles if, while y for, y en estos casos el ámbito de bloque significa que el ámbito de la variable depende de la estructura del flujo de ejecución de una función. Sin embargo, los lenguajes con ámbito de bloque normalmente también permiten el uso de bloques "desnudos", cuyo único propósito es permitir un control detallado del ámbito de la variable. Por ejemplo, se puede definir una variable auxiliar en un bloque, luego usarla (por ejemplo, agregarla a una variable con ámbito de función) y descartarla cuando finaliza el bloque, o se puede incluir un bucle while en un bloque que inicialice las variables utilizadas dentro del bucle que solo se deben inicializar una vez.
Una sutileza de varios lenguajes de programación, como Algol 68 y C (demostrado en este ejemplo y estandarizado desde C99 ), es que las variables de ámbito de bloque se pueden declarar no solo dentro del cuerpo del bloque, sino también dentro de la declaración de control, si la hay. Esto es análogo a los parámetros de función, que se declaran en la declaración de función (antes de que comience el bloque del cuerpo de la función) y en el ámbito de todo el cuerpo de la función. Esto se utiliza principalmente en bucles for , que tienen una declaración de inicialización separada de la condición del bucle, a diferencia de los bucles while, y es un modismo común.
El ámbito de bloque se puede utilizar para hacer sombra. En este ejemplo, dentro del bloque la variable auxiliar también podría haberse llamado n , haciendo sombra al nombre del parámetro, pero esto se considera un estilo deficiente debido al potencial de errores. Además, algunos descendientes de C, como Java y C#, a pesar de tener soporte para el ámbito de bloque (en el que una variable local puede quedar fuera de contexto antes del final de una función), no permiten que una variable local oculte otra. En tales lenguajes, el intento de declaración de la segunda n daría como resultado un error de sintaxis y una de las n variables tendría que ser renombrada.
Si se utiliza un bloque para establecer el valor de una variable, el ámbito del bloque requiere que la variable se declare fuera del bloque. Esto complica el uso de sentencias condicionales con asignación simple . Por ejemplo, en Python, que no utiliza el ámbito del bloque, se puede inicializar una variable de la siguiente manera:
si c : a = "foo" de lo contrario : a = ""
donde a
es accesible después de la if
declaración.
En Perl, que tiene alcance de bloque, esto requiere declarar la variable antes del bloque:
mi $a ; si ( c ) { $a = 'foo' ; } de lo contrario { $a = '' ; }
A menudo, esto se reescribe mediante una asignación múltiple, inicializando la variable con un valor predeterminado. En Python (donde no es necesario), esto sería:
a = "" si c : a = "foo"
Mientras que en Perl esto sería:
mi $a = '' ; si ( c ) { $a = 'foo' ; }
En el caso de una asignación de una sola variable, una alternativa es utilizar el operador ternario para evitar un bloqueo, pero esto en general no es posible para asignaciones de múltiples variables y es difícil de leer para una lógica compleja.
Este es un problema más importante en C, especialmente para la asignación de cadenas, ya que la inicialización de cadenas puede asignar memoria automáticamente, mientras que la asignación de cadenas a una variable ya inicializada requiere asignar memoria, una copia de la cadena y verificar que estas operaciones sean exitosas.
{ mi $contador = 0 ; sub increment_counter { return ++ $contador ; } }
Algunos lenguajes permiten que el concepto de ámbito de bloque se aplique, en distintos grados, fuera de una función. Por ejemplo, en el fragmento de código de Perl de la derecha, $counter
es un nombre de variable con ámbito de bloque (debido al uso de la my
palabra clave), mientras que increment_counter
es un nombre de función con ámbito global. Cada llamada a increment_counter
aumentará el valor de $counter
en uno y devolverá el nuevo valor. El código fuera de este bloque puede llamar a increment_counter
, pero no puede obtener ni alterar el valor de $counter
. Este modismo permite definir cierres en Perl.
Cuando el alcance de las variables declaradas dentro de una función no se extiende más allá de esa función, esto se conoce como alcance de la función . [7] El alcance de la función está disponible en la mayoría de los lenguajes de programación que ofrecen una forma de crear una variable local en una función o subrutina : una variable cuyo alcance termina (que sale de contexto) cuando la función regresa. En la mayoría de los casos, el tiempo de vida de la variable es la duración de la llamada a la función: es una variable automática , creada cuando la función comienza (o se declara la variable), destruida cuando la función regresa, mientras que el alcance de la variable está dentro de la función, aunque el significado de "dentro" depende de si el alcance es léxico o dinámico. Sin embargo, algunos lenguajes, como C, también proporcionan variables locales estáticas , donde el tiempo de vida de la variable es todo el tiempo de vida del programa, pero la variable solo está en contexto cuando está dentro de la función. En el caso de las variables locales estáticas, la variable se crea cuando el programa se inicializa y se destruye solo cuando el programa termina, como con una variable global estática , pero solo está en contexto dentro de una función, como una variable local automática.
Es importante destacar que, en el ámbito léxico, una variable con ámbito de función tiene alcance solo dentro del contexto léxico de la función: sale de contexto cuando se llama a otra función dentro de la función y vuelve a entrar en contexto cuando la función retorna; las funciones llamadas no tienen acceso a las variables locales de las funciones que llaman y las variables locales solo están en contexto dentro del cuerpo de la función en la que se declaran. Por el contrario, en el ámbito dinámico, el alcance se extiende al contexto de ejecución de la función: las variables locales permanecen en contexto cuando se llama a otra función, solo salen de contexto cuando termina la función que las define y, por lo tanto, las variables locales están en el contexto de la función en la que están definidas y todas las funciones llamadas . En lenguajes con ámbito léxico y funciones anidadas , las variables locales están en contexto para las funciones anidadas, ya que estas están dentro del mismo contexto léxico, pero no para otras funciones que no están anidadas léxicamente. Una variable local de una función envolvente se conoce como una variable no local para la función anidada. El ámbito de función también se aplica a funciones anónimas .
def cuadrado ( n ): devuelve n * ndef suma_de_cuadrados ( n ): total = 0 i = 0 mientras i <= n : total += cuadrado ( i ) i += 1 devolver total
Por ejemplo, en el fragmento de código Python de la derecha, se definen dos funciones: square
y sum_of_squares
. square
calcula el cuadrado de un número; sum_of_squares
calcula la suma de todos los cuadrados hasta un número. (Por ejemplo, square(4)
is 4 2 = 16
, y sum_of_squares(4)
is 0 2 + 1 2 + 2 2 + 3 2 + 4 2 = 30
.)
Cada una de estas funciones tiene una variable llamada n que representa el argumento de la función. Estas dos variables n son completamente independientes y no están relacionadas, a pesar de tener el mismo nombre, porque son variables locales con alcance léxico y alcance de función: el alcance de cada una es su propia función léxicamente independiente y, por lo tanto, no se superponen. Por lo tanto, sum_of_squares
se puede llamar sin que se altere square
su propia nsum_of_squares
. De manera similar, tiene variables llamadas total e i ; estas variables, debido a su alcance limitado, no interferirán con ninguna variable llamada total o i que pueda pertenecer a cualquier otra función. En otras palabras, no hay riesgo de una colisión de nombres entre estos nombres y cualquier nombre no relacionado, incluso si son idénticos.
No se produce ningún enmascaramiento de nombres: solo una variable denominada n está en contexto en un momento dado, ya que los ámbitos no se superponen. Por el contrario, si se escribiera un fragmento similar en un lenguaje con ámbito dinámico, la n en la función que realiza la llamada permanecería en contexto en la función llamada (los ámbitos se superpondrían) y quedaría enmascarada ("ensombrecida") por la nueva n en la función llamada.
El alcance de la función es significativamente más complicado si las funciones son objetos de primera clase y se pueden crear localmente en una función y luego devolverlas. En este caso, cualquier variable en la función anidada que no sea local para ella (variables no vinculadas en la definición de la función, que se resuelven en variables en un contexto envolvente) crea un cierre , ya que no solo se debe devolver la función en sí, sino también su contexto (de variables) y luego potencialmente llamarla en un contexto diferente. Esto requiere mucho más soporte del compilador y puede complicar el análisis del programa.
El alcance de un enlace de nombre es un archivo, que se conoce como alcance de archivo . El alcance de archivo es en gran medida particular de C (y C++), donde el alcance de las variables y funciones declaradas en el nivel superior de un archivo (no dentro de ninguna función) es para todo el archivo, o más bien para C, desde la declaración hasta el final del archivo fuente, o más precisamente la unidad de traducción (enlace interno). Esto puede verse como una forma de alcance de módulo, donde los módulos se identifican con archivos, y en lenguajes más modernos se reemplaza por un alcance de módulo explícito. Debido a la presencia de declaraciones de inclusión, que agregan variables y funciones al contexto interno y pueden llamar a otras declaraciones de inclusión, puede ser difícil determinar qué está en contexto en el cuerpo de un archivo.
En el fragmento de código C anterior, el nombre de la función sum_of_squares
tiene un alcance global (en C, enlace externo). Si se agrega static
a la firma de la función, se obtendrá un alcance de archivo (enlace interno).
El alcance de un enlace de nombre es un módulo, que se conoce como alcance de módulo . El alcance de módulo está disponible en lenguajes de programación modular donde los módulos (que pueden abarcar varios archivos) son la unidad básica de un programa complejo, ya que permiten ocultar información y exponer una interfaz limitada. El alcance de módulo fue pionero en la familia de lenguajes Modula , y Python (que fue influenciado por Modula) es un ejemplo contemporáneo representativo.
En algunos lenguajes de programación orientados a objetos que carecen de soporte directo para módulos, como C++ antes de C++20, [8] se proporciona una estructura similar mediante la jerarquía de clases, donde las clases son la unidad básica del programa y una clase puede tener métodos privados. Esto se entiende correctamente en el contexto de distribución dinámica en lugar de resolución de nombres y alcance, aunque a menudo desempeñan papeles análogos. En algunos casos, ambas funciones están disponibles, como en Python, que tiene módulos y clases, y la organización del código (como una función a nivel de módulo o un método privado convencional) es una elección del programador.
El alcance de un enlace de nombre es un programa completo, lo que se conoce como alcance global . Los nombres de variables con alcance global, llamados variables globales , se consideran con frecuencia una mala práctica, al menos en algunos lenguajes, debido a la posibilidad de colisiones de nombres y enmascaramiento involuntario, junto con una modularidad deficiente, y se considera preferible el alcance de función o el alcance de bloque. Sin embargo, el alcance global se usa típicamente (dependiendo del lenguaje) para varios otros tipos de nombres, como nombres de funciones, nombres de clases y nombres de otros tipos de datos . En estos casos, se usan mecanismos como los espacios de nombres para evitar colisiones.
El uso de variables locales (nombres de variables con alcance limitado, que solo existen dentro de una función específica) ayuda a evitar el riesgo de una colisión de nombres entre dos variables con nombres idénticos. Sin embargo, existen dos enfoques muy diferentes para responder a esta pregunta: ¿Qué significa estar "dentro" de una función?
En el ámbito léxico (o ámbito léxico ; también llamado ámbito estático o alcance estático ), si el ámbito de una variable nombre es una función determinada, entonces su ámbito es el texto del programa de la definición de la función: dentro de ese texto, la variable nombre existe y está vinculada al valor de la variable, pero fuera de ese texto, la variable nombre no existe. Por el contrario, en el ámbito dinámico (o ámbito dinámico ), si el ámbito de una variable nombre es una función determinada, entonces su ámbito es el período de tiempo durante el cual la función se está ejecutando: mientras la función se está ejecutando, la variable nombre existe y está vinculada a su valor, pero después de que la función retorna, la variable nombre no existe. Esto significa que si función invocaf
una función definida por separado g
, entonces bajo el ámbito léxico, función g
no tiene acceso a las variables locales de (asumiendo que el texto de no está dentro del texto de ), mientras que bajo el ámbito dinámico, función sí tiene acceso a las variables locales de (ya que se invoca durante la invocación de ).f
g
f
g
f
g
f
$ # lenguaje bash $ x = 1 $ function g () { echo $x ; x = 2 ; } $ function f () { local x = 3 ; g ; } $ f # ¿esto imprime 1 o 3? 3 $ echo $x # ¿esto imprime 1 o 2? 1
Considere, por ejemplo, el programa de la derecha. La primera línea, , crea una variable global y la inicializa en . La segunda línea, , define una función que imprime ("hace eco") del valor actual de , y luego lo establece en (sobrescribiendo el valor anterior). La tercera línea, define una función que crea una variable local (ocultando la variable global con el mismo nombre) y la inicializa en , y luego llama a . La cuarta línea, , llama a . La quinta línea, , imprime el valor actual de .x=1
x
1
function g() { echo $x ; x=2 ; }
g
x
x
2
function f() { local x=3 ; g ; }
f
x
3
g
f
f
echo $x
x
Entonces, ¿qué imprime exactamente este programa? Depende de las reglas de alcance. Si el lenguaje de este programa es uno que utiliza alcance léxico, entonces g
imprime y modifica la variable global x
(porque g
se define fuera de f
), por lo que el programa imprime 1
y luego 2
. Por el contrario, si este lenguaje utiliza alcance dinámico, entonces g
imprime y modifica f
la variable local de x
(porque g
se llama desde dentro f
de ), por lo que el programa imprime 3
y luego 1
. (De hecho, el lenguaje del programa es Bash , que utiliza alcance dinámico; por lo que el programa imprime 3
y luego 1
. Si el mismo código se ejecutara con ksh93 que utiliza alcance léxico, los resultados serían diferentes).
Con el ámbito léxico , un nombre siempre se refiere a su contexto léxico. Esta es una propiedad del texto del programa y la implementación del lenguaje la hace independiente de la pila de llamadas en tiempo de ejecución . Debido a que esta coincidencia solo requiere el análisis del texto estático del programa, este tipo de ámbito también se denomina ámbito estático . El ámbito léxico es estándar en todos los lenguajes basados en ALGOL, como Pascal , Modula-2 y Ada , así como en los lenguajes funcionales modernos como ML y Haskell . También se utiliza en el lenguaje C y sus parientes sintácticos y semánticos, aunque con diferentes tipos de limitaciones. El ámbito estático permite al programador razonar sobre referencias a objetos como parámetros, variables, constantes, tipos, funciones, etc. como simples sustituciones de nombres. Esto hace que sea mucho más fácil hacer código modular y razonar sobre él, ya que la estructura de nombres local se puede entender de forma aislada. Por el contrario, el ámbito dinámico obliga al programador a anticipar todos los posibles contextos de ejecución en los que se puede invocar el código del módulo.
programa A ; var I : número entero ; K : carbón ; procedimiento B ; var K : real ; L : número entero ; procedimiento C ; var M : real ; inicio (*ámbito A+B+C*) fin ; (*alcance A+B*) fin ; (*alcance A*) fin .
Por ejemplo, Pascal tiene un alcance léxico. Considere el fragmento del programa Pascal a la derecha. La variable I
es visible en todos los puntos, porque nunca está oculta por otra variable del mismo nombre. La char
variable K
es visible solo en el programa principal porque está oculta por la real
variable K
visible en procedimiento B
y C
solo. Variable L
también es visible solo en procedimiento B
y C
pero no oculta ninguna otra variable. Variable M
solo es visible en procedimiento C
y, por lo tanto, no es accesible ni desde procedimiento B
ni desde el programa principal. Además, procedimiento C
es visible solo en procedimiento B
y, por lo tanto, no se puede llamar desde el programa principal.
Podría haber otro procedimiento C
declarado en el programa fuera del procedimiento B
. El lugar en el programa donde C
se menciona " " determina entonces cuál de los dos procedimientos nombrados C
representa, por lo que es exactamente análogo al alcance de las variables.
La correcta implementación del alcance léxico en lenguajes con funciones anidadas de primera clase no es trivial, ya que requiere que cada valor de función lleve consigo un registro de los valores de las variables de las que depende (el par de la función y este contexto se llama cierre ). Dependiendo de la implementación y la arquitectura de la computadora , la búsqueda de variables puede volverse ligeramente ineficiente [ cita requerida ] cuando se usan funciones anidadas léxicamente muy profundas , aunque existen técnicas bien conocidas para mitigar esto. [9] [10] Además, para funciones anidadas que solo hacen referencia a sus propios argumentos y variables locales (inmediatamente), todas las ubicaciones relativas se pueden conocer en tiempo de compilación . Por lo tanto, no se incurre en ninguna sobrecarga al usar ese tipo de función anidada. Lo mismo se aplica a partes particulares de un programa donde no se usan funciones anidadas y, naturalmente, a programas escritos en un lenguaje donde las funciones anidadas no están disponibles (como en el lenguaje C).
El alcance léxico se utilizó por primera vez a principios de la década de 1960 para el lenguaje imperativo ALGOL 60 y ha sido adoptado por la mayoría de los demás lenguajes imperativos desde entonces. [4]
Lenguajes como Pascal y C siempre han tenido alcance léxico, ya que ambos están influenciados por las ideas contenidas en ALGOL 60 y ALGOL 68 (aunque C no incluía funciones anidadas léxicamente ).
Perl es un lenguaje con alcance dinámico al que luego se le añadió un alcance estático.
El intérprete original de Lisp (1960) utilizaba un alcance dinámico. El enlace profundo , que se aproxima al alcance estático (léxico), se introdujo alrededor de 1962 en LISP 1.5 (a través del dispositivo Funarg desarrollado por Steve Russell , bajo la dirección de John McCarthy ).
Todos los primeros Lisp utilizaban un alcance dinámico cuando se basaban en intérpretes. En 1982, Guy L. Steele Jr. y el Common LISP Group publican An overview of Common LISP [11], una breve revisión de la historia y las diferentes implementaciones de Lisp hasta ese momento y una revisión de las características que debería tener una implementación de Common Lisp . En la página 102, leemos:
La mayoría de las implementaciones de LISP son internamente inconsistentes, ya que, por defecto, el intérprete y el compilador pueden asignar semánticas diferentes a los programas correctos; esto se debe principalmente al hecho de que el intérprete supone que todas las variables tienen un alcance dinámico, mientras que el compilador supone que todas las variables son locales a menos que se le obligue a suponer lo contrario. Esto se ha hecho por conveniencia y eficiencia, pero puede conducir a errores muy sutiles. La definición de Common LISP evita tales anomalías al exigir explícitamente que el intérprete y el compilador impongan semánticas idénticas a los programas correctos.
Por lo tanto, se requería que las implementaciones de Common LISP tuvieran un alcance léxico . Nuevamente, de Una descripción general de Common LISP :
Además, Common LISP ofrece las siguientes funciones (la mayoría de las cuales se han tomado prestadas de MacLisp, InterLisp o Lisp Machines Lisp): (...) Variables con alcance léxico completo. El llamado " problema FUNARG " [12] [13] está completamente resuelto, tanto en el caso descendente como en el ascendente.
En el mismo año en que se publicó An overview of Common LISP (1982), se habían publicado los diseños iniciales (también de Guy L. Steele Jr.) de un Lisp compilado y con alcance léxico, llamado Scheme , y se estaban intentando implementaciones de compiladores. En ese momento, se temía comúnmente que el alcance léxico en Lisp fuera ineficiente de implementar. En A History of T , [14] Olin Shivers escribe:
Todos los lenguajes Lisp serios que se utilizaban en producción en ese momento tenían un alcance dinámico. Nadie que no hubiera leído con atención la tesis Rabbit [15] (escrita por Guy Lewis Steele Jr. en 1978) creía que el alcance léxico funcionaría; incluso las pocas personas que la habían leído estaban un poco a la expectativa de que esto fuera a funcionar en un uso serio en producción.
El término "alcance léxico" data al menos de 1967, [16] mientras que el término "alcance léxico" data al menos de 1970, donde se utilizó en el Proyecto MAC para describir las reglas de alcance del dialecto Lisp MDL (entonces conocido como "Muddle"). [17]
Con un ámbito dinámico , un nombre hace referencia al contexto de ejecución. En términos técnicos, esto significa que cada nombre tiene una pila global de enlaces. Al introducir una variable local con nombre, x
se inserta un enlace en la x
pila global (que puede haber estado vacía), que se elimina cuando el flujo de control abandona el ámbito. La evaluación x
en cualquier contexto siempre da como resultado el enlace superior. Tenga en cuenta que esto no se puede hacer en tiempo de compilación porque la pila de enlaces solo existe en tiempo de ejecución , por lo que este tipo de ámbito se denomina ámbito dinámico .
El alcance dinámico es poco común en los lenguajes modernos. [4]
Generalmente, ciertos bloques se definen para crear enlaces cuya duración es el tiempo de ejecución del bloque; esto añade algunas características del ámbito estático al proceso de ámbito dinámico. Sin embargo, dado que una sección de código puede ser llamada desde muchas ubicaciones y situaciones diferentes, puede ser difícil determinar desde el principio qué enlaces se aplicarán cuando se use una variable (o si existe alguna). Esto puede ser beneficioso; la aplicación del principio del mínimo conocimiento sugiere que el código evite depender de las razones (o circunstancias) del valor de una variable, sino que simplemente use el valor de acuerdo con la definición de la variable. Esta interpretación estrecha de los datos compartidos puede proporcionar un sistema muy flexible para adaptar el comportamiento de una función al estado actual (o política) del sistema. Sin embargo, este beneficio se basa en una documentación cuidadosa de todas las variables utilizadas de esta manera, así como en evitar cuidadosamente las suposiciones sobre el comportamiento de una variable, y no proporciona ningún mecanismo para detectar interferencias entre diferentes partes de un programa. Algunos lenguajes, como Perl y Common Lisp , permiten al programador elegir el ámbito estático o dinámico al definir o redefinir una variable. Algunos ejemplos de lenguajes que utilizan el alcance dinámico son Logo , Emacs Lisp , LaTeX y los lenguajes de shell bash , dash y PowerShell .
El alcance dinámico es bastante fácil de implementar. Para encontrar el valor de un nombre, el programa podría recorrer la pila de tiempo de ejecución, verificando cada registro de activación (el marco de pila de cada función) en busca de un valor para el nombre. En la práctica, esto se hace más eficiente mediante el uso de una lista de asociación , que es una pila de pares nombre/valor. Los pares se insertan en esta pila siempre que se realizan declaraciones y se extraen cuando las variables quedan fuera de contexto. [18] La vinculación superficial es una estrategia alternativa que es considerablemente más rápida, haciendo uso de una tabla de referencia central , que asocia cada nombre con su propia pila de significados. Esto evita una búsqueda lineal durante el tiempo de ejecución para encontrar un nombre en particular, pero se debe tener cuidado de mantener adecuadamente esta tabla. [18] Tenga en cuenta que ambas estrategias suponen un orden de último en entrar, primero en salir ( LIFO ) para las vinculaciones para cualquier variable; en la práctica, todas las vinculaciones están ordenadas de esa manera.
Una implementación aún más simple es la representación de variables dinámicas con variables globales simples. La vinculación local se realiza guardando el valor original en una ubicación anónima en la pila que es invisible para el programa. Cuando ese ámbito de vinculación termina, el valor original se restaura desde esta ubicación. De hecho, el ámbito dinámico se originó de esta manera. Las primeras implementaciones de Lisp usaban esta estrategia obvia para implementar variables locales, y la práctica sobrevive en algunos dialectos que todavía se usan, como GNU Emacs Lisp. El ámbito léxico se introdujo en Lisp más tarde. Esto es equivalente al esquema de vinculación superficial anterior, excepto que la tabla de referencia central es simplemente el contexto de vinculación de variable global, en el que el significado actual de la variable es su valor global. Mantener variables globales no es complejo. Por ejemplo, un objeto de símbolo puede tener una ranura dedicada para su valor global.
El ámbito dinámico proporciona una excelente abstracción para el almacenamiento local de subprocesos , pero si se utiliza de esa manera no puede basarse en guardar y restaurar una variable global. Una posible estrategia de implementación es que cada variable tenga una clave local de subproceso. Cuando se accede a la variable, la clave local de subproceso se utiliza para acceder a la ubicación de memoria local de subproceso (por código generado por el compilador, que sabe qué variables son dinámicas y cuáles son léxicas). Si la clave local de subproceso no existe para el subproceso que realiza la llamada, se utiliza la ubicación global. Cuando una variable está enlazada localmente, el valor anterior se almacena en una ubicación oculta en la pila. El almacenamiento local de subprocesos se crea bajo la clave de la variable y el nuevo valor se almacena allí. Las anulaciones anidadas posteriores de la variable dentro de ese subproceso simplemente guardan y restauran esta ubicación local de subproceso. Cuando finaliza el contexto de la anulación inicial más externa, se elimina la clave local de subproceso, exponiendo la versión global de la variable una vez más a ese subproceso.
Con transparencia referencial, el alcance dinámico está restringido a la pila de argumentos de la función actual únicamente y coincide con el alcance léxico.
En los lenguajes modernos, la expansión de macros en un preprocesador es un ejemplo clave de ámbito dinámico de facto. El lenguaje de macros en sí solo transforma el código fuente, sin resolver nombres, pero como la expansión se realiza en el lugar, cuando se resuelven los nombres en el texto expandido (en particular, las variables libres), se resuelven en función del lugar donde se expanden (lo que se denomina "denominado" de manera general), como si se estuviera produciendo un ámbito dinámico.
El preprocesador C , utilizado para la expansión de macros , tiene un alcance dinámico de facto, ya que no realiza la resolución de nombres por sí mismo y es independiente de dónde se define la macro. Por ejemplo, la macro:
#define ADD_A(x) x + a
se expandirá para agregar a
a la variable pasada, y este nombre será resuelto más tarde por el compilador en función de dónde ADD_A
se "llame" a la macro (adecuadamente, expandida). De manera adecuada, el preprocesador de C solo realiza análisis léxico , expandiendo la macro durante la etapa de tokenización, pero no la analiza en un árbol de sintaxis ni realiza resolución de nombres.
Por ejemplo, en el siguiente código, el nombre a
en la macro se resuelve (después de la expansión) en la variable local en el sitio de expansión:
#define ADD_A(x) x + avacío agregar_uno ( int * x ) { constante int a = 1 ; * x = ADD_A ( * x ); } vacío añadir_dos ( int * x ) { constante int a = 2 ; * x = ADD_A ( * x ); }
Como hemos visto, una de las razones principales para el ámbito es que ayuda a evitar colisiones de nombres, al permitir que nombres idénticos hagan referencia a cosas distintas, con la restricción de que los nombres deben tener ámbitos separados. A veces, esta restricción es incómoda; cuando se necesita acceder a muchas cosas diferentes a través de un programa, generalmente todas necesitan nombres con ámbito global, por lo que se requieren diferentes técnicas para evitar colisiones de nombres.
Para solucionar este problema, muchos lenguajes ofrecen mecanismos para organizar nombres globales. Los detalles de estos mecanismos y los términos utilizados dependen del lenguaje, pero la idea general es que a un grupo de nombres se le puede dar un nombre (un prefijo) y, cuando sea necesario, se puede hacer referencia a una entidad mediante un nombre calificado que consiste en el nombre más el prefijo. Normalmente, estos nombres tendrán, en cierto sentido, dos conjuntos de ámbitos: un ámbito (normalmente el ámbito global) en el que el nombre calificado es visible, y uno o más ámbitos más estrechos en los que el nombre no calificado (sin el prefijo) también es visible. Y normalmente estos grupos se pueden organizar a su vez en grupos; es decir, se pueden anidar .
Aunque muchos lenguajes admiten este concepto, los detalles varían enormemente. Algunos lenguajes tienen mecanismos, como los espacios de nombres en C++ y C# , que sirven casi exclusivamente para permitir que los nombres globales se organicen en grupos. Otros lenguajes tienen mecanismos, como los paquetes en Ada y las estructuras en Standard ML , que combinan esto con el propósito adicional de permitir que algunos nombres sean visibles solo para otros miembros de su grupo. Y los lenguajes orientados a objetos a menudo permiten que las clases u objetos singleton cumplan con este propósito (ya sea que también tengan o no un mecanismo para el cual este sea el propósito principal). Además, los lenguajes a menudo fusionan estos enfoques; por ejemplo, los paquetes de Perl son en gran medida similares a los espacios de nombres de C++, pero opcionalmente se duplican como clases para la programación orientada a objetos; y Java organiza sus variables y funciones en clases, pero luego organiza esas clases en paquetes similares a Ada.
A continuación se detallan las reglas de alcance para los idiomas representativos.
En C, el ámbito se conoce tradicionalmente como enlace o visibilidad , en particular para las variables. C es un lenguaje con ámbito léxico con alcance global (conocido como enlace externo ), una forma de ámbito de módulo o ámbito de archivo (conocido como enlace interno ) y ámbito local (dentro de una función); dentro de una función, los ámbitos se pueden anidar a través del ámbito de bloque. Sin embargo, el C estándar no admite funciones anidadas.
La duración de vida y la visibilidad de una variable están determinadas por su clase de almacenamiento . Hay tres tipos de duración de vida en C: estática (ejecución del programa), automática (ejecución de bloques, asignada en la pila) y manual (asignada en el montón). Solo la estática y la automática son compatibles con las variables y las maneja el compilador, mientras que la memoria asignada manualmente debe rastrearse manualmente en diferentes variables. Hay tres niveles de visibilidad en C: enlace externo (global), enlace interno (aproximadamente archivo) y ámbito de bloque (que incluye funciones); los ámbitos de bloque se pueden anidar y es posible tener diferentes niveles de enlace interno mediante el uso de inclusiones. El enlace interno en C es la visibilidad a nivel de la unidad de traducción , es decir, un archivo fuente después de ser procesado por el preprocesador de C , en particular incluyendo todas las inclusiones relevantes.
Los programas C se compilan como archivos de objeto separados , que luego se vinculan a un ejecutable o biblioteca a través de un enlazador . Por lo tanto, la resolución de nombres se divide entre el compilador, que resuelve los nombres dentro de una unidad de traducción (más libremente, "unidad de compilación", pero este es un concepto diferente), y el enlazador, que resuelve los nombres en las distintas unidades de traducción; consulte el enlace para obtener más información.
En C, las variables con alcance de bloque entran en contexto cuando se declaran (no al principio del bloque), salen de contexto si se llama a alguna función (no anidada) dentro del bloque, vuelven a entrar en contexto cuando la función retorna y salen de contexto al final del bloque. En el caso de las variables locales automáticas, también se asignan en el momento de la declaración y se desasignan al final del bloque, mientras que en el caso de las variables locales estáticas, se asignan en la inicialización del programa y se desasignan al finalizar el programa.
El siguiente programa demuestra una variable con alcance de bloque que entra en contexto a mitad del bloque y luego sale del contexto (y de hecho se desasigna) cuando el bloque finaliza:
#incluir <stdio.h> int main ( void ) { char x = 'm' ; printf ( "%c \n " , x ); { printf ( "%c \n " , x ); char x = 'b' ; printf ( "%c \n " , x ); } printf ( "%c \n " , x ); }
El programa genera:
metrometrobmetro
Existen otros niveles de alcance en C. [19] Los nombres de variables utilizados en un prototipo de función tienen visibilidad de prototipo de función y salen del contexto al final del prototipo de función. Dado que el nombre no se utiliza, esto no es útil para la compilación, pero puede ser útil para la documentación. Los nombres de etiqueta para la declaración GOTO tienen alcance de función.
Todas las variables que pretendemos utilizar en un programa deben haber sido declaradas con su especificador de tipo en un punto anterior del código, como hicimos en el código anterior al principio del cuerpo de la función main cuando declaramos que a, b y result eran de tipo int. Una variable puede ser de alcance global o local. Una variable global es una variable declarada en el cuerpo principal del código fuente, fuera de todas las funciones, mientras que una variable local es una declarada dentro del cuerpo de una función o un bloque.
Las versiones modernas permiten un alcance léxico anidado.
Swift tiene una regla similar para los ámbitos con C++, pero contiene diferentes modificadores de acceso.
Go tiene un alcance léxico mediante bloques. [3]
Java tiene alcance léxico.
Una clase Java tiene varios tipos de variables: [20]
En general, un conjunto de corchetes define un ámbito particular, pero las variables en el nivel superior dentro de una clase pueden diferir en su comportamiento dependiendo de las palabras clave modificadoras utilizadas en su definición. La siguiente tabla muestra el acceso a los miembros permitido por cada modificador. [21]
JavaScript tiene reglas de alcance simples , [22] pero las reglas de inicialización de variables y resolución de nombres pueden causar problemas, y el uso generalizado de cierres para devoluciones de llamadas significa que el contexto léxico de una función cuando se define (que se usa para la resolución de nombres) puede ser muy diferente del contexto léxico cuando se llama (que es irrelevante para la resolución de nombres). Los objetos de JavaScript tienen resolución de nombres para las propiedades, pero este es un tema aparte.
JavaScript tiene un alcance léxico [23] anidado en el nivel de función, siendo el contexto global el contexto más externo. Este alcance se utiliza tanto para variables como para funciones (es decir, declaraciones de funciones, a diferencia de las variables del tipo de función ). [24] El alcance de bloque con las let
palabras const
clave y es estándar desde ECMAScript 6. El alcance de bloque se puede producir envolviendo el bloque completo en una función y luego ejecutándolo; esto se conoce como el patrón de expresión de función invocada inmediatamente (IIFE).
Si bien el alcance de JavaScript es simple (léxico, a nivel de función), las reglas de inicialización y resolución de nombres asociadas son causa de confusión. En primer lugar, la asignación a un nombre que no está dentro del alcance crea de manera predeterminada una nueva variable global, no una local. En segundo lugar, para crear una nueva variable local se debe usar la var
palabra clave; la variable se crea entonces en la parte superior de la función, con valor undefined
y se le asigna su valor a la variable cuando se alcanza la expresión de asignación:
Esto se conoce como elevación de variable [26] : la declaración, pero no la inicialización, se eleva a la parte superior de la función. En tercer lugar, acceder a las variables antes de la inicialización produce undefined
, en lugar de un error de sintaxis. En cuarto lugar, para las declaraciones de funciones, la declaración y la inicialización se elevan a la parte superior de la función, a diferencia de la inicialización de variables. Por ejemplo, el siguiente código produce un cuadro de diálogo con salidaindefinido, ya que se eleva la declaración de la variable local, ensombreciendo la variable global, pero no la inicialización, por lo que la variable no está definida cuando se usa:
a = 1 ; función f () { alerta ( a ); var a = 2 ; } f ();
Además, como las funciones son objetos de primera clase en JavaScript y con frecuencia se asignan como devoluciones de llamadas o se devuelven desde funciones, cuando se ejecuta una función, la resolución del nombre depende de dónde se definió originalmente (el contexto léxico de la definición), no del contexto léxico o el contexto de ejecución donde se llama. Los ámbitos anidados de una función particular (desde el más global hasta el más local) en JavaScript, particularmente de un cierre, utilizado como devolución de llamada, a veces se denominan cadena de ámbitos , por analogía con la cadena de prototipos de un objeto.
En JavaScript, se pueden producir cierres mediante el uso de funciones anidadas, ya que las funciones son objetos de primera clase. [27] La devolución de una función anidada desde una función envolvente incluye las variables locales de la función envolvente como el contexto léxico (no local) de la función devuelta, lo que produce un cierre. Por ejemplo:
function newCounter () { // devuelve un contador que se incrementa en la llamada (empezando en 0) // y que devuelve su nuevo valor var a = 0 ; var b = function () { a ++ ; return a ; }; return b ; } c = newCounter (); alert ( c () + ' ' + c ()); // genera "1 2"
Los cierres se utilizan con frecuencia en JavaScript, debido a que se utilizan para devoluciones de llamadas. De hecho, cualquier conexión de una función en el contexto local como devolución de llamada o su devolución desde una función crea un cierre si hay variables no vinculadas en el cuerpo de la función (con el contexto del cierre basado en los ámbitos anidados del contexto léxico actual, o "cadena de ámbitos"); esto puede ser accidental. Al crear una devolución de llamada basada en parámetros, los parámetros deben almacenarse en un cierre; de lo contrario, se creará accidentalmente un cierre que hace referencia a las variables en el contexto envolvente, que puede cambiar. [28]
La resolución de nombres de las propiedades de los objetos JavaScript se basa en la herencia en el árbol de prototipos (una ruta a la raíz en el árbol se denomina cadena de prototipos ) y es independiente de la resolución de nombres de variables y funciones.
Los dialectos Lisp tienen varias reglas para el alcance.
El Lisp original utilizaba un alcance dinámico; fue Scheme , inspirado en ALGOL , el que introdujo el alcance estático (léxico) a la familia Lisp.
Maclisp utilizaba el ámbito dinámico de forma predeterminada en el intérprete y el ámbito léxico de forma predeterminada en el código compilado, aunque el código compilado podía acceder a enlaces dinámicos mediante el uso de SPECIAL
declaraciones para variables particulares. [29] Sin embargo, Maclisp trataba el enlace léxico más como una optimización de lo que uno esperaría en los lenguajes modernos, y no venía con la característica de cierre que uno podría esperar del ámbito léxico en los Lisp modernos. Una operación separada, *FUNCTION
, estaba disponible para solucionar de manera algo torpe parte de ese problema. [30]
Common Lisp adoptó el alcance léxico de Scheme , [31] al igual que Clojure .
ISLISP tiene alcance léxico para variables ordinarias. También tiene variables dinámicas, pero en todos los casos están explícitamente marcadas; deben definirse mediante una defdynamic
forma especial, estar limitadas por una dynamic-let
forma especial y se debe acceder a ellas mediante una dynamic
forma especial explícita. [32]
Algunos otros dialectos de Lisp, como Emacs Lisp , todavía utilizan el ámbito dinámico de forma predeterminada. Emacs Lisp ahora tiene un ámbito léxico disponible por búfer. [33]
Para las variables, Python tiene un ámbito de función, un ámbito de módulo y un ámbito global. Los nombres entran en el contexto al comienzo de un ámbito (función, módulo o ámbito global) y salen del contexto cuando se llama a una función no anidada o finaliza el ámbito. Si se utiliza un nombre antes de la inicialización de la variable, se genera una excepción en tiempo de ejecución. Si simplemente se accede a una variable (no se le asigna), la resolución de nombres sigue la regla LEGB (local, envolvente, global, incorporada) que resuelve los nombres en el contexto relevante más estrecho. Sin embargo, si se asigna una variable, se declara de forma predeterminada una variable cuyo ámbito comienza al comienzo del nivel (función, módulo o global), no en la asignación. Ambas reglas se pueden anular con una declaración global
o nonlocal
(en Python 3) antes del uso, lo que permite acceder a las variables globales incluso si hay una variable no local de enmascaramiento y asignar a variables globales o no locales.
Como ejemplo simple, una función resuelve una variable en el ámbito global:
>>> def f (): ... imprimir ( x ) ... >>> x = "global" >>> f () global
Tenga en cuenta que x
se define antes de f
que se llame, por lo que no se produce ningún error, aunque se defina después de su referencia en la definición de f
. Léxicamente, se trata de una referencia hacia delante , lo cual está permitido en Python.
Aquí la asignación crea una nueva variable local, que no cambia el valor de la variable global:
>>> def f (): ... x = "f" ... imprimir ( x ) ... >>> x = "global" >>> imprimir ( x ) global >>> f () f >>> imprimir ( x ) global
La asignación a una variable dentro de una función hace que se declare local a la función, por lo que su alcance es toda la función y, por lo tanto, su uso antes de esta asignación genera un error. Esto difiere de C, donde el alcance de la variable local comienza en su declaración. Este código genera un error:
>>> def f (): ... print ( x ) ... x = "f" ... >>> x = "global" >>> f () Traceback (última llamada más reciente): Archivo "<stdin>" , línea 1 , en <módulo> Archivo "<stdin>" , línea 2 , en f UnboundLocalError : variable local 'x' referenciada antes de la asignación
Las reglas de resolución de nombres predeterminadas se pueden anular con las palabras clave global
or nonlocal
(en Python 3). En el código siguiente, la global x
declaración in g
significa que x
se resuelve en la variable global. Por lo tanto, se puede acceder a ella (ya que ya se ha definido) y la asignación asigna a la variable global, en lugar de declarar una nueva variable local. Tenga en cuenta que no global
se necesita ninguna declaración in f
; dado que no asigna a la variable, se resuelve de forma predeterminada en la variable global.
>>> def f (): ... imprimir ( x ) ... >>> def g (): ... global x ... imprimir ( x ) ... x = "g" ... >>> x = "global" >>> f () global >>> g () global >>> f () g
global
También se puede utilizar para funciones anidadas. Además de permitir la asignación a una variable global, como en una función no anidada, también se puede utilizar para acceder a la variable global en presencia de una variable no local:
>>> def f (): ... def g (): ... global x ... print ( x ) ... x = "f" ... g () ... >>> x = "global" >>> f () global
Para las funciones anidadas, también existe la nonlocal
declaración para asignar a una variable no local, similar al uso global
en una función no anidada:
>>> def f (): ... def g (): ... no local x # Solo Python 3 ... x = "g" ... x = "f" ... g () ... print ( x ) ... >>> x = "global" >>> f () g >>> print ( x ) global
R es un lenguaje con alcance léxico, a diferencia de otras implementaciones de S donde los valores de las variables libres están determinados por un conjunto de variables globales, mientras que en R están determinados por el contexto en el que se creó la función. [34] Se puede acceder a los contextos de alcance utilizando una variedad de características (como parent.frame()
) que pueden simular la experiencia del alcance dinámico si el programador lo desea.
No hay alcance de bloque:
a <- 1 { a <- 2 } mensaje ( a ) ## 2
Las funciones tienen acceso al ámbito en el que fueron creadas:
a <- 1 f <- función () { mensaje ( a ) } f () ## 1
Las variables creadas o modificadas dentro de una función permanecen allí:
a <- 1 f <- función () { mensaje ( a ) a <- 2 mensaje ( a ) } f () ## 1 ## 2 mensaje ( a ) ## 1
Las variables creadas o modificadas dentro de una función permanecen allí a menos que se solicite explícitamente su asignación al ámbito que las incluye:
a <- 1 f <- función () { mensaje ( a ) a <<- 2 mensaje ( a ) } f () ## 1 ## 2 mensaje ( a ) ## 2
Aunque R tiene alcance léxico por defecto, los alcances de las funciones se pueden cambiar:
a <- 1 f <- función () { mensaje ( a ) } mi_env <- nuevo.env () mi_env $ a <- 2 f () ## 1 entorno ( f ) <- mi_env f () ## 2
No se puede acceder a las variables definidas dentro de una función desde ningún lugar fuera de la función, porque la variable está definida solo en el ámbito de la función. Sin embargo, una función puede acceder a todas las variables y funciones definidas dentro del ámbito en el que está definida.
Si la variable que se va a vincular se ha declarado como especial, la vinculación se compila como código para imitar la forma en que el intérprete vincula las variables
tiene como objetivo ayudar a resolver el "
problema funarg
", sin embargo, solo funciona en algunos casos fáciles.
*FUNCTION
*FUNCTION
MacLisp mejoró la noción de variables especiales de Lisp 1.5 ... Las principales influencias en Common Lisp fueron Lisp Machine Lisp, MacLisp, NIL, S-1 Lisp, Spice Lisp y Scheme.
Los enlaces dinámicos se establecen y se accede a ellos mediante un mecanismo independiente (es decir,
,
, y
).
defdynamic
dynamic-let
dynamic
Emacs 24 tiene un enlace léxico opcional, que se puede habilitar por búfer.