Как пишется != как хранится != как интерпретируется 🙈



Разбавлю немного ОО тематику последних постов. Одной из самых мозговзрывающих концепций, для начинающих в мире Computer Science - это то, как данные хранятся в памяти и как они выводятся нам на экран.



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



Приведу пример. В JS мы можем задать числовой тип используя множество различных литеральных форм. Формы отличаются используемой системой счисления: двоичная, восьмеричная, десятичная и шестнадцатеричная. Более того, используя функцию parseInt мы можем добавить поддержку записи до 36-тиричной системы счисления.



console.log( 0b10000000000 );

console.log( 0o2000 );

console.log( 1024 );

console.log( 0x400 );

console.log( parseInt('SG', 36) );




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



А вот для задач информатики удобнее использовать другие системы. Но влияет ли это на что-то, кроме того, как мы записываем число? И тут ответ - в большинстве ЯП однозначно нет.



В JS у нас два явных числовых типа: Number (число с плавающей точкой размером 64 бита) и BigInt (целое безразмерное знаковое число). Под капотом, VM могут вводить дополнительные числовые типы, например, SMI для хранения целых знаковых чисел не превышающих 2 ** 31.



А значит, я могу точно сказать, что 0b101 === 5. Оба эти литерала представляются типом Number, который абсолютно одинаково хранится в памяти.



Хорошо, но почему тогда отладчик число 0b101 всегда выводит как 5? А дело в том, что отладчик выводит значение типа в максимальной удобной для восприятия человеком, а так как у нас тут число, то логично использовать десятичную систему счисления. Более того, сам вывод на экран подразумевает следующую операцию: нам необходимо преобразовать байты числа в формат строки в соответствии с используемой кодировкой или таблицей символов; затем "отрендерить" эту строку на экране используя специальную схему визуализации - "шрифт" и библиотеку рендеринга шрифтов, например, DirectWrite (за нас это, как правило, делает ОС или другое окружение).



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



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



fn main() {

let a: u8 = 10;

let b: i8 = 10;

let c: u16 = 10;

let d: f32 = 10.0;



print_bytes_in_binary(&a.to_ne_bytes()); // 00001010

print_bytes_in_binary(&b.to_ne_bytes()); // 00001010

print_bytes_in_binary(&c.to_ne_bytes()); // 0000101000000000

print_bytes_in_binary(&d.to_ne_bytes()); // 00000000000000000010000001000001

}



fn print_bytes_in_binary(bytes: &[u8]) {

let mut res = String::new();



for byte in bytes {

res += &format!("{:08b}", byte)

}



println!("{:?}", res);

}




Можем видеть, что число 10 одинаково представлено в памяти для u8 и i8, а вот уже для других типов значение отличается.

А также видим, что выведение данных в отладчик является процессом интерпретации. Вот мы смотрим на байты и представляем их как десятичные числа, а вот как просто поток нулей и единиц.