Или пишем код, который не дает багать
Наверняка вы когда-нибудь программировали на языке с динамической типизацией (Python, JavaScript, …), и статической (C++, Go, Java, C#, Rust, …), и замечали, что в первых языках часто встречаются ошибки, когда вы передали не тот тип как аргумент функции, а во вторых такого не случается. Вернее, случается, но компилятор поймает это, и вам не нужно будет даже запускать программу чтобы найти и пофиксить такой баг.
// Rust
fn foo(s: &str) {
println!("{}", s.len());
}
foo(3); // compilation error
// JavaScript
function foo(str) {
console.log(str.length)
}
foo(3)
// оно даже не падает,
// а просто печатает undefined!!!
Если кратко, то в этом и идея: сделать какую-то штуку (систему типов), которая могла проверять какие-то полезные инварианты в коде на этапе компиляции, а не в рантайме.
Как к этому пришли и почему именно система типов - объяснять долго, да и я половину не знаю. Но в итоге, чем выразительнее система типов у языка - тем больше логики можно выразить через нее - тем больше ошибок можно поймать на этапе компиляции - тем быстрее их можно поймать и пофиксить - тем приятнее и быстрее можно программировать!
Я буду приводить код на Rust для любителей и чтобы п о п у л я р и з и р о в а т ь язык, но также буду приводить код на C++ для тех, кто не знает Rust, т. к. в целом все ниже перечисленное можно сделать на них обоих. С одним нюансом, правда: первый пример именно покажет разницу между стандартными имплементациями файлов в обоих языках, но дальше пример будет про абстрактную разработку обещаю 🙂
В качестве простого примера хочется сравнить std::fstream
из C++ и std::fs::File
из Rust. Многие люди считают, что в расте намного лучше развита тема выражения логики через типы, хотя по факту многие вещи из раста можно так же сделать в плюсах, но почему-то не сделали возможно потому что языку много лет. На самом деле, в конце статьи есть пример, который показывает, что в расте этот тип хорошо работает, в том числе потому что в нем есть много полезных инструментов, которых в C++ нет, и в плюсах бы этот тип мог бы работать намного хуже с точки зрения удобства использования. Но давайте по делу:
Давайте напишем простую структуру-логгер, которая принимает файл в конструкторе, и что-то пишет в него в методе:
// не обращаем внимания, если не знаете Rust
use std::io::Write;
// объявили структуру с полем file типа std::fs::File
pub struct Logger {
file: std::fs::File,
}
// по сути тут мы указываем методы структуры,
// как если бы написали их внутри класса в C++
impl Logger {
// конструктор
pub fn new(file: std::fs::File) -> Logger {
Logger {
file: file,
}
}
// self это как this
pub fn write(&mut self, value: u64) {
writeln!(&mut self.file, "value = {}", value);
}
}
fn main() {
// создаем новый файл
let file = std::fs::File::create("file.txt").unwrap();
// создаем новый логгер
let mut logger = Logger::new(file);
// вызываем метод
logger.write(3);
}
#include <iostream>
#include <fstream>
class Logger {
public:
Logger(std::ofstream&& _file) : file(std::move(_file)) {}
void write(uint64_t v) {
file << "value = " << v << "\\n";
}
private:
std::ofstream file;
};
int main() {
std::ofstream file("file.txt");
Logger logger(std::move(file));
logger.write(3);
}
Казалось бы, все одинаково. Но есть нюанс, заключающийся в том, что std::ofstream
может быть “пустым”, а std::fs::File
можно только открыть или создать, и у него нет понятия “пустого” файла. Из-за этого в C++ можно забыть открыть файл, а вот в Rust такого просто нельзя сделать (у вас тупо нет такой функции):
int main() {
std::ofstream file;
Logger logger(std::move(file));
logger.write(3);
}
И когда я запускал этот код, меня очень удивило - он не упал! И это нормально - std::ofstream
внутри создает пустой файловый буффер (что бы это ни значило), и спокойно в него пишет. Забавно то, что file.is_open() == false
, т.е. мы можем спокойно писать в закрытый файл (в чем здесь логика - для меня загадка). Кажется, что это немного не то, что хотелось бы. Для меня это аналог вывода undefined
из первого примера. Да, код работает, но на самом деле в нем есть бага и хотелось бы, чтобы он не работал...
В итоге в этом примере мы написали одинаковый класс, но благодаря тому, что в стандартной библиотеке Rust реализована идея логики через систему типов, мы физически не можем набагать в ее использовании, в отличие от C++ версии
Ладно ладно, это все понятно, многие из вас наверное о таком слышали, ничего нового. Но у меня есть более интересный пример: давайте переделаем наш логгер так, чтобы он сам внутри создавал файл, когда, например, набралось 10 строчек (вообще идея разделять логи по дням, но для простоты давайте пока так). Для этого каждый раз, когда вызывается функция write
давайте проверять какое-то условие, и если оно выполнилось, создавать новый файл. Я не буду приводить весь код, только функцию write
:
pub fn write(&mut self, value: u64) {
self.counter += 1;
if self.counter % 10 == 0 {
self.file = std::fs::File::create(
format!("log_{}.txt", self.counter) // форматируем имя файла
).unwrap();
}
writeln!(&mut self.file, "value = {}", value);
}
void write(uint64_t v) {
counter++;
if (counter % 10 == 0) {
file = std::ofstream(
"log_" + std::to_string(counter) + ".txt"
);
}
file << "value = " << v << std::endl;
}
Естественно, хочется вынести этот код в отдельную функцию:
pub fn create_file(&mut self) {
self.counter += 1;
if self.counter % 10 == 0 {
self.file = std::fs::File::create(
format!("log_{}.txt", self.counter)
).unwrap();
}
}
pub fn write(&mut self, value: u64) {
self.create_file();
writeln!(&mut self.file, "value = {}", value);
}
void create_file() {
counter++;
if (counter % 10 == 0) {
file = std::ofstream(
"log_" + std::to_string(counter) + ".txt"
);
}
}
void write(uint64_t v) {
create_file();
file << "value = " << v << std::endl;
}