Первые шаги по Rust +48



image


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


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

Сразу поясню, что я около десяти лет пишу на Java, так что рассуждать буду со своей колокольни.


Killer feature


Rust пытается занять промежуточное положение между низкоуровневыми языками типа C/C++ и высокоуровневыми Java/C#/Python/Ruby… Чем ближе язык находится к железу, тем больше контроля, легче предвидеть как код будет выполняться. Но и имея полный доступ к памяти намного проще отстрелить себе ногу. В противовес С/С++ появились Python/Java и все остальные. В них нет необходимости задумываться об очистки памяти. Самая страшная беда — это NPE, утечки не такое уж частое явление. Но чтобы это все работало необходим, как минимум, garbage collector, который в свою очередь начинает жить своей жизнью, параллельно с пользовательским кодом, уменьшая его предсказуемость. Виртуальная машина еще дает платформонезависимость, но на сколько это необходимо — спорный вопрос, не буду его сейчас поднимать.


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


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


Эту концепцию можно продемонстрировать в следующем куске кода. Из метода main() вызывается test(), в котором создается рекурсивная структура данных MyStruct, реализующая интерфейс деструктора. Drop позволяет задать логику для выполнения, перед тем как объект будет уничтожен. Чем-то похоже на финализатор в Java, только в отличие от Java, момент вызова метода drop() вполне определен.


fn main() {
    test();
    println!("End of main")
}

fn test() {
    let a = MyStruct {
        v: 1,
        s: Box::new(
            Some(MyStruct {
                v: 2,
                s: Box::new(None),
            })
        ),
    };
    println!("End of test")
}

 struct MyStruct {
     v: i32,
     s: Box<Option<MyStruct>>,
 }

 impl Drop for MyStruct {
     fn drop(&mut self) {
         println!("Cleaning {}", self.v)
     }
 }

Вывод будет следующим:


End of test
Cleaning 1
Cleaning 2
End of main

Т.е. перед выходом из test() память была рекурсивно очищена. Позаботился об этом компилятор, вставив нужный код. Что такое Box и Option опишу чуть позже.


Таким образом Rust берет безопасность от высокоуровневых языков и предсказуемость от низкоуровневых языков программирования.


Что еще интересного


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


OOP


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


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


fn main() {
    MyPrinter { value: 10 }.print();
}

trait Printer {
    fn print(&self);
}

impl Printer {
    fn print(&self) {
        println!("hello!")
    }
}

struct MyPrinter {
    value: i32
}

impl Printer for MyPrinter {
    fn print(&self) {
        println!("{}", self.value)
    }
}

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


  • У классов нет конструкторов. Есть только инициализаторы, которые через фигурные скобки задают значения полям. Если нужен конструктор, то это делается через статические методы.
  • Метод экземпляра отличается от статического наличием ссылки &self в качестве первого аргумента.
  • Классы, интерфейсы и методы также могут быть обобщенными. Но в отличие от Java, эта информация не теряется в момент компиляции.

Еще немного безопасности


Как я уже говорил Rust уделяет большое внимание надежности кода и пытается предотвратить большинство ошибок на этапе компиляции. Для этого была исключена возможность делать ссылки пустыми. Это мне чем-то напомнило nullable типы из Kotlin. Для создания пустых ссылок используется Option. Так же как и в Kotlin, при попытке обратиться к такой переменной, компилятор будет бить по рукам, заставляя вставлять проверки. Попытка же вытащить значение без проверки может привести к ошибке. Но этого уж точно нельзя сделать случайно как, например, в Java.


Мне еще понравилось то, что все переменные и поля классов по умолчанию являются неизменяемыми. Опять привет Kotlin. Если значение может меняться, это явно надо указывать ключевым словом mut. Я думаю, стремление к неизменяемости сильно улучшает читабельность и предсказуемость кода. Хотя Option почему-то является изменяемым, этого я не понял, вот код из документации:


