sábado, 3 de abril de 2021

Sintaxe dos método em Rust

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

Definindo Métodos

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

Nome do arquivo: src/main.rs

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

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

fn main() {
    let retangulo1 = Retangulo {
        width: 30,
        height: 50,
    };

    println!(
        "A área do retângulo é de {} pixels quadrados.",
        retangulo1.area()
    );
}

Listagem 5-13: Definindo um método area na estrutura Retangulo.

Para definir a função dentro do contexto de Retangulo, iniciamos um bloco (implementação) impl. Em seguida, movemos a função area para dentro das chaves de impl e alteramos o primeiro (e neste caso, apenas) parâmetro para self na assinatura e em todos os lugares dentro do corpo. Em main, onde chamamos a função area e passamos retangulo1 como um argumento, podemos usar a sintaxe dos métodos para chamar o método area em nossa instância de Retangulo. A sintaxe do método segue 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: &Retangulo porque Rust sabe que o tipo self é Retangulo devido a esse método estar dentro do contexto impl Retangulo. Observe que ainda precisamos usar o & antes de self, assim como fizemos em &Retangulo. Os métodos podem se apropriar self, emprestar imutavelmente, como fizemos aqui com self, ou emprestar mutuamente self, assim como qualquer outro parâmetro.

Escolhemos &self aqui pelo mesmo motivo que usamos &Retangulo na versão da função: não queremos assumir a propriedade e queremos apenas ler os dados na estrutura, não gravá-la. Se quiséssemos alterar a instância na qual chamamos o método como parte do que o método faz, usaríamos &mut self como o primeiro parâmetro. self É raro ter um método que assume a propriedade da instância usando apenas o primeiro parâmetro; essa técnica geralmente é usada quando o método se transforma self em outra coisa e você deseja evitar que o chamador use a instância original após a transformação.

O principal benefício de usar métodos em vez de funções, além de usar sintaxe de método e não ter que repetir o tipo de self na assinatura de cada método, é para 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 de nosso código procurem recursos de Retangulo em vários lugares da biblioteca que fornecemos.

Onde está o operador ->?

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

Rust não tem equivalente ao operador ->; em vez disso, o Rust tem um recurso chamado referência e desreferenciação automáticas. A chamada de métodos é um dos poucos lugares em Rust que tem esse comportamento.

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


#![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. Esse comportamento de referência automática funciona porque os métodos têm um receptor claro - o tipo de self. Dado o receptor e o nome de um método, Rust pode descobrir definitivamente se o método está lendo (&self), alterando (&mut self) ou consumindo (self). O fato de Rust tornar implícito o empréstimo para receptores de métodos é 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 estrutura Retangulo. Desta vez, queremos que uma instância de Retangulo tome outra instância de Retangulo e retorne true se a segunda instância de Retangulo puder caber completamente dentro de self; caso contrário, deve retornar false. Ou seja, queremos ser capazes de escrever o programa mostrado na Listagem 5-14, depois de definir o método pode_caber.

Nome do arquivo: src/main.rs

fn main() {
    let retangulo1 = Retangulo {
        width: 30,
        height: 50,
    };
    let retangulo2 = Retangulo {
        width: 10,
        height: 40,
    };
    let retangulo3 = Retangulo {
        width: 60,
        height: 45,
    };

    println!("retangulo2 cabe dentro de retangulo1? {}", retangulo1.pode_caber(&retangulo2));
    println!("retangulo3 cabe dentro de retangulo1? {}", retangulo1.pode_caber(&retangulo3));
}

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

E a saída esperada seria semelhante à seguinte, porque ambas as dimensões de retangulo2 são menores do que as dimensões de, retangulo1 mas retangulo3 são mais largas do que retangulo1:

retangulo2 cabe dentro de retangulo1? true
retangulo3 cabe dentro de retangulo1? false

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

Nome do arquivo: src/main.rs

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

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

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

fn main() {
    let retangulo1 = Retangulo {
        width: 30,
        height: 50,
    };
    let retangulo2 = Retangulo {
        width: 10,
        height: 40,
    };
    let retangulo3 = Retangulo {
        width: 60,
        height: 45,
    };

    println!("retangulo2 cabe dentro de retangulo1? {}", retangulo1.pode_caber(&retangulo2));
    println!("retangulo3 cabe dentro de retangulo1? {}", retangulo1.pode_caber(&retangulo3));
}

Listagem 5-15: Implementando o método pode_caber em Retangulo que toma outra instância de Retangulo como parâmetro.

Quando executamos esse código com a função main da Listagem 5-14, obteremos a saída desejada. Os métodos podem receber vários parâmetros que adicionamos à assinatura após o parâmetro self, e esses parâmetros funcionam da mesma forma que os parâmetros nas funções.

Funções Associadas

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

As funções associadas são frequentemente usadas para construtores que retornarão uma nova instância da estrutura. Por exemplo, poderíamos fornecer uma função associada que teria um parâmetro de dimensão e usá-lo como largura e altura, tornando mais fácil criar um quadrado Retangulo em vez de precisar especificar o mesmo valor duas vezes:

Nome do arquivo: src/main.rs

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

impl Retangulo {
    fn square(size: u32) -> Retangulo {
        Retangulo {
            width: size,
            height: size,
        }
    }
}

fn main() {
    let sq = Retangulo::square(3);
}

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

Múltiplos blocos impl

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

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

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

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

fn main() {
    let retangulo1 = Retangulo {
        width: 30,
        height: 50,
    };
    let retangulo2 = Retangulo {
        width: 10,
        height: 40,
    };
    let retangulo3 = Retangulo {
        width: 60,
        height: 45,
    };

    println!("retangulo2 cabe dentro de retangulo1? {}", retangulo1.pode_caber(&retangulo2));
    println!("retangulo3 cabe dentro de retangulo1? {}", retangulo1.pode_caber(&retangulo3));
}

Listagem 5-16: Reescrevendo a Listagem 5-15 usando vários blocos impl.

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

Resumo

Structs permitem criar tipos personalizados que são significativos para seu domínio. Usando structs, você pode manter pedaços de dados associados conectados uns aos outros e nomear cada pedaço para tornar seu código claro. Os métodos permitem que você especifique o comportamento que as instâncias de seus structs têm, e as funções associadas permitem a funcionalidade de namespace que é particular para seu struct sem ter uma instância disponível.

Mas structs não são a única maneira de criar tipos personalizados: vamos nos voltar para o recurso enum de Rust para adicionar outra ferramenta à sua caixa de ferramentas.

Traduzido por Acervo Lima. O original pode ser acessado aqui.

Licença

0 comentários:

Postar um comentário