2. Tipos y Variables

Tipo es un atributo de datos, que define el significado de los datos y las operaciones que se pueden realizar en los datos. Los tipos se pueden dividir en tipos integrados y tipos definidos por el usuario. Los tipos integrados se refieren a algunos tipos básicos integrados en el lenguaje Berry, entre los cuales los tipos que no se basan en definiciones de clase se denominan Tipo simple. Los tipos basados en definiciones de clase se denominan Tipo de clase, algunos de los tipos integrados son tipos de clase y los tipos definidos por el usuario también son tipos de clase.

2.1 Tipo incorporado

2.1.1 Tipo simple

nil

El tipo Nil es el tipo nulo, lo que significa que el objeto tiene un valor no válido, o se puede decir que el objeto no tiene un valor significativo. Este es un tipo muy especial. Aunque podríamos decir que una variable es nil, de hecho el tipo nil no tiene valor, entonces de lo que estamos hablando aquí es que el tipo de la variable es nil (no un valor).

El valor predeterminado de una variable antes de la asignación es nil. Este tipo se puede utilizar en operaciones lógicas. En este caso ‘nil’ es equivalente a ‘falso’.

Tipo entero

El tipo entero (integer) representa un entero con signo, denominado entero. El número de bits del entero representado por este tipo depende de la implementación específica y, por lo general, consta de un entero de 32 bits con signo en una plataforma de 32 bits. Integer es un tipo aritmético y admite todas las operaciones aritméticas. Preste atención al rango de valores de los enteros cuando use este tipo. El rango de valores típico de los enteros con signo de 32 bits está entre −2147483648 y 2147483647.

Cualquier valor se puede convertir a int usando la función int(); sin embargo int(nil) == nil. Si el argumento es una instancia, y si contiene un miembro toint(), se nos llamará y el valor devuelto se convertirá en int.

Tipo de número real

El tipo real (real), para ser precisos, es un tipo de coma flotante. Los tipos de números reales generalmente se implementan como números de punto flotante de precisión simple o números de punto flotante de precisión doble. El tipo de número real también es un tipo aritmético. En comparación con el tipo de número entero, el tipo de número real tiene mayor precisión y un mayor rango de valores, por lo que este tipo es más adecuado para cálculos matemáticos. Cabe señalar que el tipo de número real es en realidad un número de coma flotante, por lo que aun existen problemas de precisión. Por ejemplo, no se recomienda comparar dos valores de tipo real para la igualdad.

Cuando los enteros y los números reales participan en operaciones al mismo tiempo, los enteros generalmente se convierten en números reales.

Tipo booleano

El tipo booleano (boolean) se utiliza para operaciones lógicas. Tiene dos valores true y false, que representan los dos valores verdaderos (verdadero y falso) en lógica y álgebra booleana. El tipo booleano se utiliza principalmente para el juicio condicional. Los operandos y los valores devueltos de las expresiones lógicas y las expresiones relacionales son todos de tipo booleano, y las sentencias como if y while utilizan tipos booleanos como comprobaciones condicionales.

En muchos casos, los valores no booleanos también se pueden usar como tipos booleanos. Esto se debe a que el intérprete convertirá implícitamente los parámetros. Esta es también la razón por la que las expresiones de verificación condicional, como las declaraciones if, pueden usar cualquier tipo de parámetros. Las reglas para convertir varios tipos a tipos booleanos son:

  • nil: convertido a falso.

  • Entero: cuando el valor es 0, se convierte en falso, de lo contrario, se convierte en verdadero.

  • Número real: cuando el valor es 0.0, se convierte en falso, de lo contrario, se convierte en verdadero.

  • Cadena: cuando el valor es “” (cadena vacía) se convierte en falso de lo contrario, se convierte en verdadero.

  • Comobj y Comptr: cuando el puntero interno es NULL es convertido a falso, de lo contrario se convierte a verdadero.

  • Instancia: si la instancia contiene un método tobool(), se utilizará el valor de retorno del método, de lo contrario, se convertirá en verdadero.

  • Todos los demás tipos: converten a verdadero.

Cualquier valor se puede convertir a bool usando la función bool().

Cadena

Una cadena es una secuencia de caracteres. En términos de almacenamiento, Berry divide las cadenas en cadenas largas y cadenas cortas. Solo hay una instancia de la misma cadena de caracteres cortos en la memoria, y todas las cadenas de caracteres cortos están vinculadas en una tabla hash. Este diseño ayuda a mejorar el rendimiento de la comparación de igualdad de cadenas y puede reducir el uso de memoria. Dado que la frecuencia de uso de cadenas largas es baja y la sobrecarga de la operación hash es bastante alta, no están vinculadas a la tabla hash, por lo que puede haber varias instancias idénticas en la memoria. La cadena es de solo lectura después de que se crea. Por lo tanto, “modificar” la cadena generará una nueva cadena y la cadena original no se modificará.

Berry no se preocupa por el formato o la codificación de los caracteres. Por ejemplo, la cadena 'abc' es en realidad el código ASCII de los caracteres 'a', 'b' y 'c'. Por lo tanto, si hay caracteres anchos en la cadena (la longitud de los caracteres es superior a 1 byte), el número de caracteres de la cadena no se puede contar directamente. De hecho, usar la función size() solo puede obtener el número de bytes en la cadena. Además, para facilitar la interacción con el lenguaje C, la cadena de Berry siempre termina con los caracteres '\0'. Esta función es transparente para el programa Berry.

El tipo de cadena se puede comparar en tamaño, por lo que se puede usar en operaciones relacionales.

Función

Una función es una pieza de código que está encapsulada y disponible para llamadas, generalmente utilizada para implementar una función específica. La función es en realidad una categoría grande, que incluye varios subtipos, como cierres, funciones nativas y cierres nativos. Para el código Berry, todos los subtipos de funciones tienen el mismo comportamiento. Las funciones pertenecen al primer tipo de valor en Berry, por lo que se pueden pasar como valores. Además, se puede usar directamente en expresiones a través de la forma “literal” de “funciones anónimas”.

Una función es un objeto de solo lectura y no se puede modificar una vez definida. Puede comparar si dos funciones son iguales (si son la misma función), pero no se puede comparar el tipo de función. Función nativa y Cierre nativo se refieren a funciones y cierres implementados en lenguaje C. Uno de los propósitos principales de las funciones nativas y los cierres nativos es proporcionar funciones que el lenguaje Berry no proporciona, como operaciones de E/S y operaciones de bajo nivel. Si un fragmento de código se usa con frecuencia y tiene requisitos de rendimiento, se recomienda reescribirlo como una función nativa o un cierre nativo.

Clase

En la programación orientada a objetos, una clase es una plantilla de código de programa extensible. Las clases se utilizan para crear objetos de instancia, por lo que se puede decir que la clase es el “tipo” de la instancia. Todos los objetos de instancia son del tipo instancia y todos tienen una clase correspondiente, que se llama instancia Tipo de clase. En pocas palabras, una clase es un valor que representa el tipo de un objeto de instancia y una clase es una abstracción de las características de una instancia. Una clase también es un objeto de solo lectura, una vez definida, no se puede modificar.

Las clases solo pueden comparar iguales y desiguales, pero no pueden comparar tamaños.

Ejemplos

Una instancia es un objeto materializado generado por una clase, y el proceso de generar una instancia a partir de una clase se llama Instanciación. En la programación orientada a objetos, “instancia” suele ser sinónimo de “objeto”. Sin embargo, para distinguir los objetos que no son de instancia, no usamos el término “objeto” solo, sino que usamos “instancia” u “objeto de instancia”. Las instancias de Berry siempre se asignan dinámicamente y deben usarse con un recolector de basura. Además de la asignación de memoria, el proceso de creación de instancias también necesita inicializar la instancia, este proceso lo completa el Constructor. Además, puedes completar la destrucción del objeto a través del Destructor antes de recuperar la memoria del objeto.

En la implementación interna, la instancia contendrá una referencia a la clase, y la instancia en sí misma solo almacena variables miembro y no métodos.

2.1.2 Tipo de clase

Algunos de los tipos incorporados son tipos de clase, son list, map y range. A diferencia de los tipos personalizados, los tipos de clase integrados se pueden construir usando literales, por ejemplo, [1, 2, 3] es un literal de tipo list.

Lista

La clase List es un contenedor que proporciona soporte para tipos de datos de lista. La lista de Berry es una colección ordenada de elementos, y cada elemento de la lista tiene un índice entero único, y se puede acceder a cada elemento directamente según el índice. List admite la inserción o eliminación de elementos en cualquier posición, y el elemento puede ser de cualquier tipo. Además de usar índices, también puede usar iteradores para acceder a los elementos de la lista.

La implementación de List es una matriz dinámica y esta estructura de datos tiene un buen rendimiento de acceso aleatorio. La eficiencia de agregar y eliminar elementos al final de la lista es muy alta, pero la eficiencia de agregar y eliminar elementos en el medio de la lista es baja.

El método de inicialización literal del contenedor List es usar una lista de objetos entre corchetes y múltiples objetos separados por comas, por ejemplo:

[]
['string']
[0, 1, 2,'list']

Operaciones: ver capítulo 7.

Mapa

El mapa también es un tipo de contenedor, el mapa es una colección de pares clave-valor, y cada clave posible aparece como máximo una vez en la colección. El contenedor Mapa proporciona las siguientes operaciones básicas:

  • Agregar pares clave-valor a la colección

  • Eliminar pares clave-valor de la colección

  • Modificar el valor correspondiente a una clave existente

  • Encuentra el valor correspondiente por clave