let mut x = Some(2);
let y = x.take();
assert_eq!(x, None);
assert_eq!(y, Some(2));

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


В Rust называются enum. Только помимо ограниченного числа значений они еще могут содержать произвольные данные и методы. Таким образом это что-то среднее между перечислениями и классами в Java. Стандартный enum Option в моем первом примере как раз принадлежит к такому типу:


pub enum Option<T> {
    None,
    Some(T),
}

Для обработки таких значений есть специальная конструкция:


fn main() {
    let a = Some(1);
    match a {
        None => println!("empty"),
        Some(v) => println!("{}", v)
    }
}

А также


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


  • Любители функционального программирования не будут разочарованы, для них есть лямбды. У итератора есть методы для обработки коллекции, например, filter и for_each. Чем-то похоже на стримы из Java.
  • Конструкция match так же может быть использована для более сложных вещей, чем обычные enum, например, для обработки паттернов
  • Есть большое количество встроенных классов, например, коллекций: Vec, LinkedList, HashMap и т.д.
  • Можно создавать макросы
  • Есть возможность добавлять методы в существующие классы
  • Поддерживается автоматическое выведение типов
  • Вместе с языком идет стандартный фреймворк для тестирования
  • Для сборки и управления зависимостями используется встроенная утилита cargo

Ложки дегтя


Этот раздел необходим для полноты картины.


Killer problem


Главный недостаток происходит из главной особенности. За все приходится платить. В Rust очень неудобно работать c изменяемыми графовыми структурами данных, т.к. на любой объект должно быть не более одной ссылки. Для обхода этого ограничения есть букет встроенных классов:


  • Box — неизменяемое значение на куче, аналог оберток для примитивов в Java
  • Cell — изменяемое значение
  • RefCell — изменяемая ссылка
  • Rc — reference counter, для нескольких ссылок на один объект

И это неполный список. Для первой пробы Rust, я опрометчиво решил написать односвязный список с базовыми методами. В конечном счете ссылка на узел получилась следующая Option<Rc<RefCell<ListNode>>>:


  • Option — для обработки пустой ссылки
  • Rc — для нескольких ссылок, т.к. на последний объект ссылаются предыдущий узел и сам лист
  • RefCell — для изменяемой ссылки
  • ListNode — сам следующий элемент

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


struct ListNode {
    val: i32,
    next: Node,
}

pub struct LinkedList {
    root: Node,
    last: Node,
}

type Node = Option<Rc<RefCell<ListNode>>>;

impl LinkedList {
    pub fn add(mut self, val: i32) -> LinkedList {
        let n = Rc::new(RefCell::new(ListNode { val: val, next: None }));
        if (self.root.is_none()){
            self.root = Some(n.clone());
        }
        self.last.map(|v| { v.borrow_mut().next = Some(n.clone()) });
        self.last = Some(n);
        self
    }
...

На Kotlin то же самое выглядит намного проще:


public fun add(value: Int) {
    val newNode = ListNode(null, value);
    root = root ?: newNode;
    last?.next = newNode
    last = newNode;
}

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



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


Сложность изучения


Долгий процесс изучения Rust во многом следует из предыдущего раздела. Перед тем как написать вообще хоть что-то придется потратить время на освоение ключевой концепции владения памятью, т.к. она пронизывает каждую строчку. К примеру, простейший список у меня занял пару вечеров, в то время как на Kotlin то же самое пишется за 10 минут, при том что это не мой рабочий язык. Помимо этого многие привычные подходы к написанию алгоритмов или структур данных в Rust будут выглядеть по другому или вообще не сработают. Т.е. при переходе на него понадобится более глубокая перестройка мышления, просто освоить синтаксис будет недостаточно. Это далеко не JavaScript, который все проглотит и все стерпит. Думаю, Rust никогда не станет тем языком, на котором учат детей в школе программирования. Даже у С/С++ в этом смысле больше шансов.


В итоге


Мне показалась очень интересной идея управления памятью на этапе компиляции. В С/С++ у меня опыта нет, поэтому не буду сравнивать со smart pointer. Синтаксис в целом приятный и нет ничего лишнего. Я покритиковал Rust за сложность реализации графовых структур данных, но, подозреваю, что это особенность всех языков программирования без GC. Может быть, сравнения с Kotlin было и не совсем честным.


TODO


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


Почитать


Если вас заинтересовал Rust, то вот несколько ссылок:


Вы можете помочь и перевести немного средств на развитие сайта



Комментарии (70):

