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

Нужно думать и программировать в стиле Expressions

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

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

Вот пример того, как можно написать код в стиле Expressions:

let x = 10;
let y = if x > 5 {
    100
} else {
    50
};

Этот код более идиоматичен, чем следующий код:

let x = 10;
let y;
if x > 5 {
    y = 100;
} else {
    y = 50;
}

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

Ещё один пример идиоматического кода в стиле Expressions:

fn factorial(n: u32) -> u32 {
    if n == 0 {
        1
    } else {
        n * factorial(n - 1)
    }
}

Этот код использует рекурсию для вычисления факториала числа. Рекурсия — это ещё один способ мышления и программирования в стиле Expressions.

Важно помнить, что практически всё в Rust может быть использовано как выражение, что делает код более выразительным и компактным.

Пишите итераторы, а не циклы

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

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

Например, чтобы перебрать вектор, лучше написать:

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

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

А еще лучше воспользоваться методами итераторов:

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

v.iter().for_each(|x| println!("{}", x));

Другие полезные методы итераторов - map, filter, fold и т.д. Они позволяют писать более идиоматичный и выразительный Rust код.

// Используем итератор для фильтрации элементов в векторе
let filtered_names: Vec<_> = names.iter().filter(|x| x.starts_with("A")).collect();

Строители

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

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

Вот пример использования строителя для создания автомобиля:

struct Car {
    name: String,
    hp: u32,
}

impl Car {
    pub fn new(name: String) -> Self {
        Car {
            name,
            hp: 100,
        }
    }

    pub fn hp(mut self, hp: u32) -> Self {
        self.hp = hp;
        self
    }

    pub fn build(self) -> Car {
        self
    }
}

let car = Car::new("Model T").hp(20).build();

Строители позволяют избежать создания объектов в недопустимом состоянии. Метод build() гарантирует, что Car будет полностью инициализирован.

Строители также позволяют настраивать объекты более гибким образом. Мы можем вызвать только .hp() и .wheels(), чтобы создать частично инициализированный Car.

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

Паники

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

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

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

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

Вместо исключений Rust использует два типа ошибок:

  1. Option - возвращает значение типа T, если оно доступно, или None, если оно недоступно.

  2. Result<T, E> - возвращает значение типа T или ошибку типа E.

Option и Result<T, E> используются для обработки ошибок в более безопасном и прозрачном способе, чем исключения.

Давайте рассмотрим пример:

fn divide(a: i32, b: i32) -> i32 {
    if b == 0 {
        panic!("Division by zero is not allowed!"); // Паника при делении на ноль
    }
    a / b
}

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

fn divide(a: i32, b: i32) -> Result<i32, &'static str> {
    if b == 0 {
        Err("Division by zero is not allowed!") // Возвращаем ошибку как Result
    } else {
        Ok(a / b)
    }
}

Такой подход делает ваш код более предсказуемым и безопасным.

Обобщения (generics)

Обобщения - это способ написания кода, который может работать с любым типом данных. В Rust обобщения используются для создания типов, таких как Option и Result<T, E>.

Обобщения могут быть мощным инструментом, но их следует использовать с осторожностью. Сложные обобщения могут сделать код трудным для понимания и отладки.

Вот пример простого обобщения:

fn print_value<T>(value: T) {
    println!("{}", value);
}

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

Вот пример сложного обобщения:

fn compare_values<T: PartialEq>(value1: T, value2: T) -> bool {
    value1 == value2
}

Эта функция сравнивает два значения типа T, которые реализуют интерфейс PartialEq.

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

Разделение реализаций

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

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

Лучший способ использовать generics - это разделить реализации на отдельные модули. Например, предположим, что у вас есть структура Point, которая может содержать координаты x и y любого типа. Вы можете написать общую реализацию Point, которая будет содержать общие методы, такие как getX() и getY(). Затем вы можете написать отдельные реализации Point для конкретных типов данных, таких как Point и Point.

Вот пример кода, демонстрирующий, как это работает:

// Общая реализация Point
struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    pub fn get_x(&self) -> &T {
        &self.x
    }

    pub fn get_y(&self) -> &T {
        &self.y
    }
}

// Реализация Point для f32
impl Point<f32> {
    pub fn distance_to(&self, other: &Point<f32>) -> f32 {
        let dx = self.x - other.x;
        let dy = self.y - other.y;

        return (dx * dx + dy * dy).sqrt();
    }
}

// Реализация Point для i32
impl Point<i32> {
    pub fn distance_to(&self, other: &Point<i32>) -> i32 {
        let dx = self.x - other.x;
        let dy = self.y - other.y;

        return (dx * dx + dy * dy).sqrt();
    }
}

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

Избегать unsafe {}

