Введение в синтаксис Rust

https://mmhaskell.com/rust/syntax перевод серии статей.

Rust — очень интересный язык для сравнения с Haskell. У него похожий синтаксис. Но он не так похож, как, скажем, на Elm или Purescript. Rust также очень похож на C++. И его сходство с C++ — это то, в чем его сильные стороны.

В этой серии мы рассмотрим некоторые основы Rust. Мы рассмотрим такие вещи, как синтаксис и создание небольших проектов. В этой первой части мы проведем краткое высокоуровневое сравнение между Haskell и Rus и рассмотрим некоторые базовые примеры синтаксиса.

Почему Rust?

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

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

Однажды я предположил, что безопасность типов в Haskell подходит для программ, критичных для безопасности. В этой идее все еще есть какое-то содержание. Но конкретный пример, который я предложил, был беспилотным автомобилем, сложной системой реального времени. Но неизвестная производительность Haskell делает его плохим выбором для таких систем реального времени.

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

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

Сходства

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

Привет мир

С учетом всего сказанного, давайте приступим к написанию кода! Как и в случае с любым другим языком программирования, давайте начнем с быстрой программы «Hello World».

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

Сразу видно, что это больше похоже на программу на C++, чем на программу Haskell. Мы можем вызвать оператор печати без какого-либо упоминания IO монады. Мы видим фигурные скобки, используемые для ограничения тела функции, и точку с запятой в конце оператора. Если бы мы хотели, мы могли бы добавить больше операторов печати.

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

В сигнатуре типа этой main функции ничего нет. Но мы рассмотрим подробнее ниже.

Примитивные типы и переменные

Однако, прежде чем мы сможем перейти к сигнатурам типов, нам нужно лучше понять типы! Еще одна дань уважения C++ (или Java): Rust проводит различие между примитивными типами и другими более сложными типами. Мы увидим, что имена типов немного более сокращены, чем в других языках. Основные примитивы включают:

  1. Различные размеры целых чисел, знаковых и беззнаковых ( i32, u8и т.д.)

  2. Типы с плавающей запятой f32и f64.

  3. Логические ( bool)

  4. Символы ( char). Обратите внимание, что они могут представлять скалярные значения Unicode (т.е. за пределами ASCII).

В прошлый раз мы упоминали, что в Rust память важнее. Основное различие между примитивами и другими типами заключается в том, что примитивы имеют фиксированный размер. Это означает, что они всегда хранятся в стеке. Другие типы с переменным размером должны попадать в динамическую память. В следующий раз мы увидим, каковы некоторые из последствий этого.

Подобно do-syntax в Haskell, мы можем объявлять переменные с помощью let ключевого слова. Мы можем указать тип переменной после имени. Также обратите внимание, что мы можем использовать интерполяцию строк с помощью println.

fn main() {
let x: i32 = 5;
let y: f64 = 5.5;
println!("X is {}, Y is {}", x, y);
}

Пока очень похоже на C++. Но теперь давайте рассмотрим пару свойств, подобных Haskell. Хотя переменные имеют статическую типизацию, обычно нет необходимости указывать тип переменной. Это потому, что в Rust, как и в Haskell, есть вывод типов! Это станет более ясным, когда мы начнем писать сигнатуры типов в следующем разделе. Еще одно большое сходство заключается в том, что переменные по умолчанию неизменяемые. Учти это:

fn main() {
let x: i32 = 5;
x = 6;
}

Это вызовет ошибку! Как только x значение получает свое значение, мы не можем присвоить другое! Мы можем изменить это поведение, указав mut ключевое слово (изменяемое). Это просто работает с примитивными типами. Но, как мы увидим в следующий раз, с другими не все так просто! Следующий код компилируется нормально!

fn main() {
let mut x: i32 = 5;
x = 6;
}

Функции и сигнатуры типов

При написании функции мы указываем параметры так же, как в C++. В скобках указаны сигнатуры типов и имена переменных. Указывать типы сигнатур обязательно. Это позволяет выводам типов творить чудеса почти со всем остальным. В этом примере нам больше не нужны сигнатуры типов в main. Понятно по названию, printNumbers что x и y есть.