  1. Antervis
    /#19532622

    как часто вы пишете изменяемые графовые структуры данных?

    • zharko_mi
      /#19532694

      Не часто, но такое ограничение напрягает немного, особенно если язык претендует на общую применимость, а не узкую специализацию. А вы пишете на Rust? Сильно это мешает? Ведь чтобы перейти к следующему элементу, надо развернуть 3 обертки, т.е. сделать 3 обращения в случайное место памяти. Как там с производительностью? Не теряется ли преимущество низкоуровневости?

      • argentumbolo
        /#19532942 / +1

        Визуально обёртки можно спрятать за кастомным типом, а раскрытие матрёшки в методе возвращающем Result (тогда большая часть проверок заменяется на "?") и/или макросе.

      • Duduka
        /#19533512 / -2

        Изучая чего-нибудь…
        Шаг первый: включить капитана очевидность
        Шаг второй: использовать только свой "богатый" опыт
        Шаг третий: встать в ступор от количества непонятного
        Шаг четвертый: обозвать всех "идиотами"
        Шаг пятый: озвучить Всем! свое мнение…
        Шаг шестой: пойти искать, что-то еще более непонятное!


        Вы верно заметили, что за все приходится платить, за свое самомнение — тоже. Как только Вы сталкиваетесь с проблемой, Вы, всегда опускаете руки?! У многого в Ржавчине глубоко-академический смысл, а колосальный инженерный опыт привел к таким (сложным для вашего понимания) решениям, и если ("с наскоку") у Вас возникли вопросы в понимании, то это не причина осуждать профессиональные решения, за них расплачивались кровью и бессоными ночами.
        Я ожидал обоснованной критики языка, с точки зрения новичка, но здесь отражен только Ваш опыт (отстуствующего) навыка само-образования.
        Да, язык — это и Ваш способ понимания, но этим его функции не ограничиваются, это еще и способ понимать проблемы, способность разграничения "разумного" и "не очень..." (сказать что решаемо или вероятно нет), а у Ржавчины еще и богатая (и доступная) история принятия решений.

      • domix32
        /#19533858

        Вероятно для подобных структур стоит использовать что-то иное нежели указателей в памяти, как в C++. Что-то вроде fst.
        На конференциях были чуваки которые даже для спутников прошивки делали.

      • TargetSan
        /#19535188 / +2

        Мне кажется, тут идёт вопрос разграничения между "клиентским" и "библиотечным" кодом. Названия условные. "Клиентский код" — условно тот, который решает вашу задачу. "Библиотечный" — набор базовых строительных кирпичиков. Так вот. Односвязный список, граф и т.п. для меня — как раз такие кирпичики. И при их реализации вы будете использовать техники, которые в нормальном коде не будете. К примеру unsafe. Который не является Exterminatus Incoming, а всего лишь возможностью обойти некоторые ограничения — если вы знаете что делаете. Вы пишете в основном на Java, поэтому просто для справки — в С++, к примеру, unsafe у вас по факту везде.

      • mkpankov
        /#19535366 / +2

        На Rust структуры данных пишутся так, чтобы работало быстро, а не со счётчиками ссылок на пустом месте. Там unsafe внутри и это нормально.

    • Amomum
      /#19533992 / +1

