Rust: «Небезопасные абстракции»

http://smallcultfollowing.com/babysteps/blog/2016/05/23/unsafe-abstractions/
  • Перевод

Небезопасные (unsafe) абстракции


Ключевое слово unsafe является неотъемлемой частью дизайна языка Rust. Для тех кто не знаком с ним: unsafe — это ключевое слово, которое, говоря простым языком, является способом обойти проверку типов (type checking) Rust'а.


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


Но не все так просто, детали — под катом.


Данная заметка представляет ключевое слово unsafe и идею ограниченной "небезопасности".
Фактически это предвестник заметки, которую я надеюсь написать чуть позже.
Она обсуждает модель памяти Rust, которая указывает, что можно, а что нельзя делать в unsafe коде.


unsafe код добавляет 3 возможности:


  1. Чтение и запись статической изменяемой(static mutable) переменной
    В С такая переменная обозначается extern.
    Так как к переменной возможно одновременное обращение из нескольким потоков,
    то возникает состояние гонки(race condition), когда обращение к переменной не синхронизировано.
    Rust по-умолчанию предотвращает это, и чтобы обойти это ограничение, используется unsafe код.

static mut N: i32 = 1;

fn add_one(n: i32) -> i32 {
    n + 1
}

fn main() {
    unsafe {
        N = add_one(N); // пишем
    }

    // что-то еще выполняем

    unsafe {
        println!("{}", N); // читаем
    }
}

  1. Разыменование сырого(raw) указателя
    Компилятор не знает заранее, куда указывает указатель.
    Ответственность берет на себя программист, которые проверяет
    на то, что значение указателя указывает на память, обращение к которой допустимо.

fn add_one_ptr(n: *mut i32) {
    unsafe {
        *n = *n + 1;
    }
}

fn main() {
    let mut n = 5;

    add_one_ptr(&mut n as *mut i32); // пишем

    // что-то еще выполняем

    // safe код, ибо n - не static mutable
    // и мы ничего не разыменовываем
    println!("{}", n); // читаем
}

А вот такой код вызовет segmentation fault:


unsafe {
        let ptr = 0 as *mut i32;
        *ptr = 1;
}

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

Вот так:


unsafe fn do_dangerous_thing() {
    println!("{}", "in `unsafe` code");
}

fn main() {
    unsafe {
        do_dangerous_thing();
    }
}

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


"Небезопасный" код как плагин


Я думаю, что то, как интерпретируемые языки, подобные Ruby (или Python) используют код на C, является хорошим сопоставлением с работой unsafe в Rust. Возьмём, скажем, JSON модуль в Ruby. Он включает в себя как реализацию на Ruby (JSON::Pure), так и альтернативную реализацию на C (JSON::Ext). Обычно когда вы используете модуль JSON, вы запускаете С код, но Ruby код
не взаимодействует с ним так же как и с обычным Ruby кодом. Внешне данный код выглядит так
же как и любой другой модуль на Ruby, но внутри он может использовать разные хитрые приемы и выполнять оптимизации, которые невозможно написать только в коде на самом Ruby. (Можете почитать эту превосходную статью на Helix, чтобы узнать больше, также там можно узнать о том, как писать плагины к Ruby на Rust).


Хорошо, такое же может случиться и в Rust, но в несколько другом масштабе. Например, можно написать производительную реализацию хэш-таблицы на "чистом" Rust. Добавление же unsafe кода позволит сделать этот код еще быстрее. Если данная структура данных будет использоваться многими людьми или ее работа является очень важной для вашей программы,
то это может стоить того (Поэтому мы используем unsafe код в реализации стандартной библиотеки). Однако в любом случае, вызывающий код на Rust обращается к unsafe коду так же, как и к не-unsafe: наложенные уровни абстракции предоставляют единообразный
внешний API.


Разумеется, то, что использование unsafe кода позволяет сделать программу быстрее, не означает, что вы должны использовать его очень часто. Так же как большинство Ruby кода написано на Ruby, большинство Rust кода написано на safe Rust. Это верно еще и потому, что safe Rust код очень эффективен, так что выгоды от перехода к использованию unsafe кода для достижения высокой производительности, редко стоят приложенных на это усилий.


Думается, что самым частым случаем использования unsafe кода на Rust является использование библиотек на других языках через FFI (Foreign Function Interface). Каждый вызов C функции из Rust является unsafe, потому что компилятор никак не может судить о "безопасности" С кода.


Расширение языка посредством unsafe кода.


Я думаю, что интереснее всего писать unsafe код на Rust (или C модуль на Ruby) для того,
чтобы расширить возможности языка. Наверное, самым часто приводимым примером является тип Vec в стандартной библиотеке, которая использует unsafe код для проведения манипуляций с неинициализированной памятью. Rc и Arc, являющиеся счетчиками ссылок,
также являются показательным примером. Однако имеются гораздо более интересные примеры, как-то: CrossBeam и deque используют unsafe код для реализации неблокирующих (lock-free) структур данных или Jobsteal и Rayon используют unsafe код для реализации пула потоков (thread pool).


В данной заметке мы рассмотрим один простой пример: метод split_at_mut, который имеется в стандартной библиотеке. Данный метод работает с изменяемыми срезами (mutable slices). Также он принимает индекс (mid) и разделяет срез на две части по указанному индексу. Впоследствии он возвращает два меньших среза: один с диапазоном 0..mid, второй — в mid..


Для удобства можно представить себе split_at_mut реализованным так:


impl [T] {
    pub fn split_at_mut(&mut self, mid: usize) -> (&mut [T], &mut [T]) {
        (&mut self[0..mid], &mut self[mid..])
    }
} 

Данный код не будет скомпилирован по двум причинам:


  • В общем случае компилятор не рассматривает индекс слишком "пристально", в отрыве от включающего его массива. Это значит, что когда он видит индексирование вида foo[i], он игнорирует индекс и обращается с массивом, как с единым целым (foo[_]). Это значит, что он не может выявить то, что &mut self[0..mid] является обращением к другому участку памяти, нежели &mut self[mid..]. Это из-за того, что проведение подобно анализа потребовало бы гораздо более сложной системы типов.
  • Фактически оператор [] не является частью языка — он полностью реализован в стандартной библиотеке. Поэтому, даже если бы компилятор знал, что 0..mid и mid.. не перекрываются, из этого бы не следовало бы его знание о том, что данные диапазоны обращаться к неперекрывающимся областям памяти.

Можно себе вообразить, что возможно, изменяя компилятор, добиться того, что указанный пример кода будет компилироваться, и, возможно, мы это однажды реализуем. Но в настоящий момент мы предпочитаем реализовывать методы подобные split_at_mut посредством unsafe кода. Это позволяет нам иметь простую систему типов, имея возможность писать API подобный split_at_mut.


Границы абстракции


Взгляд на unsafe код как на подключаемый код позволяет ясно выразить идею о "границах абстракции". Когда вы пишете плагин на Rust, вы ожидаете, что когда вызывающий код на Ruby будет вызывать ваши функции, он будет предоставлять вам "родные" для Ruby переменные.
Внутри же вы можете поступать как хотите, например, использовать C массив вместо vector'а на Ruby. Но при переходе обратно к выполнению Ruby кода вы должны преобразовать ваши возвращаемые сущности в стандартные для Ruby переменные.


Также обстоит дело и с unsafe кодом на Rust. Клиентскому коду кажется, что ваш код является safe. Это значит, что можно полагать, что вызывающий код будет передавать на вход допустимые значения. Это также значит, что все ваши значения, которые вы возвращаете, должны соответствовать требованиям системы типов Rust. Находясь же внутри unsafe границ, вы можете обходить правила по своему усмотрению (разумеется, объем предоставляемых дополнительных возможностей является темой для обсуждения; я надеюсь обсудить это в последующей заметке).


Давайте посмотрим на метод split_at_mut, который мы видели в прошлом разделе. Для упрощения понимания, мы будем рассматривать только внешний интерфейс функции, представляемый сигнатурой:


impl [T] {
    pub fn split_at_mut(&mut self, mid: usize) -> (&mut [T], &mut [T]) {
        // Тело функции пропущено, так что мы можем сосредоточить внимание
        // на публичном интерфейсе. В любом случае безопасный код не должен 
        // интересоваться тем, что здесь находится. 
    }
}    

