Como moderador do Rust subreddit , regularmente encontro postagens sobre tentativas de desenvolvedores de transpor seus respectivos paradigmas de linguagem para o Rust, com resultados mistos e graus variados de sucesso.

Neste guia, vou descrever alguns dos problemas que os desenvolvedores encontram ao transpor outros paradigmas de linguagem para o Rust e propor algumas soluções alternativas para ajudá-lo a contornar as limitações do Rust.

Herança na ferrugem

Provavelmente, o recurso ausente mais questionado vindo das linguagens orientadas a objetos é a herança. Por que Rust não deixava uma estrutura herdar de outra?

Você certamente poderia argumentar que mesmo no mundo OO, a herança tem uma má reputação e os praticantes geralmente favorecem a composição, se puderem. Mas você também pode argumentar que permitir que um tipo execute um método de maneira diferente pode melhorar o desempenho e, portanto, é desejável para essas instâncias específicas.

Aqui está um exemplo clássico tirado de Java:

 interface Animal { void tell (); void pet (); void feed (comida alimentar);
} class Cat { public void tell () {System.out.println ("Meow"); } public void pet () {System.out.println ("purr"); } public void feed (Food food) {System.out.println ("lick"); }
} //esta implementação é provavelmente muito otimista...
classe Lion extends Cat { public void tell () {System.out.println ("Roar"); }
}

A primeira parte pode ser implementada com características:

 traço Animal { fn tell (& self); fn pet (& mut self); fn feed (& mut self, food: Food);
} struct Cat; impl Animal for Cat { fn tell (& self) {println! ("Meow"); } fn pet (& mut self) {println! ("purr"); fn feed (& mut self, food: Food) {println! ("lick"); }
}

Mas a segunda parte não é tão fácil:

 struct Lion; impl Animal for Lion { fn tell (& self) {println! ("Roar"); } //Erro: métodos de animal de estimação e feed ausentes
}

A maneira mais simples é, obviamente, duplicar os métodos. Sim, a duplicação é ruim. A complexidade também. Crie um método gratuito e chame-o do impl Cat e Lion se precisar desduplicar o código.

Mas espere, você pode dizer, e quanto à parte do polimorfismo da equação? É aí que fica complicado. Enquanto as linguagens OO geralmente fornecem despacho dinâmico, o Rust faz com que você escolha entre o despacho estático e dinâmico, e ambos têm seus custos e benefícios.

//envio estático
deixe cat=Cat;
cat.tell (); deixe leão=Leão;
lion.tell (); //despacho dinâmico via enum
enum AnyAnimal { Gato Gato), Leão (Leão),
} //`impl Animal for AnyAnimal` deixado como um exercício para o leitor deixe animais=[QualquerAnimal:: Gato (gato), QualquerAnimal:: Leão (Leão)];
para animal em animals.iter () { animal.tell ();
} //despacho dinâmico via ponteiro"fat"incluindo vtable
deixe animais=[& gato como & animal din, & leão como & animal din];
para animal em animals.iter () { animal.tell ();
}

Observe que, ao contrário das linguagens com coleta de lixo, cada variável deve ter um único tipo concreto no momento da compilação. Além disso, para o caso enum, delegar a implementação do traço é entediante, mas caixas como embaixador pode ajudar.

Finalmente, é possível implementar uma característica para todas as classes que implementam uma de várias outras características, mas requer especialização, que é um recurso noturno por enquanto (embora haja um solução alternativa disponível, mesmo compactado em um caixa de macro se você não quiser escrever todos os clichês necessários). As características podem muito bem herdar umas das outras, embora prescrevam apenas comportamento, não dados.

Listas vinculadas e outras estruturas de dados baseadas em ponteiros

Muitas pessoas que vêm de C ++ para Rust vão primeiro querer implementar uma lista duplamente vinculada”simples”, mas aprender rapidamente que, na verdade, está longe de ser simples. Isso porque Rust quer deixar claro a propriedade e, portanto, as listas duplamente vinculadas exigem um tratamento bastante complexo de indicadores em comparação com referências.

Um recém-chegado pode tentar escrever a seguinte estrutura:

 struct MyLinkedList  { valor: T previous_node: Option  >>, next_node: Opção  >>,
}

