Pull to refresh

С лёгким налётом ржавчины или куда делся NULL

Reading time3 min
Views8.3K
Original author: Imaculate

Давно начал следить за языком Rust, кажется, ещё до выхода версии 1.0. За это время язык сильно изменился, оформился и стал совсем взрослым, можно в производство. При этом из коробки программисту предлагается довольно много интересных концепций для разработки надёжного ПО с длительным жизненным циклом. Однако сфера промышленной автоматизации не так динамична (как нам иногда бы хотелось), поэтому пока приходится только присматриваться к Rust. Тем не менее надо знакомиться поближе. Просто так читать книжки или заметки не продуктивно, надо что-то пробовать делать. Например, можно начать с решения задачек на LeetCode (что я и решил попробовать). А при решении таких задачек иногда натыкаешься на что-то такое, с чем и Stack Overflow может не помочь, не только книжки. В результате поисков дополнительной информации наткнулся на серию заметок, которой хотелось бы поделиться с общественностью (да-да, он воровал тексты у богатых и переводил их бедным). Под катом перевод первой маленькой заметки про (отсутствие) NULL в Rust.

Большинство ключевых функциональных возможностей Rust не являются новыми или уникальными — функциональное программирование, объектно-ориентированное программирование, метапрограммирование, отсутствие NULL, управление памятью без сборщика мусора — всё это существовало и раньше. Но Rust выгодно отличается в той области, для которой он изначально задумывался — безопасная работа с памятью и производительность во время выполнения. В этой серии заметок поговорим о некоторых из перечисленных функциональных возможностях.

Как и обещал, начнем с NULL. Что на самом деле означает NULL? В большинстве языков он обозначает ничто — пустое, недействительное, нулевое или условное значение, которое может быть заменой для любого типа. NULL был придуман сэром Тони Хоаром в 1965 году и с тех пор пользуется популярностью. Rust является одним из немногих языков программирования, которые не поддерживают NULL, и небезосновательно.

Начнём с того, что сам сэр Хоар назвал NULL ошибкой в миллиард долларов. Мне, как разработчику преимущественно на C (или на C++, как автору оригинальной заметки) хорошо знакома боль от необходимости повсеместно добавлять проверки на NULL. Отсутствие хотя бы одной проверки может стать фатальным. Идея избегать ошибок, сбоев и уязвимостей звучит довольно привлекательно.

Вместо NULL язык Rust предоставляет перечисление Option, которое является общим для любого типа и имеет два варианта Some и None. Если есть вероятность, что объект типа T не имеет значения, то есть может быть None, его тип становится Option вместо T. Если попытаться использовать такой объект как T напрямую, компилятор будет люто (но понятно) ругаться. Поэтому не получится использовать объекты или ссылки, которые не определены, что устраняет целый класс ошибок.

Рассмотрим следующую программу на C++ в качестве примера:

#include <iostream>

using namespace std;

struct Wrapper
{
    int value;

    void print()
    {
        cout << value << endl;
    }
};

int main()
{
    cout << "Entrance" << endl;
    Wrapper *w = nullptr;
    w->print();
    cout << "Exit" << endl;
}

Программа замечательно компилируется, но её запуск приводит к сбою. Ошибка сегментации памяти возникает, когда разыменовывается нулевой указатель. Программу легко исправить, потому что она короткая и простая. Но в реальности это часто не так. С увеличением сложности, при передаче указателя в другие функции, ошибиться становится проще простого. Это позволит ошибке проявиться у заказчика, и может потребоваться несколько дней (хорошо если так) для отладки.

Буквальный перевод программы на Rust будет выглядеть следующим образом, но, конечно, он не будет компилироваться:

fn main()
{
    println!("Entrance");
    let w: Wrapper = None;
    w.print();
    println!("Exit");
}

struct Wrapper
{
    value: i32
}

impl Wrapper
{
    fn print(&self)
		{
        println!("{}", self.value);
    }
}

Объект w не может иметь тип Wrapper и иметь значение None. Если не получается присвоить ему значение, он должен иметь тип Option, который затем необходимо будет развернуть, чтобы получить объект Wrapper. Простой и безопасный способ — оператор if (в данном случае if let), который использует w, если это какое-то значение, и игнорирует его в противном случае. Следующая версия программы делает именно это и работает корректно:

fn main()
{
    println!("Entrance");
    let w: Option<Wrapper> = None;
    if let Some(v) = w
    {
        v.print();
    }

    println!("Exit");
}

И хотя в C++ есть аналогичный необязательный тип Optional со значением NULL, он настолько же полезный, насколько обязательный. Компилятор Rust, наоборот, не будет компилировать программу без всех необходимых проверок, если используется Option (а без него нельзя использовать None, т.к. NULL же нет).

Итак, какое впечатление от Rust на данный момент? Неудобно для идеального результата? Больше чем просто разница в синтаксисе? Чрезмерно усердный компилятор? Всё вышеперечисленное? Верно. Следите за обновлениями и мы вместе узнаем, что делает Rust таким, какой он есть.

Другие заметки цикла:

Tags:
Hubs:
Total votes 21: ↑14 and ↓7+7
Comments19

Articles