Tipos de dato
Rust tiene muchos tipos de dato que permiten trabajar con números, caracteres y otros. Algunos son simples, otros son más complicados e, incluso, puedes crear tus propios tipos de dato.
Tipos de dato primitivos
Puedes ver este capítulo en YouTube en inglés
Rust tiene tipos simples que llaman tipos primitivos (primitivo = muy básico). Comenzaremos con los números enteros y los char
(caracteres). Los enteros son números sin coma decimal. Existen dos tipos de enteros:
- Enteros con signo.
- Enteros sin signo.
"Con signo" significa que disponen de +
(signo más) y -
(signo menos), por lo que los enteros con signo pueden ser positivos o negativos (por ejemplo, +8, -8). Por el contratio, los enteros sin signo solo pueden ser positivos ya que no tienen signo.
Los enteros con signo son: i8
, i16
, i32
, i64
, i128
, e isize
.
Los enteros sin signo son: u8
, u16
, u32
, u64
, u128
, e usize
.
El número tras la i o la u indica el número de bits que se usan para el entero. Así, los números con más bits pueden ser mayores. 8 bits = un byte por lo que i8
ocupa un byte, y puede contener valores entre el -128 y el 127. Por lo tanto i64
ocupa 64 bits o, lo que es lo mismo, 8 bytes y puede representar números entre el -9223372036854775808 y el 9223372036854775807.
Los tipos numéricos con mayor tamaño pueden representar valores mayores. Por ejemplo, el tipo u8
puede representar del 0 al 255, el tipo u16
puede representar del 0 al 65635, y el tipo u128
puede representar a un número entre el 0 y el 340282366920938463463374607431768211455.
¿Y qué representan isize
y usize
? El número de bits del tipo nativo del procesador de tu ordenador (El número de bits nativo de tu procesador se denomina la arquitectura de tu procesador). Así que isize
y usize
en un ordenador de 32-bits son equivalentes a i32
y u32
. En un ordenador de 64-bits son equivalentes a i64
y u64
.
Hay muchas razones para disponer de todos estos tipos de números enteros. Una razón es el rendimiento del ordenador: es más rápido procesar un número menor de bytes. Por ejemplo, el número -10 representado como un i8
es 11110110
, pero como un i128
es 11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110110
.
veamos otros usos:
Los caracteres en Rust se denominan char
. Todo char
se representa por un número: la letra A
es lel número 65, mientras que el carácter 友
("amigo" en chino) es el número 21451. La lista de estos números que representan a caracteres se denomina "Unicode". Unicode usa números más pequeños para los caracteres que se usan más, como los de la A a la Z, los dígitos de 0 a 9 o el espacio.
fn main() { let primera_letra = 'A'; let espacio = ' '; // Un espacio entre ' ' también es un char let char_en_otro_idioma = 'Ꮔ'; // Gracias a Unicode, otros lenguajes, como el Cherokee, también se visualizan bien let cara_gato = '😺'; // Emojis también son char }
Los caracteres que se utilizan más se representan or números menores al 256 y así pueden caber en un u8
. Recuerda que un u8
permite números entre el 0 y el 255, lo que facilita la representación de 256 caracteres en total. Esto significa que Rust puede convertir de forma segura del tipo de datos u8
al char
, utilizando la palabra reservada del lenguaje as
(Considera el dato de este tipo u8
como si fuese char
).
La conversión de tipos de datos utilizando as
es muy útil ya que Rust es muy estricto. Siempre necesita conocer el tipo de dato y, además, no deja utilizar de forma conjunta dos tipos de datos diferentes incluso aunque sean de la misma familia (como los enteros). Por ejemplo, este código no funcionará:
fn main() { // main() es la función a partir de la que se // inician los programas Rust. El código entre las llaves {} let mi_numero = 100; // No hemos indicado el tipo de datos entero // que Rust debe utilizar, // así que Rust elige i32. Rust siempre // elige i32 para los enteros si no se le indica // que utilice otro tipo de datos diferente println!("{}", mi_numero as char); // ⚠️ }
Esta es la razón:
error[E0604]: only `u8` can be cast as `char`, not `i32`
--> src\main.rs:10:20
|
3 | println!("{}", mi_numero as char);
| ^^^^^^^^^^^^^^^^^ invalid cast
Afortunadamente, podemos corregir esto fácilmente con as
. No podemos convertir un i32
a char
, pero podemos convertir un i32
a u8
. Y después, podemos realizar la conversión de u8
a char
. Po tanto, en una línea usamos as
para convertir mi_numero a u8
y después lo convertimos a char
. Ahora sí compilará:
fn main() { let mi_numero = 100; println!("{}", mi_numero as u8 as char); }
Se imprime d
porque es el char
que está representado en Unicode con este número.
Existe una forma más fácil, sin embargo, de conseguir este resultado: indicarle a Rust que mi_numero
es de tipo u8
. Así:
fn main() { let mi_numero: u8 = 100; // se indica de forma expresa que // el tipo de la variable mi_numero es u8 println!("{}", mi_numero as char); }
Las anteriores, son dos de las razones para la existencia de todos estos tipos de datos numéricos en Rust. Hay otra razón más: usize
es el tamaño que Rust utiliza para indexar (Indexar significa "conocer cual elemento va primero", "cual va segundo", etc.). usize
es el mejor tamaño para el indexado porque:
- Un índice no puede ser negativo, por lo que tiene que ser uno de los tipos de dato con una u.
- Debe ser grande porque en muchas ocasiones necesitas indexar muchas cosas, pero
- No puede ser un u64 porque los ordenadores de 32-bits no lo pueden manejar.
Por eso Rust usa usize
para indexar y facilitar que tu ordenador pueda utilizar el tipo de datos mayor de que disponga.
Vamos a aprender algo más sobre char
. Ya vimos que char
siempre es un carácter y utiliza ''
en lugar de ""
.
Todos los char
usan 4 bytes de memoria, puesto que son necesarios 4 bytes para contener cualquier clase de carácter:
- Las letras y símbolos básicos suelen necesitar solo 1 de los 4 bytes:
a b 1 2 + - = $ @
- Otras letras como las diéresis y tildes necesitan 2 de los 4 bytes:
ä ö ü ß è é à ñ
- Los caracteres coreanos, japoneses o chinos necesitan 3 de los cuatro bytes:
国 안 녕
Cuando los caracteres se usan como parte de una cadena, esta se codifica para usar la menor cantidad de memoria necesaria para cada carácter1.
1 N.T.: Rust codifica las cadenas en UTF-8.
Para observar esto podemos usar .len()
:
fn main() { println!("Tamaño de un char: {}", std::mem::size_of::<char>()); // 4 bytes // .len() devuelve el tamaño de una cadena de texto en bytes println!("Tamaño de una cadena que contiene la 'a': {}", "a".len()); println!("Tamaño de una cadena que contiene la 'ß': {}", "ß".len()); println!("Tamaño de una cadena que contiene la '国': {}", "国".len()); println!("Tamaño de una cadena que contiene la '𓅱': {}", "𓅱".len()); }
Lo que imprime:
Tamaño de un char: 4
Tamaño de una cadena que contiene la 'a': 1
Tamaño de una cadena que contiene la 'ß': 2
Tamaño de una cadena que contiene la '国': 3
Tamaño de una cadena que contiene la '𓅱': 4
Puedes ver que la a
ocupa un byte, la 'ß' alemana ocupa dos, la japonesa 国
ocupa tres, y el carácter del antiguo egipto 𓅱
ocupa cuatro bytes.
fn main() { let fragmento = "¡Hola!"; println!("El fragmento ocupa {} bytes.", fragmento.len()); let fragmento2 = "안녕!"; // Coreano de "hola" println!("El fragmento2 ocupa {} bytes.", fragmento2.len()); }
Que imprime:
El fragmento ocupa 7 bytes.
El fragmento2 ocupa 7 bytes.
El primer fragmento consta de seis caracteres y ocupa 7 bytes (la apertura de exclamación ocupa dos bytes). El segundo fragmento consta de tres caracteres y ocupa 7 bytes (los dos primeros caracteres ocupan tres bytes cada uno).
Si .len()
devuelve el tamaño en bytes, ¿cómo se puede conocer el tamaño de una cadena de texto en caracteres? Aprenderemos esto más tarde, en este momento basta con recordar que se hace con .chars().count()
. La primera función .chars()
devuelve los caracteres separados y luego cuenta cuántos son.
fn main() { let fragmento = "¡Hola!"; println!("El fragmento ocupa {} bytes y son {} caracteres.", fragmento.len(), fragmento.chars().count()); let fragmento2 = "안녕!"; println!("El fragmento2 ocupa {} bytes y son {} caracteres.", fragmento2.len(), fragmento2.chars().count()); }
Que imprime:
El fragmento ocupa 7 bytes y son 6 caracteres.
El fragmento2 ocupa 7 bytes y son 3 caracteres.