Prática de Sintaxe de Métodos em Rust

Beginner

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

Introdução

Bem-vindo(a) à Sintaxe de Métodos. Este laboratório faz parte do Livro de Rust. Você pode praticar suas habilidades em Rust no LabEx.

Neste laboratório, os métodos são declarados com a palavra-chave fn e um nome, podem ter parâmetros e um valor de retorno, e são definidos dentro do contexto de uma struct, com o primeiro parâmetro sendo sempre self para representar a instância da struct em que o método está sendo chamado.

Sintaxe de Métodos

Métodos são semelhantes a funções: nós os declaramos com a palavra-chave fn e um nome, eles podem ter parâmetros e um valor de retorno, e contêm algum código que é executado quando o método é chamado de outro lugar. Diferentemente das funções, os métodos são definidos dentro do contexto de uma struct (ou de um enum ou de um objeto trait, que abordaremos no Capítulo 6 e no Capítulo 17, respectivamente), e seu primeiro parâmetro é sempre self, que representa a instância da struct em que o método está sendo chamado.

Definindo Métodos

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

Nome do arquivo: src/main.rs

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

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

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

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

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

Para definir a função dentro do contexto de Rectangle, iniciamos um bloco impl (implementação) para Rectangle [1]. Tudo dentro deste bloco impl será associado ao tipo Rectangle. Em seguida, movemos a função area para dentro das chaves do impl [2] e mudamos o primeiro (e, neste caso, único) parâmetro para ser self na assinatura e em todos os lugares dentro do corpo. Em main, onde chamamos a função area e passamos rect1 como um argumento, podemos, em vez disso, usar a sintaxe de método para chamar o método area na nossa instância de Rectangle [3]. A sintaxe do método vem depois de uma instância: adicionamos um ponto seguido pelo nome do método, parênteses e quaisquer argumentos.

Na assinatura de area, usamos &self em vez de rectangle: &Rectangle. O &self é, na verdade, uma abreviação de self: &Self. Dentro de um bloco impl, o tipo Self é um alias para o tipo para o qual o bloco impl é destinado. Os métodos devem ter um parâmetro chamado self do tipo Self para seu primeiro parâmetro, então o Rust permite que você abrevie isso com apenas o nome self no primeiro lugar do parâmetro. Observe que ainda precisamos usar o & na frente da abreviação self para indicar que este método empresta a instância Self, assim como fizemos em rectangle: &Rectangle. Os métodos podem assumir a propriedade de self, emprestar self imutavelmente, como fizemos aqui, ou emprestar self mutavelmente, assim como podem fazer com qualquer outro parâmetro.

Escolhemos &self aqui pela mesma razão que usamos &Rectangle na versão da função: não queremos assumir a propriedade e só queremos ler os dados na struct, não escrever nela. Se quiséssemos mudar a instância em que chamamos o método como parte do que o método faz, usaríamos &mut self como o primeiro parâmetro. Ter um método que assume a propriedade da instância usando apenas self como o primeiro parâmetro é raro; essa técnica é geralmente usada quando o método transforma self em outra coisa e você quer impedir que o chamador use a instância original após a transformação.

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

Observe que podemos escolher dar a um método o mesmo nome de um dos campos da struct. Por exemplo, podemos definir um método em Rectangle que também se chama width:

Nome do arquivo: src/main.rs

impl Rectangle {
    fn width(&self) -> bool {
        self.width > 0
    }
}

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

    if rect1.width() {
        println!(
            "The rectangle has a nonzero width; it is {}",
            rect1.width
        );
    }
}

Aqui, estamos escolhendo fazer com que o método width retorne true se o valor no campo width da instância for maior que 0 e false se o valor for 0: podemos usar um campo dentro de um método com o mesmo nome para qualquer finalidade. Em main, quando seguimos rect1.width com parênteses, o Rust sabe que queremos dizer o método width. Quando não usamos parênteses, o Rust sabe que queremos dizer o campo width.

Frequentemente, mas nem sempre, quando damos métodos com o mesmo nome de um campo, queremos que ele apenas retorne o valor no campo e não faça mais nada. Métodos como este são chamados de getters, e o Rust não os implementa automaticamente para campos de struct como algumas outras linguagens fazem. Getters são úteis porque você pode tornar o campo privado, mas o método público, e, assim, habilitar o acesso somente leitura a esse campo como parte da API pública do tipo. Discutiremos o que são público e privado e como designar um campo ou método como público ou privado no Capítulo 7.

Onde está o operador ->?

Em C e C++, dois operadores diferentes são usados para chamar métodos: você usa . se estiver chamando um método no objeto diretamente e -> se estiver chamando o método em um ponteiro para o objeto e precisar desreferenciar o ponteiro primeiro. Em outras palavras, se object é um ponteiro, object->something() é semelhante a (*object).something().

