Русский
Русский
English
Статистика
Реклама

Типы данных

Перевод Свод правил по работе с целыми числами в CC

09.04.2021 12:18:12 | Автор: admin

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

Всякий раз, когда я читаю или пишу код на C/C++, мне приходится вспоминать и применять эти правила в тех или иных ситуациях, например при выборе подходящего типа для локальной переменной/элемента массива/поля структуры, при преобразовании типов, а также в любых арифметических операциях или сравнениях. Обратите внимание, что типы чисел с плавающей запятой мы затрагивать не будем, так как это большей частью относится к анализу и обработке ошибок аппроксимации, вызванных округлением. В противоположность этому, математика целых чисел лежит в основе как программирования, так и компьютерной науки в целом, и в теории вычисления здесь всегда точны (не считая проблем реализации вроде переполнения).

Типы данных


Базовые целочисленные типы


Целочисленные типы устанавливаются с помощью допустимой последовательности ключевых слов, взятых из набора {char, short, int, long, signed, unsigned}.

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

  • char: минимум 8 бит в ширину;
  • short: минимум 16 бит и при этом не меньше char;
  • int: минимум 16 бит и при этом не меньше short;
  • long: минимум 32 бит и при этом не меньше int;
  • long long: минимум 64 бит и при этом не меньше long.

Наличие знака


  • Стандартный сhar может иметь знак или быть беззнаковым, что зависит от реализации.
  • Стандартные short, int, long и long long идут со знаком. Беззнаковыми их можно сделать, добавив ключевое слово unsigned.
  • Числа со знаком можно кодировать в двоичном формате в виде дополнительного кода, обратного или как величину со знаком. Это определяется реализацией. Заметьте, что обратный код и величина со знаком имеют различные шаблоны битов для отрицательного нуля и положительного, в то время как дополнительный код имеет уникальный нуль.
  • Символьные литералы (в одинарных кавычках) имеют тип (signed) intв C, но (signed или unsigned) char в C++.

Дополнительные правила


  • sizeof(char) всегда равен 1, независимо от битовой ширины char.
  • Битовая ширина не обязательно должна отличаться. Например, допустимо использовать char, short и int, каждый шириной в 32 бита.
  • Битовая ширина должна быть кратна 2. Например, int может иметь ширину 36 бит.
  • Есть разные способы написания целочисленного типа. К примеру, в каждой следующей строке перечислен набор синонимов:
    • int, signed, signed int, int signed;
    • short, short int, short signed, short signed int;
    • unsigned long long, long unsigned int long, int long long unsigned.


Типы из стандартных библиотек


  • size_t (определен в stddef.h) является беззнаковым и содержит не менее 16 бит. При этом не гарантируется, что его ширина будет как минимум равна int.
  • ptrdiff_t (определен в stddef.h) является целочисленным типом со знаком. Вычитание двух указателей будет давать этот тип. При этом не стоит ожидать, что вычитание двух указателей даст int.
  • В stdint.h определена конкретная ширина типов: uint8_t, int8_t, 16, 32 и 64. Будьте внимательны к операциям, подразумевающим продвижение типов. Например, uint8_t + uint8_t даст int (со знаком и шириной не менее 16 бит), а не uint8_t, как можно было предположить.


Преобразования


Представим, что значение исходного целочисленного типа нужно преобразовать в значение целевого целочисленного типа. Такая ситуация может возникнуть при явном приведении, неявном приведении в процессе присваивания или при продвижении типов.

Как происходит преобразование?

Главный принцип в том, что, если целевой тип может содержать значение исходного типа, то это значение семантически сохраняется.

Говоря конкретнее:

  • Когда исходный тип расширяется до целевого типа с аналогичной знаковой характеристикой (например, signed char -> int или unsigned short -> unsigned long), каждое исходное значение после преобразования сохраняется.
  • Даже если исходный и целевой типы имеют разные диапазоны, все значения в их пересекающейся части будут сохранены. Например, int, содержащий значение в диапазоне [0, 255], будет без потерь преобразован в unsigned char.

В более точной форме эти правила звучат так:

  • При преобразовании в беззнаковый тип новое значение равняется старому значению по модулю 2целевая ширина в битах. Объяснение:
    • Если исходный тип беззнаковый и шире целевого, тогда старшие биты отбрасываются.
    • Если исходный тип имеет знак, тогда в процессе преобразования берется исходное значение, и из него/к нему вычитается/прибавляется 2целевая ширина в битах до тех пор, пока новое значение не впишется в диапазон целевого типа. Более того, если число со знаком представлено в дополнительном коде, то в процессе преобразования старшие биты отбрасываются, как и в случае с беззнаковыми числами.

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


Арифметика


Продвижение/преобразование


  • Унарный арифметический оператор применяется только к одному операнду. Примеры: -, ~.
  • Бинарный оператор применяется к двум операндам. Примеры: +, *, &. <<.
  • Если операнд имеет тип bool, char или short (как signed, так и unsigned), тогда он продвигается до int (signed), если int может содержать все значения исходного типа. В противном случае он продвигается до unsigned int. Процесс продвижения происходит без потерь. Примеры:
    • В реализации присутствуют 16-битный short и 24-битный int. Если переменные x и y имеют тип unsigned short, то операцияx & y продвигает оба операнда до signed int.
    • В реализации присутствуют 32-битный char и 32-битный int. Если переменные x и y имеют тип unsigned char, то операцияx y продвигает оба операнда до unsigned int.

  • В случае двоичных операторов оба операнда перед арифметической операцией неявно преобразуются в одинаковый общий тип. Ранги преобразования возрастают в следующем порядке: int, long, long long. Рангом общего типа считается старший ранг среди типов двух операндов. Если оба операнда являются signed/unsigned, то их общий тип будет иметь ту же характеристику. Если же операнд с беззнаковым типом имеет старший или равный ранг по отношению ко второму операнду, то их общий тип будет беззнаковым. В случае, когда тип операнда со знаком может представлять все значения другого типа операнда, общий тип будет иметь знак. В противном случае общий тип получается беззнаковым. Примеры:
    • (long) + (long) (long);
    • (unsigned int) * (int) (unsigned int);
    • (unsigned long) / (int) (unsigned long);
    • если int является 32-битным, а long 64-битным: (unsigned int) % (long) (long);
    • если int и long оба являются 32-битными: (unsigned int) % (long) (unsigned long).