El mapa se implementa utilizando una tabla hash y tiene una alta eficiencia de búsqueda. La operación de agregar y eliminar pares clave-valor consumirá más tiempo si se produce un “rehashing”.

El contenedor Map también se puede inicializar con valores literales, escritos entre llaves para encerrar una lista de pares clave-valor, separados por dos puntos entre claves y valores, y separados por comas entre pares clave-valor. P.ej:

{}
{'str':'hola'}
{'str':'hola','int': 45, 78: nil}

Operaciones: ver capítulo 7.

Rango

El contenedor Range representa un rango de enteros, que generalmente se usa para iterar en un rango de enteros. Este tipo tiene un miembro __lower__ y un miembro __upper__, que representan los límites inferior y superior del rango, respectivamente. El valor literal de Range es un par de enteros conectados mediante el operador ..:

0 .. 10
-5 .. 5

Cuando la clase Range se usa para la iteración, los elementos de la iteración son todos valores enteros desde el límite inferior hasta el límite superior, incluidos los valores límite. Por ejemplo, el resultado de la iteración de 0..5 es:

0 1 2 3 4 5

Por tanto, cabe señalar que para un rango de x .. (x+n), el número de iteraciones es n + 1. Una construcción común para iterar a través de los elementos de una lista por elemento es:

for i: 0..size(l)-1

Rango abierto: si omite el último rango, se reemplaza implícitamente con MAXINT.

> r = 10..
> r
(10..9223372036854775807)

Bytes

El objeto Bytes denota un búfer de bytes que se puede usar para manipular búferes de bytes o para leer/escribir algunas áreas o estructuras de memoria C.

Consulte el Capítulo 7.

2.2 Variables

Una variable es un espacio de almacenamiento con un nombre, y los datos o la información almacenados en el espacio de almacenamiento se denominan valor de la variable. Los nombres de variables se utilizan para hacer referencia a las variables en el código fuente. En diferentes ámbitos, un nombre de variable puede vincular varias variables independientes, pero las variables no tienen alias. El valor de la variable se puede acceder o cambiar en cualquier momento durante la ejecución del programa. Berry es un lenguaje de tipo dinámico, por lo que el tipo de valor de la variable se determina en tiempo de ejecución y la variable puede almacenar cualquier tipo de valor.

2.2.1 Definir variables

La primera forma de definir una variable es usar una declaración de asignación para asignar un valor a un nuevo nombre de variable:

’var’ =  expresión

var es el nombre de la variable, y el nombre de la variable es un identificador (consulte la sección identificador). expresión es la expresión para inicializar la variable.

a = 1
b ='str'

Sin embargo, este método de definición de variables tiene algunas limitaciones. Tome el siguiente código como ejemplo:

i = 0
do
    i = 1
    print(i) # 1
end
print(i) # 1

La instrucción do en la rutina constituye el alcance interno. Modificamos el valor de la variable i en la línea 3, y el valor de i sigue siendo 1 después de dejar el alcance interno en la línea 6. Si queremos que la variable i del ámbito interno sea una variable independiente, el método de definir la variable mediante la asignación directa al nuevo nombre de variable no puede cumplir el requisito, porque el identificador i ya existe en el ámbito externo. En este caso, la variable se puede definir mediante la palabra clave var:

’var’ variable
’var’ variable = expresión

Hay dos formas de usar var para definir una variable: la primera es seguir el nombre de la variable variable después de la palabra clave var, en este caso la variable se inicializará a nil, y la otra se escribe en la variable y se inicializa al mismo tiempo que se define la variable. En este caso, se requiere una expresión de valor inicial expresión. Usar var para definir una variable tiene dos posibles resultados: si el alcance actual no define la variable de variable, definir e inicializar la variable, de lo contrario, es equivalente a reinicializar la variable. Por lo tanto, la variable definida con var protegerá a la variable con el mismo nombre en el ámbito externo.

Ahora cambiamos el ejemplo anterior para usar la palabra clave var para definir variables:

i = 0
do
    var i = 1
    print(i) # 1
end
print(i) # 0

A partir de la rutina modificada, se puede encontrar que el valor de la variable i en el ámbito interno es 1, y su valor en el ámbito externo es 0. Esto prueba que después de usar la palabra clave var, se define una nueva variable i en el ámbito interno y se bloquea la variable con el mismo nombre en el ámbito externo. Una vez que finaliza el ámbito interno, el identificador i vuelve a vincularse a la variable i en el ámbito externo.

Al usar la palabra clave var para definir una variable, también puede usar una lista de múltiples nombres de variables, separados por comas. También puede inicializar una o más variables al definir variables:

var a = 0, b, c ='test'

2.2.2 Alcance y Ciclo de Vida

