Funções

Funções são difundidas em códigos em Rust. Você já viu uma das mais importantes funções da linguagem: a função main, que é o ponto de entrada de diversos programas. Você também já viu a notação fn, que permite você declarar uma nova função.

Códigos em Rust usam, por convenção, o estilo snake case para nomes de função e variável. No snake case, todas as letras são minúsculas e sublinhado (underline) separa as palavras. Aqui está um programa que contém uma definição de função de exemplo:

Nome do arquivo: src/main.rs

fn main() {
    println!("Olá, mundo!");

    outra_funcao();
}

fn outra_funcao() {
    println!("Outra função.");
}

As definições de funções em Rust iniciam com fn e tem um par de parênteses depois do nome da função. As chaves dizem ao compilador onde o corpo da função começa e termina.

Podemos chamar qualqer função que tenhamos definido, inserindo seu nome, seguido de um conjunto de parenteses. Pelo fato da outra_funcao ter sido definida no programa, ela pode ser chamada dentro da função main. Note que definimos outra_funcao depois da função main; poderíamos ter definido antes também. Rust não se importa onde você definiu suas funções, apenas que elas foram definidas em algum lugar.

Vamos começar um novo projeto binário, chamado funcoes para explorar mais funções. Coloque o exemplo outra_funcao em src/main.rs e execute-o. Você verá a seguinte saída:

