В языке для написания критичного к ошибкам кода нужен особый подход к целочисленным типам, позволяющий уменьшить вероятность переполнения, а потому способствующий правильности программ.
Целочисленные типы должны быть разделены на две группы:- Ограниченное число предопределённых типов, подходящих для большинства задач, и к которым применимы встроенные арифметические операции: +, -, *, /, %.
- Типы из разделов, для работы с которыми необходимо использовать функции из тех же разделов с соответствующими названиями: add, sub, mul, div, mod и т.д.
Разделение сделает более удобным, а потому и более желанным использование типов, для которых риск совершения ошибки ниже, оставив возможность использовать более редкие особые случаи, которые будут применяться только в тех задачах, где они нужны.
Предопределённые типы
В первую группу должны войти:Название | Отрезок допустимых значений |
---|---|
int | -(231 - 1) .. (231 - 1) |
longint | -(263 - 2) .. (263 - 2) |
int и longint — знаковые, симметричные относительно 0 типы. Равенство по модулю максимального и минимального значений упрощает учёт правильных вычислений, в частности, гарантирует, что смена знака и значение по модулю всегда выполнимы без переполнения. Это важней желания вместить на одно допустимое значение больше, как это позволяет наиболее распространённый способ кодирования отрицательных чисел — дополнительный код. В большинстве случаев минимальное отрицательное (- 231, - 263
) даже не нужно решения задачи, но непропорционально своему значению требует избыточного внимания в случае необходимости достижения полной правильности, из-за создания особого случая. Эти значения зарезервированы для использования в качестве неопределённого значения.
Главным целочисленным типом является int, так как он даёт хороший компромисс по предоставляемому отрезку чисел, достаточного для большинства задач, и возможностью эффективной обработки. В частности, количество элементов массива не может превышать максимального значения int. longint нужен как из-за того, что в современных условиях нередки ситуации, где отрезка 32-битных значений всё же не хватает, например, для кодирования времени в наносекундах или обработки больших файлов, так и потому что расширенный отрезок чисел естественным образом возникает при вычислениях в полном множестве int.
byte занимает особое место — он нужен в первую очередь как строительный элемент, и сам по себе не является целочисленным типом, но легко может быть приведён к нему, равно как и в обратном направлении. byte соответствует отрезку значений 0 .. 28 - 1
.
Остальные целочисленные типы других диапазонов вынесены из ядра языка, так как потребность в них ниже основных, что, однако, не препятствует нередкому злоупотреблению ними и может приводить к лишней потребности сопряжения разных ёмкостей значений, что служит дополнительным источником ошибок.
Беззнаковые типы по размеру совпадающие с int и longint не включены в предопределение по схожей причине. Кроме того, близость 0 — нижней границы повышает вероятность переполнения даже в случае вычисления малых значений.
***
В арифметических выражениях, в которых смешиваются операнды разных целочисленных типов, операнды меньшего типа рассматриваются как эквивалентные операнды наибольшего. Вычисления выражений для операндов int и longint происходят в диапазоне longint, кроме тех операций с int, которые гарантировано не приводят к увеличению необходимого диапазона, например, сложения целых, приведённых от byte, или просто при делении.
Контроль переполнения происходит как в выражениях, так и при сохранении значения. Если диапазон приёмника меньше диапазона типа выражения, требуется явное преобразование типа. Если такой же или больше, то не требуется, но контроль переполнения выполняется в любом случае. Такое правило позволяет уменьшить вероятность переполнений и не требует избыточных явных приведений типа.
Например, следующие операции, которые во многих других языках могут содержать ошибки переполнения при определённых значениях переменных, в защитном языке оказываются всегда правильны:
Защитный язык | Java |
---|---|
var (a,b,c,m int; d longint; f bool) a = max(int) / 2; b = a + 2; c = 300; m = min(int); f = a + b < c; // false d = a * b; // 1152921504606846975 c = b * 2 / 3; // 715827883 m = -m; // 2147483647 |
int a, b, c, m; long d; boolean f; a = Integer.MAX_VALUE / 2; b = a + 2; c = 300; m = Integer.MIN_VALUE; f = a + b < c; // true d = a * b; // -1 c = b * 2 / 3; // -715827882 m = -m; //-2147483648 |
Типы предопределённых разделов
Вторую группу целочисленных типов должны представить типы из состава специальных разделов, отличающихся знаковостью и разрядностью гарантированных диапазонов. Разделы должны предоставлять почти одинаковые наборы функций для работы с собственными целочисленными типами. Имена разделов можно вывести из синтаксического уравнения – имя = [u]int(8|16|32|64)
, где u
обозначает беззнаковость, а число, естественно — разрядность типа. Диапазоны допустимых значений определяются по формулам - [0 .. 2n-1]
для беззнаковых и [-2n-1 .. 2n-1-1]
для чисел со знаком, так как они должны быть закодированы через двоичное дополнение.
type (t) // сам целочисленный тип
const (min, max) // минимальное и максимальное значение
// Группа функций, которые трактуют переполнение как ошибку.
// Если при их выполнении не было явно получено состояние правильности,
// то в случае переполнения они завершают выполнение программы.
func add(addend1, addend2 t) (sum t);
func sub(minuend, subtrahend t) (difference t);
func mul(factor1, factor2 t) (product t);
func div(divident, divisor t) (ratio t);
func mod(divident, divisor t) (remainder t);
// Группа процедур, для которых переполнение сопровождается воображаемым
// отбросом старших разрядов.
proc mod_add(addend1, addend2, > sum t) (overflow bool);
proc mod_sub(minuend, subtrahend, > difference t) (overflow bool);
proc mod_mul(factor1, factor2, > product t) (overflow bool);
// Функции для преобразования к основным типам и обратно. Поскольку при
// этом может возникнуть переполнение, то функции также подразумевают
// явное взятие правильности, либо завершение работы.
func to_byte (value? t) (result byte);
func to_int (value? t) (result int);
func to_longint(value? t) (result longint);
func from_byte (value byte) (result t);
func from_int (value int) (result t);
func from_longint(value longint) (result t);
// Функции интерпретации стандартных типов как соответствующих их
// диапазону типов из разделов противоположной знаковости и наоборот.
// в разделе uint64:
func as_int64 (value t) (result int64.t);
func as_longint(value t) (result longint);
func as_t(value longint) (result t);
// в разделе uint32:
func as_int32 (value t) (result int32.t);
func as_int (value t) (result int);
func as_longint(value t) (result longint);
func as_t (value int) (result t);
// в разделе int8:
func as_byte (value t) (result byte);
func as_int (value t) (result int);
func as_longint(value t) (result longint);
func as_t (value byte) (result t);
Битовые операции для встроенных целочисленных типов не предусмотрены как неуместные. Вместо них вводятся:
- Встроенная функция для возведения в степень
func pow(v, n int) (res longint)
или отдельная операцияv**n
. Выбор будет сделан позднее. - Типы-множества, которые можно приводить к целочисленным и обратно и о которых подробней будет рассказано в отдельной заметке.
Дополнительные материалы: