tgoop.com/paniccode/18
Last Update:
А поделиться я ей решил, потому что ее суть напомнила мне об одной мысли из какого-то древнего доклада, который я не могу откопать теперь(
Если верить моей памяти, то суть его была в том, что чуваку досталась какая-то легаси кодовая база с кучей багосов, и он решил поанализировать, какого рода баги там есть
И пришел к тому, что бОльшая часть самых неприятных ошибок, это... условия. Конкретно одинаковые по смыслу условия, разбросанные по коду
Ну например, вы проверяете, что строка начинается с какого-то префикса при обработке запроса, и в какой-то внутренней функции
Основная проблема в том, что код постоянно меняется, и вы, например, поменяете префикс в одном месте, но не в другом, хоп и получили баг
По сути, есть разные категории ошибок: проезды по памяти, гонки, логические ошибки и прочее. И многие ошибки мы умеем довольно быстро находить: (safe) Rust не даст вам скомпилировать код с проездом по памяти или гонкой, всякие санитайзеры помогут найти в C++
Но вот логические ошибки, это не поймать инструментами особо. Самый лучший инструмент - это написание тестов, что как мы все знаем, довольно часто игнорируется.
Еще после просмотра того доклада, я как-то начал относиться к любым "логическим" if
ам с осторожностью: по сути, каждый из них может быть причиной бага.
И вот как раз этот подход из статьи выше, как мне кажется, помогает минимизировать количество таких ошибок: нужно пытаться выносить валидацию ("логические" проверки) в одно место, и после этого в системе типов как бы помечать этот объект "провалидированым", а в функциях и прочем уже использовать этот провалидированный тип
В статье в целом есть хорошие примеры, но я все равно приведу еще один: допустим, мы пишем какую-то библиотеку с математическими функциями, и многие функции могут принимать только положительные числа.
Если это целочисленные типы, то у нас уже(!) есть встроенное решение: unsized int
и его вариации:fn sqrt(val: u32) -> u32
Все, нам не нужны никакие проверки внутри функции, система типов гарантирует это. И я думаю, что вы согласитесь, что да, это разумное решение и у него нет особо минусов.
Что делать, если мы получаем число от пользователя (из json реквеста, ввод с клавиатуры, не важно)? Провалидировать его как можно раньше и засунуть в unsigned int
тип, а дальше работать только с ним.
Забавно то, что если вы начнете писать библиотеку в такой идеологии, то все функции такого рода будут принимать uint
, а следовательно они как бы будут заставлять вас провалидировать значение как можно раньше:fn sqrt(val: u32) -> u32 // базовая функция
fn round_sqrt(val: u32) -> u32 // какая-то функция, которая использует первуюНо что если мы хотим еще и работать с числами с плавающей точкой? В большинстве языков нет встроенного типа для этого. Дак давайте сделаем свой!
fn handle(val: String) { // наш обработчик
let res = round_sqrt(val???); // хотим вызвать "сложную" функцию, но она сразу требует провалидированный тип
}
pub struct uf32 {По сути, это тоже самое, что и
val: f32, // приватное
}
fn new(val: f32) -> uf32 {
if val < 0.0 {
panic!("expected positive value"); // или, еще лучше, можно возвращать ошибку
}
return uf32{ val };
}
unsafe fn new_unchecked(val: f32) -> uf32 { // явно помечаем, что это небезопасно
return uf32{ val };
}
// арифм операции и прочее
uint
: чтобы его получить, (по хорошему) нужно проверить, что значение действительно соответсвует типу. И после этого, можно его использовать уже без всяких проверокfn sqrt(val: uf32) -> uf32; // без проверок
BY Panic! At the 0xC0D3
Share with your friend now:
tgoop.com/paniccode/18