$ cargo run
   Compiling funcoes v0.1.0 (file:///projects/funcoes)
    Finished dev [unoptimized + debuginfo] target(s) in 0.28 secs
     Running `target/debug/funcoes`
Olá, mundo!
Outra função.

As linhas são executadas na ordem em que aparecem na função main. Primeiro, a mensagem "Olá, mundo!" é exibida, e então outra_funcao é chamada e exibida a mensagem.

Parâmetros de função

Funções também podem ser definidas tendo parâmetros, que são variáveis especiais que fazem parte da assinatura da função. Quando uma função tem parâmetros, você pode fornecer tipos específicos para esses parâmetros. Tecnicamente, os valores definidos são chamados de argumentos, mas informalmente, as pessoas tendem a usar as palavras parâmetro e argumento para falar tanto de variáveis da definição da função como os valores passados quando você chama uma função.

A seguinte versão (reescrita) da outra_funcao mostra como os parâmetros aparecem no Rust:

Nome do arquivo: src/main.rs

fn main() {
    outra_funcao(5);
}

fn outra_funcao(x: i32) {
    println!("O valor de x é: {}", x);
}

Tente executar este programa; você verá a seguinte saída:

$ cargo run
   Compiling funcoes v0.1.0 (file:///projects/funcoes)
    Finished dev [unoptimized + debuginfo] target(s) in 1.21 secs
     Running `target/debug/funcoes`
O valor de x é: 5

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

Nas assinaturas de função, você deve declarar o tipo de cada parâmetro. Essa é decisão deliberada no design do Rust: exigir anotações de tipo na definição da função, significa que o compilador quase nunca precisará que as use em outro lugar do código para especificar o que você quer.

Quando você precisa que uma função tenha vários parâmetros, separe as declarações de parâmetros com vírgula, como a seguir:

Nome do arquivo: src/main.rs

fn main() {
    outra_funcao(5, 6);
}

fn outra_funcao(x: i32, y: i32) {
    println!("O valor de x é: {}", x);
    println!("O valor de y é: {}", y);
}

Este exemplo cria uma função com dois parâmetros, ambos com o tipo i32. Então a função exibe os valores de ambos os parâmetros. Note que os parâmetros de função não precisam ser do mesmo tipo, isto apenas aconteceu neste exemplo.

Vamos tentar executar este código. Substitua o programa src/main.rs, atualmente em seu projeto funcoes com o exemplo anterior e execute-o usando cargo run:

$ cargo run
   Compiling funcoes v0.1.0 (file:///projects/funcoes)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
     Running `target/debug/funcoes`
O valor de x é: 5
O valor de y é: 6

Porque nós chamamos a função com 5 sendo o valor de x e 6 é passado como o valor de y, as duas cadeias são impressas com esses valores.

Corpos de função

Corpos de função são constituídos por uma série de declarações que terminam, opcionalmente, em uma expressão. Até agora, foram apresentadas apenas funções sem uma expressão final, mas você viu uma expressão como parte de instruções. Porque Rust é uma linguagem baseada em expressão, essa é uma importante distinção a ser entendida. 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 elas afetam o corpo das funções.

Declarações e Expressões

Na verdade, já usamos declarações e expressões. Declarações são instruções que executam alguma ação e não retornam um resultado. E expressões retornam um resultado. Vamos ver alguns exemplos.

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: A declaração da função main contendo uma declaração.

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

Definições não retornam valores. Assim sendo, você não pode atribuir uma declaração let para 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ê rodar esse programa, o erro será o seguinte:

$ cargo run
   Compiling funcoes v0.1.0 (file:///projects/funcoes)
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

A declaração let y = 6 não retorna um valor, então não existe nada para o x se ligar. Isso é diferente do que acontece em outras linguagens, como C e Ruby, onde a atribuição retorna o valor atribuído. Nestas linguagens, você pode escrever x = y = 6 e ter ambos, x e y contendo o valor 6; esse não é o caso em Rust.

Expressões avaliam algo e compõem a maior parte do código que você escreverá em Rust. Considere uma simples operação matemática, como um 5 + 6, que é uma expressão que avalia 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 o valor 6. A chamada de função é uma expressão. Chamar uma macro é uma expressão. O bloco que vamos usar para criar um novo escopo, {}, é uma expressão, por exemplo:

Nome do arquivo: src/main.rs

fn main() {
    let x = 5;

    let y = {
        let x = 3;
        x + 1
    };

    println!("O valor de y é: {}", y);
}

A expressão:

{
    let x = 3;
    x + 1
}

é um bloco que, nesse exemplo, avalia 4. Esse valor fica vinculado ao y como parte da declaração let. Note o x + 1 sem um ponto e vírgula no final, que é diferente da maioria das linhas vistas até agora. Expressões não terminam com ponto e vírgula. Se você adicionar um ponto e vírgula ao fim de uma expressão, você a transforma em uma declaração, que então não retornará um valor. Tenha isso em mente, enquanto explora os valores e expressões de retorno da função a seguir.

Funções com valor de retorno

Funções podem retornar valores para o código que os chama. Não nomeamos valores de retorno, mas declaramos o tipo deles depois de 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 cedo 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. Veja um exemplo de uma função que retorna um valor:

Nome do arquivo: src/main.rs

fn cinco() -> i32 {
    5
}

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

    println!("O valor de x é: {}", x);
}

Não há chamadas de função, macros ou até mesmo declarações let na função cinco

  • 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 ficar assim:
$ cargo run
   Compiling funcoes v0.1.0 (file:///projects/funcoes)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30 secs
     Running `target/debug/funcoes`
O valor de x é: 5

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


#![allow(unused)]
fn main() {
let x = 5;
}

Em segundo lugar, a função cinco não tem parâmetros e define o tipo de valor de retorno, mas o corpo da função é um 5 solitário 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 = soma_um(5);

    println!("O valor de x é: {}", x);
}

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

A execução deste código irá imprimir O valor de x é: 6. Mas se colocarmos um ponto e vírgula no final da linha que contém x + 1, alterando-o de expressão para uma declaração, receberemos um erro.

Nome do arquivo: src/main.rs

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

    println!("O valor de x é: {}", x);
}

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

Executar este código produz um erro, da seguinte maneira:

error[E0308]: mismatched types
 --> src/main.rs:7:28
  |
7 |   fn soma_um(x: i32) -> i32 {
  |  ____________________________^
8 | |     x + 1;
  | |          - help: consider removing this semicolon
9 | | }
  | |_^ expected i32, found ()
  |
  = note: expected type `i32`
             found type `()`

A principal mensagem de erro, "tipos incompatíveis", revela o problema central com este código. A definição da função soma_um diz que retornará uma i32, mas as declarações não avaliam um valor expresso por(), a tupla vazia. Portanto, nada é retornado, o que contradiz a função definição e resulta em erro. Nesta saída, Rust fornece uma mensagem para possivelmente ajudar a corrigir este problema: sugere a remoção do ponto e vírgula, que iria corrigir o erro.