Bem, eles adicionarão a Option e a Box quando observarem que, de outra forma, isso falhará. Mas, uma vez que eles tentam implementar a inserção, eles têm uma surpresa desagradável:

 impl  MyLinkedList  { fn insert (& mut self, value: T) { deixe next_node=self.next_node.take (); self.next_node=Some (Box:: new (MyLinkedList { valor, previous_node: Some (Box:: new (* self)),//Ouch next_node, })); }
}

Claro, o verificador de empréstimo não vai permitir isso. A propriedade de valores está completamente confusa. Box possui os dados que contém e, portanto, cada nó na lista seria propriedade do nó anterior e seguinte na lista. Rust só permite um proprietário por dados, então isso exigirá pelo menos um Rc ou Arc para funcionar. Mas mesmo isso se torna complicado rapidamente, sem mencionar a sobrecarga das contagens de referência.

Felizmente, você não precisa escrever uma lista duplamente vinculada porque a biblioteca padrão já contém uma ( std:: Collections:: LinkedList ). Além disso, é muito raro que isso forneça um bom desempenho em comparação com Vec s simples, então você pode querer medir de acordo.

Se você realmente deseja escrever uma lista duplamente vinculada, pode consultar “ Aprendendo a ferrugem com muitas listas vinculadas , ”que pode ajudá-lo a escrever listas vinculadas e aprender muito sobre Rust inseguro no processo.

(À parte: listas com links simples são absolutamente aceitáveis ​​para construir a partir de uma cadeia de caixas. Na verdade, o compilador Rust contém um implementação .)

O mesmo se aplica principalmente a estruturas de gráficos, embora você provavelmente precise de uma dependência para lidar com estruturas de dados de gráficos. petgraph é o mais popular no momento, fornecendo a estrutura de dados e uma série de algoritmos de gráfico.

Tipos de autorreferência

Quando confrontado com o conceito de tipos de autorreferência, é justo perguntar:”Quem é o proprietário disso?”Novamente, esta é uma ruga na história de propriedade com a qual o verificador de empréstimo geralmente não fica satisfeito.

Você encontrará esse problema quando tiver uma relação de propriedade e quiser armazenar o objeto de propriedade e o de propriedade em uma estrutura. Experimente ingenuamente e você terá problemas tentando fazer com que as vidas funcionem.

Podemos apenas imaginar que muitos Rustáceos adotaram um código inseguro, o que é sutil e muito fácil de errar. É claro que usar um ponteiro simples em vez de uma referência removerá as preocupações de toda a vida, pois os ponteiros não duram toda a vida. No entanto, isso está assumindo a responsabilidade de gerenciar o tempo de vida manualmente.

Felizmente, existem alguns engradados que pegam a solução e apresentam uma interface segura, como o rental e caixas once_self_cell .

Estado mutável global

Pessoas vindas de C e/ou C ++-ou, com menos frequência, de linguagens dinâmicas-às vezes estão acostumadas a criar e modificar o estado global em todo o código. Para o exemplo , um Redditor declarou que “É completamente seguro, mas Rust não permite que você faça isso. ”

Aqui está um exemplo ligeiramente simplificado:

 #include 
int i=1; int main () { std:: cout <

Em Rust, isso se traduziria aproximadamente em:

 estático I: u32=1; fn main () { imprimir! ("{}", I); i=2;//<-Erro: não é possível alterar o estado global imprimir! ("{}", I);
}

Muitos Rustáceos dirão que você simplesmente não precisa que esse estado seja global. Claro, em um exemplo tão simples, isso é verdade. Mas, para um bom número de casos de uso, você realmente precisa do estado mutável global-por exemplo, em alguns aplicativos incorporados.

Existe, é claro, uma maneira de fazer isso, usando inseguro . Mas antes de chegar a isso, dependendo do seu caso de uso, você pode querer apenas usar um Mutex para ter certeza. Ou, se a mutação for necessária apenas uma vez para a inicialização, um OnceCell ou lazy_static resolverá o problema perfeitamente.

Dito isso, especialmente no mundo embarcado, onde cada contagem de bytes e recursos são frequentemente mapeados na memória, ter uma estática mutável costuma ser a solução preferida. Então, se você realmente precisa fazer isso, ficaria assim:

 mut estático DATA_RACE_COUNTER: u32=1; fn main () { imprimir! ("{}", DATA_RACE_COUNTER); //Eu juro solenemente que não estou fazendo nada de bom, e também de thread único. inseguro { DATA_RACE_COUNTER=2; } imprimir! ("{}", DATA_RACE_COUNTER);
}

Novamente, você não deve fazer isso a menos que seja realmente necessário. E se você precisar perguntar se é uma boa ideia, a resposta é não.

‘Apenas’ inicializando uma matriz

Um neófito pode ser tentado a declarar uma matriz da seguinte maneira:

 let array: [usize; 512]; para i em 0..512 { matriz [i]=i;
}

Isso falha porque a matriz nunca foi inicializada. Em seguida, tentamos atribuir valores a ele, mas sem dizer ao compilador, ele nem mesmo reserva um lugar para escrevermos na pilha. A ferrugem é exigente assim; ele distingue o array de seu conteúdo. Além disso, requer que ambos sejam inicializados antes de podermos lê-los.

Inicializando let array=[0usize; 512]; , resolvemos este problema ao custo de uma inicialização dupla, que pode ou não ser otimizada-ou, dependendo do tipo, pode até ser impossível. Consulte “ Unsafe Rust: Como e quando (não) usá-lo ”para uma solução.

A postagem Coisas você não pode fazer no Rust (e o que fazer no lugar) apareceu primeiro no LogRocket Blog .

Source link