Miembros

Constructores

Rust no tiene ninguna noción de constructores. En su lugar, simplemente escribes funciones factory que retornan una instancia del tipo. Las funciones Factory pueden ser independientes o funciones asociadas al tipo. En términos de C# las funciones asociadas son como tener metodos estaticos en un tipo. Por convención, si hay solo una función factory para una estructura, se le llama new:

struct Rectangle {
    x1: i32, y1: i32,
    x2: i32, y2: i32,
}

impl Rectangle {
    pub fn new(x1: i32, y1: i32, x2: i32, y2: i32) -> Self {
        Self { x1, y1, x2, y2 }
    }
}

Dado que las funciones en Rust (ya sean asociadas u otras) no admiten sobrecarga; las funciones factory tienen que tener nombres únicos. Por ejemplo, a continuación se presentan algunos ejemplos de las funciones constructores o factory disponibles en String.

  • String::new: crea un string vació.
  • String::with_capacity: crea un string con una capacidad de buffer inicial.
  • String::from_utf8: crea un string desde bytes de texto codificado en UTF-8.
  • String::from_utf16: crea un string desde bytes de texto codificado en UTF-16.

En el caso de un tipo enum en Rust, las variantes actúan como constructores. Mira [la sección de tipos Enumerados][ennums] para ver más.

Mira también:

Métodos (estáticos y basados en instancias)

Al igual que en C#, los tipos de Rust (tanto enum como struct) pueden tener métodos estáticos y basados en instancias. En la terminología de Rust, un método siempre es basado en instancia y se identifica por el hecho de que su primer parametro se llama self. El parametro self no tiene una anotación de tipo, ya que siempre es el tipo al que pertenece el método. Un método estático se llama función asociada. En el ejemplo de a continuación, new es una función asociada y el resto (length, width, y area) son métodos de el tipo.

struct Rectangle {
    x1: i32, y1: i32,
    x2: i32, y2: i32,
}

impl Rectangle {
    pub fn new(x1: i32, y1: i32, x2: i32, y2: i32) -> Self {
        Self { x1, y1, x2, y2 }
    }

    pub fn length(&self) -> i32 {
        self.y2 - self.y1
    }

    pub fn width(&self)  -> i32 {
        self.x2 - self.x1
    }

    pub fn area(&self)  -> i32 {
        self.length() * self.width()
    }
}

Constantes

Al igual que en C#, un tipo en Rust puede tener constantes. Sin embargo, el aspecto más interesante de notar es que Rust permite que una instancia de tipo se defina como una constante.

struct Point {
    x: i32,
    y: i32,
}

impl Point {
    const ZERO: Point = Point { x: 0, y: 0 };
}

En C#, lo mismo requeriría un campo de solo lectura estático.

readonly record struct Point(int X, int Y)
{
    public static readonly Point Zero = new(0, 0);
}

Eventos

Rust no tiene soporte incorporado para que los miembros de tipo anuncien y disparen eventos, como lo tiene C# con la palabra clave event.

Propiedades

En C#, los campos de un tipo suelen ser privados. Luego, se protegen/encapsulan mediante miembros de propiedades miembro como métodos de acceso (get y set) para leer o escribir, para validar el valor al establecerlo o calcular un valor al leerlo. Rust solo tiene métodos donde un getter tiene el mismo nombre que el campo (En Rust, los nombres de los métodos pueden compartir el mismo identificador que un campo) y el setter utiliza un prefijo set_

A continuación, se muestra un ejemplo que muestra cómo suelen lucir los métodos de acceso similares a propiedades para un tipo en Rust:

struct Rectangle {
    x1: i32, y1: i32,
    x2: i32, y2: i32,
}

impl Rectangle {
    pub fn new(x1: i32, y1: i32, x2: i32, y2: i32) -> Self {
        Self { x1, y1, x2, y2 }
    }

    // como getters de propiedades (cada uno comparte el mismo nombre que el campo)

    pub fn x1(&self) -> i32 { self.x1 }
    pub fn y1(&self) -> i32 { self.y1 }
    pub fn x2(&self) -> i32 { self.x2 }
    pub fn y2(&self) -> i32 { self.y2 }

    // como setters de propiedades

    pub fn set_x1(&mut self, val: i32) { self.x1 = val }
    pub fn set_y1(&mut self, val: i32) { self.y1 = val }
    pub fn set_x2(&mut self, val: i32) { self.x2 = val }
    pub fn set_y2(&mut self, val: i32) { self.y2 = val }

    // como propiedades calculadas

    pub fn length(&self) -> i32 {
        self.y2 - self.y1
    }

    pub fn width(&self)  -> i32 {
        self.x2 - self.x1
    }

    pub fn area(&self)  -> i32 {
        self.length() * self.width()
    }
}

Métodos de Extensión