Что мы может понять из этой сигнатуры?
Начнем с того, что split_at_mut полагается на то, что все ее входные данные являются допустимыми (В safe-коде, компилятор проверяет, что это действительно так). unsafe семантика метода split_at_mut может быть выражена в следующих правилах:


  • self аргумент имеет тип mut [T]. Из этого следует, что мы получим ссылку, указывающую на некоторое (N) количество элементов типа T. Это изменяемая (mutable) ссылка, поэтому мы знаем, что к памяти, к которой обращается self, не может обращаться больше никто (пока изменяемая ссылка не перестанет существовать). Мы также знаем, что память инициализирована.
  • mid аргумент имеет тип usize. Все, что мы знаем, так это то, что данная переменная представляет собой неотрицательное целое число.

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


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


Возможные реализации


Давай посмотрим на несколько возможных реализаций split_at_mut и определим, являются ли они рабочими вариантами или нет. Мы уже видели, что реализация, написанная на "чистом" Rust не работает (не компилируется). Попробуем реализовать функцию, используя сырые (raw) указатели:


impl [T] {
    pub fn split_at_mut(&mut self, mid: usize) -> (&mut [T], &mut [T]) {
        use std::slice::from_raw_parts_mut;

        // `unsafe` блок дает доступ к операциям с *сырым* указателем.
        // Используя `unsafe` блок, мы заявляем, что никакие наши действия
        // не будут причиной UB(undefined behaviour).
        unsafe {
            // получить *сырой* указатель на первый элемент 
            let p: *mut T = &mut self[0]; 
            // получить указатель на `mid` элемент
            let q: *mut T = p.offset(mid as isize);
            // количество элементов после `mid`
            let remainder = self.len() - mid;
            // "собрать" подмассив из элементов в диапазоне `0..mid`
            let left: &mut [T] = from_raw_parts_mut(p, mid);
            // "собрать" подмассив из элементов в диапазоне `mid..`
            let right: &mut [T] = from_raw_parts_mut(q, remainder);
            (left, right)
        }
    }
}    

Эта версия наиболее приближена к той, которая реализована в стандартной библиотеке.
Однако данный код основывается на предположении, которое не обосновано входными значениями: код предполагает, что mid находится в границах массива. Нигде не проверяется, что mid <= len. Это значит, что q может быть вне границ массива, также это значит, что вычисление remainder может вызвать переполнение типа и обертывание (wrap around),
Это некорректная реализация, потому что требует больше гарантий, чем требуется
от вызывающего кода.


Мы может исправить данную реализацию добавлением assert'а того, что mid является
допустимым индексом (заметьте, что assert в Rust всегда выполняется, даже в оптимизированном коде):


impl [T] {
    pub fn split_at_mut(&mut self, mid: usize) -> (&mut [T], &mut [T]) {
        use std::slice::from_raw_parts_mut;
        // проверка, что `mid` находится в границах массива:
        assert!(mid <= self.len());

        // как и раньше, но без комментариев
        unsafe {
            let p: *mut T = &mut self[0]; 
            let q: *mut T = p.offset(mid as isize);
            let remainder = self.len() - mid;
            let left: &mut [T] = from_raw_parts_mut(p, mid);
            let right: &mut [T] = from_raw_parts_mut(q, remainder);
            (left, right)
        }
    }
}    

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


Расширяем границы абстракции


Конечно, могло так случиться, что мы на самом деле хотели считать, будто mid находится в допустимых границах, и хотели обойтись без этой проверки. Мы не можем сделать этого, потому что split_at_mut является частью стандартной библиотеки. Однако вы можете представить себе вспомогательный метод для вызывающего кода, который бы удостоверял это предположение, так что мы бы обходились без дорогостоящей проверки на нахождение индекса в границах массива во время выполнения. В этом случае, split_at_mut полагается на вызывающий вспомогательный код для того, чтобы можно было гарантировать нахождение
mid в границах массива. Это значит, что split_at_mut больше не является safe-кодом, потому что имеет дополнительные требования к входным значениям, чтобы гарантировать безопасную работу с памятью.