Неопределенное поведение


Знаковое переполнение:

  • При выполнении арифметических операций над целочисленным типом переполнение считается неопределенным поведением (UB). Такое поведение может вызывать верные, несогласованные и/или неверные действия как сразу, так и в дальнейшем.
  • При выполнении арифметики над беззнаковым целым (после продвижений и преобразований) любое переполнение гарантирвоанно вызовет оборот значения. Например, UINT_MAX + 1 == 0.
  • Выполнение арифметики над беззнаковыми целыми фиксированного размера может привести к едва уловимым ошибкам. Например:
    • Пусть uint16_t = unsigned short, и int равен 32-битам. Тогда uint16_t x=0xFFFF, y=0xFFFF, z=x*y; x и y будут продвинуты до int, и x * y приведет к переполнению int, вызвав неопределенное поведение.
    • Пусть uint32_t = unsigned char, и int равен 33-битам. Тогда uint32_t x=0xFFFFFFFF, y=0xFFFFFFFF, z=x+y; x и y будут продвинуты до int, и x + y приведет к переполнению int, то есть неопределенному поведению.
    • Чтобы обеспечить безопасную арифметику с беззнаковыми целыми, нужно либо прибавить 0U, либо умножить на 1U в качестве пустой операции. Например: 0U + x + y или 1U * x * y. Это гарантирует, что операнды будут продвинуты как минимум до ранга int и при этом останутся без знаков.


Деление/остаток:

  • Деление на нуль и остаток с делителем нуля также относятся к неопределенному поведению.
  • Беззнаковое деление/остаток не имею других особых случаев.
  • Деление со знаком может вызывать переполнение, например INT_MIN / -1.
  • Остаток со знаком при отрицательных операндах может вызывать сложности, так как некоторые части являются однообразными, в то время как другие определяются реализацией.

Битовые сдвиги:

  • Неопределенным поведением считается битовый сдвиг (< < и >>)на размер, который либо отрицателен, либо равен или больше битовой ширины.
  • Левый сдвиг беззнакового операнда (после продвижения/преобразования) считается определенным правильно и отклонений в поведении не вызывает.
  • Левый сдвиг операнда со знаком, содержащего неотрицательное значение, в следствии которого 1 бит переходит в знаковый бит, является неопределенным поведением.
  • Левый сдвиг отрицательного значения относится к неопределенному поведению.
  • Правый сдвиг неотрицательного значения (в типе операнда без знака или со знаком) считается определенным правильно и отклонений в поведении не вызывает.
  • Правый сдвиг отрицательного значения определяется реализацией.

Счетчик цикла


Выбор типа


Предположим, что у нас есть массив, в котором нужно обработать каждый элемент последовательно. Длина массива хранится в переменной len типа T0. Как нужно объявить переменную счетчика цикла i типа T1?

  • Самым простым решением будет использовать тот же тип, что и у переменной длины. Например:

uint8_t len = (...);for (uint8_t i = 0; i < len; i++) { ... }
  • Говоря обобщенно, переменная счетчика типа T1 будет работать верно, если диапазон T1 будет являться (не строго) надмножетсвом диапазона T0. Например, если len имеет тип uint16_t, тогда отсчет с использованием signed long (не менее 32 бит) сработает.
  • Говоря же более конкретно, счетчик цикла должен просто покрывать всю фактическую длину. Например, если len типа int гарантированно будет иметь значение в диапазоне [3,50] (обусловленное логикой приложения), тогда допустимо отсчитывать цикл, используя char без знака или со знаком (в котором однозначно можно представить диапазон [0,127]).
  • Нежелательно использовать переменную длины и переменную счетчика с разной знаковостью. В этом случае сравнение вызовет неявное сложное преобразование, сопровождаемое характерными для платформы проблемами. К примеру, не стоит писать такой код:

size_t len = (...);  // Unsignedfor (int i = 0; i < len; i++) { ... }

Отсчет вниз


Для циклов, ведущих отсчет вниз, более естественным будет использовать счетчик со знаком, потому что тогда можно написать:

for (int i = len - 1; i >= 0; i--) {    process(array[i]);}

При этом для беззнакового счетчика код будет таким:

for (unsigned int i = len; i > 0; i--) {    process(array[i - 1]);}

Примечание: сравнение i >= 0 имеет смысл только, когда i является числом со знаком, но всегда будет давать true, если оно будет беззнаковым. Поэтому, когда это выражение встречается в беззнаковом контексте, значит, автор кода скорее всего допустил ошибку в логике.

Заблуждения


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

  • char всегда равен 8 битам. int всегда равен 32 битам.
  • sizeof(T) представляет число из 8-битных байтов (октетов), необходимых для хранения переменной типа T. (Это утверждение ложно, потому что если, скажем, char равняется 32 битам, тогда sizeof(T) измеряется в 32-битных словах).
  • Можно использовать int в любой части программы и игнорировать более точные типы вроде size_t, uint32_t и т.д.
  • Знаковое переполнение гарантированно вызовет оборот значения. (например, INT_MAX + 1 == INT_MIN).
  • Символьные литералы равны их значениям в коде ASCII, например A == 65. (Согласно EBCDIC это утверждение ложно).
  • Преобразование указателя в int и обратно в указатель проихсодит без потерь.
  • Преобразование {указателя на один целочисленный тип} в {указатель на другой целочисленный тип} безопасно. Например, int *p (); long *q = (long*)p;. (см. каламбур типизации и строгий алиасинг).
  • Когда все операнд(ы) арифметического оператора (унарного или бинарного) имеют беззнаковые типы, арифметическая операция выполняется в беззнаковом режиме, никогда не вызывая неопределенного поведения, и в результате получается беззнаковый тип. Например: предположим, что uint8_t x; uint8_t y; uint32_t z;, тогда операция x + y должна дать тип вроде uint8_t, беззнаковый int, или другой разумный вариант, а +z по-прежнему будет uint32_t. (Это не так, потому что при продвижении типов предпочтение отдается типам со знаком).