O Rust não tem um equivalente ao operador ->; em vez disso, o Rust tem um recurso chamado referenciamento e desreferenciamento automáticos. Chamar métodos é um dos poucos lugares no Rust que tem esse comportamento.

Veja como funciona: quando você chama um método com object.something(), o Rust adiciona automaticamente &, &mut ou * para que object corresponda à assinatura do método. Em outras palavras, o seguinte é o mesmo:

p1.distance(&p2);
(&p1).distance(&p2);

O primeiro parece muito mais limpo. Este comportamento de referenciamento automático funciona porque os métodos têm um receptor claro --- o tipo de self. Dado o receptor e o nome de um método, o Rust pode descobrir definitivamente se o método está lendo (&self), mutando (&mut self) ou consumindo (self). O fato de o Rust tornar o empréstimo implícito para receptores de método é uma grande parte de tornar a propriedade ergonômica na prática.

Métodos com Mais Parâmetros

Vamos praticar o uso de métodos implementando um segundo método na struct Rectangle. Desta vez, queremos que uma instância de Rectangle receba outra instância de Rectangle e retorne true se o segundo Rectangle puder caber completamente dentro de self (o primeiro Rectangle); caso contrário, deve retornar false. Ou seja, depois de definirmos o método can_hold, queremos ser capazes de escrever o programa mostrado na Listagem 5-14.

Nome do arquivo: src/main.rs

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

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

Listagem 5-14: Usando o método can_hold, ainda não escrito

A saída esperada seria semelhante à seguinte, porque ambas as dimensões de rect2 são menores que as dimensões de rect1, mas rect3 é mais largo que rect1:

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

Sabemos que queremos definir um método, então ele estará dentro do bloco impl Rectangle. O nome do método será can_hold, e ele receberá um empréstimo imutável de outro Rectangle como parâmetro. Podemos dizer qual será o tipo do parâmetro olhando para o código que chama o método: rect1.can_hold(&rect2) passa &rect2, que é um empréstimo imutável para rect2, uma instância de Rectangle. Isso faz sentido porque só precisamos ler rect2 (em vez de escrever, o que significaria que precisaríamos de um empréstimo mutável), e queremos que main mantenha a propriedade de rect2 para que possamos usá-lo novamente após chamar o método can_hold. O valor de retorno de can_hold será um booleano, e a implementação verificará se a largura e a altura de self são maiores que a largura e a altura do outro Rectangle, respectivamente. Vamos adicionar o novo método can_hold ao bloco impl da Listagem 5-13, mostrado na Listagem 5-15.

Nome do arquivo: src/main.rs

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

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

Listagem 5-15: Implementando o método can_hold em Rectangle que recebe outra instância de Rectangle como parâmetro

Quando executamos este código com a função main na Listagem 5-14, obteremos a saída desejada. Os métodos podem receber múltiplos parâmetros que adicionamos à assinatura após o parâmetro self, e esses parâmetros funcionam como parâmetros em funções.

Funções Associadas

Todas as funções definidas dentro de um bloco impl são chamadas de funções associadas porque estão associadas ao tipo nomeado após o impl. Podemos definir funções associadas que não têm self como seu primeiro parâmetro (e, portanto, não são métodos) porque não precisam de uma instância do tipo para trabalhar. Já usamos uma função como essa: a função String::from que é definida no tipo String.

Funções associadas que não são métodos são frequentemente usadas para construtores que retornarão uma nova instância da struct. Elas são frequentemente chamadas de new, mas new não é um nome especial e não está embutido na linguagem. Por exemplo, poderíamos escolher fornecer uma função associada chamada square que teria um parâmetro de dimensão e usaria isso como largura e altura, tornando mais fácil criar um Rectangle quadrado em vez de ter que especificar o mesmo valor duas vezes:

Nome do arquivo: src/main.rs

impl Rectangle {
    fn square(size: u32) -> 1 Self  {
      2 Self  {
            width: size,
            height: size,
        }
    }
}

As palavras-chave Self no tipo de retorno [1] e no corpo da função [2] são aliases para o tipo que aparece após a palavra-chave impl, que neste caso é Rectangle.

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

Múltiplos Blocos impl

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

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

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

Listagem 5-16: Reescrevendo a Listagem 5-15 usando múltiplos blocos impl

Não há razão para separar esses métodos em múltiplos blocos impl aqui, mas esta é uma sintaxe válida. Veremos um caso em que múltiplos blocos impl são úteis no Capítulo 10, onde discutiremos tipos genéricos e traits.

Resumo

Parabéns! Você concluiu o laboratório de Sintaxe de Métodos. Você pode praticar mais laboratórios no LabEx para aprimorar suas habilidades.