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 u8puede 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 charsiempre 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.