Введение в программирование на Rust

Rust  —  это перспективный язык программирования, набирающий рекордную популярность для низкоуровневых систем, таких как операционные системы и компиляторы.

В 2020 году по итогам опроса разработчиков Stack Overflow самым любимым языком программирования уже пятый год подряд был признан Rust. Многие разработчики уверены в том, что Rust скоро обгонит C и C++ благодаря своему средству проверки заимствований и решению давних проблем, таких как управление памятью, а также неявная и явная типизация.

Сегодня мы поможем вам начать работу с Rust независимо от вашего уровня опыта. Мы расскажем, что отличает Rust от других языков, изучим его основные компоненты и поможем написать вашу первую программу на Rust!

Вот что мы рассмотрим в статье.

  • Что такое Rust?
  • «Hello World!» на Rust.
  • Основы синтаксиса Rust.
  • Промежуточный Rust: владение и структуры.
  • Система сборки Rust: Cargo.
  • Продвинутые концепции для дальнейшего изучения.

Что такое Rust?

Rust  —  это мультипарадигмальный статически типизированный язык программирования с открытым исходным кодом, используемый для создания операционных систем, компиляторов и других программно-аппаратных средств. Он был разработан Грейдоном Хором в Mozilla Research в 2010 году.

Rust оптимален с точки зрения производительности и безопасности, причем акцент здесь сделан на безопасном параллелизме. Этот язык больше всего похож на C или C++, но использует средство проверки заимствований для подтверждения безопасности ссылок.

Rust  —  это идеальный язык системного программирования для разработки встроенного программного обеспечения для платформ без операционной системы. Наиболее распространено применение Rust в низкоуровневых системах, например ядрах операционных систем или в микроконтроллерах.

Rust отличается от других низкоуровневых языков отличной поддержкой параллельного программирования с предотвращением гонки данных.

Зачем изучать Rust?

Язык программирования Rust идеально подходит для низкоуровневого системного программирования из-за системы выделения памяти с уникальной концепцией владения и приверженности оптимальному и безопасному параллелизму. И хотя его все еще нечасто используют в крупных компаниях, Rust остается одним из языков, получающих самые высокие оценки.

Rust продолжает совершенствоваться в условиях непрекращающегося роста требований к низкоуровневым системам и вполне способен стать языком завтрашних операционных систем. Попробуйте себя в роли разработчика Rust уже сейчас, чтобы получить желаемую должность, надолго обеспечив себя работой с высокой оплатой.

«Hello World!» на Rust

Лучший способ освоить Rust  —  реальная практика. Для начала напишем первую программу hello-world.

fn main() {
    println!("Hello World!");
}

Разберем все части этого кода.

fn

fn  —  это сокращение от function («Функция»). В Rust (как и в большинстве других языков программирования) функция как бы говорит: «Сообщите мне информацию, а я сделаю то-то и то-то и затем дам ответ».

main

Функция main  —  это то место, где начинается программа.

()

Скобки содержат список параметров для этой функции. Сейчас он пуст, то есть параметров нет. Но скоро мы увидим много функций с параметрами.

{ }

Фигурные скобки. Ими обозначается начало и конец тела кода. Тело сообщает, что делает функция main.

println!

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

("Hello, world!")

А это список параметров для вызова макроса. Мы как бы говорим: «Вызовите макрос println с этими параметрами». Макрос println такой же, как функция main, только у него параметр вместо списка параметров. Позже мы еще увидим функции и параметры.

"Hello, world!"