Rust известен своей фокусировкой на безопасности. Он предоставляет мощные средства для работы с низкоуровневой памятью, но иногда новички могут быть склонны использовать unsafe {} блоки, чтобы обойти систему типов и безопасности. Однако часто существуют более безопасные и быстрые альтернативы, которые не требуют unsafe.

Давайте рассмотрим пример. Предположим, у нас есть вектор чисел, и мы хотим получить сумму всех элементов. Мы могли бы сделать это с использованием unsafe {}:

fn unsafe_sum(numbers: &[i32]) -> i32 {
    let mut sum = 0;
    for i in 0..numbers.len() {
        unsafe {
            sum += *numbers.get_unchecked(i);
        }
    }
    sum
}

Но более безопасный и идиоматический способ сделать это:

fn safe_sum(numbers: &[i32]) -> i32 {
    numbers.iter().sum()
}

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

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

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

Комментарии (33)


  1. tbl
    02.09.2023 18:21
    +24

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

    И да, про идеоматичный раст лучше здесь почитать: https://rust-unofficial.github.io/patterns/idioms/index.html


    1. aegoroff
      02.09.2023 18:21
      +1

      угу, особенно что там написано что for более идеоматичен в широком смысле а for_each просто более удобен в случае длинных цепочек и иногда просто быстрее работает:

      "This is equivalent to using a for loop on the iterator, although break and continue are not possible from a closure. It’s generally more idiomatic to use a for loop, but for_each may be more legible when processing items at the end of longer iterator chains. In some cases for_each may also be faster than a loop, because it will use internal iteration on adapters like Chain."


    1. orekh
      02.09.2023 18:21
      +12

      В статье какой-то сборник неоднозначных советов:

      1. почему рекурсия под заголовком "выражения"?

      2. тут нет ни отдельной струкруры для строителя, ни необходимости в методе build (который ничего не делает), это вообще не паттерн Строитель

      3. раст позволяет использовать строку как тип ошибки, но они не реализуют автоматически трейт Ошибка, так что будут проблемы при использовании dyn Err. Обычно тут используют структуру, перечисление либо тип ошибки из std.

      4. generics... ок, хорошо, это основные знания

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


      1. ivankudryavtsev
        02.09.2023 18:21

        transmute жеж :)


  1. aamonster
    02.09.2023 18:21
    +30

    (в слезах) Ну почему опять рекурсия на примере факториала? Да ещё не в виде хвостовой рекурсии?

    Прежде, чем такое писать – надо твёрдо знать:

    1. Умеет ли используемый вами компилятор TCO (tail call optimization)?

    2. Гарантируется ли TCO стандартом языка?

    3. Преобразует ли компилятор подобный код без хвостовой рекурсии (последний вызов – не рекурсивный, а умножение на n) в нужный вид, чтобы затем применить TCO?

    4. Знают ли другие пользователи языка ответы на пункты 1-3?


    1. Anarchist
      02.09.2023 18:21

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

      Что, впрочем, не отменяет кривого факториала. :)


  1. Sixshaman
    02.09.2023 18:21
    +13

    Вы предлагаете делать код "выразительным и компактным" и "чистым и функциональным". А зачем, вы можете объяснить?

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


    1. panzerfaust
      02.09.2023 18:21
      +20

      Expressions в большинстве случаев сложнее читать, чем императивный стиль

      Мне кажется, вы путаете "сложно" и "незнакомо". Без опыта в погромировании сложно читать императивный стиль. Без опыта в ООП сложно читать код на классах. Без опыта в ФП сложно читать код на функциях высшего порядка и монадах. И так далее.


      1. ReadOnlySadUser
        02.09.2023 18:21

        У меня есть небольшой, но всё же опыт написания кода на Haskell. ФП-шнее некуда.

        До сих пор считаю императивный код более читабельным.


    1. ivankudryavtsev
      02.09.2023 18:21
      +4

      У меня правило простое, если внутри итератора ожидаются всякие break, return, bail!, то я пишу цикл. Если ничего такого, пишу итераторы, если ради итераторов приходится начинать городить flat_map чтобы внутри как-то разруливать contol flow, читать это может быть сложно.

      Но думаю, что основная засада с итераторами знать поведение некоторых функций при работе с Result и Option.


  1. alyxe
    02.09.2023 18:21
    -10

    Rust - очень странный язык для извращенцев. Такое впечатление я получил после нескольких статей о нем. Конечно, у него есть много преимуществ, которыми болеет C, однако стоят ли они того комфорта и выразительности.

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

    В любом случае, спасибо за статью. Но она не добавила ни копейки (цента) в желание хотя бы попробовать язык. Натерпелся с Ruby в свое время, хотя это азиатское изобретение — действительно полюбилось.


    1. anonymous
      02.09.2023 18:21

      НЛО прилетело и опубликовало эту надпись здесь


      1. MountainGoat
        02.09.2023 18:21

        Inform7. Когда нет синтаксиса - и докопаться не к чему. ¯\_(ツ)_/¯


        1. aamonster
          02.09.2023 18:21

          Natural language based? Вы серьёзно?

          (имею некоторый опыт применения AppleScript... Какая боль).


    1. Helltraitor
      02.09.2023 18:21
      +3

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

      Как бонус, прошу привести пример языка с хорошим синиаксисом и помечаниями, почему он хорош, спасибо


  1. SerpentFly
    02.09.2023 18:21
    +4

    Добавлю, пример с print_value не корректен, не скомпилируется без fn print_value<T: std::fmt::Display>(value: T)


  1. ivankudryavtsev
    02.09.2023 18:21
    +7

    Почему-то про «?» ничего не написали, про trait From, про много чего… идиоматический Rust - это когда автор кода хорошо знает Rust. Если человек только начал писать на Rust, идиоматически не выйдет… но, опыт сын ошибок, как говорится…

    Еще напрашивается про .map(f) для tuple-like объектов с одним параметром и f(x) вместо `.map(|x| f(x))`. Да и вообще, `cargo clippy` научит.

    Билдеры легче через крейт derive_builder делать, а не руками.


  1. smeyanoff
    02.09.2023 18:21
    +3

    Учу раст после питона.

    Спасибо за статью


  1. SpiderEkb
    02.09.2023 18:21
    -2

    А вот пример из реальной жизни. По условию задачи нужна некая выборка из БД по набору условий. Для каждого выбранного элемента (набор данных) нужно произвести некоторое действие (конкретно - сформировать сообщение в очередь + добавить запись в таблицу что сообщение сформировано и отправлено).

    Если следовать описанной выше идеологии RUST, то сначала пишем выражение (expression), которое делает выборку и возвращает вектор с набором отобранных данных.

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

    Я правильно понял?

    Извините, но это немножко бред. Концептуально, но не эффективно.

    Итератор - это только снаружи красиво. А внутри - тот же самый цикл. Тут на ум приходит цитата Фаины Раневской:

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

    Подход RUST концептуален, но даст нам два цикла - первый в выражении (выборка и занесение ее результатов вектор), второй - в итераторе.

    Плюс мы не знаем объем выборки - туда может отобраться миллионы элементов. А это расход памяти + накладные затраты на ее динамическое выделение и потом освобождение.

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

    И да. Все это можно оформить как "специфический итератор". Но ценой написания лишнего кода (вся эта формализация, обертки и прочее).

    Тут еще одна цитата на ум приходит

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


    1. funny_falcon
      02.09.2023 18:21
      +2

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

      Я не уверен, правда. Знатоки языка, что скажете?


      1. SpiderEkb
        02.09.2023 18:21

        Вот весь вопрос в том - "а как оно будет на самом деле?". А как компилятору вступить в голову, так оно и будет.

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

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

        Vector<T> vec = selectData();
        vec.for-each(x, processData(x));

        выполнится в одном цикле? В отличии от

        exec sql declare selDataCur cursor for ....;
        exec sql open selDataCur;
        
        dou sqlCode <> 0;
          exec sql fetch selDataCur into :Data;
          processData(Data);
        enddo;

        Вместо скуля тут может быть какая-то функция типа getNextData(Data)

        Это кусочек из вполне реальной задачи. Схематично.


        1. DarthVictor
          02.09.2023 18:21
          +2

          Гарантия обычно в исходниках Vector


          1. SpiderEkb
            02.09.2023 18:21

            Это понятно. Но каждый раз копаться в исходниках? Есть задача, есть сроки ее выполнения.

            Я же не к тому плохой RUST или хороший. Наверное хороший для каких-то вещей.

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

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


      1. ivankudryavtsev
        02.09.2023 18:21
        +5

        Итератор - это trait Iterator, выдающий next(). Комбинации итераторов работают, внезапно, как итераторы, поэтому все зависит от того, как устроена цепочка, это может быть хоть np-полный перебор, хоть линейный цикл, хоть O(1).

        Итератор - это способ выдачи доступа к последовательности чего-то. Его реализация определяет эффективность, это не высечено в камне.

        https://doc.rust-lang.org/std/iter/trait.Iterator.html


      1. DarkEld3r
        02.09.2023 18:21
        +2

        Итераторы ленивые, так что тут всё хорошо. Правда не очень понял пример выше: если мы можем всё записать в виде цепочки итераторов, то тут проблем не будет, если конечно посредине не делать collect, но в этом нет никакого смысла. Если же мы из базы получили данные в виде вектора, то тут тоже без разницы — обрабатывать их циклом или нет.


    1. ivankudryavtsev
      02.09.2023 18:21
      +7

      Кажется, что Вы на пустом месте соорудили чудовищие Франкенштейна на основе своих заблуждений. Как раз для работы с БД, итератор - идеальное решение. И по факту будет что-то типа:

      for r in db.query("SELECT * FROM names").max_rows(10).limit(50).run()? {
         handle(r)
      }
      
      /// или так
      
      db.query("SELECT * FROM names").max_rows(10).limit(50).run()?.for_each(handle)
      

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

      let x = if true {
        true
      } else {
        false
      };
      
      let x = match a {
        1 => "1",
        2 => "2",
        3 => "3"
      }
      
      // вместо
      
      let mut x = false;
      if true {
         x = true;
      } 

      Вот и вся история. Вы же что-то на пустом месте разнервничались. Еще и афоризмы решили себе в помощь призвать для авторитетности.

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

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


      1. SpiderEkb
        02.09.2023 18:21

        db.query("SELECT * FROM names").max_rows(10).limit(50).run()?.for_each(handle)

        Ну я такой вариант предусматривал. Да, это хорошо. За одним маленьким "но"

        Запрос на 99% будет параметрическим. Т.е. содержать ссылки на хост-переменные. Т.е.не

        SELECT * FROM names

        но (например)

        SELECT * FROM names where name like :hNam and age > :hAge

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

        В нашей системе это будет т.н. "статический" SQL - план запроса строится на этапе компиляции, в рантайме будут только открытие-закрытие курсора (можно и динамический SQL использовать - там строка формируется "на лету" и план запроса в рантайме, но это сильно в минус по эффективности).

        Есть ли в RUST такие тонкости? Как все это работает?


        1. ivankudryavtsev
          02.09.2023 18:21
          +4

          А rust-то при чем? Изучайте крейты доступа к СУБД.


        1. Guul
          02.09.2023 18:21
          +1

          Зависит от того что используется для подключения к бд. Например, SQLx можно просить проверить запросы во время компиляции. Он в этом случае просто вызывает бд и просит её рассказать о запросе. Если запрос некорректный, код не скомпилируется. Если столбец - строка, а ты пробуешь записать значение в число, код не скомпилируется. Если возвращается INT NULL, а ты пишешь в i32 вместо option, код не скомпилируется. При этом sqlx -не orm. Пишешь обычный sql, а не тратишь время на выяснение какие методы как строят sql.


        1. aegoroff
          02.09.2023 18:21
          +1

          Есть

          let mut stmt = self.conn.prepare("SELECT id, title, created, short_text, markdown \
                              FROM post INNER JOIN post_tag ON post_tag.post_id = post.id 
                              WHERE is_public = 1 AND post_tag.tag = ?3 ORDER BY created DESC LIMIT ?1 OFFSET ?2")?;
          let files = stmt.query_map(
              [limit.to_string(), offset.to_string(), tag],
              Sqlite::map_small_post_row,
          )?;
          files.filter_map(std::result::Result::ok).collect()


  1. vadimr
    02.09.2023 18:21
    -4

    Чтобы писать программы на Лиспе, гораздо лучше подходит язык Лисп.


  1. Helltraitor
    02.09.2023 18:21
    +4

    fn factorial(n: u32) -> u32 {
        if n == 0 {
            return 1;
        }
        n * factorial(n - 1)
    }

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

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

    Чо

    Вместо исключений Rust использует два типа ошибок:

    Option - возвращает значение типа T, если оно доступно, или None, если оно недоступно.

    Чо. С каких пор Option является типом ошибки?

    Между Eq и PartialEq на самом деле есть разница. Если интересно, это обсуждали, например, здесь: https://users.rust-lang.org/t/what-is-the-difference-between-eq-and-partialeq/15751/11

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

    Может, я не прав, но это в корне не верно. В данных примерах мы используем примитивы, но если представить, что у нас более сложные типы, то намного лучше предоставить реализацию по умолчанию с T: Trait1 + Trait2, а там, где необходимо предоставить другие трейты или типы. Таким образом, например, реализованы типы из std (Box, например).

    чтобы обойти систему типов и безопасности

    Ключевое слово unsafe ничего не выключает, чтобы называть это обходом. Например, мы можем сделать каст &T => *mut T, но обращение к нему все еще может быть только в unsafe блоке.

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

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


  1. mastan
    02.09.2023 18:21

    Разделение реализаций

    ...

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

    Возможно это в каком-то случае было бы правильно, но в приведённом примере код для f32 и i32 идентичный и выглядит просто как нарушение принципа DRY. И вообще сложно вообразить, для какого же типа distance_to будет выглядеть иначе.