      Практически любой GUI-фреймворк обычно выглядит как изменяемый граф виджетов.

      • TargetSan
        /#19534712 / +1

        GUI фреймворк это как правило всё же дерево, причём готовое.

        • Amomum
          /#19535238

          Я к тому, что из-за этого фреймворков-то особо и нету до сих пор, слишком болезненно это делается.

          • TargetSan
            /#19535268 / +1

            Не факт. Контрпример — назовите пример хорошего GUI фреймворка для Golang. Который на порядок проще.

            • Amomum
              /#19535286 / +1

              Не являюсь специалистом по Go, поэтому ничего не могу сказать конкретного про него. Рискну предположить, что Go позиционируется как язык для бэкэнда и там GUI просто «нинужен».

              • TargetSan
                /#19535314

                А Rust — как системный. И Go, вследствие наличия GC, для GUI и граф-подобных структур, должен подходить сильно больше. Но поди ж ты.

                • Amomum
                  /#19535332

                  Окей, а каково ваше мнение на этот счет? Почему ни для Go ни для Rust до сих пор нет хорошего GUI-фреймвокра?

                  Если что, я насчет Go вообще не в курсе — есть они там или нету.

                  • TargetSan
                    /#19535406

                    Моё скромное мнение — серьёзный продакшен GUI подался в web или electron. Там уже не ролляет, на чём писать бизнес-логику. А старые крупные проекты просто не имеют желания шевелиться куда-то.

                  • Antervis
                    /#19536310

                    Окей, а каково ваше мнение на этот счет? Почему ни для Go ни для Rust до сих пор нет хорошего GUI-фреймвокра?

                    а не потому ли что Go/Rust значительно моложе всех достойных GUI-framework'ов, и что им нужно время?

                • potan
                  /#19535708

                  Плюсы для десктопа очень широко используются. Было бы хорошо их там заменить на Rust.

          • Для веба и wasm есть похожий на реакт Yew для десктопа Conrod. В глубокой альфе да но есть еще Orbtk, Relm и Azul. Да, у них разработка в процессе но и язык очень молодой.

        • zharko_mi
          /#19535272

          Почему я отказался от Rust — тут описаны проблемы реализации GUI на Rust.

          • TargetSan
            /#19535446

            Там таки есть вполне валидные замечания. Для меня лично — переиспользование структур и поля в трэйтах, в качестве требований. Первую проблему можно было бы решить как в Go — alias член автоматом торчит из структуры всеми своими трейтами. Вторая ЕМНИП решена в Scala — но не в близких к системным языках. Могу конечно ошибаться.

      • potan
        /#19535702 / +1

        И это плохо.
        По моему опыту подход Elm гораздо надежнее. Но реализации его для Rust пока сыроватые.

  2. mayorovp
    /#19533060 / +1

    RefCell — это не «изменяемая ссылка», а изменяемое значение, доступное по ссылке.

  3. worldmind
    /#19533254 / +2

    Rust это интересная попытка исправить си, но пока они не доделали async/await я точно туда не полезу, мне системное программирование не нужно, а вот что-то быстрое для тех задач куда сейчас любят пихать go может быть интересно.
    Т.е. язык ещё бурно развивается и не для всех задач удобен.

    • humbug
      /#19533356

      Попробуйте romio, он дружит с модными async/await.

      • tgz
        /#19533664 / +1

        Речь скорее всего про то, что оно должно появиться в stable. А потом должны на нем переписать tokio, а потом actix, а потом…

    • potan
      /#19535722 / +2

      Лучше бы они сделали HKT, монады и for comprehension. async/await — громозкий, неудобный и негибкий костыль.

      • red75prim
        /#19536522

        Не вы первый об этом думали. Например: http://blog.paralleluniverse.co/2015/08/07/scoped-continuations/. Если коротко: монады не подходят, так как Раст — системный язык, который должен быть близок к железу, а железо у нас реализует не бета-редукции и эта-конверсии, а императивный поток выполнения.

