Los lectores que provienen de un entorno de Solidity o Javascript pueden encontrar que el uso y la sintaxis de Rust para &, mut, <_>, unwrap(), y ? son raros (o incluso feos). Este capítulo explica qué significan estos términos.
No te preocupes si no asimilas todo de inmediato. Siempre puedes volver a este tutorial más adelante si olvidas las definiciones de la sintaxis.
Ownership y Borrowing (referencias & y operador deref *):
El copy type en Rust
Para entender & y *, primero necesitamos entender el “copy type” (tipo de copia) en Rust. Un copy type es un tipo de datos que es lo suficientemente pequeño como para que la sobrecarga de copiar su valor sea trivial. Los siguientes valores son copy types:
- enteros, sin signo y enteros de punto flotante (floats)
- booleanos
- char
La razón por la que son “copy types” es porque tienen un tamaño fijo pequeño.
Por otro lado, los vectores, los strings y los structs pueden ser arbitrariamente grandes, por lo que no son copy types.
Por qué Rust hace una distinción entre copy types y non-copy types
Considera el siguiente código de Rust:
pub fn main() {
let a: u32 = 2;
let b: u32 = 3;
println!("{}", add(a, b)); // a and b a are copied to the add function
let s1 = String::from("hello");
let s2 = String::from(" world");
// if s1 and s2 are copied, this could be a huge data transfer
// if the strings are very long
println!("{}", concat(s1, s2));
}
// implementations of add() and concat() are not shown for brevity
// this code does not compile
En la primera sección de código donde a y b se suman, solo se necesitan copiar 64 bits de datos de las variables a la función (32 bits * 2 variables).
Sin embargo, en el caso del string, no siempre sabemos de antemano cuántos datos estamos copiando. Si el string tuviera 1 GB de longitud, el programa se ralentizaría significativamente.
Rust quiere que seamos explícitos sobre cómo queremos que se manejen los grandes volúmenes de datos. No los copiará en segundo plano como lo hacen los lenguajes dinámicos.
Por lo tanto, cuando hacemos algo tan simple como asignar un string a una nueva variable, Rust hará algo que muchos consideran inesperado, como veremos en la siguiente sección.
Ownership en Rust
Para los tipos non-copy (Strings, vectores, structs, etc.), una vez que el valor se asigna a la variable, esa variable lo “posee” (tiene el ownership). Las implicaciones de esta propiedad se demostrarán en breve.
El siguiente código no compilará. La explicación está en los comentarios:
// Example of changing ownership on a non-copy datatype (string)
let s1 = String::from("abc");
// s2 becomes the owner of `String::from("abc")`
let s2 = s1;
// The following line will fail to compile because s1 can no longer access its string value.
println!("{}", s1);
// This line compiles successfully because s2 now owns the string value.
println!("{}", s2);
Para arreglar el código anterior tenemos dos opciones: usar el operador & o clonar s1.
Opción 1: darle a s2 una vista de s1
En el código a continuación, nota el importante & precediendo a s1:
pub fn main() {
let s1 = String::from("abc");
let s2 = &s1; // s2 can now view `String::from("abc")` but not own it
println!("{}", s1); // This compiles, s1 still holds its original string value.
println!("{}", s2); // This compiles, s2 holds a reference to the string value in s1.
}
Si queremos que otra variable “vea” el valor (es decir, obtenga acceso de solo lectura), usamos el operador &.
Para darle a otra variable o función una vista de una variable poseída (owned), la precedemos con &.
Puede ser útil pensar en & como un modo de “solo vista” para un tipo non-copy. La palabra técnica para lo que estamos llamando “solo vista” es borrowing (préstamo).
Opción 2: clonar s1
Para entender cómo podríamos clonar un valor, considera el siguiente ejemplo:
fn main() {
let mut message = String::from("hello");
println!("{}", message);
message = message + " world";
println!("{}", message);
}
El código anterior imprimirá “hello” y luego “hello world” como se esperaba.
Sin embargo, si agregamos otra variable y que vea a message, el código ya no compilará:
// Does not compile
fn main() {
let mut message = String::from("hello");
println!("{}", message);
let mut y = &message; // y is viewing message
message = message + " world";
println!("{}", message);
println!("{}", y); // should y be "hello" or "hello world"?
}
Rust no acepta el código anterior porque la variable message no puede ser reasignada mientras está siendo vista.
Si queremos que y pueda copiar el valor de message sin interferir con message más adelante, podemos clonarlo en su lugar:
fn main() {
let mut message = String::from("hello");
println!("{:?}", message);
let mut y = message.clone(); // change this to clone
message = message + " world";
println!("{:?}", message);
println!("{:?}", y);
}
El código anterior imprimirá:
hello
hello world
hello
El Ownership es solo un problema con los tipos non-copy
Si reemplazamos nuestro String (que es un tipo non-copy) por un copy type (como un entero), no nos encontraremos con ninguno de los problemas anteriores. Rust copiará felizmente el copy type porque la sobrecarga es insignificante.
let s1 = 3;
let s2 = s1;
println!("{}", s1);
println!("{}", s2);
La palabra clave mut
Por defecto, todas las variables son inmutables en Rust a menos que se especifique la palabra clave mut.
El siguiente código no compilará:
pub fn main() {
let counter = 0;
counter = counter + 1;
println!("{}", counter);
}
Si intentamos compilar el código anterior, obtendremos el siguiente error:

Afortunadamente, si olvidas incluir la palabra clave mut, el compilador suele ser lo suficientemente inteligente como para señalar el error con claridad. El siguiente código inserta la palabra clave mut, permitiendo que el código compile:
pub fn main() {
let mut counter = 0;
counter = counter + 1;
println!("{}", counter);
}
Generics en Rust: la sintaxis < >
Consideremos una función que toma un valor con un tipo arbitrario y devuelve un struct con un campo foo que contiene ese valor. En lugar de escribir un montón de funciones para cada tipo posible, podemos usar un generic (tipo genérico).
El struct de ejemplo a continuación puede ser un i32 o un bool.
// derive the debug trait so we can print the struct to the console
#[derive(Debug)]
struct MyValues<T> {
foo: T,
}
pub fn main() {
let first_struct: MyValues<i32> = MyValues { foo: 1 }; // foo has type i32
let second_struct: MyValues<bool> = MyValues { foo: false }; // foo has type bool
println!("{:?}", first_struct);
println!("{:?}", second_struct);
}
He aquí por qué esto es útil: cuando guardamos valores “en almacenamiento” (storage) en Solana, queremos ser muy flexibles si vamos a almacenar un número, un string u otra cosa.
Si nuestro struct tuviera más de un campo, la sintaxis para parametrizar los tipos es la siguiente:
struct MyValues<T, U> {
foo: T,
bar: U,
}
Los generics son un tema muy amplio en Rust, por lo que de ninguna manera estamos dando un tratamiento completo aquí. Sin embargo, esto es suficiente para obtener una comprensión decente de la mayoría de los programas de Solana.
Options, Enums y Deref *
Para mostrar la importancia de los options y los enums, consideremos el siguiente ejemplo:
fn main() {
let v = Vec::from([1,2,3,4,5]);
assert!(v.iter().max() == 5);
}
El código falla al compilar con el siguiente error:
6 | assert!(v.iter().max() == 5);
| ^ expected `Option<&{integer}>`, found integer
La salida de max() no es un entero debido al caso especial (corner case) de que el vector v podría estar vacío.
El Option en Rust
Para manejar este caso especial, Rust devuelve un Option en su lugar. Un Option es un enum que puede contener el valor esperado, o un valor especial que indica que “no había nada ahí”.
Para convertir un Option en el tipo subyacente, usamos unwrap(). unwrap() causará un panic si recibimos “nada”, por lo que solo deberíamos usarlo en situaciones donde queremos que ocurra el panic o estamos seguros de que no obtendremos un valor vacío.
Para hacer que el código funcione como se espera, podemos hacer lo siguiente:
fn main() {
let v = Vec::from([1,2,3,4,5]);
assert!(v.iter().max().unwrap() == 5);
}
El operador deref *
¡Pero todavía no funciona! Esta vez obtenemos un error:
19 | assert!(v.iter().max().unwrap() == 5);
| ^^ no implementation for `&{integer} == {integer}`
El término en el lado izquierdo de la igualdad es una vista (es decir, &) de un entero y el término de la derecha es un entero real.
Para convertir una “vista” de un entero a un entero regular, necesitamos usar la operación “dereference” (desreferencia). Esto es cuando precedemos el valor con un operador *.
fn main() {
let v = Vec::from([1,2,3,4,5]);
assert!(*v.iter().max().unwrap() == 5);
}
Dado que los elementos del array son copy types, el operador deref copiará silenciosamente el 5 devuelto por max().unwrap().
Puedes pensar en * como una forma de “deshacer” un & sin alterar el valor original.
Usar el operador * en tipos non-copy es un tema complicado. Por ahora, todo lo que necesitas saber es que si recibes una vista (borrow) de un copy type y necesitas convertirlo al tipo “normal”, usa el operador *.
Result vs Option en Rust
Un option se usa cuando podríamos recibir algo “vacío”. Un Result (el mismo Result que los programas de Anchor han estado devolviendo) se usa cuando podríamos recibir un error.
Enum Result
El enum Result<T, E> en Rust se usa cuando la operación de una función puede tener éxito y devolver un valor de tipo T (un tipo genérico) o fallar y devolver un error de tipo E (tipo de error genérico). Está diseñado para manejar operaciones que pueden resultar ya sea en un resultado exitoso o en una condición de error.
enum Result<T, E> {
Ok(T),
Err(E),
}
En Rust, el operador ? se usa para el enum Result<T, E>, mientras que unwrap() se usa tanto para los enums Result<T, E> como para Option<T>.
El operador ?
El operador ? solo se puede usar en funciones que devuelven un Result, ya que es syntactic sugar (azúcar sintáctico) para devolver tanto un Err como un Ok.
El operador ? se usa para extraer datos del enum Result<T, E> y devolver la variante Ok(T) si la ejecución de la función es exitosa, o propagar (bubble up) un error Err(E) si hay un error. El método unwrap() funciona de la misma manera pero para los enums Result<T, E> y Option<T>, sin embargo, debe usarse con precaución debido a su potencial de hacer que el programa colapse si ocurre un error.
Ahora, considera el siguiente código a continuación:
pub fn encode_and_decode(_ctx: Context<Initialize>) -> Result<()> {
// Create a new instance of the `Person` struct
let init_person: Person = Person {
name: "Alice".to_string(),
age: 27,
};
// Encode the `init_person` struct into a byte vector
let encoded_data: Vec<u8> = init_person.try_to_vec().unwrap();
// Decode the encoded data back into a `Person` struct
let data: Person = decode(_ctx, encoded_data)?;
// Logs the decoded person's name and age
msg!("My name is {:?}, I am {:?} years old.", data.name, data.age);
Ok(())
}
pub fn decode(_accounts: Context<Initialize>, encoded_data: Vec<u8>) -> Result<Person> {
// Decode the encoded data back into a `Person` struct
let decoded_data: Person = Person::try_from_slice(&encoded_data).unwrap();
Ok(decoded_data)
}
El método try_to_vec() codifica un struct en un vector de bytes y devuelve un enum Result<T, E> donde T es el vector de bytes, mientras que el método unwrap() se usa para extraer el valor del vector de bytes de Ok(T). Esto hará que el programa colapse si el método falla al convertir el struct en un vector de bytes.
Aprende más con RareSkills
Este tutorial es parte de nuestro curso de Solana gratuito.
Publicado originalmente el 14 de febrero de 2024