Мутный self в Rust или как раз и навсегда его понять

self в Rust

На начальном у всех новичков возникают проблемы с тем что значит self в rust и чем он отличается от Self.

Ниже приведен пример кода из книги «Rust Профессиональное программирование». В книге допущена ошибка, я отметил ее в комментарии.

Ожидался self, но случайно был напечатан Self и программа перестала компилироваться. Посмотрите этот код, он отлично иллюстрирует сразу несколько особенностей языка, но раз уж речь пошла о self, обратите внимание как он используется в примере.

/*
*   В этом списке отсутствуют операции вставки.
*   Можно попробовать реализовать их самостоятельно.
*   Список никогда не будет пустым, недействительным или содержать нулевые указатели.
*/

// В каждом элементе списка имеются упакованные в нем данные. 
// Поле не может быть пустым или нулевым.
struct ListItem {
    data: Box,
    next: Option<Box<ListItem>>, // Значение None = конец списка
}
// Структура списка содержит только заголовок
// упаковка которого нас не волнует поскольку он обязателен
struct SinglyLinkedList {
    head: ListItem 
}

impl ListItem {
    fn new(data: T) -> Self {
        ListItem {
            // Новые данные перемещаются в кучу при помощи Box
            data: Box::new(data),
            next: None
        }
    }
    // Next - возвращает для каждого элемента опциональную ссылку
    // если она существует. Нужен для упрощения кода распаковки элементов.
    fn next(&self) -> Option<&Self> {
        // проверка наличия связи для элемента
        if let Some(next) = &self.next {
            // Возвращение следующей ссылки на элемент
            // Some(&*next) - эквивалентно
            Some(next.as_ref())
        } else {
            None
        }
    }
    
    fn mut_tail(&mut self) -> &mut Self {
        // if let здесь не сработает, поскольку мы не можем одновременно
        // заимствовать self.next и возвращать изменяемую ссылку на внутренний указатель.
        if self.next.is_some() {
            // Box внутри Options требует распаковки из изменяемой ссылки
            // и вернуть изменяемую ссылку изнутри
            self.next.as_mut().unwrap().mut_tail()
        } else {
            // Если элемента нет, это хвост и возвращаем self
            self // В книге здесь Self
        }
    }
    // Получаем 
    fn data(&self) -> &T {
        self.data.as_ref() 
    }
}

impl SinglyLinkedList {
    // Для нового списка нужен первый элемент. Чтобы допустить наличие 
    // пустого списка задан как Option()
    fn new(data: T) -> Self {
        SinglyLinkedList {
            head: ListItem::new(data),
        }
    }
    
    fn append(&mut self, data: T) {
        let tail = self.head.mut_tail();
        // Добавляем новый элемент в хвост
        tail.next = Some(Box::new(ListItem::new(data)))
    }
    
    fn head(&self) -> &ListItem {
        &self.head
    }
}

fn main() {
    let mut list = SinglyLinkedList::new("head");
    list.append("middle");
    list.append("tail");
    let mut item = list.head();
    
    loop {
        println!("item: {}", item.data());
        if let Some(next_item) = item.next() {
            item = next_item;
        } else {
            break;
        }
    }
}

/*
*   "Для новичка Rust связянный список будет отличной возможностью узнать об уникальных особенностях языка". 
*   Бренден Мэтьюз. Rust профессиональное программирование
*/

В Rust Self и self — это совершенно разные сущности, несмотря на схожесть написания. Разберём их по пунктам.

self (строчная буква)

Что это: первый параметр метода, представляющий экземпляр типа, для которого вызывается метод.

Особенности:

  • Всегда пишется со строчной буквы.
  • Является значением (конкретным объектом).
  • Может иметь разные формы владения:
    • self — передаёт владение (объект «уходит» в метод);
    • &self — неизменяемая ссылка (только чтение);
    • &mut self — изменяемая ссылка (можно менять поля).

Self (заглавная буква)

Что это: ключевое слово, обозначающее текущий тип в контексте реализации.

Особенности:

  • Пишется с заглавной буквы.
  • Является типом, а не значением.
  • Автоматически подставляется компилятором как синоним текущего типа.
  • Особенно полезен в трейтах и при обобщённом программировании.

 

Следуя рекомендациям по синтаксису Rust мы знаем, что типажи и структуры пишутся с заглавной буквы, в то время как переменные принято писать со строчной. Когда втягиваешься, это оказывается удобно. Пример с S(s)elf явное тому доказательство.

В некоторых языках применяются this и self. И такой подход так же создает немало путаницы не только у новичков.

 

Так как же никогда не запутаться? Ответ прост: никак. В своем коде вы будете допускать подобные ошибки. К сожалению это неизбежно, когда для разных обстоятельств применяется одно слово и разница между совершенно разными сущностями — регистр заглавной буквы, ошибки неизбежны.

Чтобы отлавливать такие ошибки в больших массивах кода, научитесь читать сообщения об ошибках компилятора. Внимательно и вдумчиво изучая, что заставило компилятор вывести эти сообщения.

StackUP