Моя критика


  • Если вкратце, то знание и постоянное использование всех этих правил сильно нагружает мышление. Допущение же ошибки в их применении приводит к риску написания неверного или непортируемого кода. При этом такие ошибки могут как всплыть сразу, так и таиться в течение дней или даже долгих лет.
  • Сложности начинаются с битовой ширины базовых целочисленных типов, которая зависит от реализации. Например, int может иметь 16, 32, 64 бита или другое их количество. Всегда нужно выбирать тип с достаточным диапазоном. Но иногда использование слишком обширного типа (например, необычного 128-битного int) может вызвать сложности или даже внести уязвимости. Усугубляется это тем, что такие типы из стандартных библиотек, как size_t, не имеют связи с другими типами вроде беззнакового int или uint32_t; стандарт позволяет им быть шире или уже.
  • Правила преобразования совершенно безумны. Что еще хуже, практически везде допускаются неявные преобразования, существенно затрудняющие аудит человеком. Беззнаковые типы достаточно просты, но знаковые имеют очень много допустимых реализаций (например, обратный код, создание исключений). Типы с меньшим рангом, чем int, продвигаются автоматически, вызывая труднопонимаемое поведение с диапазонами и переполнение. Когда операнды отличаются знаковостью и рангами, они преобразуются в общий тип способом, который зависит от определяемой реализацией битовой ширины. Например, выполнение арифметики над двумя операндами, как минимум один из которых имеет беззнаковый тип, приведет к преобразованию их обоих либо в знаковый, либо в беззнаковый тип в зависимости от реализации.
  • Арифметические операции изобилуют неопределенным поведением: знаковое переполнение в add/sub/mul/div, деление на нуль, битовые сдвиги. Не сложно создать такие условия неопределенного поведения по случайности, но сложно вызвать их намеренно или обнаружить при выполнении, равно как выявить их причины. Необходима повышенная внимательность и усилия для проектирования и реализации арифметического кода, исключающего переполнение/UB. Стоит учитывать, что в последствии становится сложно отследить и исправить код, при написании которого не соблюдались принципы защиты от переполнения/UB.
  • Присутствие signed и unsigned версии каждого целочисленного типа удваивает количество доступных вариантов. Это создает дополнительную умственную нагрузку, которая не особо оправдывается, так как типы со знаком способны выполнять практически все те же функции, что и беззнаковые.
  • Ни в одном другом передовом языке программирования нет такого числа правил и подводных камней касательно целочисленных типов, как в С и C++. Например:
    • В Java целые числа ведут себя одинаково в любой среде. В этом языке определено конкретно 5 целочисленных типов (в отличие от C/C++, где их не менее 10). Они имеют фиксированную битовую ширину, практически все из них имеют знаки (кроме char), числа со знаком должны находится в дополнительном коде, неявные преобразования допускают только их варианты без потерь, а вся арифметика и преобразования определяются точно и не вызывают неоднозначного поведения. Целочисленные типы в Java поддерживают быстрое вычисление и эффективное упаковывание массивов в сравнении с языками вроде Python, где есть только bigint переменного размера.
    • Java в значительной степени опирается на 32-битный тип int, особенно для перебора массивов. Это означает, что этот язык не может эффективно работать на малопроизводительных 16-битных ЦПУ (часто используемых во встраиваемых микроконтроллерах), а также не может непосредственно работать с большими массивами в 64-битных системах. К сравнению, C/C++ позволяет писать код, эффективно работающий на 16, 32 и/или 64-битных ЦПУ, но при этом требует от программиста особой осторожности.
    • В Python есть всего один целочисленный тип, а именно signed bigint. В сравнении с C/C++ это сводит на нет все рассуждения на тему битовой ширины, знаковости и преобразований, так как во всем коде правит один тип. Тем не менее за это приходится платить медленной скоростью выполнения и несогласованным потреблением памяти.
    • В JavaScript вообще нет целочисленного типа. Вместо этого в нем все выражается через математику float64 (double в C/C++). Из-за этого битовая ширина и числовой диапазон оказываются фиксированными, числа всегда имеют знаки, преобразования отсутствуют, а переполнение считается нормальным.
    • Язык ассемблера для любой конкретной машинной архитектуры (x86, MIPS и т.д.) определяет набор целочисленных типов фиксированной ширины, арифметические операции и преобразования с редкими случаями неопределенного поведения или вообще без них.


Дополнительная информация (англ.)




Подробнее..

UUID и браузеры. Почему фронтенд живет без страшных айдишников?

20.02.2021 18:04:03 | Автор: admin

Решил я делать свой пет-проект по учету прочитанных книг на PWA. Покорять новые технологии и все такое. Расчет был на то, что с его выложу и установлю на телефон и вот у меня есть мобильное приложение, которое можно использовать оффлайн. Хочу я сгенерировать UUID, чтобы сохранить книгу, а не нахожу API. Предлагаю разобраться почему.


Что такое UUID


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


UUID представляет собой 16-байтное число в HEX'е формате:


xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx


Где:


x - [0 - f] (Закодированные данные)M - [0 - 5] (Версия UUID)N - [8 - b] (Вариант UUID)

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


Способы генерации UUID



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


1 и 2 версии использовали время с точностью до 0.1 микросекунды + MAC адрес, что гарантировало практически полное отсутствие возможности получить дубликат. Чтобы полностью добить эту вероятность первая версия добавляет рандомную соль, а вторая ничего не делает (вторую версию мы не любим, она вообще может сгенерировать только 64 уникальных id за семь минут).