fn main() {
let x = 5;
let y = 7;
printNumbers(x, y);
}
fn printNumbers(x: i32, y: i32) {
println!("X is {}, Y is {}", x, y);
}

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

fn printNumbers(x: i32, y: i32) -> () {
println!("X is {}, Y is {}", x, y);
}

Однако мы также можем указать реальный возвращаемый тип. Обратите внимание, что здесь нет точки с запятой! Это важно!

fn add(x: i32, y: i32) -> i32 {
x + y
}

Это связано с тем, что значение должно возвращаться посредством выражения, а не оператора. Давайте разберемся в этом различии.

Утверждения против выражений

В Haskell большая часть нашего кода — это выражения. Они информируют нашу программу о том, что «такое» функция, а не дают набор шагов, которым нужно следовать. Но когда мы используем монады, мы часто используем в do синтаксисе что-то вроде операторов.

addExpression :: Int -> Int -> Int addExpression x y = x + y addWithStatements ::Int -> Int -> IO Int addWithStatements x y = do
putStrLn "Adding: "
print x
print y
return $ x + y

В Rust есть обе эти концепции. Но гораздо чаще смешивать операторы с вашими выражениями в Rust. Заявления не возвращают значений. Они заканчиваются точкой с запятой. Назначение переменных с помощью let и печать — это выражения.

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

fn main() {
let x = 45;
let y = branch(x);
}
fn branch(x: i32) -> i32 {
if x > 40 {
println!("Greater");
x * 2
} else {
x * 3
}
}

В отличие от Haskell, можно иметь if выражение без else ветки. Но в приведенном выше примере это не сработает, так как нам нужно возвращаемое значение! Как и в Haskell, все ветки должны иметь один и тот же тип. Если в ветвях есть только операторы, этот тип может быть ().

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