Дальше идет строка. Строки состоят из нескольких собранных вместе букв или символов. Для обозначения строки эти символы помещаются в кавычки ("). Затем строки передаются для макросов типа println! и других функций, с которыми мы еще поиграем.

;

А это точка с запятой. Она обозначает конец одной инструкции, как точка в предложении. Инструкции  —  это указания компьютеру выполнить конкретное действие. Чаще всего инструкция состоит из всего одной строки кода. В нашем случае она вызывает макрос. Есть и другие виды инструкций, которые мы скоро увидим.

Основы синтаксиса Rust

Теперь рассмотрим основные части программы на Rust и способы их реализации.

Переменные и их изменяемость

Переменные  —  это точки данных, которые сохраняются и помечаются для последующего использования. Формат объявлений переменных таков:

let [variable_name] = [value];

Имя переменной должно быть информативным, т. е. описывать, чем является ее значение. Например:

let my_name = "Ryan";

Здесь создана переменная my_name со значением "Ryan".

Совет💡 Всегда давайте переменным названия, начинающиеся со строчной буквы, а новое слово начинайте с заглавной.

В Rust переменные неизменяемы по умолчанию, т. е. их значение нельзя изменить после того, как они заданы.

Например, вот этот код выдаст ошибку во время компиляции:

fn main() {

    let x = 5;

    println!("The value of x is: {}", x);

    x = 6;

    println!("The value of x is: {}", x);

}

Ошибка в строке 4, где мы попытались установить значение x = 6. Но значение x уже задано в строке 2 и изменить его нельзя.

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

Представьте, что у вас есть две функции: functionA, которая использует переменную, имеющую значение 10, и функция functionB, которая изменяет эту же переменную. Выполнение функции functionA будет прервано!

Чем больше у вас становится переменных и функций, тем легче случайно изменить их значения. Такого рода ошибки поддаются отладке с трудом, поэтому в Rust предпочитают избегать их в принципе.

Чтобы переопределить это значение по умолчанию и создать изменяемую переменную, объявим ее вот так:

let mut x = 5;

Изменяемые переменные чаще всего используются как переменные-итераторы или как переменные в структурах цикла while.

Типы данных

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

В Rust имеется такая функциональная особенность, как определение типа. Она позволяет компилятору предположить, какой тип данных должен быть у той или иной переменной, даже в отсутствие четкого указания. Так экономится время при написании объявлений переменных с очевидными типами, например для строки my_name.

Указав между именем переменной и ее значением : &[type], мы явно определим тип для этой переменной.

В этом случае наш пример с объявлением my_name будет переписан следующим образом:

let my_name = "Ryan"; // с явно определенным типом

let my_name: &str = "Ryan"; // с явно определенным типом

Явная типизация позволяет соответствующим образом определять тип переменной и избегать ошибок в тех случаях, когда тип переменной неочевиден. Rust сделает наиболее правильное с его точки зрения предположение, но это может привести к неожиданному поведению.

Допустим, имеется переменная answer, которая записывает ответ пользователя в форму.

let answer = "true";

Rust неявно определит строковый тип для этой переменной, так как она приводится в кавычках. Тогда как переменная наверняка булева, что подразумевает выбор между двумя вариантами: true и false.

Во избежание недопонимания со стороны других разработчиков и для недопущения синтаксической ошибки поменяем объявление следующим образом:

let answer: bool = true;

Основные типы на Rust:

  • Integer: целочисленный (целые числа).
  • Float: числа с плавающей запятой (с десятичными знаками).
  • Boolean: логический (булев) с выбором между двумя вариантами (true или false).
  • String: строковый (набор символов, заключенных в кавычки).
  • Char: скалярное значение Юникод, представляющее конкретный символ.
  • Never: тип без значения (обозначается как !).

Функции

Функции  —  это наборы связанного кода на Rust, объединенные под кратким условным обозначением и вызываемые из других частей программы.

Пока что мы использовали только базовую функцию main(). Rust также позволяет создавать дополнительные, собственные функции, и это очень важная для большинства программ возможность. Функции часто представляют собой одну повторяющуюся задачу, например addUser (добавление пользователя) или changeUsername (изменение имени пользователя). Эти функции затем повторно используются всякий раз, когда требуется выполнить то же самое поведение.

Функции, отличные от main, должны иметь уникальное имя и возвращать результат. Кроме того, они передают параметры (один или несколько), которые представляют собой входные данные для использования внутри функции.

Вот формат для объявления функции:

fn [functionName]([parameterIdentifier]: [parameterType]) {

    [functionBody]

}

fn

Это уже знакомое нам сокращение от function («Функция»). За ним в коде Rust следует объявление функции.

[functionName]

Здесь находится идентификатор функции, который будет использоваться при ее вызове.

()

Эти скобки заполняются любыми параметрами, которые нужны функции. В данном случае никаких параметров не передается, поэтому скобки оставлены пустыми.

[parameterIdentifier]

А здесь передаваемому значению присваивается имя. Это имя выступает в роли имени переменной, ссылающейся на параметр в любом месте тела функции.

[parameterType]

После параметра необходимо явно указать тип. Во избежание путаницы неявная типизация параметров в Rust запрещена.

{}

Фигурные скобки обозначают начало и конец блока кода. Код внутри скобок выполняется при каждом вызове идентификатора функции.

[functionBody]

А это заполнитель для кода функции. Лучше не включать сюда никакого кода, не связанного прямо с выполнением задачи функции.

Добавим немного кода. Переделаем hello-world в функцию say_hello():

fn say_hello() {

    println!("Hello, world!");

}

Совет💡 Увидели ()  —  значит, вы имеете дело с вызовом функции. Если параметров нет, получаем внутри скобок пустое поле параметров. Сами скобки все равно остаются, указывая на то, что это функция.

Функция создана, теперь вызовем ее из других частей программы. Программа начинается в main(), поэтому вызовем say_hello() оттуда.

Вот как будет выглядеть полная программа:

fn say_hello() {

    println!("Hello, world!");

}

fn main() {

    say_hello();

}

Комментарии

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

В Rust есть два способа писать комментарии. Первый  —  использовать двойную косую черту //. В этом случае все, вплоть до конца строки, игнорируется компилятором. Например:

fn main() {

    // Эта строка полностью игнорируется

    println!("Hello, world!"); // А эта напечатала сообщение

    // Все готово. Пока!

}

Второй способ  —  предварять комментарий косой чертой со звездочкой /* и завершать его звездочкой с косой чертой */. Преимущества такого способа оформления комментариев: 1) есть возможность помещать комментарии в середину строки кода и 2) так легче писать многострочные комментарии. Недостаток в том, что во многих случаях приходится задействовать больше символов, чем просто //.