3 и 5 хешируют пространство имен (Url, FQDN, OID) + само имя. Таким образом в каждый момент времени мы получаем абсолютно идентичные UUID для одних и тех же входных параметров.


Отличие 3 и 5 версии только в том, что 3 использует для хеширования MD-5, а 5 SHA-1.

4 же версия просто использует рандом _()_/.


Почему его нет в браузере


JS не имеет доступа к данным машины


Мы не можем получить MAC-адрес пользователя, мы не можем получить данные его IP, а так же вообще что-либо с его машины без разрешения пользователя.
Да, мы можем загружать фалы и делать красивые file-инпуты на фронте, но мы можем получить только конкретный файл, который нам предоставит пользователь. Но согласитесь, как бы не шибко удобно запрашивать на каждый UUID по файлу. Неудобно их запрашивать даже каждый раз при входе на сайт.
Сделано же это из благих целей: представьте, что читаете вы Хабр, а тут:



import * as console from 'console';console.run('rm -rf /**/kursach*final-(\d+)?.docx')

И больше никаких проблем с высшим образованием.


Потому что до не давних пор он был просто не нужен


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


Вы можете возразить, что есть PWA, и что оно есть аж с 2007 года. Но так уж вышло, что PWA никому не нужен, примерно, с того же самого времени. (Хотя нынче Play Market позволяет загружать PWA как приложения, но...). Сами посудите, много вы PWA приложений установили? Я даже Хабр не поставил.


Другой вопрос почему до сих тор нет никакого API типа getUUID(), чтобы переложить эту проблему на браузер? Скорее всего ответ кроется все в той же ссылке на то, что в 99 случаев из 100 сайту это просто-напросто незачем.


Но осадочек остался.


Какие трудности вас ждут


Точность времени


Я бы не стал называть это большой проблемой.


Мы можем получить время с точностью только до миллисекунды, в то время как первая версия UUID делала это с точностью до 100 наносекунд.


Ну чисто теоретически мы можем получить и с точностью до 1 микросекунды, но это будет время от открытия вкладки (это если мы сейчас про performance.now()), что уже не так заманчиво.


Идентификация браузера


Браузеры вообще не уникальны и сейчас я вам это докажу.


Для идентификации клиента HTML Living Standard нам предлагает использовать The Navigator object.


А теперь внимание сравним то, что нам предлагают сравнивать


Браузер appCodeName appName platform product productSub vendor vendorSub
Chrome Mozilla Netscape Win32 Gecko 20030107 Google Inc. -
Mozilla 75 Mozilla Netscape Win32 Gecko 20100101 - -
Mozilla 45 Mozilla Netscape Win32 Gecko 20100101 - -
Internet Explorer Mozilla Netscape Win32 Gecko - - -
Microsoft Edge Mozilla Netscape Win32 Gecko 20030107 Google Inc. -

Как вам такое? Почувствовали все разнообразие клиентов? Вот и я нет.


Но радо признать, что местами отличаются userAgent и appVersion:


Браузер appVersion userAgent
Chrome 5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36
Mozilla 75 5.0 (Windows) Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:85.0) Gecko/20100101 Firefox/85.0
Mozilla 45 5.0 (Windows) Mozilla/5.0 (Windows NT 10.0; WOW64; rv:45.0) Gecko/20100101 Firefox/45.0
Internet Explorer 5.0 (Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729; Tablet PC 2.0; Zoom 3.6.0; rv:11.0) like Gecko Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729; Tablet PC 2.0; Zoom 3.6.0; rv:11.0) like Gecko
Microsoft Edge 5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36 Edg/88.0.705.74 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36 Edg/88.0.705.74

Тут Edge впереди планеты всей, так как он отображает IP, и мы можем использовать его. Но это только в Edge. А так, как видите, многого с навигатором не навоюешь.


Как это реализовал я



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


  1. Книги очень сложно добавлять несколько раз в миллисекунду. Даже просто тыкать на кнопочку сложно.
  2. Книги может добавлять только авторизованный пользователь.

Я первые 6 байт из 16 отвел под миллисекунды timestamp'а, что дает нам возможность генерировать ключи аж до 10889-08-02Т10:31:50.655. Хватит с запасом.


Последние 6 байт я беру из SHA-1 хеша логина можно идентифицировать 281,474,976,710,656 уникальных пользователей (если взять расчет на то, что не будет коллизий). Тоже с запасом (у меня их всего 30).


1 байт у нас отводится на версию (M) и вариант (N).


Оставшиеся 3 байта я солю рандомом.


Что в итоге:


Если вдруг мое приложение станет супер-пупер популярным и 100,000 и они будут за минуту каждый делать по 100 книг, то за миллисекунду будет генерироваться:


$$
100,000 * 100 / 60,000 = 166
$$


Вероятность того, что совпадут два:


$$
166 1/256^3 1/256^5 = 166 1/255^8 = 166 / 18 10^{18}
$$


Это очень мало и этого мне хватает


Реализацию можно посмотреть тут.


Предвещая вопрос "А почему же не рандом?"


Да, есть такой легендарный код


function uuidv4() {    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {        var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);        return v.toString(16);    });}

В моем случае на бэкенде UUID используется как первичный ключ.


Когда первые байты ключа идут по порядку больше вероятность, что новая запись встанет в конец таблицы. Даже если на клиенте будет запущена синхронизация. Ведь вряд ли юзер выполнит синхронизацию данных внесенных полгода назад и СУБД будет сдвигать половину таблицы.


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

Подробнее..

Ко-вариантность и типы данных

04.06.2021 02:06:03 | Автор: admin

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

Я надеюсь что может у меня получиться объяснить эту тему с другой стороны используя метафоры присвоения в разрезе лямбд.

Зачем вообще эта вариантность нужна ?

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

Ко-вариантность это о типах данных и их контроле со стороны компиляторов. И ровно с этого места надо откатиться и сказать о типах данных и зачем это нам нужно.