        • potan
          /#19537446

          А извращение async/await для системного языка подходит?
          Монада — это интерфейс, который по факту реализуют много типов, от Vec и Result и до Future, и отсутствие HKT не дает этим пользоваться. К системному программированию это не имеет ни какого отношения, монады уже есть и без всякой функциональщины, но только интерфейс с каждой из них отдельный.

          • Tyranron
            /#19537620 / +1

            Не всё так просто. Rust ведь тоже не дураки дизайнят, и там много людей которые тоже хотели бы видеть HKT в составе. Увы, на практике, до сих пор толком до конца никто не представляет как их красиво вписать и реализовать в Rust. Этот вопрос считается открытым и требующим глубокого исследования. Кратко со списком проблем можно ознакомиться в этом треде от withoutboats (core team).


            Ждать пока этот исследовательский момент как-нибудь разрешится сообщество тоже не готово. Все хотят уже брать и писать крутые штуки на Rust. Без вменяемой асинхронности этого не получится. Соответственно, в данный момент для async/await не придумано лучших альтернатив в Rust. Это прагматичный выбор. Тем не менее он не отменяет монадок/HKT в будущем, если таки придумают как туда их красиво вписать.


            Если же у Вас есть хорошие идеи/предложения по реализации HKT в Rust — добро пожаловать на официальный форум и в RFC-процесс. Выскажите свои идеи. Сообщество будет Вам очень благодарно, на самом деле.

        • potan
          /#19537572

          И, кстати, в системном языке C++ есть HKT, и широко применяется в библиотеках.

      • worldmind
        /#19536748

        Так зачем повторять хаскел?

        • potan
          /#19537458

          Потому что это удобно. Scala, хоть и вполне поддерживает императивный стиль, и даже макросы async/await, все расно «повторияет хаскел». И мой опыт говорит, что единственный способ разобраться с async/await-кодом — переписать его на for.

  4. pawlo16
    /#19533332

    Самая страшная беда — это NPE,

    откуда такая информация?
    утечки не такое уж частое явление

    Но для их локализации требуется как минимум профилирование под нагрузкой. А в случае NPE в основном стектрейса достаточно. А для предотвращения NPE достаточно самых элементарных юнит тестов

    • mayorovp
      /#19533350 / +1

      Кажется, вы неправильно прочитали фразу. Если в Си или С++ при неаккуратном обращении с указателями можно незаметно поломать в программе вообще всё, то в языках с GC ничего страшнее NPE не возникнет.

      • pawlo16
        /#19534270

        Кажется, вы неправильно прочитали фразу.

        Я не понял почему вы так решили.

        В С++ любая проблема страшнее и решается через боль в разы большую, чем в языках с управляемыми средами. Не понятно, чем утечки памяти в С++ лучше, чем NPE.

        «в языках с GC ничего страшнее NPE не возникнет» — вот только это не правда, и выше я объяснил почему.

        Ещё разок — не понятно, на каком основании автор считает проблему NPE более важной чем проблему утечек памяти. Хотелось бы увидеть ссылку на соответствующие исследования например.

      • В Языках с GC вообще еще и утечки памяти бывают. Вот сталкивался с тем что WCF под Mono тёк. Вот видел Java сервис который надо было иногда пере запускать потому что заканчивалась память на сервере. Так что такое себе. Не NPE единым.

  5. pawlo16
    /#19533816 / -1

    del

  6. AlexTheLost
    /#19534272

    Статья видится из разряда услышал звон, но не понял где он. Хотя я и не знаю Rust, но этого и не нужно что бы понимать что язык создавался для системного программирования, и сравниваться его с Java, Kotlin или уже темболее Ruby абсурд, сферы применения различные.
    Это мне напоминает схожие ставить где одни пишут как сделать web приложение на Go, а другие какой гемор Go. Хотя со вторыми я и согласен, но нужно учитывать под какие нужды создавался язык. А то что его пихают везде наивыне головы, а потом отхватывают другая проблема.