Rust позволяет выражать то, что весь код функции является unsafe посредством помещения ключевого слова unsafe в сигнатуре функции. После такого перемещения, "небезопасность" кода больше не является внутренней деталью реализации функции, теперь это часть интерфейса функции. Так что мы можем сделать вариант split_at_mutsplit_at_mut_unchecked — который не проверяет нахождение mid в допустимых границах:


 impl [T] {
     // Здесь данная функция объявлена как `unsafe`. Вызов данной
     // функции является `unsafe` действием для вызывающего кода,
     // потому что они должны гарантировать инвариант: `mid <= self.len()`.
     unsafe pub fn split_at_mut_unchecked(&mut self, mid: usize) -> (&mut [T], &mut [T]) {
         use std::slice::from_raw_parts_mut;
         let p: *mut T = &mut self[0]; 
         let q: *mut T = p.offset(mid as isize);
         let remainder = self.len() - mid;
         let left: &mut [T] = from_raw_parts_mut(p, mid);
         let right: &mut [T] = from_raw_parts_mut(q, remainder);
         (left, right)
     }
 }    

Когда fn объявлена как unsafe подобно тому, как это сделано выше, вызов ее тоже становится unsafe. Это значит, что человек, который пишет вызывающий код, должен ознакомиться с документацией функции и убедиться, чтобы все условия соблюдены.
А в данном конкретном случае вызывающий код должен убедиться, что mid <= self.len().


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


Используя split_at_mut_unchecked, мы можем изменить реализацию split_at_mut так, чтобы она внутри себя, проводя необходимые проверки, вызывала split_at_mut_unchecked:


impl [T] {
    pub fn split_at_mut(&mut self, mid: usize) -> (&mut [T], &mut [T]) {
        assert!(mid <= self.len());

        // Помещая `unsafe`-блок в функции, мы заявляем, что мы знаем
        // что дополнительные условия, наложенные на `split_at_mut_unchecked`,
        // выполнены, и поэтому вызов этой функции является безопасным действием.
        unsafe {
            self.split_at_mut_unchecked(mid)
        }
    }

    // **NB:** требует, что `mid <= self.len()`.
    pub unsafe fn split_at_mut_unchecked(&mut self, mid: usize) -> (&mut [T], &mut [T]) {
        ... // как и ранее.
    }
}

Небезопасные абстракции и приватность.


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


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


pub struct Vec<T> {
    pointer: *mut T, // указатель на начало выделенной области памяти
    capacity: usize, // количество выделенной памяти
    length: usize, // количество инициализированной памяти
}

Код реализации Vec тщательно поддерживает инвариант, согласно которому pointer и первые length элементов, к которым он обращается, всегда являются допустимыми. Можно подумать, что если бы length было открытым (pub) полем, то верхний инвариант был не возможен: любой вызывающий внешний код мог бы изменить длину Vec на произвольную.


Исходя из этой причины, границы "небезопасности" склонны попадать в одну из двух категорий:


  • единичные функции, подобные split_at_mut
  • тип, который содержится в своём собственном модуле, например, Vec
    • данный тип, как правильно, имеет приватные вcпомогательные функции
    • также может содержать вспомогательные функции, которые являются unsafe

Типы с unsafe интерфейсами


Как мы видели ранее, иногда может быть полезным создавать unsafe функции подобные split_at_mut_unchecked, которые могут служить строительным блоком для безопасных абстракций. Это справедливо также и для типов. Посмотрев на реализацию Vec из стандартной библиотеки, вы увидите, что она выглядит подобно приведенному выше коду.


pub struct Vec<T> {
    buf: RawVec<T>,
    len: usize,
}

Что это за тип, RawVec? Выясняется, что это вспомогательный unsafe тип который содержит в себе указатель (pointer) и емкость (capacity):


pub struct RawVec<T> {
    // `Unique` является еще одним вспомогательным `unsafe` типом,
    // который обозначает *сырой* указатель с единственным владельцем(uniquely owned).
    ptr: Unique<T>,
    cap: usize,
}