Como se mencionó anteriormente, los nombres de las variables se pueden vincular a varias entidades de variables (espacios de almacenamiento) y los nombres de las variables se vinculan a una sola entidad en cada posición. La entidad vinculada por el nombre de la variable debe determinarse de acuerdo con la posición en la que aparece el nombre de la variable.

Ámbito se refiere al área de código donde el nombre y la entidad están vinculados de forma única. Fuera del alcance, el nombre puede estar vinculado a otras entidades, o no estar vinculado a ninguna entidad. La entidad solo es visible en el alcance asociado al nombre, es decir, la variable solo es válida en su alcance. Un bloque de código (ver bloque) es un alcance. Una variable solo está disponible dentro del bloque, y los nombres en diferentes bloques pueden vincular diferentes entidades variables. El siguiente ejemplo demuestra el alcance de las variables:

var i = 0
do
    var j ='str'
    print(i, j) # 0 str
end
#  La variable j no está disponible aquí
print(i) # 0

Los nombres i y j se definen en esta rutina. El nombre i se define fuera de la oración do, y el nombre definido en el bloque más externo tiene Alcance global. El nombre con alcance global está disponible en todo el programa después de la personalización. El nombre j se define en el bloque en la oración do, y el nombre de este tipo de definición en el bloque no exterior tiene Ámbito local. No se puede acceder a un nombre con un ámbito local fuera del ámbito.

Berry tiene algunos objetos integrados, que están todos en el ámbito global. Sin embargo, los objetos integrados y las variables globales definidas en los scripts no están en el mismo ámbito global. Los objetos integrados en realidad pertenecen al Alcance integrado. El alcance es visible globalmente como el alcance global ordinario, pero puede estar cubierto por el alcance global ordinario. Los objetos incorporados incluyen funciones y clases en la biblioteca estándar. Estos objetos incluyen funciones de impresión, funciones de tipo y clases de mapa. A diferencia de otros ámbitos, las variables en el ámbito integrado son de solo lectura, por lo que la “asignación” a las variables en el ámbito integrado en realidad define una variable con el mismo nombre en el ámbito global, que anula los símbolos en el ámbito en el alcance incorporado.

Alcance anidado

Ámbito anidado significa que el ámbito contiene otro ámbito. Llamamos al ámbito contenido Ámbito interno y al ámbito que contiene el ámbito interno Ámbito externo. Se puede acceder al nombre definido en el ámbito externo en todos los ámbitos internos. El ámbito interno también puede volver a vincular el nombre ya definido en el ámbito externo. El ejemplo anterior usando var para definir variables describe este escenario.

Ciclo de vida variable

No existe el concepto de nombres de variables cuando el programa se está ejecutando, y las variables existen en forma de entidades en este momento. El “período de validez” de una variable durante la ejecución del programa es el ciclo de vida de la variable. Las variables en tiempo de ejecución solo son válidas dentro del alcance. Después de salir del alcance, las variables se destruirán para recuperar recursos.

Las variables definidas en el ámbito global se denominan Variable global y tienen Ciclo de vida estático. Se puede acceder a dichas variables durante todo el programa en ejecución y no se destruirán. Las variables definidas en el ámbito local se denominan Variable local y tienen Ciclo de vida dinámico. No se puede acceder a dichas variables después de abandonar el alcance y se destruirán.

Debido a los diferentes ciclos de vida, las variables locales y las variables globales usan diferentes formas de asignar el espacio de almacenamiento. Las variables locales se asignan en una estructura llamada Pila (stack), y los objetos asignados en función de la pila se pueden reclamar rápidamente al final del alcance. Las variables globales se asignan en Tabla global (tabla global). Los objetos de la tabla global no se reciclarán una vez creados y se puede acceder a la tabla desde cualquier parte del programa.

2.2.3 Tipo de variable

Berry determina el tipo de variable en tiempo de ejecución. En otras palabras, la variable puede almacenar cualquier tipo de valor. Por lo tanto, Berry es un lenguaje de escritura dinámica. El intérprete no deduce el tipo de la variable en tiempo de compilación, lo que puede provocar que se expongan algunos errores en tiempo de ejecución. Por ejemplo, el error generado al ejecutar la expresión '1' + 1 es un error de tiempo de ejecución y no un error del compilador. La ventaja de usar tipos dinámicos es que se pueden simplificar muchos diseños y el programa será más flexible, sin mencionar la necesidad de diseñar un sistema de inferencia de tipos complejo.

Debido a que el intérprete no verifica el tipo, es posible que el código de usuario deba determinar el tipo de valor por sí mismo, y esta característica también se puede usar para implementar algunas operaciones especiales. Esta característica también hace que las funciones sobrecargadas sean innecesarias. Por ejemplo, la función nativa type acepta cualquier tipo de parámetro y devuelve una cadena que describe el tipo de parámetro.