Los métodos de extensión en C# permiten al desarrollador adjuntar nuevos métodos vinculados estáticamente a tipos existentes, sin necesidad de modificar la definición original del tipo. En el siguiente ejemplo de C#, se añade un nuevo método Wrap a la clase StringBuilder mediante una extensión:

using System;
using System.Text;
using Extensions; // (1)

var sb = new StringBuilder("Hello, World!");
sb.Wrap(">>> ", " <<<"); // (2)
Console.WriteLine(sb.ToString()); // Muestra: >>> Hello, World! <<<

namespace Extensions
{
    static class StringBuilderExtensions
    {
        public static void Wrap(this StringBuilder sb,
                                string left, string right) =>
            sb.Insert(0, left).Append(right);
    }
}

Ten en cuenta que para que un método de extensión esté disponible (2), se debe importar el namespace con el tipo que contiene el método de extensión (1). Rust ofrece una facilidad muy similar a través de traits, llamada extension traits. El siguiente ejemplo en Rust es equivalente al ejemplo de C# anterior; extiende String con el método wrap:

#![allow(dead_code)]

mod exts {
    pub trait StrWrapExt {
        fn wrap(&mut self, left: &str, right: &str);
    }

    impl StrWrapExt for String {
        fn wrap(&mut self, left: &str, right: &str) {
            self.insert_str(0, left);
            self.push_str(right);
        }
    }
}

fn main() {
    use exts::StrWrapExt as _; // (1)

    let mut s = String::from("Hello, World!");
    s.wrap(">>> ", " <<<"); // (2)
    println!("{s}"); // Prints: >>> Hello, World! <<<
}

Al igual que en C#, para que el método en el trait de extensión esté disponible (2), el trait de extensión debe importarse (1). También ten en cuenta que el identificador del trait de extensión StrWrapExt puede descartarse mediante _ en el momento de la importación sin afectar la disponibilidad de wrap para String.

Modificadores de Visibilidad/Acceso

C# tiene varios modificadores de accesibilidad o visibilidad:

  • private
  • protected
  • internal
  • protected internal (familia)
  • public

En Rust, una compilación se compone de un árbol de módulos en el que los módulos contienen y definen elementos como tipos, traits, enums, constantes y funciones. Casi todo es privado por defecto. Una excepción es, por ejemplo, elementos asociados en un trait público, que son públicos por defecto. Esto es similar a cómo los miembros de una interfaz de C# declarados sin ningún modificador público en el código fuente son públicos por defecto. Rust solo tiene el modificador pub para cambiar la visibilidad con respecto al árbol de módulos. Hay variaciones de pub que cambian el alcance de la visibilidad pública:

  • pub(self)
  • pub(super)
  • pub(crate)
  • pub(in PATH)

Para obtener más detalles, consulta la sección Visibility and Privacy en la referencia de Rust.

La tabla a continuación es una aproximación de la correspondencia entre los modificadores de C# y Rust:

C#RustNote
private(default)Mirar nota 1.
protectedN/AMirar nota 2.
internalpub(crate)
protected internal (familia)N/AMirar nota 2.
publicpub
  1. No existe una palabra clave para denotar visibilidad privada; es la configuración predeterminada en Rust.

  2. Dado que no hay jerarquías de tipos basadas en clases en Rust, no hay un equivalente de protected.

Mutabilidad

Al diseñar un tipo en C#, es responsabilidad del desarrollador decidir si un tipo es mutable o inmutable; si admite mutaciones destructivas o no destructivas. C# admite un diseño inmutable para tipos con una positional record declaration (record class o readonly record struct). En Rust, la mutabilidad se expresa en los métodos a través del tipo del parámetro self, como se muestra en el siguiente ejemplo:

struct Point { x: i32, y: i32 }

impl Point {
    pub fn new(x: i32, y: i32) -> Self {
        Self { x, y }
    }

    // self no es mutable

    pub fn x(&self) -> i32 { self.x }
    pub fn y(&self) -> i32 { self.y }

    // self es mutable

    pub fn set_x(&mut self, val: i32) { self.x = val }
    pub fn set_y(&mut self, val: i32) { self.y = val }
}

En C#, puedes realizar mutaciones no destructivas usando with:

var pt = new Point(123, 456);
pt = pt with { X = 789 };
Console.WriteLine(pt.ToString()); // Muestra: Point { X = 789, Y = 456 }

readonly record struct Point(int X, int Y);

No hay with en Rust, pero para emular algo similar en Rust, debe estar integrado en el diseño del tipo:

struct Point { x: i32, y: i32 }

impl Point {
    pub fn new(x: i32, y: i32) -> Self {
        Self { x, y }
    }

    pub fn x(&self) -> i32 { self.x }
    pub fn y(&self) -> i32 { self.y }

    // los siguientes métodos consumen self y devuelven una nueva instancia:

    pub fn set_x(self, val: i32) -> Self { Self::new(val, self.y) }
    pub fn set_y(self, val: i32) -> Self { Self::new(self.x, val) }
}