  7. boblenin
    /#19534752 / +5

    Rust ставит достаточно высокую планку для того, кто хочет начать писать осмысленный код. Начать с hello world и перейти сразу скажем к написанию web-сервиса, или обертки для C библиотеки, или игры типа тетриса — не получится. Надо разбираться с владением, заимствованием, системой видимости пакетов, своим взглядом на наследование, обработку ошибок. Ну т.е. если с тем же go можно взять примеры и начать их делать, то с rust лучше будет сначала прочитать книжку, а потом начинать делать примеры. По крайней мере это мой опыт.

    • Antervis
      /#19536324 / +1

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

      • boblenin
        /#19536610 / +1

        Ну в достаточной степени знать C++ как мне кажется вообще не возможно. А минимально знаний, для того чтобы накалякать что-то типа консольного тетриса можно набраться за неделю. Да у вас будет течь память, да вы скорее всего не правильно будете ловить исключения, да скорее всего будете забивать на const, но сможете что-то скомпилировать и оно будет даже работать как-то.

        • Antervis
          /#19537726

          вам кажется. Знать на 100% не получится — голова заболит, а вот всё необходимодостаточное вполне можно выучить и за год в среде с культурой написания кода.

          • boblenin
            /#19537872 / +2

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

            • Gorthauer87
              /#19538268

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

            • Antervis
              /#19539418 / -1

              это уже смотря как учить и что считать достаточным. Я за свои 5 лет писал на плюсах с 03 по 17-е и в достаточности своих познаний уверен. Представляете, знать весь стандарт назубок для написания корректных программ необязательно!

  8. freecoder_xx
    /#19535538 / +4

    Хотя Option почему-то является изменяемым

    Нет, не Option является изменяемым, а метод take вы можете вызвать только на изменяемом объекте. Потому что:


    pub fn take(&mut self) -> Option<T> {
        mem::replace(self, None)
    }

    take заменяет значение на None, что невозможно сделать на иммутабельном объекте.

    • zharko_mi
      /#19535916 / +1

      Интересный подход просто, когда мутабильность определяется не в самом классе, а в момент создания экземпляра. В Java Optional — immutable. Небольшой разрыв шаблона.

      • Browning
        /#19536836 / +2

        Даже не в момент создания экземпляра, а в момент привязки к переменной. Если написать "let v1 = Vec::new(); let mut v2 = v1;", то v2 будет мутабельный вектор.

  9. SergeiMinaev
    /#19535820 / +2

    Как вы думаете стоит ли переходить на Rust?

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

  10. epishman
    /#19536040 / +3

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

    • Tangeman
      /#19536124 / -4

      Утечки в C++ (как и в почти любом другом языке) решаются с помощью GC. Правда, все подключенные либы тоже должны его использовать, разумеется.

  11. Tangeman
    /#19536104 / -1

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

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

    Но наследование вообще (точнее, его отсутствие), увы, это слабое место Rust, и даже его автор этого не скрывает. Было много дискуссий на эту тему, и довольно немало людей жалуется что это одна из причин по которой на Rust очень сложно писать UI и другие подобные древовидные конструкции.

    Если грубо, то получится что-то типа (псевдокод):

    Window {
      x
      y
      width
      height
      show()
      hide()
    }
    
    Widget {
      Window win
      ...
    }
    

    В Rust, если у нас есть объект типа Widget wg, к его окошку (для координат, к примеру) нам придется лезть весьма коряво — wg.win.show(), т.е. в любом случае нужно хорошо знать что у слоника внутри и пользоваться этим косвенно, вместо того чтобы в любом нужном объекте напрямую сделать что хочется, без тупого дублирования ссылок на методы в родительских классах.

