Definindo Funções Rust no LabEx

Beginner

This tutorial is from open-source community. Access the source code

Introdução

Bem-vindo(a) a Functions (Funções). Este laboratório faz parte do Rust Book. Você pode praticar suas habilidades em Rust no LabEx.

Neste laboratório, você aprenderá como definir e chamar funções em Rust usando a palavra-chave fn e a convenção de nomenclatura snake case.

Funções

As funções são predominantes no código Rust. Você já viu uma das funções mais importantes na linguagem: a função main, que é o ponto de entrada de muitos programas. Você também já viu a palavra-chave fn, que permite declarar novas funções.

Crie um novo projeto chamado functions:

cargo new functions
cd functions

O código Rust usa snake case como o estilo convencional para nomes de funções e variáveis, no qual todas as letras são minúsculas e sublinhados separam as palavras. Aqui está um programa que contém um exemplo de definição de função:

Nome do arquivo: src/main.rs

fn main() {
    println!("Hello, world!");

    another_function();
}

fn another_function() {
    println!("Another function.");
}

Definimos uma função em Rust inserindo fn seguido por um nome de função e um conjunto de parênteses. As chaves dizem ao compilador onde o corpo da função começa e termina.

Podemos chamar qualquer função que definimos inserindo seu nome seguido por um conjunto de parênteses. Como another_function é definida no programa, ela pode ser chamada de dentro da função main. Observe que definimos another_function depois da função main no código-fonte; poderíamos tê-la definido antes também. Rust não se importa onde você define suas funções, apenas que elas sejam definidas em algum lugar em um escopo que possa ser visto pelo chamador.

Vamos iniciar um novo projeto binário chamado functions para explorar as funções mais a fundo. Coloque o exemplo another_function em src/main.rs e execute-o. Você deve ver a seguinte saída:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.28s
     Running `target/debug/functions`
Hello, world!
Another function.

As linhas são executadas na ordem em que aparecem na função main. Primeiro, a mensagem "Hello, world!" é impressa, e então another_function é chamada e sua mensagem é impressa.

Parâmetros

Podemos definir funções para ter parâmetros, que são variáveis especiais que fazem parte da assinatura de uma função. Quando uma função tem parâmetros, você pode fornecer a ela valores concretos para esses parâmetros. Tecnicamente, os valores concretos são chamados de argumentos, mas em conversas informais, as pessoas tendem a usar as palavras parâmetro e argumento de forma intercambiável, tanto para as variáveis na definição de uma função quanto para os valores concretos passados ​​quando você chama uma função.

Nesta versão de another_function, adicionamos um parâmetro:

Nome do arquivo: src/main.rs

fn main() {
    another_function(5);
}

fn another_function(x: i32) {
    println!("The value of x is: {x}");
}

Tente executar este programa; você deve obter a seguinte saída:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 1.21s
     Running `target/debug/functions`
The value of x is: 5

A declaração de another_function tem um parâmetro chamado x. O tipo de x é especificado como i32. Quando passamos 5 para another_function, a macro println! coloca 5 onde o par de chaves contendo x estava na string de formatação.

Nas assinaturas de funções, você deve declarar o tipo de cada parâmetro. Esta é uma decisão deliberada no design do Rust: exigir anotações de tipo nas definições de funções significa que o compilador quase nunca precisa que você as use em outro lugar no código para descobrir qual tipo você quer dizer. O compilador também é capaz de fornecer mensagens de erro mais úteis se souber quais tipos a função espera.

Ao definir vários parâmetros, separe as declarações de parâmetro com vírgulas, assim:

Nome do arquivo: src/main.rs

fn main() {
    print_labeled_measurement(5, 'h');
}

fn print_labeled_measurement(value: i32, unit_label: char) {
    println!("The measurement is: {value}{unit_label}");
}

Este exemplo cria uma função chamada print_labeled_measurement com dois parâmetros. O primeiro parâmetro é chamado value e é um i32. O segundo é chamado unit_label e é do tipo char. A função então imprime texto contendo tanto o value quanto o unit_label.

