El alcance ( en inglés scope ) en programación es una parte del programa , dentro de la cual el identificador , declarado como el nombre de alguna entidad del programa (generalmente una variable , tipo de dato o función ), queda asociado a esta entidad, es decir, te permite referirse a él a través de sí mismo. Se dice que un identificador de objeto es "visible" en un lugar determinado del programa si puede usarse para referirse al objeto dado en ese lugar. Fuera del ámbito, un mismo identificador puede estar asociado a otra variable o función, o ser libre (no asociado a ninguna de ellas). El alcance puede, pero no necesariamente, ser el mismo que el alcance del objeto al que está asociado el nombre.
La vinculación de identificadores ( en inglés binding ) en la terminología de algunos lenguajes de programación es el proceso de definición de un objeto de programa, cuyo acceso otorga un identificador en un lugar específico del programa y en un momento específico de su ejecución. Este concepto es esencialmente sinónimo de alcance , pero puede ser más conveniente al considerar algunos aspectos de la ejecución del programa.
Los ámbitos encajan entre sí y forman una jerarquía , desde un ámbito local, limitado por una función (o incluso parte de ella), hasta un ámbito global, cuyos identificadores están disponibles a lo largo del programa. Además, dependiendo de las reglas de un lenguaje de programación en particular, los alcances se pueden implementar de dos maneras: léxicamente (estáticamente) o dinámicamente .
El ámbito también puede tener sentido para los lenguajes de marcado : por ejemplo, en HTML , el ámbito de un nombre de control es formulario (HTML) de <form> a </form> [1] .
En un programa monolítico (de un solo módulo) sin funciones anidadas y sin el uso de OOP, solo puede haber dos tipos de alcance: global y local. Otros tipos existen solo si hay ciertos mecanismos sintácticos en el lenguaje.
En los lenguajes OOP , además de lo anterior, se pueden admitir restricciones de alcance especiales que se aplican solo a los miembros de la clase (identificadores declarados dentro de la clase o relacionados con ella):
En los casos más simples, el alcance está determinado por el lugar donde se declara el identificador. En los casos en que el lugar de la declaración no pueda especificar sin ambigüedad el alcance, se aplican mejoras especiales.
La lista anterior no agota todos los matices de definir el alcance que puede estar disponible en un lenguaje de programación en particular. Entonces, por ejemplo, son posibles diferentes interpretaciones de combinaciones de alcance modular y visibilidad declarada de miembros de una clase OOP. En algunos lenguajes (por ejemplo, C++), declarar un ámbito privado o protegido para un miembro de la clase restringe el acceso a él desde cualquier código que no esté relacionado con los métodos de su clase. En otros (Object Pascal), todos los miembros de la clase, incluidos los privados y protegidos, son totalmente accesibles dentro del módulo en el que se declara la clase, y las restricciones de alcance se aplican solo en otros módulos que importan este.
Los ámbitos de un programa forman naturalmente una estructura en capas, con algunos ámbitos anidados dentro de otros. La jerarquía de áreas suele construirse en todos o en algunos niveles del conjunto: "global - paquete - modular - clases - local" (el orden específico puede variar ligeramente en diferentes idiomas).
Los paquetes y los espacios de nombres pueden tener varios niveles de anidamiento, por lo que sus ámbitos también estarán anidados. La relación entre los ámbitos del módulo y de la clase puede variar mucho de un idioma a otro. Los espacios de nombres locales también se pueden anidar, incluso en los casos en que el idioma no admita funciones y procedimientos anidados. Entonces, por ejemplo, no hay funciones anidadas en el lenguaje C++, pero cada declaración compuesta (que contiene un conjunto de comandos encerrados entre llaves) forma su propio ámbito local, en el que es posible declarar sus variables.
La estructura jerárquica permite la resolución de ambigüedades que surgen cuando se utiliza el mismo identificador en más de un valor en un programa. La búsqueda del objeto deseado parte siempre del ámbito en el que se encuentra el código de acceso al identificador. Si hay un objeto con el identificador deseado en el ámbito dado, entonces es ese objeto el que se usa. Si no lo hay, el traductor continúa la búsqueda entre los identificadores visibles en el ámbito envolvente, si tampoco lo hay, en el siguiente nivel de jerarquía.
programa Ejemplo1 ; var a , b , c : Entero ; (* Variables globales. *) procedimiento f1 ; var b , c : Integer (* Variables locales del procedimiento f1. *) begin a := 10 ; (* Cambios globales a. *) b := 20 ; (* Cambia local b. *) c := 30 ; (* Cambia local c. *) writeln ( ' 4: ' , a , ',' , b , ',' , c ) ; fin ; procedimiento f2 ; var b , c : Integer (* Variables locales del procedimiento f2. *) procedimiento f21 ; var c : Integer (* Procedimiento variable local f21. *) begin a := 1000 ; (* Cambios globales a. *) b := 2000 ; (* Cambia el local b del procedimiento f2. *) c := 3000 ; (* Cambia el c local del procedimiento f21.*) writeln ( ' 5: ' , a , ',' , b , ',' , c ) ; fin ; comienza un := 100 ; (* Cambios globales a. *) b := 200 ; (* Cambia local b. *) c := 300 ; (* Cambia local c. *) writeln ( ' 6: ' , a , ',' , b , ',' , c ) ; f21 ; writeln ( ' 7: ' , a , ',' , b , ',' , c ) ; fin ; begin (* Inicialización de variables globales. *) a := 1 ; segundo := 2 ; c := 3 ; writeln ( ' 1: ' , a , ',' , b , ',' , c ) ; f1 ; writeln ( ' 2: ' , a , ',' , b , ',' , c ) ; f2 ; writeln ( ' 3: ' , a , ',' , b , ',' , c ) ; fin _Entonces, cuando ejecute el programa Pascal anterior, obtendrá el siguiente resultado:
1:1,2,3 4:10,20,30 2:10,2,3 6: 100,200,300 5: 1000,2000,3000 7: 1000,2000,300 3:1000,2,3En una función, f1las variables by cestán en el ámbito local, por lo que sus cambios no afectan a las variables globales del mismo nombre. Una función f21contiene sólo una variable en su ámbito local c, por lo que modifica tanto la variable global acomo bla local en la función que la encierra f2.
El uso de variables locales, que tienen un alcance limitado y solo existen dentro de la función actual, ayuda a evitar conflictos de nombres entre dos variables con el mismo nombre. Sin embargo, hay dos enfoques muy diferentes a la pregunta de qué significa "estar dentro" de una función y, en consecuencia, dos opciones para implementar el alcance local:
Para las funciones "puras" que operan solo con sus propios parámetros y variables locales, los ámbitos léxico y dinámico son siempre los mismos. Los problemas surgen cuando una función utiliza nombres externos, como variables globales o variables locales de funciones de las que forma parte o desde las que se llama. Entonces, si una función fllama a una función que no está anidada en ella g, entonces con el enfoque léxico, la función g no tiene acceso a las variables locales de la función f. Sin embargo, con el enfoque dinámico, la función g tendrá acceso a las variables locales de la función fporque se gllamó en tiempo de ejecución f.
Por ejemplo, considere el siguiente programa:
x = 1 función g () { echo $x ; x = 2 _ } función f () { local x = 3 ; g ; } f # imprime 1 o 3? echo $x # generará 1 o 2?La función g()muestra y cambia el valor de la variable x, pero esta variable no es g()ni un parámetro ni una variable local, es decir, debe estar asociada a un valor del ámbito que contiene g(). Si el idioma en el que está escrito el programa utiliza ámbitos léxicos, entonces el nombre «x»interior g()debe estar asociado con una variable globalx . La función g()llamada desde f()imprimirá el valor inicial de global х , luego lo cambiará, y el valor modificado se imprimirá en la última línea del programa. Es decir, el programa mostrará primero 1, luego 2. Los cambios en la xfunción local en el texto de la función f()no afectarán esta salida de ninguna manera, ya que esta variable no es visible ni en el ámbito global ni en la función g().
Si el lenguaje usa ámbitos dinámicos, entonces el nombre se asocia «x»internamente g()con la variable localx de la función f(), ya que g() se llama desde dentro f() y entra en su ámbito. Aquí, la función g()mostrará la variable local xde la función f()y la cambiará, pero esto no afectará el valor de la x global de ninguna manera, por lo que el programa mostrará primero 3, luego 1. Ya que en este caso el programa está escrito en bash , que utiliza un enfoque dinámico, en realidad esto sucederá exactamente.
Tanto el enlace léxico como el dinámico tienen sus pros y sus contras. En la práctica, la elección entre uno y otro la hace el desarrollador basándose tanto en sus propias preferencias como en la naturaleza del lenguaje de programación que se está diseñando. La mayoría de los lenguajes imperativos de alto nivel típicos, originalmente diseñados para usar un compilador (en el código de la plataforma de destino o en el código de bytes de la máquina virtual, no importa), implementan un alcance estático (léxico), ya que se implementa más convenientemente en el compilador. El compilador trabaja con un contexto léxico que es estático y no cambia durante la ejecución del programa, y al procesar la referencia a un nombre, puede determinar fácilmente la dirección en la memoria donde reside el objeto asociado con el nombre. El contexto dinámico no está disponible para el compilador (ya que puede cambiar durante la ejecución del programa, porque se puede llamar a la misma función en muchos lugares, y no siempre de forma explícita), por lo que para proporcionar un alcance dinámico, el compilador debe agregar soporte dinámico para la definición de objetos. al código al que se refiere el identificador. Esto es posible, pero reduce la velocidad del programa, requiere memoria adicional y complica el compilador.
En el caso de los lenguajes interpretados (por ejemplo, scripting ), la situación es fundamentalmente diferente. El intérprete procesa el texto del programa directamente en el momento de la ejecución y contiene estructuras internas de apoyo a la ejecución, incluidas tablas de nombres de variables y funciones con valores reales y direcciones de objetos. Es más fácil y rápido para el intérprete realizar enlaces dinámicos (una simple búsqueda lineal en una tabla de identificadores) que realizar un seguimiento del alcance léxico todo el tiempo. Por lo tanto, los lenguajes interpretados suelen admitir la vinculación dinámica de nombres.
Dentro del enfoque tanto dinámico como léxico del enlace de nombres, puede haber matices asociados con las peculiaridades de un lenguaje de programación en particular o incluso con su implementación. Como ejemplo, considere dos lenguajes de programación tipo C: JavaScript y Go . Los lenguajes son sintácticamente bastante cercanos y ambos usan alcance léxico, pero sin embargo difieren en los detalles de su implementación.
El siguiente ejemplo muestra dos fragmentos de código textualmente similares en JavaScript y Go. En ambos casos, una variable scopeinicializada con la cadena "global" se declara en el alcance global, y el f()valor del alcance se deduce primero en la función, luego una declaración local de una variable con el mismo nombre inicializada con la cadena "local" , y finalmente se vuelve a inferir el valor scope. El siguiente es el resultado real de ejecutar la función f()en cada caso.
JavaScript | Vamos |
---|---|
var ámbito = "global" ; función f () { alerta ( alcance ); // ? var ámbito = "local" ; alerta ( alcance ); } | var alcance = función "global" f () { fmt . println ( alcance ) //? var ámbito = "local" fmt . println ( alcance ) } |
local indefinido |
mundial local |
Es fácil ver que la diferencia radica en qué valor se muestra en la línea marcada con un comentario con un signo de interrogación.
Otro matiz en la semántica del ámbito léxico es la presencia o ausencia de la llamada "visibilidad de bloque", es decir, la capacidad de declarar una variable local no solo dentro de una función, procedimiento o módulo, sino también dentro de un bloque separado. de comandos (en lenguajes tipo C, encerrados entre corchetes {}). El siguiente es un ejemplo de código idéntico en dos idiomas, dando diferentes resultados al ejecutar la función f().
JavaScript | Vamos |
---|---|
función f () { var x = 3 ; alerta ( x ); para ( var i = 10 ; i < 30 ; i += 10 ) { var x = i ; alerta ( x ); } alerta ( x ); // ? } | función f () { var x = 3 fmt . Println ( x ) para i := 10 ; yo < 30 ; yo += 10 { var x = yo fmt . println ( x ) } fmt . imprimir ( x ) // ? } |
3 10 20 20 |
3 10 20 3 |
La diferencia está en qué valor generará la última declaración en la función f()marcada con un signo de interrogación en el comentario.
La visibilidad de un identificador no debe equipararse con la existencia del valor con el que está asociado el identificador. La relación entre la visibilidad de un nombre y la existencia de un objeto se ve afectada por la lógica del programa y la clase de almacenamiento del objeto. A continuación se muestran algunos ejemplos típicos.