    Если у нас будет слой потолще, в духе Window > Widget > ListView > TreeListView, то можно будет просто умереть от лишней писанины, не говоря уже о том что в каждом классе придётся вручную прописать обращение к конструкторам (неважно что под ними подразумевается) и ещё кучу всего.

    Да, само по себе это не является препятствием для написания UI и вообще чего угодно, в конце концов, всё это пишется на C где вообще нет классов, не говоря уже про наследование — но остальные свойства (требования) языка делают написание подобных вещей адским трудом, т.е. с точки зрения ООП это шаг назад, однако. Если рассматривать его как «лучший C» — да, может быть, но всё же полноценный ООП лучше, да есть и другие «лучшие C» без таких проблем.

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

    • humbug
      /#19536188 / +1

      даже его автор этого не скрывает

      Какой? Первый раз об этом слышу.


      Но наследование вообще (точнее, его отсутствие), увы, это слабое место Rust

      И почему же? Кто это говорит?


      В Rust, если у нас есть объект типа Widget wg, к его окошку (для координат, к примеру) нам придется лезть весьма коряво — wg.win.show(), т.е. в любом случае нужно хорошо знать что у слоника внутри

      Вообще-то решается проще:


      trait Showable {
         show()
      }
      
      struct Window {
        x
        y
        width
        height
      }
      
      impl Showable for Window {
        show() { ... }
      }
      
      struct Widget {
        Window win
        ...
      }
      
      impl Showable for Widget {
        show() { self.win.show() }
      }

      И все. Снаружи пользователю не обязательно знать, что находится внутри Widget.


      Все эти недопонимания возникают от попыток совместить и данные, и поведение в одном месте (классе). К счастью в Rust данные описываются отдельно от поведения, и это решает множество проблем. В комментах к статье Почему я отказался от Rust еще два года назад объяснили эти моменты.

      • Tangeman
        /#19536200

        Какой? Первый раз об этом слышу.

        Этот.

        show() { self.win.show() }

        Это именно то о чём я и говорю — лишний мартышкин труд для разработчика. И возможность допустить ошибку.

        • humbug
          /#19536204 / -2

          И какого рода ошибку здесь можно допустить?


          И вы не ответили на мой вопрос:


          даже его автор этого не скрывает

          Кто этот автор? Откуда этот вброс без фактов?

          • Tangeman
            /#19536240

            И какого рода ошибку здесь можно допустить?

            Вы не в курсе какие ошибки можно допустить, дуплицируя код и ссылаясь на что-то много раз? Или что можно сослаться не туда в конце трудного рабочего дня?
            Кто этот автор? Откуда этот вброс без фактов?

            Вы не заметили ссылку в моём сообщении? Да, я её добавил через минуту, но просто потому что нажал «отправить» вместо «предосмотр». Почитайте её. Пусть он и не прямой автор языка, но всё же имеет достаточный вес чтобы быть к нему причастным.

            А дискуссии на эту тему в сообществе Rust вы и сами нагуглите, по словам «rust object inheritance».

            Честно говоря, мне несколько непонятно, почему такая агрессия на вполне справедливое замечание — что отсутствие нормально наследования является неудобным и увеличивает вероятность ошибок. Нравится вам всё делать ручками, повторяясь — делайте.

            • freecoder_xx
              /#19537208 / +2

              Проблема в том, что наследование реализации во многих популярных ООП-языках совмещено с образованием подтипа. И это несколько запутывает ситуацию. К каким проблемам это может приводить?


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


              Другая проблема — раздувание классов. В простейшем случае, у вас есть типы Rectangle и Square. И по логике Square должен быть подтипом Rectangle, но последний для хранения своего состояния требует больше памяти, чем первый (a + b против просто a). В итоге, применяя наследование для образование подтипа, вы чрезмерно раздувает Square. И когда у вас глубокая иерерхия наследования, это делает ваши объекты просто монструозными.


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

