Sintaxe do Método

Methods (métodos) são semelhantes às funções: eles são declarados com a chave fn e o seu nome, eles podem ter parâmetros e valor de retorno, e eles contêm algum código que é executado quando eles são chamados de algum outro lugar. No entanto, métodos são diferentes das funções, porque são definidos no contexto de uma struct (ou um objeto enum ou uma trait, que nós cobrimos nos Capítulos 6 e 17, respectivamente), o seu primeiro parâmetro é sempre self, que representa a instância da struct do método que está a ser chamado.

Definindo Métodos

Vamos alterar a função area que tem uma instância de Rectangle como um parâmetro e, em vez disso, fazer um método area definido na struct Rectangle como mostrado na Lista 5-13:

Filename: src/main.rs

#[derive(Debug)]
struct Rectangle {
    length: u32,
    width: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.length * self.width
    }
}

fn main() {
    let rect1 = Rectangle { length: 50, width: 30 };

    println!(
        "The area of the rectangle is {} square pixels.",
        rect1.area()
    );
}

Lista 5-13: Definindo um método area na struct Rectangle

Para definir a função dentro do contexto de Rectangle, vamos iniciar um bloco impl (Implementação). Depois movemos a função area dentro do corpo ({}) do impl e alteramos o primeiro (e neste caso, unico) parâmetro a ser self na assinatura e em todos os lugares dentro do corpo. Em main, onde chamamos a função area e passamos ct1 como um argumento, podemos usar a sintaxe de método (method sintax) para chamar o método área na nossa instância Rectangle. A sintaxe de método vem em seguida a uma instância: adicionamos um ponto seguido pelo nome do método, parênteses e os argumentos.

Na assinatura de area, usamos &self em vez de rectangle: &Rectangle porque Rust sabe que o tipo de self é Rectangle devido a este método estar dentro do contexto do impl Rectangle. Note que ainda precisamos usar o & antes de self, tal como fizemos em &Rectangle. Métodos podem tomar posse de self, pedir emprestado self imutavel como temos feito aqui, ou pedir emprestado self mutavel, assim como qualquer outro parâmetro.

Escolhemos &selft aqui pela mesma razão usamos &Rectangle na versão função: nós não queremos tomar posse, nós apenas queremos ler os dados da struct, e não escrever nela. Se quisermos mudar a instância da qual chamamos o método como parte do que o método faz, usariamos &mut self como o primeiro parâmetro. Ter um método que toma posse da instância, usando apenas self como primeiro parâmetro é raro; esta técnica é geralmente utilizada quando o método transforma o self em algo mais e queremos evitar que o chamador use a instância original após a transformação.

A principal vantagem do uso de métodos em vez de funções, além de usar a sintaxe do método e não ter de repetir o tipo de self em cada assinatura do método, é a organização. Nós colocamos todas as coisas que nós podemos fazer com uma instância de um tipo em um bloco impl em vez de fazer os futuros utilizadores do nosso código procurar as capacidades de Rectangle em vários lugares na biblioteca que fornecemos.

Onde está o Operador ->?

Em linguagens como C++, dois operadores diferentes são usados para chamar métodos: você usa . se você está chamando um método do objeto diretamente e -> se você está chamando o método em um apontador para o objeto e necessita de desreferenciar o apontadr primeiro. Em outras palavras, se objeto é um apontador, objeto->algo() é semelhante a (*objeto).algo().

Rust não tem um equivalente para o operador ->; em vez disso, Rust tem um recurso chamado referenciamento e desreferenciamento automático. Chamada de métodos é um dos poucos lugares em Rust que têm este comportamento.

Eis como funciona: quando você chamar um método com objecto.algo(), Rust adiciona automaticamente &, &mut ou * para que objecto corresponda à assinatura do método. Em outras palavras, as seguintes são as mesmas:


#![allow(unused)]
fn main() {
#[derive(Debug,Copy,Clone)]
struct Point {
    x: f64,
    y: f64,
}

impl Point {
   fn distance(&self, other: &Point) -> f64 {
       let x_squared = f64::powi(other.x - self.x, 2);
       let y_squared = f64::powi(other.y - self.y, 2);

       f64::sqrt(x_squared + y_squared)
   }
}
let p1 = Point { x: 0.0, y: 0.0 };
let p2 = Point { x: 5.0, y: 6.5 };
p1.distance(&p2);
(&p1).distance(&p2);
}

O primeiro parece muito mais limpo. Este comportamento de referenciamento automático funciona porque métodos têm um receptor claro- o tipo self. Dado o receptor e o nome de um método, Rust pode descobrir definitivamente se o método é leitura (&self), mutação (&mut self), ou consumo (self). O fato de que Rust faz o empréstimo para receptores de método implícito é em grande parte porque o ownership é ergonomico na prática.

Métodos com Mais Parametros