Flashback к типам

Типы данных сами по себе тоже не являются сверхважной темой, есть языки в которых тип данных не особенно нужны, например ассемблер, brainfuck, РЕФАЛ.

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

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

> 'str-a' - 'str-b'NaN

JS (JavaScript) Спокойно этот код проглатывает, мне скажут что этоне баг, это фича, ок, допустим, тогда я возьму Python

>>> 'str-a' - 'str-b'Traceback (most recent call last):  File "<stdin>", line 1, in <module>TypeError: unsupported operand type(s) for -: 'str' and 'str'

Или Java

jshell> "str-a" - "str-b"|  Error:|  bad operand types for binary operator '-'|    first type:  java.lang.String|    second type: java.lang.String|  "str-a" - "str-b"|  ^---------------^

То есть я клоню к тому, что считать багом или фичей - зависит от создателей языка.

А мне как пользователю например вообще без разницы на каком языке написана та или иная программа, мне важно чтоб она работала.

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

Еще пример: может быть такой мой сценарий, допустим вчера я написал на Groovy вот такой код

groovy> def fun1( a, b ){groovy>   return a - bgroovy> }groovy> println 'fun1( 5, 2 )='+fun1( 5, 2 )groovy> println "fun1( 'aabc', 'b' )="+fun1( 'aabc', 'b' )groovy> println 'fun1( [1,2,3,4], [2,3] )='+fun1( [1,2,3,4], [2,3] )fun1( 5, 2 )=3fun1( 'aabc', 'b' )=aacfun1( [1,2,3,4], [2,3] )=[1, 4]

А сегодня так на JS в другом проекте

> fun1 = function( a, b ){ return a - b }[Function: fun1]> fun1( 5, 2 )3> fun1( 'aabc', 'b' )NaN> fun1( [1,2,3,4], [2,3] )NaN

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

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

Речь о типах данных

Вариантность как и ко/контр вариантность - это речь о типах данных и их отношениях между собой.

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

Один из способов избежать - это введение системы типов данных.

Вот пример на языке TypeScript

function sub( x : number, y : number ) {    return x - y;}console.log( sub(5,3) )

Этот код успешно скомпилируется в JS.

А вот этот

function sub( x : number, y : number ) {    return x - y;}console.log( sub("aa","bb") )

Уже не скомпилируется - и это хорошо:

> tsc ./index.tsindex.ts:5:18 - error TS2345: Argument of type 'string' is not assignable   to parameter of type 'number'.5 console.log( sub("aa","bb") )~~~~Found 1 error.

В примере выше функцияsubтребует принимать в качестве аргументов переменные определенного типа, не любые, а именноnumber.

Контроль за типы данных я возлагаю уже компилятору TypeScript (tsc).

Инвариантность

Рассмотрим пока понятие Инвариантность, согласно определению

Инвариант это свойство некоторого класса (множества) математических объектов, остающееся неизменным при преобразованиях определённого типа.

Пусть A множество и G множество отображений из A в A. Отображение f из множества A в множество B называется инвариантом для G, если для любых a A и g G выполняется тождество f(a)=f(g(a)).

Очень невнятное для не посвященных определение, давай те чуть проще:

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

Рассмотрим пример операции присвоения переменной, в JS допускается вот такой код

> fun1 = function( a, b, c ){... let r = b;... if( a ) r = c;... return r + r;... }[Function: fun1]> fun1( 1==1, 2, 3 )6> fun1( 1==1, "aa", "b" )'bb'> fun1( 1==1, 3, "b" )'bb'> fun1( 1!=1, 3, "b" )6> fun1( 1!=1, {x:1}, "b" )'[object Object][object Object]'

В примере переменная r - может быть и типа string и number и объектом, со стороны интерпретатора сказать какого типа данных возвращает функция fun1 нельзя, пока не запустишь программу.

Так же нельзя сказать какого типа будет переменная r. Тип результата и тип переменной r зависит от типов аргументов функции.

Переменная r по факту может иметь два разных типа:

  • В конструкцииlet r = b, переменная r будет иметь такой же тип, как и переменная b.

  • В конструкцииr = c, переменная r будет иметь такой же тип, как и переменная c.

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

Можно наложить явным образом ограничения на вызов функции и проверять какого типа аргументы, например так:

> fun1 = function( a, b, c ){... if( typeof(b)!=='number' )throw "argument b not number";... if( typeof(c)!=='number' )throw "argument c not number";... let r = b;... if( a ) r = c;... return r + r;... }[Function: fun1]> fun1( true, 1, 2 )4> fun1( true, 'aa', 3 )Thrown: 'argument b not number'

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

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

В языках со строгой типизацией операция конструкцияlet r = bи следующая за нейr = cне допустима, она может быть допустима если мы укажем типы аргументов.

Пример Typescript:

function fun1( a:boolean, b:number, c:number ){    let r = b;    if( a ) r = c;    return r + r;}function fun2( a:boolean, b:number, c:string ){    let r = b;    if( a ) r = c;    return r + r;}

И результат компиляции

> tsc ./index.ts index.ts:9:13 - error TS2322: Type 'string' is not assignable to type 'number'.9     if( a ) r = c;~Found 1 error.

Здесь в ошибки говориться явно, что переменная типаstringне может быть присвоена переменной типаnumber.

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

Инвариантность- это такой случай, когда переменной одного типа присваивается (другая или эта же) переменная этого же типа.

Теперь вернемся к строгому определению:выполняется тождество f(a)=f(g(a))

То есть допустим у нас есть функции TypeScript:

function f(a:number) : number {    return a+a;}function g(a:number) : number {    return a;}console.log( f(1)===f(g(1)) )

Этот код - вот прям сторого соответствует определению.

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

function f(a:number) : number {    return a+a;}function g(a:number) : number {    return a-1;}let r = f(1)r = f(g(1))

а такой код

function f(a:number) : number {    return a+a;}function g(a:number) : string {    return (a-1) + "";}let r = f(1)r = f(g(1))