Vamos tentar executar este código. Substitua o programa atualmente no arquivo src/main.rs do seu projeto functions pelo exemplo anterior e execute-o usando cargo run:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/functions`
The measurement is: 5h

Como chamamos a função com 5 como o valor para value e 'h' como o valor para unit_label, a saída do programa contém esses valores.

Declarações e Expressões

Os corpos das funções são compostos por uma série de declarações, opcionalmente terminando em uma expressão. Até agora, as funções que cobrimos não incluíram uma expressão final, mas você viu uma expressão como parte de uma declaração. Como Rust é uma linguagem baseada em expressões, esta é uma distinção importante de entender. Outras linguagens não têm as mesmas distinções, então vamos ver o que são declarações e expressões e como suas diferenças afetam os corpos das funções.

  • Declarações: são instruções que executam alguma ação e não retornam um valor.
  • Expressões: avaliam para um valor resultante. Vamos ver alguns exemplos.

Na verdade, já usamos declarações e expressões. Criar uma variável e atribuir um valor a ela com a palavra-chave let é uma declaração. Na Listagem 3-1, let y = 6; é uma declaração.

Nome do arquivo: src/main.rs

fn main() {
    let y = 6;
}

Listagem 3-1: Uma declaração de função main contendo uma declaração

Definições de funções também são declarações; todo o exemplo anterior é uma declaração em si.

Declarações não retornam valores. Portanto, você não pode atribuir uma declaração let a outra variável, como o código a seguir tenta fazer; você receberá um erro:

Nome do arquivo: src/main.rs

fn main() {
    let x = (let y = 6);
}

Quando você executa este programa, o erro que você receberá se parece com isto:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
error: expected expression, found statement (`let`)
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^^^^^^^
  |
  = note: variable declaration using `let` is a statement

error[E0658]: `let` expressions in this position are unstable
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^^^^^^^
  |
  = note: see issue #53667 <https://github.com/rust-lang/rust/issues/53667> for
more information

A declaração let y = 6 não retorna um valor, então não há nada para x associar. Isso é diferente do que acontece em outras linguagens, como C e Ruby, onde a atribuição retorna o valor da atribuição. Nessas linguagens, você pode escrever x = y = 6 e fazer com que tanto x quanto y tenham o valor 6; esse não é o caso em Rust.

Expressões avaliam para um valor e compõem a maior parte do restante do código que você escreverá em Rust. Considere uma operação matemática, como 5 + 6, que é uma expressão que avalia para o valor 11. Expressões podem fazer parte de declarações: na Listagem 3-1, o 6 na declaração let y = 6; é uma expressão que avalia para o valor 6. Chamar uma função é uma expressão. Chamar uma macro é uma expressão. Um novo bloco de escopo criado com chaves é uma expressão, por exemplo:

Nome do arquivo: src/main.rs

fn main() {
  1 let y = {2
        let x = 3;
      3 x + 1
    };

    println!("The value of y is: {y}");
}

A expressão [2] é um bloco que, neste caso, avalia para 4. Esse valor é associado a y como parte da declaração let [1]. Observe a linha sem ponto e vírgula no final [3], que é diferente da maioria das linhas que você viu até agora. Expressões não incluem pontos e vírgulas finais. Se você adicionar um ponto e vírgula ao final de uma expressão, você a transforma em uma declaração, e ela não retornará um valor. Tenha isso em mente ao explorar os valores de retorno de funções e expressões a seguir.

Funções com Valores de Retorno

As funções podem retornar valores para o código que as chama. Não nomeamos valores de retorno, mas devemos declarar seu tipo após uma seta (->). Em Rust, o valor de retorno da função é sinônimo do valor da expressão final no bloco do corpo de uma função. Você pode retornar antecipadamente de uma função usando a palavra-chave return e especificando um valor, mas a maioria das funções retorna a última expressão implicitamente. Aqui está um exemplo de uma função que retorna um valor:

Nome do arquivo: src/main.rs

fn five() -> i32 {
    5
}

fn main() {
    let x = five();

    println!("The value of x is: {x}");
}

Não há chamadas de função, macros ou mesmo declarações let na função five --- apenas o número 5 por si só. Essa é uma função perfeitamente válida em Rust. Observe que o tipo de retorno da função também é especificado, como -> i32. Tente executar este código; a saída deve ser semelhante a esta:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/functions`
The value of x is: 5

O 5 em five é o valor de retorno da função, e é por isso que o tipo de retorno é i32. Vamos examinar isso com mais detalhes. Existem duas partes importantes: primeiro, a linha let x = five(); mostra que estamos usando o valor de retorno de uma função para inicializar uma variável. Como a função five retorna um 5, essa linha é a mesma que a seguinte:

let x = 5;

Segundo, a função five não tem parâmetros e define o tipo do valor de retorno, mas o corpo da função é um solitário 5 sem ponto e vírgula porque é uma expressão cujo valor queremos retornar.

Vamos ver outro exemplo:

Nome do arquivo: src/main.rs

fn main() {
    let x = plus_one(5);

    println!("The value of x is: {x}");
}

fn plus_one(x: i32) -> i32 {
    x + 1
}

Executar este código imprimirá The value of x is: 6. Mas se colocarmos um ponto e vírgula no final da linha contendo x + 1, mudando-o de uma expressão para uma declaração, receberemos um erro:

Nome do arquivo: src/main.rs

fn main() {
    let x = plus_one(5);

    println!("The value of x is: {x}");
}

fn plus_one(x: i32) -> i32 {
    x + 1;
}

Compilar este código produz um erro, como segue:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
error[E0308]: mismatched types
 --> src/main.rs:7:24
  |
7 | fn plus_one(x: i32) -> i32 {
  |    --------            ^^^ expected `i32`, found `()`
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
8 |     x + 1;
  |          - help: remove this semicolon

A principal mensagem de erro, mismatched types (tipos incompatíveis), revela o problema central com este código. A definição da função plus_one diz que ela retornará um i32, mas as declarações não avaliam para um valor, o que é expresso por (), o tipo unitário. Portanto, nada é retornado, o que contradiz a definição da função e resulta em um erro. Nesta saída, Rust fornece uma mensagem para possivelmente ajudar a corrigir este problema: ele sugere remover o ponto e vírgula, o que corrigiria o erro.

Resumo

Parabéns! Você concluiu o laboratório de Funções. Você pode praticar mais laboratórios no LabEx para aprimorar suas habilidades.