Vamos praticar usando métodos através da aplicação de um segundo método sobre o struct Rectangle . Desta vez, queremos uma instância de Rectangle para ter outra instância de Rectangle e retornar verdadeiro se o segundo Rectangle pode caber completamente dentro de self, caso contrário deve retornar falso. Isto é, queremos ser capazes de escrever o programa mostrado na Lista 5-14, uma vez que você definiu o método can_hold(pode conter):

Filename: src/main.rs

fn main() {
    let rect1 = Rectangle { length: 50, width: 30 };
    let rect2 = Rectangle { length: 40, width: 10 };
    let rect3 = Rectangle { length: 45, width: 60 };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

Lista 5-14: Demonstrando o uso do método ainda não escrito can_hold

E o resultado esperado seria o seguinte, porque ambas as dimensões de rect2 são menores do que as dimensões de rect1, mas rect3 é mais amplo do que rect1:

Can rect1 hold rect2? true
Can rect1 hold rect3? false

Sabemos que queremos definir um método, por isso vai ser dentro do bloco impl Rectangle. O nome do método será can_hold, e vai tomar um empréstimo imutável de um outro Rectangle como parâmetro. Podemos dizer qual o tipo do parâmetro, olhando o código que chama o método: rect1.can_hold(&rect2) passa &rect2, que é um empréstimo imutável de rect2, uma instância do Rectangle. Isso faz sentido porque nós só precisamos de ler rect2 (em vez de escrever, que precisaria de um empréstimo mutável),e nós queremos que main conserve a propriedade de rect2 para que possamos utilizá-lo novamente depois de chamar o método can_hold. O valor de retorno de can_hold será um booleano, e a aplicação irá verificar se o comprimento e a largura de self são ambos maiores que o comprimento e a largura do outro Rectangle, respectivamente. Vamos adicionar o novo método can_hold ao bloco impl da Lista 5-13, mostrado na Lista 5-15:

Filename: src/main.rs


#![allow(unused)]
fn main() {
#[derive(Debug)]
struct Rectangle {
    length: u32,
    width: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.length * self.width
    }

    fn can_hold(&self, other: &Rectangle) -> bool {
        self.length > other.length && self.width > other.width
    }
}
}

Lista 5-15: Implementando o can_hold em Rectangle que toma outra instância de Rectangle como um parâmetro

Quando executamos este código com a função main na Lista 5-14, vamos obter a informação desejada. Métodos podem ter vários parâmetros que nós adicionamos à assinatura depois do parametro de self, e esses parâmetros funcionam como parâmetros em funções.

Funções Associadas

Outro recurso útil dos blocos impl é que podemos definir funções dentro dos blocos impl que não recebem self como um parâmetro. Estas são chamadas de funções associadas porque elas estão associados com a struct. Elas ainda são funções, não métodos, porque elas não têm uma instância da struct para trabalhar. Você já usou a função associada String::from.

Funções associadas são usados frequentemente para construtores que retornam uma nova instância da struct. Poderíamos, por exemplo, fornecer uma função associada que teria um parametro dimensão e usar esse parâmetro como comprimento e largura, tornando assim mais fácil criar um retângulo Rectangle em vez de ter que especificar o mesmo valor duas vezes:

Filename: src/main.rs


#![allow(unused)]
fn main() {
#[derive(Debug)]
struct Rectangle {
    length: u32,
    width: u32,
}

impl Rectangle {
    fn square(size: u32) -> Rectangle {
        Rectangle { length: size, width: size }
    }
}
}

Para chamar esta função associada, usamos a sintaxe :: com o nome da struct, como let sq = Rectangle::square(3);, por exemplo. Esta função é nomeada pela struct: a sintaxe :: é utilizada tanto para funções associadas e namespaces criados por módulos, que discutiremos no Capítulo 7.

Multiplos Blocos impl

Cada struct pode ter vários blocos impl. Por exemplo, a Lista 5-15 é equivalente ao código mostrado na Lista 5-16, que tem cada método em seu próprio bloco impl:


#![allow(unused)]
fn main() {
#[derive(Debug)]
struct Rectangle {
    length: u32,
    width: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.length * self.width
    }
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.length > other.length && self.width > other.width
    }
}
}

Lista 5-16: Reescrevendo Lista 5-15 usando vários blocos impl

Não há nenhuma razão para separar estes métodos em vários blocos impl aqui, mas é uma sintaxe válida. Vamos ver um caso quando vários blocos impl são úteis no Capítulo 10 quando falamos de tipos genéricos e traços.

Sumário

As Structs permitem-nos criar tipos personalizados que são significativos para o nosso domínio. Usando structs, podemos manter pedaços de dados associados ligados uns aos outros e nomear cada pedaço para fazer nosso código claro. Métodos ajudam-nos a especificar o comportamento que as instâncias das nossas structs têm, funções associadas dão-nos a funcionalidade de namespace que é particular à nossa struct sem ter uma instância disponível.

Mas structs não são a única maneira que nós podemos criar tipos personalizados: vamos ao recurso do Rust, enum, para adicionar uma outra ferramenta à nossa caixa de ferramentas.