              • Gorthauer87
                /#19538258

                А нужны ли эти делегаты в самом языке? Проблема прекрасно решается при помощи макросов.

                • freecoder_xx
                  /#19541926

                  К сожалению, при помощи макросов проблема не решается. Если только плагин к компилятору написать. Не решается потому, что макрос — это абстракция на уровне синтаксиса, а нам важна семантика. Например, вы не можете в макросе сделать такое:


                  #[derive_methods(Foo)]
                  struct MyStruct;

                  Потому что с точки зрения макроса, Foo — это path, а не полноценная структура/типаж с объявленными методами, доступа к внутренностям Foo макрос не имеет.

            • Crandel
              /#19538004 / +1

              что отсутствие нормально наследования является неудобным и увеличивает вероятность ошибок

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

      • Antervis
        /#19536350

        Вообще-то решается проще:
        struct Widget {
          Window win
          ...
        }
        
        impl Showable for Widget {
          show() { self.win.show() }
        }
        


        То есть методы всех базовых классов надо определить для всех наследников? А не всплывут ли там конструкции вида customTCB.tristateCB.cb.button.widget.show()?

        • freecoder_xx
          /#19537226 / +1

          Нет, скорее будет так: custom_tcb.show(), внутри него self.tristate_cb.show(), внутри него self.cb.show() — и так далее.

        • Gorthauer87
          /#19538250

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

  12. wasdd
    /#19537364 / +1

    zharko_mi интересные первые впечатления, спасибо за статью.


    Box — неизменяемое значение на куче

    это ссылка, значение по ссылке менять можно


    В первом примере если заменить


    Box<Option<MyStruct>>

    на


    Opttion<Box<MyStruct>>

    то можно будет писать None вместо Box::new(None)


    А ещё можно делать так:


    let value = 1;
    let next = None;
    let node = Node { value, next }; // привет любителям JS!

    Удобно для конструкторов:


    struct Point {
      x: i32,
      y: i32,
    }
    impl Point {
      fn new(x: i32, y: i32) -> Self {
        Point { x, y }
      }
    }

    Пример со списком можно немного аккуратнее сделать (ссылка на плэйграунд):


    fn add(&mut self, value: i32) {
      let next = Link::new(value);
      if let Some(ref last) = self.last {
        last.borrow_mut().next = next.clone();
      } else {
        self.root = next.clone();
      }
      self.last = next;
    }

    Вообще люди из ML языков находят в расте много знакомого, кто раньше писал на скале — тоже.
    Мне после JavaScript замыканий почему-то многие базовые концепты из раста очень легко дались.

  13. blandger
    /#19537366

    Я на Java тоже много лет, несколько больше чем вы, но тоже активно изучаю Rust в свободное время. Надеюсь он займет свою нишу для написания быстрых микро-макро сервисов, в том числе и для Enteprise ниши, которую занимает java/.net/scala, а не только blockhchain. Много чего для таких задач не хватает с точки зрения библиотек. Мне он также интересен для embedded. Спасибо.

    • zharko_mi
      /#19537374

      На данный момент основная ниша Rust — это блокчейн? Не знал этого. Почему так считаете?

      • blandger
        /#19537438 / +1

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

        • humbug
          /#19538310

          На самом деле тут двойная ситуация: блокчейн хочет Rust, потому что Rust чрезвычайно надежен, что крайне важно, когда работаешь с деньгами. А позволить себе Rust разрабов (которые хотят много денег за свои знания) могут только жирные фирмы, готовые за сотрудника выложить 300+ в Мск.


          Ну и на самом деле кроме блокчейна есть еще куча разных вкусных вакансий, но 70% таки да, блокчейн.

  14. Crandel
    /#19538014 / +1

    Добавьте пожалуйста в ссылки еще каналы в гиттере: easy — для новичков, general — для более опытных.
    Мне очень много раз помогали там.