fn main(/* я могу это сделать! */) {

    /* первый комментарий  */

    println!("Hello, world!" /* второй комментарий */);

    /* Все готово. Пока!

       третий комментарий

     */

}

Совет💡 используйте комментарии для «закомментирования» разделов кода, выполнение которых не требуется, но которые позже понадобится добавить.

Условные инструкции

Условные инструкции  —  это способ создания поведения, которое имеет место только в случае истинности некоего набора условий. С помощью этих инструкций получаются адаптируемые функции, которые отлично справляются с различными программными ситуациями без использования второй функции.

Все условные инструкции содержат проверяемую переменную и целевое значение, а оператор условия (==, < или >) определяет их соотношение. В зависимости от состояния переменной применительно к целевому значению возвращается одно из двух логических выражений: true («истинно»), если переменная удовлетворяет целевому значению, и false («ложно»), если нет.

Допустим, нужно сделать функцию для создания учетной записи для любого пользователя, у которого еще нет учетной записи для авторизации в системе.

Это пример условного оператора if. Фактически происходит вот что: если hasAccount соответствует false, учетная запись будет создана. И пользователь будет в ней авторизован независимо от того, уже имелась у него учетная запись или нет.

Вот как выглядит формат оператора if:

if [variable] [conditionOperator] [targetValue] {

        [code body]

    }

Есть три основных условных оператора: if, if else и while.

  • if: если условие истинно, происходит выполнение. В противном случае пропускаем и идем дальше.
  • if else: если условие истинно, выполняется тело кода A. В противном случае выполняется тело кода B.
fn main() {

    let is_hot = false;

    if is_hot {

        println!("It's hot!");

    } else {

        println!("It's not hot!");

    }

}
  • while: тело кода многократно выполняется, пока условие true («истинно»). Как только условие становится false («ложным»), мы идем дальше.
while is_raining() {

    println!("Hey, it's raining!");

}

Совет💡 необходимо, чтобы в циклах while проверяемая переменная была изменяемой. Если переменная никогда не меняется, такой цикл будет продолжаться бесконечно.

Промежуточный Rust: владение и структуры

Владение

Владение  —  это центральная особенность Rust и одна из причин такой его популярности.

Во всех языках программирования должна предусматриваться система освобождения неиспользуемой памяти. В некоторых языках, таких как Java, JavaScript или Python, есть сборщики мусора, которые автоматически удаляют неиспользуемые ссылки. В низкоуровневых языках типа C или C++ от разработчиков требуется всякий раз, когда это необходимо, выделять и освобождать память вручную.

Ручное выделение памяти сопряжено с многочисленными проблемами, поэтому практиковать его затруднительно. Когда память выделяется на очень продолжительное время, она расходуется впустую. А слишком раннее освобождение памяти, как и выделение одной и той же памяти дважды, приводит к ошибке.

Rust выгодно отличает от всех этих языков система владения, которая управляет памятью с помощью набора правил, применяемых компилятором во время компиляции.

Вот эти правила владения.

  • У каждого значения в Rust есть переменная, которая называется его владельцем.
  • В каждый конкретный момент времени у значения есть только один владелец.
  • Когда владелец выходит из области видимости, значение удаляется.

А теперь посмотрим, как владение уживается с функциями. Для объявленных переменных память выделяется, пока они используются. Если эти переменные передаются в качестве параметров в другую функцию, выделение перемещается или копируется к другому владельцу и используется у него.

fn main() {

     let x = 5; // переменная x владеет значением 5

     function(x);

}

 fn function (number : i32)   { // number становится владельцем значения 5

        let s = "memory";  // начинается область видимости переменной s, здесь s становится действительной

        // здесь с s выполняются какие-то действия

    }                                  // эта область видимости заканчивается, и теперь s

                                       // больше недействительна

Главный вывод касается разного использования s и x. Сначала x владеет значением 5, но после выхода ее из области видимости функции main() переменная x должна передать владение параметру number. Ее использование в качестве параметра позволяет продолжить область видимости выделения памяти под значение 5 за пределы исходной функции.

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

