Una mirada objectiva a Rust

Aquest article forma part de TechXchange: Programació Rusty

El que aprendràs

  • Com suporta Rust els objectes.
  • Quins són els trets de Rust?
  • Reptes de la interfície multiidioma.

C++ i Java segueixen un enfocament tradicional de programació orientada a objectes (OOP) que utilitza una estructura de classes jeràrquica amb herència per als objectes. Tots dos admeten classes abstractes per proporcionar definicions d’interfície. No aprofundirem en aquests detalls ja que es tornen força complexos; en canvi, examinarem com Rust ofereix suport de programació modular, ja que no segueix l’arquitectura OOP.

Rust no té classes, més aviat té trets. Consulteu Reading Rust per a programadors de C si no heu escanejat cap codi Rust.

Per mostrar els conceptes bàsics, implementem una pila senzilla amb un nombre limitat de funcions, com ara push, pop i top (of stack) mitjançant elements de mida fixa. Aquesta no és una implementació pràctica, ja que el tipus Rust estàndard Vector (Vec) fa tot això i més, com ara gestionar qualsevol tipus d’element.

Així mateix, aquesta presentació està dissenyada per destacar l’ús bàsic dels trets versus la POO basada en classes; per tant, no utilitzeu necessàriament cap d’aquests exemples en una aplicació. A més, existeixen trets en altres llenguatges de programació, inclosos C++ i Java.

Implementació d’una pila bàsica

Per començar, un tret defineix essencialment una interfície similar a una classe abstracta en altres idiomes. La diferència principal és que el tret només defineix els mètodes utilitzats i no l’estructura que normalment fa referència la paraula clau jo mateix. En aquest cas, definim un tret d’interfície Stack molt bàsic per als elements d’ús.

trait Stack {
    fn push(&mut self, value:usize);
    fn pop(&mut self);
    fn top(self) -> usize;
}

A continuació, definim una estructura particular per a la nostra pila que contindrà 10 elements. També incloem una implementació per defecte per inicialitzar la nostra estructura Stack10. Una vegada més, no és així com algú voldria implementar fins i tot una pila senzilla, perquè utilitza un nombre fix d’elements i el tipus d’element és use. Rust és expert en donar suport a definicions orientades a plantilles i genèriques, però això és per a un altre article.

La definició que fem servir assumeix les matrius d’una dimensió de base zero de Rust. No es fa cap comprovació de límits explícits, tot i que les comprovacions implícites en temps d’execució detectarien coses com ara errors de desbordament.

#[derive(Copy,Clone)]
struct Stack10 {
  values: [usize; 10],
  index: usize,
}

impl Default for Stack10 {
    fn default() -> Self {
        Stack10 {
            values: [0_usize; 10],
            index: 0,
        }  
    }
}

#[derive(Copy,Clone)] defineix els trets de còpia i clonació per a Stack10 que s’utilitzen implícitament més endavant al programa. El tret per defecte s’utilitza per inicialitzar la nostra estructura. Hi ha altres maneres de fer-ho, però semblava l’enfocament més senzill. La matriu de valors s’inicialitza amb zeros i el valor de l’índex també es posa a zero. També és el nombre d’elements de la pila.

A part, la inicialització de valors es fa amb un valor de 0_usize. És la manera de Rust d’escriure un valor literal escrit. En aquest cas, podríem haver escrit només 0 com el tipus està implícit. Tanmateix, Rust s’inclina cap a l’especificació explícita per fer que el compilador conegui la intenció del programador.

Ara que tenim els nostres trets i estructura definits, podem definir la implementació de les funcions/mètodes per a Stack10. De nou, això és explícit en lloc d’un enfocament més típicament genèric per definir la implementació. Push només desa el valor del paràmetre i actualitza l’índex mentre que el pop simplement disminueix l’índex. El mètode superior retorna la part superior actual de la pila.

impl Stack for Stack10 {
    fn push(&mut self, value:usize) {
        self.values[self.index] = value;
        self.index = self.index + 1;
    }
    fn pop(&mut self) {
        self.index = self.index - 1;
    }
    fn top(self) -> usize {
        return self.values[self.index-1];
    }
}

Ara una aplicació de mostra que utilitza l’estructura i les definicions. La funció principal comença inicialitzant la variable Stack10 anomenada x. A continuació, fa algunes pressions i un pop seguit de la impressió de l’estat actual. Tingueu en compte el x.pop al principi que s’ha comentat. Generaria un error d’execució perquè el mètode pop intentaria disminuir l’índex de zero a -1. Com que l’índex no està signat, aquest desbordament insuficient provoca un error d’execució.

fn main() {
    let mut x:Stack10 = Default::default();
    // x.pop(); // will generate a runtime error
    x.push(10);
    x.push(11);
    x.push(12);
    x.pop();
    println!("top is {}",x.top());
    println!("index is {}",x.index);
    println!("stack is {:?}",x.values);
}

Aquest és el resultat imprès del nostre programa senzill:

top is 11
index is 2
stack is [10, 11, 12, 0, 0, 0, 0, 0, 0, 0]