fn add(x: i32, y: i32) -> i32 {
x + y; // << Need to remove the semicolon! }

Это поведение сильно отличается от C++ и Haskell, поэтому нужно немного привыкнуть к нему!

Кортежи, массивы и фрагменты

Как и в Haskell, в Rust есть простые составные типы, такие как кортежи и массивы (в отличие от списков в Haskell). Однако эти массивы больше похожи на статические массивы в C++. Это означает, что они имеют фиксированный размер. Один интересный эффект от этого заключается в том, что массивы включают свой размер в свой тип. Между тем кортежи имеют сигнатуры типов, аналогичные Haskell:

fn main() {
let my_tuple: (u32, f64, bool) = (4, 3.14, true);
let my_array: [i8; 3] = [1, 2, 3];
}

Массивы и кортежи, состоящие из примитивных типов, сами по себе примитивны! В этом есть смысл, потому что они имеют фиксированный размер.

Еще одно понятие, относящееся к коллекциям, — это идея среза. Это позволяет нам смотреть на непрерывную часть массива. Тем не менее, для фрагментов используется оператор &. Почему еще — разберемся в следующей статье!

fn main() {
let an_array = [1, 2, 3, 4, 5];
let a_slice = &a[1..4]; // Gives [2, 3, 4] }

Управление памятью в Rust

Rust позволяет больше контролировать использование памяти, как C++. В C++ мы явно выделяем память в куче с помощью new и освобождаем ее с помощью delete. В Rust мы выделяем и освобождаем память в определенных точках нашей программы. Таким образом, в нем нет сборки мусора, как в Haskell. Но он работает не так, как C++.

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

Область действия (с примитивами)

Прежде чем мы перейдем к собственнику, мы хотим понять пару идей. Во-первых, давайте рассмотрим понятие объема. Если вы пишете код на Java, C или C++, это должно быть вам знакомо. Мы объявляем переменные в определенной области, например, в цикле for или в определении функции. Когда этот блок кода заканчивается, переменная выходит за рамки. Мы больше не можем получить к ней доступ.

int main() {
for (int i = 0; i < 10; ++i) {
int j = 0;
// Do something with j...
}
// This doesn't work! j is out of scope!
std::cout << j << std::endl;
}

Ржавчина работает точно так же. Когда мы объявляем переменную в блоке, мы не можем получить к ней доступ после завершения блока. (На таком языке, как Python, на самом деле это не так!)

fn main() {
let j: i32 = {
let i = 14;
i + 5
};
// i is out of scope. We can't use it anymore.
println!("{}", j);
}

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

fn main() {
let mut i: i32 = 10;
let j = i;
i = 15;
// Prints 15, 10
println!("{}, {}", i, j);
}

j Переменная является полной копией. Изменение значения i не меняет значения j. Теперь впервые поговорим о непримитивном типе String.

Тип строки

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

let mut my_string = "Hello";
my_string.append(" World!"); // << This doesn't exist for literals!

Вместо этого мы можем использовать String тип. Это непримитивный тип объекта, который выделяет память в куче. Вот как мы можем использовать это и добавить к одному:

let mut my_string = String::from("Hello");
my_string.push_str(" World!");

Теперь давайте рассмотрим некоторые последствия применения области видимости для типов объектов.

Объём со строками

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

fn main() {
let str_length = {
let s = String::from("Hello");
s.len()
}; // s goes out of scope here
// Fails!
println!("{}", s);
}

Что круто, так это то, что как только наша строка выходит за пределы области видимости, Rust очищает для нее кучу памяти! Нам не нужно вызывать, delete как в C++. Мы определяем очистку памяти для объекта, объявляя drop функцию. Мы рассмотрим это более подробно в следующей статье.

C++ не освобождает нас автоматически! В этом примере мы должны удалить myObject в конце for блока цикла. Мы не можем освободить его после, так что это приведет к утечке памяти!

int main() {
for (int i = 0; i < 10; ++i) {
// Allocate myObject
MyType* myObject = new MyType(i);
// Do something with myObject … // We MUST delete myObject here or it will leak memory!
delete myObject;
}
// Can't delete myObject here!
}

Так что приятно, что Rust выполняет удаление за нас. Но из этого есть несколько интересных выводов.

Копирование строк

Что происходит, когда мы пытаемся скопировать строку?

let len = {
let s1 = String::from("Hello");
let s2 = s1;
s2.len()
};

Эта первая версия работает нормально. Но надо подумать, что будет в этом случае:

let len = {
let mut s1 = String::from("123");
let mut s2 = s1;
s1.push_str("456");
s1.len() + s2.len()
};

Для людей, пришедших с C++ или Java, есть две возможности. Если копирование в s2 неглубокую копию, мы ожидаем, что общая длина будет равна 12. Если это глубокая копия, сумма должна быть 9.

Но этот код вообще не компилируется в Rust! Причина в собственности.

Право собственности

Глубокие копии часто намного дороже, чем думает программист. Таким образом, язык, ориентированный на производительность, такой как Rust, по умолчанию избегает использования глубокого копирования. Но давайте подумаем, что произойдет, если приведенный выше пример представляет собой простую неглубокую копию. Когда s1 и s2 выйдет за рамки, Rust обратится drop к ним обоим. И они освободят ту же память! Подобное «двойное удаление» — большая проблема, которая может привести к сбою вашей программы и вызвать проблемы с безопасностью.

Вот что могло бы произойти в Rust с приведенным выше кодом. Использование let s2 = s1 сделает мелкую копию. Так s2 будет указывать на ту же кучу памяти. Но в то же время, он будет недействительным в s1 переменной. Таким образом, когда мы пытаемся s1 передать значения, мы будем использовать недопустимую ссылку. Это вызывает ошибку компилятора.

Сначала s1 «владеет» памятью кучи. Поэтому, когда он s1 выходит за рамки, он освобождает память. Но объявление s2 передает s2 ссылку на владение этой памятью. Так s1 что теперь недействительно. У памяти может быть только один владелец. Это основная идея, с которой нужно познакомиться.

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

fn main() {
let s1 = String::from("Hello");
let length = add_to_length(s1);
// This is invalid! s1 is now out of scope!
println!("{}", s1);
}
// After this function, drop is called on s
// This deallocates the memory!
fn add_to_length(s: String) -> i32 {
5 + s.len()
}

Похоже, это было бы проблематично. Разве мы не захотим вызывать разные функции с одной и той же переменной в качестве аргумента? Мы могли бы обойти это, вернув ссылку через возвращаемое значение. Это требует, чтобы функция возвращала кортеж.

fn main() {
let s1 = String::from("Hello");
let (length, s2) = add_to_length(s1);
// Works
println!("{}", s2);
}
fn add_to_length(s: String) -> (i32, String) {
(5 + s.len(), s)
}

Но это громоздко. Есть способ получше.

Заимствование ссылок

Как и в C++, мы можем передавать переменную по ссылке. Для этого мы используем оператор амперсанда ( &). Это позволяет другой функции «заимствовать» право собственности, а не «брать» собственность. Когда это будет сделано, исходная ссылка останется действительной. В этом примере s1 переменная снова становится владельцем памяти после завершения вызова функции.

fn main() {
let s1 = String::from("Hello");
let length = add_to_length(&s1);
// Works
println!("{}", s1);
}
fn add_to_length(s: &String) -> i32 {
5 + s.len()
}

Это работает как const ссылка на C++. Если вам нужна изменяемая ссылка, вы также можете сделать это. Исходная переменная должна быть изменяемой, и тогда вы указываете ее mut в сигнатуре типа.

fn main() {
let mut s1 = String::from("Hello");
let length = add_to_length(&mut s1);
// Prints "Hello World!"
println!("{}", s1);
}
fn add_to_length(s: &mut String) -> i32 {
s.push_str(", World!");
5 + s.len()
}

Но есть одна большая загвоздка! У вас может быть только одна изменяемая ссылка на переменную за раз! В противном случае ваш код не будет компилироваться! Это помогает предотвратить большую категорию ошибок!

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

fn main() {
let s1 = String::from("Hello");
let s2 = s1.clone();
// Works!
println!("{}", s1);
println!("{}", s2);
}

Заметки о срезах

Мы можем подытожить пару мыслей о срезах. Срезы дают нам неизменяемую ссылку фиксированного размера на непрерывную часть массива. Часто мы можем использовать строковый литерал str как часть объекта String. Срезы — это либо примитивные данные, хранящиеся в стеке, либо они относятся к другому объекту. Это означает, что они не владеют и, следовательно, не освобождают память, когда выходят за пределы области видимости.

Типы данных Rust

В этой части мы изучим основы определения типов данных. Как мы уже видели, Rust сочетает в себе идеи как объектно-ориентированных языков, так и функциональных языков. Мы продолжим видеть эту тенденцию в том, как мы определяем данные. В Haskell появятся некоторые идеи, которые мы знаем и любим. Но мы также увидим некоторые идеи, которые исходят от C++.

Определение структуры

В Haskell есть один основной способ объявить новый тип данных: data ключевое слово. Мы также можем переименовывать типы определенным образом с помощью type и newtype, но data это суть всего этого. Rust немного отличается тем, что использует несколько разных терминов для обозначения новых типов данных. Все они соответствуют определенным структурам Haskell. Первый из этих условий — это struct.

Название struct является возвратом к C и C++. Но для начала мы можем рассматривать его как особый тип продукта в Haskell. То есть тип с одним конструктором и множеством именованных полей. Предположим, у нас есть User тип с именем, адресом электронной почты и возрастом. Вот как мы могли бы сделать этот тип структурой в Rust:

struct User {
name: String,
email: String,
age: u32,
}

Это очень похоже на следующее определение Haskell:

data User = User
{ name :: String
, email :: string
, age :: Int
}

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

let user1 = User {
name: String::from("James"),
email: String::from("james@test.com"),
age: 25,
};
println!("{}", user1.name);

Если мы объявляем экземпляр структуры изменяемым, мы также можем изменить значение его полей, если захотим!

let mut user1 = User {
name: String::from("James"),
email: String::from("james@test.com"),
age: 25,
};
user1.age = 26;

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

Кортежные структуры

В Rust также есть понятие «кортежная структура». Они похожи на структуры, за исключением того, что они не называют свои поля. Версия Haskell будет «непримечательным типом продукта». Это тип с одним конструктором, множеством полей, но без имен. Обратите внимание на это:

// Rust
struct User(String, String, u32);
-- Haskell
data User = User String String Int

Мы можем деструктурировать и сопоставить с образцом в структурах кортежей. Мы также можем использовать числа в качестве индексов с . оператором вместо имен пользовательских полей.

struct User(String, String, u32);
let user1 = User("james", "james@test.com", 25);
// Prints "james@test.com"
println!("{}", user1.1);

В Rust также есть идея «единичной структуры». Это тип, к которому не привязаны данные. Это кажется немного странным, но они могут быть полезны, как в Haskell:

// Rust
struct MyUnitType;
-- Haskell
data MyUnitType = MyUnitType

Перечисления

Последний основной способ создания типа данных — это перечисление. В Haskell мы обычно используем этот термин для обозначения типа, который имеет много конструкторов без аргументов. Но в Rust перечисление — это общий термин для типа с множеством конструкторов, независимо от того, сколько данных у каждого из них. Таким образом, он охватывает весь спектр того, что мы можем делать с data в Haskell. Рассмотрим этот пример:

// Rust
struct Point(f32, f32);
enum Shape {
Triangle(Point, Point, Point),
Rectangle(Point, Point, Point, Point),
Circle(Point, f32),
}
-- Haskell
data Point = Point Float Float
data Shape =
Triangle Point Point Point |
Rectangle Point Point Point Point |
Circle Point Float

Сопоставление с образцом не так просто, как в Haskell. Мы не делаем несколько определений функций с разными шаблонами. Вместо этого Rust использует match оператор, чтобы мы могли их отсортировать. Каждое совпадение должно быть исчерпывающим, хотя вы можете использовать его _ как подстановочный знак, как в Haskell. Выражения в совпадении могут использовать фигурные скобки или нет.

fn get_area(shape: Shape) -> f32 {
match shape {
Shape::Triangle(pt1, pt2, pt3) => {
// Calculate 1/2 base * height
},
Shape::Rectangle(pt1, pt2, pt3, pt4) => {
// Calculate base * height
},
Shape::Circle(center, radius) => (0.5) * radius * radius * PI
}
}

Обратите внимание, что мы должны поместить имена конструкторов в пространство имен! Пространство имен — это один из элементов, который кажется более знакомым по C++. Посмотрим на другое.

Блоки реализации

До сих пор мы рассматривали наши новые типы только как тупые данные, как в Haskell. Но в отличие от Haskell, Rust позволяет нам прикреплять реализации к структурам и перечислениям. Эти определения могут содержать методы экземпляра и другие функции. Они действуют как определения классов из C++ или Python. Мы начинаем раздел реализации с impl ключевого слова.

Как и в Python, у любого метода «экземпляра» есть параметр self. В Rust эта ссылка может быть изменяемой или неизменной. (В C++ он называется this, но это неявный параметр методов экземпляра). Мы вызываем эти методы, используя тот же синтаксис, что и C++, с .оператором.

impl Shape {
fn area(&self) -> f32 {
match self {
// Implement areas
}
}
}
fn main() {
let shape1 = Shape::Circle(Point(0, 0), 5);
println!("{}", shape1.area());
}

Мы также можем создавать «связанные функции» для наших структур и перечислений. Это функции, которые не принимают self в качестве параметра. Они похожи на статические функции в C++ или любую функцию, которую мы бы написали для типа в Haskell.

impl Shape {
fn shapes_intersect(s1: &Shape, s2: &Shape) -> bool
}
fn main() {
let shape1 = Shape::Circle(Point(0, 0), 5);
let shape2 = Shape::Circle(Point(10, 0), 6);
if Shape::shapes_intersect(&shape1, &shape2) {
println!("They intersect!");
} else {
println!("No intersection!");
};
}

Обратите внимание, что нам все еще нужно пространство имен для имени функции, когда мы ее используем!

Общие типы

Как и в Haskell, мы также можем использовать общие параметры для наших типов. Давайте сравним определение Haskell Maybeс типом Rust Option, который делает то же самое.

// Rust
enum Option<T> {
Some(T),
None,
}
-- Haskell
data Maybe a =
Just a |
Nothing

Здесь не сильно отличается, кроме синтаксиса.

Мы также можем использовать универсальные типы для функций:

fn compare<T>(t1: &T, t2: &T) -> bool

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

Черты

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

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

struct Point(f32, f32);
struct Rectangle {
top_left: Point,
top_right: Point,
bottom_right: Point,
bottom_left: Point,
}
struct Triangle {
pt1: Point,
pt2: Point,
pt3: Point,
} struct Circle {
center: Point,
radius: f32,
}

Теперь мы можем создать черту для расчета площади, и позволить каждой форме реализовать эту черту! Вот как выглядит синтаксис для его определения и последующего использования в универсальной функции. Мы можем ограничить, какие дженерики может использовать функция, как в Haskell:

pub trait HasArea {
fn calculate_area(&self) -> f32;
}
impl HasArea for Circle {
fn calculate_area(&self) -> f32 {
self.radius * self.radius * PI
}
}
fn double_area<T: HasArea>(element: &T) -> f32 {
2 * element.calculate_area()
}

Также, как и в Haskell, мы можем получить определенные черты с помощью одной строки! Эта Debug черта работает так Show:

#[derive(Debug)] struct Circle

Что дальше

Это должно дать нам более полное представление о том, как мы можем определять типы данных в Rust. Мы видим интересное сочетание концепций. Некоторые идеи, например методы экземпляра, пришли из объектно-ориентированного мира C++ или Python. Другие идеи, такие как сопоставимые перечисления, пришли из более функциональных языков, таких как Haskell.

Управление пакетами

Теперь мы собираемся взглянуть на более практическую сторону вещей. Мы рассмотрим, как на самом деле создать проект с помощью Cargo. Cargo — это система сборки и менеджер пакетов Rust. Это аналог Stack and Cabal в Rust. Мы рассмотрим создание, сборку и тестирование проектов. Мы также рассмотрим, как добавить зависимости и связать наш код вместе.

Cargo

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

cargo new my_first_rust_project

Это создает простое приложение с несколькими файлами. Первый из них Cargo.toml. Это файл описания нашего проекта, сочетающий в себе роли .cabal файла и stack.yaml. Его первоначальный макет на самом деле довольно прост! У нас есть четыре строки, описывающие наш пакет, а затем пустой раздел зависимостей:

[package]name = "my_first_rust_project"
version = "0.1.0"
authors = ["Your Name <you@email.com>"]edition = "2018"
[dependencies]

Инициализация Cargo предполагает, что вы используете Github. Он извлечет ваше имя и адрес электронной почты из глобальной конфигурации Git. Он также создает для вас .git каталог и .gitignore файл.

Единственный другой файл, который он создает, — это src/main.rs файл с простым приложением Hello World:

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

Строительство и запуск

Cargo, конечно, тоже может построить наш код. Мы можем запустить cargo build, и это скомпилирует весь код, необходимый для создания исполняемого файла main.rs. В Haskell наши артефакты сборки попадают в .stack-work каталог. Cargo помещает их в target каталог. Наш исполняемый файл оказывается внутри target/debug, но мы можем запустить его с помощью cargo run.

Также есть простая команда, которую мы можем запустить, если только хотим проверить, компилируется ли наш код. Использование cargo check будет проверять все без создания исполняемых файлов. Это работает намного быстрее, чем обычная сборка. Вы можете сделать это с помощью Stack, используя GHCI и перезагрузив код с помощью :r.

Как и большинство хороших систем сборки, Cargo может определять, изменились ли какие-либо важные файлы. Если мы запустим cargo build и файлы изменились, то он не будет повторно компилировать наш код.

Добавление зависимостей

Теперь давайте посмотрим на пример использования внешней зависимости. Мы будем использовать rand ящик для генерации случайных значений. Мы можем добавить его в наш Cargo.toml файл, указав конкретную версию:

[dependencies]rand = "0.7"

Rust использует семантическое управление версиями, чтобы гарантировать отсутствие конфликтов между зависимостями. Он также использует .lockфайл, чтобы гарантировать воспроизводимость ваших сборок. Но (по крайней мере, насколько мне известно) в Rust пока нет ничего подобного Stackage. Это означает, что вы должны указать фактические версии для всех ваших зависимостей. Кажется, это одна из областей, в которой у Stack есть явное преимущество.

Теперь «rand» в данном случае — это имя «ящика». Крейт — это либо исполняемый файл, либо библиотека. В этом случае мы будем использовать его как библиотеку. «Пакет» — это набор ящиков. Это чем-то похоже на пакет Haskell. Мы можем указать различные компоненты в нашем .cabal файле. У нас может быть только одна библиотека, но много исполняемых файлов.

Теперь мы можем включить случайную функциональность в наш исполняемый файл Rust с помощью use ключевого слова:

use rand::prelude::Rng;
fn main() {
let mut rng = rand::thread_rng();
let random_num: i32 = rng.gen_range(-100, 101);
println!("Here's a random number: {}", random_num);
}

Когда мы указываем импорт, rand это имя ящика. Тогда prelude это имя модуля и Rng имя трейта, который мы будем использовать.

Создание библиотеки

Теперь давайте улучшим наш проект, добавив небольшую библиотеку. Запишем этот файл в формате src/lib.rs. По соглашению Cargo этот файл будет скомпилирован в библиотеку нашего проекта. Мы можем очертить различные «модули» в этом файле, используя mod ключевое слово и называя блок. Мы можем раскрыть функцию в этом блоке, объявив ее с помощью pub ключевого слова. Вот модуль с простой функцией удвоения:

pub mod doubling {
pub fn double_number(x: i32) -> i32 {
x * 2
}
}

Мы также должны сделать сам модуль pub для экспорта и использования! Чтобы использовать эту функцию в нашем основном двоичном файле, нам нужно импортировать нашу библиотеку. Обращаемся к ящику с библиотекой именем нашего проекта. Затем мы назначаем импорт по модулю и выбираем конкретную функцию (или, *если хотите).

use my_first_rust_project::doubling::double_number;
use rand::prelude::Rng;
fn main() {
let mut rng = rand::thread_rng();
let random_num: i32 = rng.gen_range(-100, 101);
println!("Here's a random number: {}", random_num);
println!("Here's twice the number: {}", double_number(random_num));
}

Добавление тестов

Конечно, Rust также позволяет проводить тестирование. В отличие от большинства языков, Rust имеет соглашение о размещении модульных тестов в том же файле, что и исходный код. Они входят в другой модуль этого файла. Чтобы сделать тестовый модуль, мы ставим cfg(test)перед ним аннотацию. Затем мы отмечаем любую тестовую функцию test аннотацией.

// Still in lib.rs!
#[cfg(test)]mod tests {
use crate::doubling::double_number;
#[test] fn test_double() {
assert_eq!(double_number(4), 8);
assert_eq!(double_number(5), 10);
}
}

Обратите внимание, что он по-прежнему должен импортировать наш другой модуль, даже если он находится в том же файле! Конечно, интеграционные тесты должны быть в отдельном файле. Cargo по-прежнему понимает, что если мы создаем tests каталог, он должен искать там тестовый код.

Теперь мы можем запускать наши тесты с помощью cargo test. Благодаря аннотациям Cargo не будет тратить время на компиляцию нашего тестового кода при запуске cargo build. Это помогает сэкономить время.

Добавить комментарий

Ваш адрес email не будет опубликован.

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.