Структуры

Еще одним важным понятием в Rust являются структуры, называемые struct. Это пользовательские типы данных, создаваемые для представления типов объектов. При создании определяется набор полей, для которых все структуры этого типа должны иметь какие-то значения.

Аналогом этих структур в таких языках, как Java и Python, являются классы.

Вот синтаксис объявления структуры:

struct [identifier] {

    [fieldName]: [fieldType],

   [secondFieldName]: [secondFieldType],

}
  • struct сообщает Rust, что следующее объявление определит тип данных struct.
  • [identifier]  —  это имя типа данных, используемого при передаче параметров, таких как string или i32, в строковые и целочисленные типы соответственно.
  • {} эти фигурные скобки обозначают начало и конец переменных, необходимых для структуры.
  • [fieldName]  —  это место, где вы называете первую переменную, которую должны иметь все экземпляры этой структуры. Переменные внутри структуры называются полями.
  • [fieldType]  —  это место, где во избежание путаницы явно определяется тип данных переменной.

Например, создадим структуру struct Car, которая включает в себя переменную строкового типа brand и переменную целочисленного типа year.

struct Car{

    brand: String,

    year: u16,

};

Каждый создаваемый экземпляр типа Car должен иметь значения для этих полей. Поэтому создадим экземпляр Car для конкретного автомобиля со значениями для brand (модели) и year (года выпуска).

let my_car = Car {

    brand: String:: from ("BMW"), // с явно заданным строковым типом

    year: 2009,

};

Точно так же, как при определении переменных с примитивными типами, определяем переменную Car с идентификатором, на который будем ссылаться позже.

let [variableIdentifier] = [dataType] {

// поля

}

Оттуда будем использовать значения этих полей с синтаксисом [variableIdentifier].[field]. Rust интерпретирует эту инструкцию как «каково значение [поля] для идентификатора [переменной]?».

println!(

        "My car is a {} from {}",

        my_car.brand, my_car.year

    );

}

Вот как выглядит вся структура целиком:

fn main () {

struct Car{

    brand: String,

    year: u16,

};

let my_car = Car {

        brand: String:: from ("BMW"),

    year: 2009,
};

println!(

        "My car is a {} from {}",

        my_car.brand, my_car.year

    );

}

В целом структуры отлично подходят для хранения вместе всей информации, относящейся к тому или иному типу объекта, для реализации и обращения к ней в программе.

Система сборки Rust: Cargo

Cargo  —  это система сборки и диспетчер пакетов Rust. Это важный инструмент для организации проектов на Rust. Здесь приводится перечень библиотек, необходимых проекту (они называются зависимостями). Он автоматически загружает любые отсутствующие зависимости и собирает программы на Rust из исходного кода.

Программы, с которыми мы имели дело до сих пор, достаточно просты, и поэтому зависимости для них не нужны. А вот при создании более сложных программ вам понадобится Cargo с возможностями инструментов, недоступных в рамках стандартной библиотеки. Cargo также используется для загрузки проектов в портфолио на GitHub, так как они хранят все части и зависимости вместе.

Если скачать Rust с официального сайта, Cargo автоматически устанавливается вместе с компилятором (rustc) и генератором документации (rustdoc) как часть набора инструментальных средств Rust. Убедиться, что Cargo установлен, помогает ввод в командной строке следующей команды:

$ cargo --version

Для создания проекта с Cargo запустите в интерфейсе командной строки операционной системы следующее:

$ cargo new hello_cargo

$ cd hello_cargo

Первой командой создается новый каталог hello_cargo. А второй командой этот новый каталог выбирается.

Генерируется манифест под названием Cargo.toml, который содержит все метаданные, необходимые Cargo для компиляции пакета, а также файл main.rs, отвечающий за компиляцию проекта.

Чтобы все это увидеть, наберите:

$ tree

Перейдите к местоположению вашего каталога и откройте файл Cargo.toml. Внутри вы найдете информацию о проекте. Выглядит это следующим образом:

[package]

name = "hello_cargo"

version = "1.43.0"

authors = ["Your Name <[email protected]>"]

edition = "2020"

[dependencies]

Все зависимости приведены в категории dependencies.

После завершения проекта введите команду $ cargo run: проект будет скомпилирован и запущен.

Продвинутые концепции для дальнейшего изучения

Несмотря на то, что многие из этих компонентов кажутся маленькими, с каждым из них можно шаг приблизиться к полному освоению Rust! Год от года Rust становится все более популярным, а это значит, что сейчас самое время обзавестись навыками для создания низкоуровневых систем будущего.

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи The Educative Team: An Introduction to Programming in Rust

Предыдущая статьяДолгожданные инструкции Switch-Case в Python
Следующая статьяТОП-5 законов для каждого UX-дизайнера