Что делает RawVec вспомогательным unsafe типом? В отличие от функций, понятие "unsafe тип" является довольно размытым. Я определяю такой тип как тип, который не позволяет вам делать ничего полезного без использования unsafe кода. Безопасный (safe) код позволяет конструировать RawVec, он даже позволяет изменять размер буфера, который лежит в основе Vec, но если вы хотите обратиться к значению, которое находится в данном буфере, вы можете это сделать, только используя метод ptr, который возвращает *mut T. Это "сырой" указатель, так что его разыменование является unsafe действием. Это значит, что для того, чтобы предоставлять полезный функционал, RawVec должен быть включен в другую unsafe абстракцию (подобную Vec, которая отслеживает инициализацию.


Вывод


unsafe абстракции являются довольно мощным инструментом. Они позволяют вам использовать практически любые хитрые приемы, которые вы только можете себе вообразить, или использовать одну из возможностей вашей системы, в то же время имея безопасный и относительно простой язык программирования. Мы используем "небезопасность" для реализации некоторого числа ключевых абстракций в стандартной библиотеке, включая такие основные структуры данных как Vec и Rc. Данные абстракции скрывают unsafe код под безопасным API, поэтому пользователи данного кода ничем не рискуют.


Как далеко можно зайти?


Одной из вещей, которую я не обсуждал в данной заметке, является то, что именно можно, а что нельзя делать внутри unsafe кода. Очевидно то, что unsafe кода нужен для того, чтобы обходить правила, но насколько сильно мы можем их обходить? В настоящий момент у нас мало выпущенный указаний, касающихся данной темы. Это то, чем мы хотим заняться. Был даже RFC, касающийся этой темы, хотя, я думаю, что предстоит еще немало поработать, до того как мы придём к окончательным и ясным выводам.


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


Здесь есть интересный момент. Чем больше возможностей unsafe кода допускается, тем труднее компилятору оптимизировать данный код. Это потому что он в таких случаях не всегда может точно определять aliasing адресов и не всегда может переставлять местами выражения (statements reordering).


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


UPD: добавил описание 3 возможностей unsafe кода.

Часто ли вы используете `unsafe` код на Rust?

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

  • +39
  • 5,8k
  • 9
Поделиться публикацией
Похожие публикации
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 9
  • +6

    Отличная статья. Новички в Ржавчине часто не понимают какова роль unsafe в языке и или боятся его как огня и пытаются избегать любыми путями, или наоборот неоправданно активно его используют. В обоих крайностях качество кода сильно страдает — или вместо одного аккуратного сырого указателя получаются монструозные и неуклюжие нагромождения безопасных абстракций, или весь код представляет из себя тотальное нагромождение UB. Даешь больше разъяснительных статей! :)

    • +2

      А что думаете по поводу недавнего поста
      (https://manishearth.github.io/blog/2017/12/24/undefined-vs-unsafe-in-rust/?utm_source=newsletter_mailer&utm_medium=email&utm_campaign=weekly)?


      … The reason it is still unsafe is because it’s possible to trigger UB by only changing the “safe” caller code. I.e. “changes to code outside unsafe blocks can trigger UB if they include calls to this function”.

      Плюс список того, что считается UB (https://doc.rust-lang.org/beta/reference/behavior-considered-undefined.html).


      Проблемы, как я понимаю, могу возникнуть в случае использования какого-нибудь чужого крейта. Так например у Tock https://www.tockos.org/blog/2017/crates-are-not-safe/ возникают сложности, т.к. вынуждает их вручную контроллировать зависимости и их изменения.

      • +4

        Статья "Undefined vs Unsafe in Rust" — тоже очень хорошая.


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

    • +4

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

      • +6
        Как человек, который в глаза не видел Rust, я восхищен.
        unsafe — это как сказать компилятору и всем коллегам: вот здесь я делаю страшные вещи, я уверен, что я делаю то, что нужно, а если чё-то вдруг за сегфолтилось, то ищите причину здесь, больше негде.
        • +5

          Да. Стоит заметить, что, например, в коде самого Rust компилятора(!), только 4% кода является unsafe, да и то unsafe там используется в основном для взаимодействия с C библиотеками посредством FFI(Foreign Function Interface).

          • +4
            Позволю себе добавить, что помимо сегфолтов, Rust позволяет статически (на этапе компиляции) доказать отсутствие в программе гонок по данным так же, как он предотвращает обращение к неинициализированной или уже освобожденной памяти без необходимости GC. Причем без каких-либо накладных расходов, то есть производительность от этого не страдает.
            • 0
              А в каких случаях страдает производительность, что вынуждает использовать unsafe? Написано «используйте unsafe для улучшения производительности», и ни примера, ни цифр. У вас случайно нет таких примеров?
              • +6
                Тут сразу стоит сказать что сам по себе `unsafe` не делает ничего особенного и не увеличивает магическим образом производительность на 146% самим фактом своего использования. Он всего лишь позволяет программисту сказать компилятору «я знаю что делаю» и успокоить его анализатор, который в противном случае бы не дал написать потенциально небезопасную конструкцию.

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

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

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

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

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

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

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

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