Podem gestionar un tipus de canvi al nostre sistema amb la nostra definició de trets actual, però no un altre. El primer seria canviar els elements numèrics, ja que el tret Stack no hi fa referència explícitament. Tot i que podríem definir fàcilment un Stack20 o Stack100 per a piles de diferents mides, el tipus d’element continua sent ús. Aquesta és la part que no podem canviar només amb la implementació. Les plantilles genèriques de Rust poden abordar aquesta situació. La definició seria així:

trait Stack {
    fn push(&mut self, value:T);
    fn pop(&mut self);
    fn top(self) -> T;
}

La resta del codi ha de canviar per abordar aquest canvi i és possible utilitzar paràmetres constants per especificar la mida de la matriu.

Polimorfisme rovellat

Fins ara, hem estat tractant amb definicions estàtiques. El compilador pot esbrinar quina funció subjacent s’ha de cridar. Rust admet l’enviament dinàmic semblant als mètodes de classe virtual en altres llenguatges OOP, però com funciona així com la sintaxi és una mica diferent.

Per a aquesta part, proposem un nou tret per a un animal i generem una col·lecció d’animals amb diferents atributs. Aquí, només tenim diferents implementacions per a nom funció que imprimeix el nom de l’animal. Òbviament, el conjunt de funcions del tret i les estructures utilitzades poden ser més complexos que el nostre exemple, però la idea és la mateixa.

trait Animal {
    fn name (&self);
}

struct Rabbit;
impl Animal for Rabbit {
    fn name(&self) {
        println!("Rabbit");
    }
}

struct Cat;
impl Animal for Cat {
    fn name(&self) {
        println!("Cat");
    }
}

struct Dog;
impl Animal for Dog {
    fn name(&self) {
        println!("Dog");
    }
}

fn main() {
  let mut animals:Vec>= Vec::new();
  animals.push(Box::new(Dog));
  animals.push(Box::new(Rabbit));
  animals.push(Box::new(Cat));
  
  for animal in animals.iter() {
      animal.name();
  }
}

Aquest exemple també va utilitzar el integrat Vector tipus, que implementa una matriu d’elements expandible dinàmicament. El tipus inclou funcions com push i pop, però el tipus és molt més complex que el nostre exemple anterior. També aprofitem el Caixa tret per empaquetar estructures que s’empenyen al vector.

La línia que defineix animals incloure Caixa i la dinàmic paraula clau. Els elements reals de la matriu són un parell de punters (veure figura). Un apunta a l’estructura dels elements reals, mentre que l’altre apunta a una taula de salt que coincideix amb el tret. Rust sap utilitzar la taula de salt per trobar la funció a la línia que imprimeix el nom de l’animal, nom.animal().

Per descomptat, en el nostre exemple, no hi ha cap estructura i la taula de salt només té una funció. Tanmateix, en aplicacions més avançades, les estructures poden variar de mida i la taula probablement farà referència a més d’una funció.

Llenguatges com C++ utilitzen taules de salt per a objectes associats a classes que tenen mètodes virtuals, però les estructures d’objectes posen el punter de la taula de salt a l’inici de l’objecte. Això té l’avantatge de necessitar només un punt. El desavantatge, però, és que l’estructura de dades ara està prefixada amb un punter. Això no és un gran problema si l’objecte és un element autònom, però si està incrustat en una altra estructura, pot ser que no sigui tan desitjable.

Barrejant Rust amb C++

Tot i que és possible connectar Rust amb C i C++, normalment es fa utilitzant els denominadors comuns més baixos: funcions autònomes, estructures bàsiques i valors de dades bàsics com els nombres enters. És possible replicar l’estructura polimòrfica de Rust en C i viceversa, però això pot ser una mica ardu. El mateix passa amb les funcions i el suport OOP de C++.

Les coses es tornen més difícils si Rust es barreja amb codi escrit en altres llenguatges de programació quan es tracta de la gestió de la memòria de Rust i altres comprovacions, que són molt importants per als programadors de Rust. C gairebé no té cap verificació, mentre que idiomes com Rust, C++ i Ada en tenen una quantitat considerable. Aquesta comprovació està dissenyada per facilitar-ho als programadors i ajudar a reduir els errors fent que el compilador comprovi l’aplicació.

Si voleu jugar amb Rust sense instal·lar-lo a la vostra màquina, només necessiteu un navegador per accedir a https://play.rust-lang.org/. Podeu enganxar els exemples aquí o explorar Rust. L’eina és limitada, però si esteu experimentant amb Rust. probablement no teniu un parell de megabytes de codi font.

Un altre recurs que pot ser útil per als programadors de C++ és la pàgina C++ vs Rust de MaulingMonkey. Inclou una sèrie d’enllaços a recursos relacionats amb Rust. Ve amb un avís útil:

“ADVERTÈNCIA: a continuació es poden trobar explicacions terribles (sic) de Rust, fetes per algú que realment no ho sap”.

Probablement podria afegir aquesta advertència aquí, ja que fins ara no he fet moltes aplicacions a Rust.

Llegeix més articles a TechXchange: Programació Rusty

Leave a Comment

Your email address will not be published. Required fields are marked *