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 afalso
.Entero: cuando el valor es
0
, se convierte enfalso
, de lo contrario, se convierte enverdadero
.Número real: cuando el valor es
0.0
, se convierte enfalso
, de lo contrario, se convierte enverdadero
.Cadena: cuando el valor es “” (cadena vacía) se convierte en
falso
de lo contrario, se convierte enverdadero
.Comobj y Comptr: cuando el puntero interno es
NULL
es convertido afalso
, de lo contrario se convierte averdadero
.Instancia: si la instancia contiene un método
tobool()
, se utilizará el valor de retorno del método, de lo contrario, se convertirá enverdadero
.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.