Дмитpий Hecтepук

Блог о программировании — C#, F#, C++, архитектура, и многое другое

Posts Tagged ‘rust

.NET-ный enum в стиле Rust

2 комментария

Как вы уже наверное знаете, у разных языков (C#, Java, Rust, …) понимание того что такое enum (перечисление) абсолютно разное. Иногда мне в C# хочется чтобы было как в Rust, да и в C++ тоже. Поэтому это рассказ про то, как сделать «хорошо» на примере C#.

Однородный тип

Давайте начнем. Например, чтобы писать

Color color = Color.Red;

нужно чтобы Color.Red имел тот же тип что и Color. То есть, если утрировать, можно и так написать:

public class Color
{
  private byte r, g, b, a;
  public Color(byte r, byte g, byte b, byte a)
  {
    this.r = r;
    this.g = g;
    this.b = b;
    this.a = a;
  }
  public static Color Red = new Color(255, 0, 0, 255);
}

Вопрос теперь только в том как генерить все это многообразие. В Rust ничего особо писать не надо, а в C#, как видите, нужно сделать очень много телодвижений. Но результат практически тот же.

Что-то мы потеряли, а что-то приобрели. Потеряли мы все Enum.GetValues(), т.е. возможность получить все предопределенные цвета. Как их вернуть? Ну наверное как-то вот так:

public static Color Red = new Color(255, 0, 0, 255);
public static Color Blue = new Color(0, 0, 255, 255);
public static Color[] Values = new[] {Red, Blue};

Что ещё продолбано? Ну, имена этих переменных. Ой! А вот это непросто будет сделать:

private BiDictionary<string, Color> colors = new BiDictionary<string, Color>
{
  ["Red"] = Red,
  ["Blue"] = Blue
};
public IEnumerable<string> Names => colors.Keys;
public IEnumerable<Color> Values => colors.Values;
public Color this[string name] => colors[name];
public string this[Color color] => colors.Reverse[color]; // возможно излишне?
private string ToStringImpl()
{
  return $"{nameof(r)}: {r}, {nameof(g)}: {g}, {nameof(b)}: {b}, {nameof(a)}: {a}";
}
public override string ToString()
{
  if (colors.Reverse.ContainsKey(this)) return colors.Reverse[this];
  return ToStringImpl();
}

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

Итак, всё ли мы реализовали? Да вроде да, можно использовать

Color red = Color.Red;
Color my = new Color(255, 0, 255, 0);

и вывод будет вот такой:

Red
r: 255, g: 0, b: 255, a: 0

Дискриминированное объединение

Пока все просто, давайте пример посложнее уже. Типичный пример вычисления со всякой неоднородной начинкой выглядит как-то так:

type IntOrBool =
  | Boolean of bool
  | Integer of int // это не раст, это F#

Ха! То есть смотрите, у вас есть два абсолютно разных типа, с разными данными, и они должны приводиться к некому типу IntOrBool. Первое что приходит в голову — это абстрактный класс вроде

public abstract class IntOrBool
{
  public int Tag => this is Integer ? 0 : 1;
  public bool IsInteger => this is Integer;
  public bool IsBoolean => this is Boolean;
  public static Boolean NewBoolean(bool value) => new Boolean(value);
  public static Integer NewInteger(int value) => new Integer(value);
}

Ну и соответственно каждый из наследников должен выглядеть как-то вот так:

public class Integer : IntOrBool
{
  public int Value { get; set; }
  public Integer(int value)
  {
    Value = value;
  }
  // делаем вид что мы int
  public static implicit operator Integer(int value)
  {
    return new Integer(value);
  }
  public static implicit operator int(Integer integer)
  {
    return integer.Value;
  }
}

Но дальше у нас те же проблемы что и ранее — как сделать ToString(), как вывести список всех возможных типов объединения? Мы точно знаем как это делает F# — он для этого использует атрибуты, и по сути поиск всех классов превращается в использование Reflection. Что уныло. А что если сделать вот так?

public static Type[] Cases = new[] {typeof(Boolean), typeof(Integer)};

В конечном счете, что мы теряем? Ладно, пора уже попробовать использовать все это в типичном (насколько это возможно для такого синтетического примера) сценарии:

IntOrBool foo = 0; // FAIL :(

Опа, уже нифига не работает. Хотели удобство а получилось не очень. Что, еще операторов? Пожалуйста

public static implicit operator IntOrBool(int value) => NewInteger(value);
public static implicit operator IntOrBool(bool value) => NewBoolean(value);

ок, теперь можно инициализировать, а можно ли проверять if-ом?

IntOrBool foo = 0;
if (foo is Integer bar)
{
  int i = bar;
  WriteLine(i);
}

Кривоватенько, но работает. foo is int bar конечно же писать нельзя. Ну и выборку по шаблону в стиле case Integer(i) пока тоже в C# не сделали.

Да, насчет ToString() — тут будет просто GetType().ToString() для кейсов у которых нет собственных данных. То есть будут порождаться типы даже там, где они не нужны. Для F# это как бы норма (в F# например, любой переданный оператор превращается в struct), но в C# это не очень хорошо, хотя опять же, ничего критичного.

Вообщем вот такой финт ушами, не дающий особых плющек, т.к. ко всему этому нужен умный switch и полноценный pattern matching. Да, и Deconstruct(), если вы его реализуете, не поможет для типов которые имеют один member. Сорри! ■

Реклама

Written by Dmitri

14 апреля 2017 at 0:47

Опубликовано в C#

Tagged with

Заметки про Rust

4 комментария

Содержание

Я не знаю как с экономической точки зрения в России можно не работать треть месяца, но ладно — для многих водка решает, а тех кому нечем заняться ждет обзор языка Rust с точки зрения человека который пишет в основном на C++/C# (хотя я теперь модный и знаю сколько-то Kotlin’а, что тоже полезно, т.к. Java трогать ну совсем не хочется).

Так вот, давайте по порядку и с заголовками, чтобы красиво было. (Да, подсветка синтаксиса тут во F#-у, т.к. wordpress.com пока не разродился на Rust.)

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

Начнем с целочисленных типов: тут все хорошо и стандартизированно: вместо всяких unsigned long long которые в тех же плюсах вообще непонятно что значат, в Rust тип u8 значит ‘unsigned и 8 битов’ что очень понятно, в принципе, ну и по аналогии i32 это 32-битный int.

Есть также usize/isize – это биты размера системы, то есть если вы на 64-бит (я надеюсь), и что хорошо, так это то что в отличии от C++, где есть весьма существенные непонятки с типом size_t, а также языками C++/C#/Java, где индекс массива может быть signed, в Rust-е индекс массива это строго usize. Это же здорово!

Да, насчет чисел с плавающей точкой — тут f32/f64, все просто.

Операторы

Все как в С-образных языках, но жестко выпилены операторы ++/--, а логические операторы можно применять только к булевым переменным и, наоборот, побитовые – только к целочисленным (то есть к f32/64 не получится). Это хорошо т.к. снижает кол-во возможных непоняток.

Переменные

Объявление переменной в Rust несложно, например:

let mut a:i32 = 2+3*4;

Тип, как видите, идет после названия. В примере выше он не нужен т.к. type inference в Rust очень умен и умеет делать вывод на этапе вызова (я позже покажу какого это).

Переменные в Rust — хитрые твари потому что:

  • Примитивы выделаются на стэке если только ты их вручную не задвинешь в кучу через Box<>
  • By default немутабельны, но могут быть сделаны таковыми с помощью ключевого слова mut
  • Владеют той памятью на которую ссылаются. Об этом позже т.к. идея сложная

Помимо локальных есть еще глобальные переменные:

  • Константы которые просто инлайнятся и сами адреса не имеют
    const MEANING_OF_LIFE:u8 = 42;
    
  • Статические переменные, которые можно даже делать мутабельными, но если ты так сделал – то придется ващевеськод оборачивать в unsafe т.к. система не может мониторить что ты правильно работаешь с глобальной переменной, в которую вообще все могут писать
    static mut Z:i32 = 123;
    

И да, язык хочет чтобы глобальные переменные были константами. И вообще, code style inspections встроены в сам компилятор что, имхо, дурной тон и так вообще не надо делать.

Control flow

If

Тут много интересного. Во-первых, нет тернарного оператора ?: — всесто этого if возвращает значение:

let day = if temp > 20 {"sunny"} else {"cloudy"};

Где return, спросите вы? А вот… в старой доброй традиции терминального ввода, MATLAB’а и прочих, правило Rust такое – если после чего-то нет точки с запятой ;, то это – return value. По мне так достаточно косячно т.к. ну очень плохо читается. Вариант использовать return тоже есть, конечно, но уверен что растаманы будут ругаться.

Да, эти if-ы всегда требуют фигурные скобки вокруг блоков, но зато не требуют круглых скобок вокруг проверки условия.

While/loop

Что касается while, то тут все то же самое, continue и break работают. А вот do-while не сделали, зато сделали loop который чем-то аналогичен while(true) или for(;;). И это в языке который печется о безопасности.

For

Цикл for работает как-то вот так:

for x in 1..11 
{
  // skip 3
  if (x == 3) { continue; }
  // stop at 7
  if (x == 8) { break; }
  println!("x = {}", x);
}

Как вы догадались, 1..11 это range, то есть набор чисел от 1 до 10 включительно. Такой вот «обходной» подход к for сравним с обычным IEnumerable, то есть ничего особенного, просто очередная редукция энтропии. Кстати, 11..1 не получится и приведет к косякам, ну и шаг хождения (как 1:101:2 в MATLAB) тоже указать нельзя.

А да, и если нужен индекс итерируемого элемента, есть специальный метод который вернет вам не просто элемент, а кортеж индекс-элемент:

for (pos,y) in (30..41).enumerate()
{
  println!("{}: {}", pos, y);
}

Match

Теперь насчет switch — его нет, есть match, и он раз в 100 мощнее. Ну вот например он умеет обрабатывать range’ы:

let country_code = 999;
let country = match country_code 
{
  44 => "UK",
  46 => "Sweden",
  7 => "Russia",
  1...1000 => "unknown",
  _ => "invalid" // try commenting this out - must cover all cases!
};

И да, он тоже, как и if возвращает значение, которое можно присвоить. Только вот тут есть один косяк… приглядитесь! Видете диапазон 1...1000? Там три точки. А ранее было две! И ранее 1..11 означало от 1 до 10 включительно, а тут — от 1 до 1000 включительно. Такой вот когнитивный диссонанс. И причем авторы языка это специально сделали, «чтобы люди не путались». Ну-ну.

Структуры данных

Массивы

Начнем с массивов. Ну вот как-то так:

let mut a/*:[i32;5]*/ = [1,2,3,4,5];

Тип массива я, опять же, закомментил — вывод типов и тут работает на ура. Массив на стэке. Можно менять по индексу, при неправильном индексе получим панику. Есть метод len() для длины, т.е. ходить по массиву можно так:

let b = [1u16; 10];
for i in 0..b.len()
{
  println!("{}", b[i]);
}

Не знаю как вам, а мне форма записи 0..b.len() как-то не очень. Зато синтаксис выше — [1u16; 10] — это для заполнения десяти элементов массива одним и тем же значением. Удобно.

Многомерные массивы тоже реальны как массивы массивов:

let mtx:[[f32; 3]; 2] = 
[
  [1.0, 0.0, 0.0],
  [0.0, 2.0, 0.0]
];
println!("{:?}", mtx);

Слайсы

Слайсы – это как бы ссылки на куски массива, т.е. диапазоны памяти. То есть, если у нас есть массив, можно его кусок одолжить и че-то с ним поделать:

fn use_slice(slice: &mut [i32])
{
  println!("first elem is {}, len = {}", slice[0], slice.len());
  slice[0] = 4321;
  // will crash
  //let z = slice[10];
}
fn slices()
{
  // a slice is part of an array
  // its size is not known at compile time
  let mut data = [1,2,3,4,5];
  
  // start w/o mut, borrow as a slice
  use_slice(&mut data[1..4]);
  use_slice(&mut data); // entire array
  println!("data after slice use = {:?}", data);
}

В отличии от массивов, слайсы могут быть хз какого размера.

Вектора

Вектор — дженерик тип Vec — это динамический массив, причем в куче. Можно добавлять и удалять данные.

let mut a = Vec::new();
a.push(123);
println!("a = {:?}", a);
let idx:usize = 0;
println!("a[0] = {}", a[idx]);

Из кода выше должен возникнуть резонный вопрос: где название типа данных? Почему мы можем вообще вызывать Vec::new() если нигде нет аннотаций типа? Ответ на этот вопрос – очень умный вывод типов Rust’а.

Да, поскольку в Rust есть Option<T>, доступ к элементам безопасен и может быть сделан вот так:

match a.get(5)
{
  Some(x) => println!("a[5] = {}", x),
  None => println!("error, no such element")
}

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

Да, насчет итерации вектора, тут все просто:

for x in &a { println!("{}", x); }

Вот этот амперсанд & перед названием ветора перед операции означает «одолжить». То есть на момент итерации, вектор «одолжен». Да, и варианта менять вектор во время итерации тут нет, спасибо Rust’у с его безопасностью.

Что еще? Вектор ведет себя как стэк (т.е. имеет push/pop методы). Вот если нужно например его опустошить в обратном порядке, можно так:

while let Some(x) = a.pop()
{
  println!("{}", x);
}

Выше — немного магии. Как это у нас в while внезапно не true/false а Option<T>. А вооот! Rust поддерживает if let/while let, которые проверяют на None/Some<T>. Очень удобно!

Строки

Тут все хорошо и плохо одновременно. Хорошо потому что Rust гарантирует что строки это всегда валидные UTF-8 последовательности, а плохо потому что есть два типа строк — String и &str.

Начнем с &str — это т.н. ‘string slice’, но не в классическом смысле: слайс строки как Vec<u8> крайне бессмысленнен т.к. это байты Юникодной последовательности, а &str – это особный тип, который дает доступ к строке, которую можно разбить на последовательность буков и делать с ними что-то:

let s:&'static str = "hi there!";
// s = "bar"; // cannot reassign immutable
//let a = s[0]; // cannot index
  
for c in s.chars().rev() // reversed! also as_bytes()
{
  println!("{}", c);
}

В примере выше, мы статически аллоцируем текст и берем его как &str а дальше с помошью chars() разбираем на буквы и делаем что хотим.

А теперь про String — вот этот тип как раз для того чтобы менять, т.е. это мутабельный Vec<u8> в который можно аппендить и вообще:

let mut letters = String::new();
let mut a = 'a' as u8;
while a <= ('z' as u8)
{
  letters.push(a as char);
  letters.push_str(","); // note the _str
  a = a+1;
}
println!("{}", letters);

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

Option<T>

Это специальная структура данных (аналог boost::optional или F#‘ному Option<'t>) которая является перечислением, и имеет два возможных значения

  • None — значит «данных нет» и тут нечего ловить.
  • Some(T) где T — это то значение что вернули.

Ну вот представьте, вы захотели поделить одно число на другое, но на ноль делить нельзя (математика, 1 класс общеобразовательной школы), поэтому вы пишете что-то вроде:

let x = 3.0;
let y = 0.0;
let result:Option<f64> =
  if y != 0.0 { Some(x/y) } else { None };
match result {
  Some(z) => println!("{}/{}={}", x, y, z),
  None => println!("cannot divide {} by {}", x, y)
}

Ну и опять же, растаманская магия дает сразу выписать результат, если он есть, вот так:

if let Some(z) = result { println!("result = {}", z); }

Кортежи

Не, ну а как же без них? Хотите вернуть из функции и сумму и произведение двух чисел? Пожалуста:

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

Теперь можно их выписать например вот так:

let x = 3;
let y = 4;
let sp = sum_and_product(3, 4);  
println!("sp = {:?}", sp);
println!("{0} + {1} = {2}, {0} * {1} = {3}", 
  x, y, sp.0, sp.1);

Не нравится индексиция через точку? Вы еще С++ не видели. Ну ладно, ладно, давайте тогда деструктурируем:

let (a, b) = sp;
println!("a = {}, b = {}", a, b);

Кортежи-кортежей тоже в принципе реально, но написать foo.1.2 вам никто не даст:

let sp2 = sum_and_product(4,7);
let combined = (sp, sp2);
println!("last element is {}", (combined.1).1); 

Зато деструктуризация кортежа-кортежей выглядит красиво:

let ((c,d),(e,f)) = combined;

Что еще: а, да, кортеж это ж где разные типы можно хранить. Вот, пожалуйста. И да, одноэлементные кортежи немного кривовато делаются:

let foo = (true, 42.0, -1i8);
let meanings = (42,);

Структуры

Старый добрый сишный struct, никакого буллшита:

struct Point
{
  x: f64,
  y: f64
}
struct Line
{
  start: Point,
  end: Point
}

Методы в его тело не добавляются, а конструкторов-деструкторов как таковых нет как феномен, инициализация идет как-то вот так:

let p = Point { x: 3.0, y: 4.0 };
println!("point p is at ({},{})", p.x, p.y);
let p2 = Point { x: 5.0, y: 10.0 };
let myline = Line { start: p, end: p2 };

Для страктов, как и для кортежей (и всего остального) тоже работает деструктуризация. Как это выглядит мы увидим когда посмотрим на…

Перечисления (enum-ы)

Старый добрый… а, что, не такой? А, ну да. Enum в понимании Rust – это совокупность просто лейблов, кортежей и struct’ов, типа используйте что хотите (и да, generic enums тоже возможны, смотрите на Option). Вот например как можно описать цвет:

enum Color
{
  Red,
  Green,
  Blue,
  RgbColor(u8,u8,u8),
  CmykColor{cyan:u8,magenta:u8,yellow:u8,black:u8},
}

Ну а для разбора такого счастья можно использовать всю мощь pattern-matching’а:

let c = Color::CmykColor{cyan: 0, magenta: 128, 
  yellow: 0, black: 255};
match c
{
  Color::Red => println!("r"),
  Color::Green => println!("g"),
  Color::Blue => println!("b"),
  Color::RgbColor(0,0,0) 
  | Color::CmykColor{black:255,..} 
    => println!("black"),
  Color::RgbColor(r,g,b) => println!("rgb({},{},{}", r, g, b),
  _ => ()
}

Заметили .. выше? Это не range, конечно, это описание того что «пофиг чему равны другие поля».

Функции

Тут все, вообщем-то, просто:

Просто функции

Функция начинается с fn, далее берет один или несколько аргументов (тип, как всегда, после имени):

fn print_value(x:i32)
{
  println!("value = {}", x);
}

Функции могут также возвращать значения: тип возврата пишется через стрелочку:

fn product(x: i32, y: i32) -> i32 // return value
{
  let z = x * y;
  z // no semicolons
}

Возврат, как уже говорил, идет того значения, у которого нет ;.

Аргумент можно передать «по ссылке» – для этого используется & как на типе аргумента так и в функции, а для «дереференса» ссылки используется *. То есть поведение совсем уж в разрез с С++.

fn increase(x: &mut i32) // start with i32
{
  *x += 1;
}
let mut z = 1;
increase(&mut z); // lend z

Методы

Помните я сказал, что у struct’ов как таковых нету собственных функций? Но их можно добавить! Делается это вот так:

struct Point
{
  x: f64,
  y: f64
}
struct Line
{
  start: Point,
  end: Point
}
impl Line
{
  fn len(&self) -> f64
  {
    let dx = self.start.x - self.end.x;
    let dy = self.start.y - self.end.y;
    (dx*dx+dy*dy).sqrt()
  }
}

Ключевое слово impl позволяет определить реализацию того или иного метода для структуры. Заметьте как, выше, функция sqrt() вызывается на типе f64! Хотя я конечно же мог бы вызвать ее как f64::sqrt(), тут как кому удобнее.

Замыкания

Ну вот есть у вас функция:

fn say_hello() { println!("hello"); }

Вы можете взять и сделать из нее переменную. И потом вызвать ее как функцию:

let sh = say_hello;
sh();

Это никого не должно удивлять. Как и вариант создания таких переменных прямо в месте где хочется их использовать. Тут у нас Ruby синтаксис! Серьезно, вот:

let plus_one = |x:i32| -> i32 { x + 1 };
let a = 6;
println!("{} + 1 = {}", a, plus_one(a));

То есть plus_one это вполне себе функция. Сразу вопрос: а что с захватом окружения? А вот тут включается весь злой арсенал проверок Rust: если что-то из окружения захвачено, мы не отпустим пока сама лямбдочка не будет удалена. Как? А вот так:

let mut two = 2;
{
  let plus_two = |x|
  {
    let mut z = x;
    z += two;
    z
  };
  println!("{} + 2 = {}", 3, plus_two(3));
}
let borrow_two = &mut two;

Выше — искусственно сделанный scope, который гарантирует что plus_two будет удалена и, тем самым, «отпустит» two, которую она захватила.

Вот как-то так: передали контроль над переменной: все, пиши пропало. Поэтому лучше одалживать.

Время жизни

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

Владение

Каждая переменная «владеет» контентом, но поведение отличается в зависимости от того, на стэке она или в куче. Вот если она в стеке, например

let v = vec![3,2,1];

то это значит что такое вот невинное, казалось бы, присваивание как

let v2 = v;

означает фактически, что v2 забирает управление памятью, и что отныне переменную v использовать нельзя, т.к. у нее «забрали» управление. Это значит что вот это не скомпилируется:

println!("{:?}", v);

То же самое происходит если лямбду написать как

let foo = |v:Vec<i32>| ();
foo(v);

Поскольку функция foo забрала вектор, использовать его пока лямбда в scope — нельзя.

Это поведение на языке C++ называется move semantics, т.е. объект как бы «перемещается», а старая его копия больше не валидна. Работает это, правда, только для тех типов которые не определили трейт Copy (о трейтах позже). Для примитивов работает этот трейт, и у нас копирование вместо move’а:

let u = 1;
let u2 = u;
println!("u = {}", u); // компилится без проблем!

Как вернуть управление из функции? Ну, можно вот так:

let print_vector = |x:Vec<i32>| -> Vec<i32>
{
  println!("{:?}", x);
  x
};

Но это гемор, поэтому Rust вводит такое понятие как…

Одалживание (borrowing)

Итак, мы с вами договорились до того, что в любой момент только одна переменная «владеет» доступом к данным, а все другие — в пролёте. В терминах C++, можно сказать, что передача такого параметра — это передача unique_ptr.

Что делать если хочется просто поработать над объектом (т.е. shared_ptr)? Тогда можно передать ссылке на него, и эта ссылка означает «одалживание» (borrowing) объекта:

let print_vector = |x:&Vec<i32>| // take a reference
{
  println!("x[0] = {}", x[0]);
};
let v = vec![3,2,1];
print_vector(&v);
println!("v[0] = {}", v[0]);

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

Lifetime

Вы не видите lifetime’ов точно так же, как на выводе типов вы не пишете вручную аннотаций. А они есть. Например, типичная функция выглядит вот так:

fn bar<'a>(x: &'a mut i32) -> &mut i32 // lifetime elision
{
  x
}

Вот это вот <'a> может показаться type parameter’ом из F#, но нееет, это время жизни, и оно подчиняется разным хитрым правилом. Есть время жизни 'static, которое обозначает жизнь программы. Другие определения — это на ваше усмотрение.

Про lifetime’ы можно написать отдельный пост (или несколько). Это очень крутая штука, но она вирусная (как GPL) и способна просочиться через весь ваш код.

Всякая всячина

Крейты (ящики)

Rust поставляется с хитрой системой сродни NuGet’у. Проектным файлом служит файл в формате TOML, который описывает не только то, что мы строим (не беспокойтесь, файлы руками описывать не нужно), а также зависимости которые взяты из репозитария crates.io или откуда-то ещё.

Соответственно, помимо компилятора, есть еще crate.exe, который может как собрать ваш проект, так и запустить его или, например протестировать.

Модули

В Rust все делится на модули (ключевое слово mod) которые можно либо держать в одном файле либо распихать по файлам и папкам. Вот например тут

pub mod greetings
{
  pub mod english;
  pub mod french
  {
    pub fn hello() -> String { return "bonjour".to_string(); }
    pub fn goodbye() -> String { return "au revoir".to_string(); }
  }
}

описан файл lib.rs (значит будет собрана DLLка), и хоть модуль french включен прямо тут, модуль english лежит в файле greeting/english.rs. Convention over configuration, однако!

Ключевое слово pub определяет, что видно наружу, а что нет. Импортируется и ипользуется это очень просто:

extern crate phrases;
use phrases::greetings::french;
fn main() {
    println!("English: {}, {}", 
      phrases::greetings::english::hello(), 
      phrases::greetings::english::goodbye()
    );
    println!("French: {}, {}", 
      french::hello(), 
      french::goodbye()
    );
}

Ключевое слово use — что-то вроде using namespace в C++.

Да, забыл сказать — пакеты компилируются на стороне пользователя, т.е. поставляются как сорцы. Упс!

Тестирование

Как и язык D, Rust просто влепил тестирование прямо в суть языка на уровне набора атрибутов. Вот например

#[cfg(test)]
mod tests
{
  extern crate phrases;
  #[test]
  #[should_panic]
  #[ignore]
  fn english_greeting_correct()
  {
    assert_eq!("helloo", phrases::greetings::english::hello());
  }
}

Делает конфигурацию test, добавляет тест который ожидает падения, ну а дальше все как и в других языках. Запускается это дело с помощью cargo test. Как и в D, это очень удобно, не надо качать сторонние фреймворки.

Документация

Специальные комментарии /// для кусков кода и //! для всего модуля целиком дают rustc.exe сгенерить красивую документацию по вашему коду. Highly recommended!

Заключение

Я тут постарался как-то описать Rust по сравнению с C#/C++. Не могу сказать что хочется вот прямо взять и начать на нем писать, но некоторые умные идеи я из него почерпнул. ■

Written by Dmitri

4 января 2016 at 23:02

Опубликовано в Technology

Tagged with