Уже невалиден (не корректен), так как:

  • функция g возвращает тип string

  • а функция f требует тип number в качестве аргумента

и вот такую ошибку обнаружит компилятор TypeScript.

Первый итог

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

Ко-вариантность

Для объяснения ко-вариантности и контр-вариантности, мне придется прибегнуть не к TypeScript, а к другому языку - Scala, причины я поясню ниже.

Вы наверно уже слышали про ООП и наследование, про различные принципы Solid

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


Ко-вариантностьэто такое качество операции присвоения значения переменной значение переменной другого типа, при котором сохраняются все свойства и операции.

Есть несколько типов чисел и их можно расположить в следующей иерархии:

  1. Натуральные числа N

    • N натуральные числа, включая ноль: {0, 1, 2, 3, }

    • N* натуральные числа без нуля: {1, 2, 3, }

  2. Целые числа Z - обладают знаком (+/-) включают в себя натуральные

  3. Рациональные числа Q - дроби (два целых числа), включают в себя все бесконечное множество Z

  4. Вещественные числа R - это и рациональные и иррациональные числа (например ПИ, e, )

  5. Комплексные числа C - числа вида a+bi, где a,b - вещественные числа, а i - мнимая единица

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

Числа мы можем условно расположить согласно такой иерархии

  • any - любой тип данных

    • number - некое число

      • int - целое число

      • double - (приближенное) дробное число

    • string - строка

так мы можем в языке TypeScript написать функции

function sum_of_int( a:int, b:int ) : int { return a+b; }function sum_of_double( a:double, b:double ) : double { return a+b; }function compare_equals( a:number, b:number ) : boolean { a==b }

в случае

let res1 : int = sum_of_int( 1, 2 )

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

Рассмотрим случайко-вариантногоприсваивания

let res1 : number = sum_of_int( 1, 2 )    res1          = sum_of_double( 1.2, 2.3 )

В данном примере res1 - это тип number.

В первом вызове res1 = sum_of_int( 1, 2 ), переменная res1 примет данные типа int, и это корректно, т.к. int это подтип number и по определению сохраняются все свойства и методы класса number

Во втором вызове res1 = sum_of_double( 1.2, 2.3 ) - переменная res1 примет данные типа double и это тоже корректно, так же по определению

О каких же операциях говорят что сохраняются? а все те же, мы все так же как и в первом, так и во втором случае можем выполнить операции проверки на равенство и д.р. для переменной res1:

let res1 : number = sum_of_int( 1, 2 )let res2 : number = sum_of_doube( 1.2, 2.3 )if( compare_equals(res1, res2) ){  ...}

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

Допустим у нас есть фигуры: прямоугольник Box и круг Circle

class Box {    width : number    height : number    constructor( w: number, h: number ){        this.width = w;        this.height = h;    }}class Circle {    radius : number    constructor( r: number ){        this.radius = r    }}

И нам надо подсчитать сумму площадей, прямоугольники можно хранить в одном массиве, а круги в другом

let boxs : Box[] = [ new Box(1,1), new Box(2,2) ]let circles : Circle[] = [ new Circle(1), new Circle(2) ]

Мы напишем 2 функции по подсчету площади, одну для прямоугольников, другую для кругов

function areaOfBox( shape:Box ):number { return shape.width * shape.height }function areaOfCircle( shape:Circle ):number { return shape.radius * shape.radius * Math.PI }

Тогда для подсчета общей суммы площадей код будет примерно таким:

boxs.map( areaOfBox ).reduce( (a,b,idx,arr)=>a+b ) + circles.map( areaOfCircle ).reduce( (a,b,idx,arr)=>a+b )

Все выше выглядит ужасно, если вы знакомы с ООП или/и с базовой логикой (родовые, видовые понятия).

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

А по сему можно выделить общее абстрактное понятиеФигураи добавить в это абстрактное метод/свойство - area():number.

interface Shape {    area():number}

Вторым шагом, это указать что классы Box и Circle реализуют интерфейс Shape, и перенести areaOfBox, areaOfCircle как реализацию area.

class Box implements Shape {    width : number    height : number    constructor( w: number, h: number ){        this.width = w;        this.height = h;    }    area():number {        return this.width * this.height    }}class Circle implements Shape {    radius : number    constructor( r: number ){        this.radius = r    }    area():number {        return this.radius * this.radius * Math.PI    }}

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

let shapes : Shape[] = [ new Box(1,1), new Box(2,2), new Circle(1), new Circle(2) ]shapes.map( s => s.area() ).reduce( (a,b,idx,arr)=>a+b )

И в данном примере,ко-вариантностьпроявляется в инициализации массива

Массив определен как массив элементов типа Shape, мы инициализируем (т.е. присваиваем начальное значение) элементами другого типа под типа (Box, Circle).

Ключевой момент в том, что Box и Circle реализуют необходимые свойства и методы которые требует интерфейс Shape.

Компилятор отслеживает что присваиваемые значения реализуют заданное соглашение, т.е.

Компилятор по факту отслеживает конструкциюlet a = b, и возможны несколько сценариев:

  1. переменная a и b - одного типа, тогдаинвариантнаяоперация присвоения

  2. переменная a является базовым типом, а переменная b - подтипом переменной a - тогдако-вариантнаяоперация присвоения

  3. переменная a является подтипом переменной b, а переменная b - базовым (родительским) типом - тогда этоконтр-вариантнаяоперация - и обычно компилятор блокирует такое поведение.

  4. между переменными a и b - нет общих связей - и тут компилятор блокирует то же поведение.

И вот пример, по пробуем добавить еще один класс который не реализует интерфейс Shape

class Foo {}let shapes : Shape[] = [ new Box(1,1), new Box(2,2), new Circle(1), new Circle(2), new Foo() ]shapes.map( s => s.area() ).reduce( (a,b,idx,arr)=>a+b )

Результат компиляции - следующая ошибка:

> tsc index.tsindex.ts:31:84 - error TS2741: Property 'area' is missing in type 'Foo' but required in type 'Shape'.31 let shapes : Shape[] = [ new Box(1,1), new Box(2,2), new Circle(1), new Circle(2), new Foo() ]                                                                                    ~~~~~~~~~index.ts:2:5    2     area():number        ~~~~~~~~~~~~~    'area' is declared here.Found 1 error.

Для типа Foo не найдено свойство area, которое определенно в типе Shape.

Тут уместно упомянуть о SOLID

L - LSP - Принцип подстановки Лисков (Liskov substitution principle): объекты в программе должны быть заменяемыми на экземпляры их подтипов без изменения правильности выполнения программы. См. такжеконтрактное программирование.

Контр-вариантность

Контр-вариантность, уже сложнее объяснить, для меня примеры с длегатами действовали на нервы, я же разобрался на примере с лямбд.

В качестве примера, возьму язык Scala и подробно попытаюсь его разобрать:

package xyz.cofe.sample.invobject App {  // Функция, на вход String, на выход Boolean, или кратко: (String)=>Boolean   def strCmp(a:String):Boolean = a.contains("1")  // Функция, на вход Int, на выход Boolean, или кратко: (Int)=>Boolean  def intCmp(a:Int):Boolean = a==1  // Функция, на вход String, на выход Boolean, или кратко: (Any)=>Boolean  def anyCmp(a:Any):Boolean = true  def main(args:Array[String]):Unit = {        // Инвариантное присвоение Boolean = Boolean    val call1 : Boolean = strCmp("a")        // Ко-вариантное присвоение Any = Boolean    val call2 : Any = strCmp("b")    // Инвариантное присвоение: (String)=>Boolean = (String)=>Boolean    val cmp1 : (String)=>Boolean = App.strCmp;    // Ко-вариантное присвоение (String)=>Boolean = (Any)=>Boolean    val cmp2 : (String)=>Boolean = App.anyCmp    // Инвариантное присвоение: (String)=>Boolean = (String)=>Boolean    val cmp3 : (Any)=>Boolean = App.anyCmp    // !!!!!!!!!!!!!!!!!!!!!!!    // Тут будет ошибка    // Контр-вариантное присвоение (Any)=>Boolean = (String)=>Boolean    val cmp4 : (Any)=>Boolean = App.strCmp  }}

Что нужно знать о Scala:

  • ТипAny- это базовый тип для всех типов данных

  • ТипInt, Boolean, String- это подтипыAny

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

  • Тип лямбды записывается в следующей форме:(тип_аргументов,через_запятую)=>тип_результата

  • Любой метод с легкостью преобразуется в лямбдупеременная = класс.метод/переменная = объект.метод

  • valв Scala, то же что иconstв JS

В примере мы можем увидеть уже знакомые

Инвариантностьв присвоении переменных:

// Инвариантное присвоение Boolean = Booleanval call1 : Boolean = strCmp("a")// Инвариантное присвоение: (String)=>Boolean = (String)=>Booleanval cmp1 : (String)=>Boolean = App.strCmp;

cmp1 - это переменная содержащая лямбду, при том аргументы и результат которые заданы в определении типа лямбды, полностью совпадают с присваемым значением:

Ожидаемый тип  (String)=>BooleanПрисваемый тип (String)=>Boolean

Ко-вариантность

// Ко-вариантное присвоение Any = Booleanval call2 : Any = strCmp("b")// Ко-вариантное присвоение (String)=>Boolean = (Any)=>Booleanval cmp2 : (String)=>Boolean = App.anyCmp

Если в случае присвоения call2, тут все понятно, то может быть непонятно с cmp2.

Ожидаемый тип  (String) => BooleanПрисваемый тип (Any)    => Boolean

Внезапно отношение String -> к -> Any становится другим - контр-вариантным.

В этом месте, уместно задаться WTF? - Все нормально!

Рассмотрим функции выше

// Функция, на вход String, на выход Boolean, или кратко: (String)=>Boolean def strCmp(a:String):Boolean = a.contains("1")// Функция, на вход String, на выход Boolean, или кратко: (Any)=>Booleandef anyCmp(a:Any):Boolean = true

При вызовеcmp2( "abc" )аргумент"abc"будет передан вanyCmp(a:Any), а по скольку String является под типом Any, то аргумент не дано преобразовывать и можно передать как есть.

Иначе говоря вызовanyCmp( "string" )иanyCmp( 1 ),anyCmp( true )- со стороны проверки типов допустимы операции, по скольку

  • принимаемые аргументы являются подтипами для принимающей стороны, тела функции

  • тип принимаемого аргумента является родительским типом (надтипом) со стороны вызова функции

Т.е. можно при передаче аргументов, действуютко-вариантностьсо стороны принимающей, а со стороны передающейконтр-вариантность.

Еще более наглядно это можно выразить стрелками:

Операция присвоения должна быть ко-вариантна или инвариантна

assign a <- b

А операция вызова функции на оборот - контр-варианта или инвариантна

call a -> b

Этим правилом руководствуются многие компиляторы, и они определяют функции так:

  • Операции передачиаргументовв функции по умолчанию являютсяконтр-вариантны, со стороны вызова функции

  • Операции присвоениярезультатвызова функции по умолчанию являетсяко-вариантны, со стороны вызова функции

Я для себя запомню так

Почему Scala, а не TypeScript

К моему удивлению TypeScript версии 4.2.4 не отрабатывает контр-вариантность в случае функций/лямбд

Вот мой исходник

interface Shape {    area():number}class Box implements Shape {    width : number    height : number    constructor( w: number, h: number ){        this.width = w;        this.height = h;    }    area():number {        return this.width * this.height    }}class Circle implements Shape {    radius : number    constructor( r: number ){        this.radius = r    }    area():number {        return this.radius * this.radius * Math.PI    }}class Foo {}const f1 : (number)=> boolean = a => true;const f2 : (object)=> boolean = a => typeof(a)=='function';const f3 : (any)=>boolean = f1;const f4 : (number)=>boolean = f3;const _f1 : (Box)=>boolean = a => trueconst _f2 : (any)=>boolean = _f1const _f3 : (Shape)=>boolean = _f1

В строкеconst f3 : (any)=>boolean = f1;и вconst _f3 : (Shape)=>boolean = _f1(а так же предыдущей) компилятор по моей логике должен был ругаться, но он этого не делал

user@user-Modern-14-A10RB:03:14:17:~/code/blog/itdocs/code-skill/types:> ./node_modules/.bin/tsc -versionVersion 4.2.4user@user-Modern-14-A10RB:03:16:53:~/code/blog/itdocs/code-skill/types:> ./node_modules/.bin/tsc --strictFunctionTypes index.ts user@user-Modern-14-A10RB:03:18:26:~/code/blog/itdocs/code-skill/types:> ./node_modules/.bin/tsc --alwaysStrict index.ts user@user-Modern-14-A10RB:03:19:04:~/code/blog/itdocs/code-skill/types:

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

Ко-вариантность/Контр-вариантность и типы

Еще одна важная оговорка связанная с типами и ООП.


Вариантностьэто не только про иерархию наследования!


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

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

Контр-вариантность- ровно та же ситуация с противоположным знаком.

Тут надо дать пояснение словасовместимость

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

ООП с наследованием - это всего лишь способ, задать иерархию реальных типов объектов.

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

Например я могу выстроить разные наборы иерархий для одних и тех же сущностей:

Например:

  • Человек (общий класс)

    • Национальность (под класс)

      • Социальный статус (под класс)

или наоборот

  • Человек (общий класс)

    • Пол (под класс)

      • Социальный статус (под класс)

Это я клоню к тому, что для одной и той же сущности может существовать множество способов квалификации.

И один из подходов - эту сложную сушность (как например человек) можно рассматривать с различных сторон - и вот уже эти стороны можно выделить в виде интерфейсов.

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

Вариантность- это в первую очередь наличие интересующих нах свойств/методов для наших задач. И это механизм контроля со стороны компилятора, для гарантии наличия этих свойств.

Так, например тот или иной объект может быть не только каким либо под классом, но и реализовывать (через интерфейсы) интересующие нас свойства/методы - именно это я понимаю под словомсовместимость.

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

Подробнее..

Java Core для самых маленьких. Часть 2. Типы данных

15.02.2021 14:04:42 | Автор: admin

Вступление

В этой статье мы не будем использовать ранее установленную IDE и JDK. Однако не беспокойтесь, ваш труд не был напрасным. Уже в следующей статье мы будет изучать переменные в Java и активно кодить в IDEA. Эта же статья является обязательным этапом. И в начале вашего обучения вы, возможно, будете не раз к ней возвращаться.

1998 - пин-код от моей кредитки является ничем иным как числом. По-крайней мере для нас - для людей. 36,5 - температура, которую показывают все термометры в разных ТРЦ. Для нас это дробное число или число с плавающей запятой. "Java Core для самых маленьких" - а это название данной серии статей, и мы воспринимает это как текст. Так к чему же я веду. А к тому, что Джаве (так правильно произносить, на тот случай если кто-то произносит "ява"), как и человеку, нужно понимать с чем она имеет дело. С каким типом данных предстоит работать.

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

Что для нас означает строгая типизация? Это значит, что все данные и каждое выражение имеет конкретный тип, который строго определен. А также то, что все операции по передаче данных будут проверяться на соответствие типов. Поэтому давайте поскорее узнаем какие типы данных представлены в Java!

Примитивы

В языке Java существует 8, оскорбленных сообществом, примитивных типов данных. Их также называют простыми. И вот какие они бывают:

  • Целые числа со знаком: byte, short, int, long;

  • Числа с плавающей точкой: float, double;

  • Символы: char;

  • Логические значения: boolean.

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

Тип byte

Является наименьшим из целочисленных. 8-разрядный тип данных c диапазоном значений от -2^7 до 2^7-1. Или простыми словами может хранить значения от -128 до 128. Используется для работы с потоками ввода-вывода данных, эта тема будет рассмотрена позже.

Тип short

16-разрядный тип данных в диапазоне от -2^15 до 2^15-1. Может хранить значения от -32768 до 32767. Самый редко применяемый тип данных.

Тип int

Наиболее часто употребляемый тип данных. Содержит 32 разряда и помещает числа в диапазоне от -2^31 до 2^31-1. Другими словами может хранить значения от -2147483648 до 2147483647.

Тип long

64-разрядный целочисленный тип данных с диапазоном от -2^63 до 2^63-1. Может хранить значения от -9223372036854775808 до 9223372036854775807. Удобен при работе с большими целыми числами.

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

Тип float

32-разрядный тип данных с плавающей точкой. Требует в два раза меньше памяти и в некоторых процессорах выполняется быстрее по сравнению с double. Но если значения слишком велики или слишком малы, то не обеспечивает требуемую точность вычислений. Используется когда нужно число с дробной частью, но без особой точности.

Тип double

На хранение требуется 64 бита. Рационально пользоваться double когда нужно сохранить точность многократно повторяющихся вычислений или манипулировать большими числами.

Тип char

16-разрядный тип данных в диапазоне от 0 до 2^16. Хранит значения от 0 до 65536. Этот тип может хранить в себе полный набор международных символов на всех известных языках мира (кодировка Unicode). То есть по сути каждый символ представляет из себя какое-то число. А тип данных char позволяет понять, что это число является символом.

Тип boolean

Может принимать только 2 значения true или false. Употребляется в условных выражениях, к примеру 1 > 10 вернет false, а 1 < 10 - true.

На этом примитивные типы данных в Java закончились. В следующей статье мы будем объявлять переменные конкретного типа данных. Поговорим о том, что такое литералы. А еще узнаем, что такое приведение типов данных. Вообщем следующая статья будет очень насыщенной и познавательной!

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru