Pull to refresh

Rust на примерах. Часть 1

Reading time7 min
Views49K
Original author: Jorge Aparicio
Этот цикл статей является вольным переводом книги «Rust by Example», которую пишет Хорхе Апарисио на Github.

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

Давайте начинать!

Содержание


  1. Привет, мир!
  2. Форматированный вывод
  3. Литералы и операторы
  4. Переменные
  5. Типы

Примечание:
  1. При написании программ использовался компилятор версии Nightly (0.10), не забудьте про это.
  2. Запустить код можно в Rust Playpen (кнопка «evaluate»): http://play.rust-lang.org

1. Привет, мир!


Это код традиционной программы «Hello World»:
// Комментарии игнорируются компилятором

// Это основная функция
fn main() {
    // Вывести текст в консоль
    println!("Hello World!");
}

println! это макрос (мы рассмотрим их позже), который печатает текст в консоль.

Программа может быть сгенерирована с помощью компилятора Rust rustc:
$ rustc hello.rs

rustc создаст бинарный файл «hello», который можно запустить:
$ ./hello
Hello World!

2. Форматированный вывод


Макрос println! не только выводит в консоль, а также способен форматировать текст и сериализованные значения. Корректность проверяется во время компиляции.

fn main() {
    // `print!`, как `println!`, но он не добавляет новую строку в конце
    print!("January has ");

    // `{}` это заполнители для аргументов, которые будут строками
    println!("{} days", 31i);
    // `i` суффикс указывает компилятору, что этот литерал имеет тип: целое
    // число со знаком, смотрите следующую главу для более подробной информации

    // Позиционные аргументы могут быть повторно использованы по шаблону
    println!("{0}, this is {1}. {1}, this is {0}", "Alice", "Bob");

    // Аргументы можно называть
    println!("{subject} {verb} {predicate}",
             predicate="over the lazy dog",
             subject="the quick brown fox",
             verb="jumps");

    // Специальное форматирование может быть указано в заполнителе после `:`, `t` это бинарное представление
    println!("{} of {:t} people know binary, the other half don't", 1i, 2i);

    // Ошибка! Не хватает аргумента для вывода
    println!("My name is {0}, {1} {0}", "Bond");
    // ИСПРАВЬТЕ ^ добавьте отсутствующий аргумент: "James"
}

Дополнительная информация о форматировании здесь: std​::fmt​

3. Литералы и операторы


Целые числа 1, с плавающей точкой 1.2, символы 'a', строки "abc", логические true и значения пустого типа () могут быть выражены с помощью литералов.

Также целые числа можно выразить через шестнадцатеричное, восьмеричное или двоичное обозначение, используя один из префиксов: 0x, 0o или 0b.

В числовые литералы можно вставлять подчёркивания для читабельности, например, 1_000 такой же, как и 1000, а 0.000_001 такой же, как и 0.000001.

Мы должны сказать компилятору, какой из литералов мы используем. Сейчас мы будем использовать суффикс u, указывающий, что литерал является целым числом без знака, суффикс i чтобы указать, что это знаковое целое число. Мы рассмотрим систему типов в 5 главе, а также подробную информацию о аннотировании литералов.

Доступные операторы и их приоритет похож на C-подобных языках.

fn main() {
    // Целочисленное сложение
    println!("1 + 2 = {}", 1u + 2);

    // Вычитание
    println!("1 - 2 = {}", 1i - 2);
    // Попробуйте изменить `1i` на `1u` и понять, почему тип важен

    // Булева логика
    println!("true AND false is {}", true && false);
    println!("true OR false is {}", true || false);
    println!("NOT true is {}", !true);

    // Битовые операции
    println!("0011 AND 0101 is {:04t}", 0b0011u & 0b0101);
    println!("0011 OR 0101 is {:04t}", 0b0011u | 0b0101);
    println!("0011 XOR 0101 is {:04t}", 0b0011u ^ 0b0101);
    println!("1 << 5 is {}", 1u << 5);
    println!("0x80 >> 2 is 0x{:x}", 0x80u >> 2);

    // Используйте подчеркивания, чтобы улучшить читаемость
    println!("One million is written as {}", 1_000_000u);
}

4. Переменные


Значения (как и литералы) могут быть связаны с переменными, используя обозначение let.

fn main() {
    let an_integer = 1u;
    let a_boolean = true;
    let unit = ();

    // скопировать значение `an_integer` в `copied_integer`
    let copied_integer = an_integer;

    println!("An integer: {}", copied_integer);
    println!("A boolean: {}", a_boolean);
    println!("Meet the unit value: {}", unit);

    // Компилятор предупреждает о неиспользуемых переменных; эти предупреждения можно
    // отключить используя подчёркивание перед именем переменной
    let _unused_variable = 3u;
    let noisy_unused_variable = 2u;
    // ИСПРАВЬТЕ ^ Добавьте подчёркивание
}

4.1 Изменяемость

По умолчанию переменные нельзя изменять, но это можно исправить, добавив модификатор mut.

fn main() {
    let _immutable_variable = 1i;
    let mut mutable_variable = 1i;

    println!("Before mutation: {}", mutable_variable);

    // Ок
    mutable_variable += 1;

    println!("After mutation: {}", mutable_variable);

    // Ошибка!
    _immutable_variable += 1;
}

Компилятор будет выводить сообщения об ошибке изменчивости.

4.2 Области и видимость

Переменные имеют локальную область, и имеют видимость в блоке (блок представляет собой набор операторов, заключённых в фигурные скобки {}). Кроме того, допускается скрытие переменной.

fn main() {
    // Эта переменная живет в области функции main
    let long_lived_variable = 1i;

    // Это блок, он имеет меньший объем нежели основная функция
    {
        // Эта переменная существует только в этом блоке
        let short_lived_variable = 2i;

        println!("inner short: {}", short_lived_variable);

        // Эта переменная не видна внешней функции
        let long_lived_variable = 5_f32;

        println!("inner long: {}", long_lived_variable);
    }
    // Конец блока

    // Ошибка! `short_lived_variable` не существует в этой области
    println!("outer short: {}", short_lived_variable);
    // ИСПРАВЬТЕ ^ Закомментируйте строку

    println!("outer long: {}", long_lived_variable);
}

4.3 Предварительное объявление

Можно сперва объявлять переменные, а инициализировать их позже. Но эта форма редко используется, так как это может привести к использованию неинициализированных переменных.

fn main() {
    // Объявляем переменную
    let a_variable;

    {
        let x = 2i;

        // Инициализируем переменную
        a_variable = x * x;
    }

    println!("a variable: {}", a_variable);

    let another_variable;

    // Ошибка! Использование неинициализированной переменной
    println!("another variable: {}", another_variable);
    // ИСПРАВЬТЕ ^ Закомментируйте строку

    another_variable = 1i;

    println!("another variable: {}", another_variable);
}

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

5. Типы


Rust обеспечивает безопасность с помощью статической проверки типов. Тип переменной может быть явно указан при её объявлении. Тем не менее, в большинстве случаев, компилятор сможет определить тип переменной из контекста, если это очевидно.

fn main() {
    // Аннотированный тип переменной
    let a_float: f64 = 1.0;

    // Эта переменная типа `int`
    let mut an_integer = 5i;

    // Ошибка! Тип переменной нельзя изменять
    an_integer = true;
}

Это краткое изложение примитивных типов в Rust:
  • целые числа: i8, i16, i32, i64 и int (размер зависит от платформы)
  • целые числа без знака: u8, u16, u32, u64 и uint (размер зависит от платформы)
  • с плавающей точкой: f32, f64
  • char значения Unicode: 'a', 'α' и '∞' (4 байта каждый)
  • bool true или false
  • кортежи ()

5.1 Приведение типов

Rust не предоставляет неявного преобразования типов (coercion) между примитивами, но, явное приведение типов (casting) может быть достигнуто с помощью ключевого слова as.

fn main() {
    let decimal = 65.4321_f32;

    // Ошибка! Нет неявного преобразования
    let integer: u8 = decimal;
    // ИСПРАВЬТЕ ^ Закомментируйте строку

    // Явное преобразование
    let integer = decimal as u8;
    let character = integer as char;

    println!("Casting: {} -> {} -> {}", decimal, integer, character);
}

5.2 Литералы

В числовых литералах тип может быть аннотирован, добавив тип в качестве суффикса, за исключением uint, использующей суффикс u и int, который использует суффикс i.

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

fn main() {
    // Литералы с суффиксами, их вид известен при инициализации
    let x = 1u8;
    let y = 2u;
    let z = 3f32;

    // Литералы без суффикса, их вид зависит от того, как они используются
    let i = 1;
    let f = 1.0;

    // `size_of_val` возвращает размер переменной в байтах
    println!("size of `x` in bytes: {}", std::mem::size_of_val(&x));
    println!("size of `y` in bytes: {}", std::mem::size_of_val(&y));
    println!("size of `z` in bytes: {}", std::mem::size_of_val(&z));
    println!("size of `i` in bytes: {}", std::mem::size_of_val(&i));
    println!("size of `f` in bytes: {}", std::mem::size_of_val(&f));

    // Ограничения (слагаемые должны иметь тот же тип) для `i` и `f`
    let _constraint_i = x + i;
    let _constraint_f = z + f;
    // Закомментируйте эти две строки
}

Есть некоторые понятия, используемые в предыдущем коде, которые не были объяснены раньше, вот краткое объяснение для нетерпеливых читателей:
  • fun(&foo) используется, чтобы передать аргумент в функцию по ссылке, а не по значению fun(foo).
  • std::mem::size_of_val является функцией, но вызывается с указанием полного пути. Код можно разделить на логические единицы, называемые модулями. Здесь функция size_of_val определена в модуле mem, а модуль mem определен в крэйте std.

5.3 Логический вывод

Логический вывод типов довольно умён. Тип добавляемой переменной используется как определитель типа для второй переменной. Вот продвинутый пример:
fn main() {
    // Использование локального вывода, компилятор знает, что `elem` имеет тип `u8`
    let elem = 5u8;

    // Создадим пустой вектор (расширяемый массив)
    let mut vec = Vec::new();
    // В этот момент компилятор не знает точный тип `vec`, он
    // просто знает, что это вектор `Vec<_>`

    // Вставим `elem` в вектор
    vec.push(elem);
    // Ага! Теперь компилятор знает, что `vec` это вектор `u8` (`Vec<u8>`)
    // Попробуйте закомментировать строку `vec.push(elem)`

    println!("{}", vec);
}

Отсутствует необходимость в аннотации типа переменной, компилятор счастлив как и программист!

5.4 Псевдонимы (алиасы)

Оператор type может быть использован, чтобы задать новое имя существующему типу. Тип должен быть в стиле CamelCase, либо компилятор выдаст предупреждение. Исключением из этого правила являются примитивные типы: uint, f32 и другие.

// `NanoSecond` это новое имя для `u64`
type NanoSecond = u64;
type Inch = u64;

// Используйте этот атрибут, чтобы не выводить предупреждение
#[allow(non_camel_case_types)]
type uint64_t = u64;
// Попробуйте удалить атрибут

fn main() {
    // `NanoSecond` = `Inch` = `uint64_t` = `u64`
    let nanoseconds: NanoSecond = 5 as uint64_t;
    let inches: Inch = 2 as uint64_t;

    // Обратите внимание, что псевдонимы новых типов не предоставляют
    // дополнительную безопасность, из-за того, что они не нового типа
    println!("{} nanoseconds + {} inches = {} unit?",
             nanoseconds,
             inches,
             nanoseconds + inches);
}

Основное применение псевдонимов это снижение количества кода, например, тип IoResult является псевдонимом типа Result<T, IoError>.

Заключение


Присоединяйтесь к google-группе: Rust по-русски для получения дополнительной информации по этому языку.
Можно помочь с переводом на Github: github.com/eg0r/rust-by-example

Все замечания, ошибки или неточности отправляйте мне в почту.
Tags:
Hubs:
+39
Comments9

Articles

Change theme settings