Добавлен , опубликован

Курс JASS + vJASS

Содержание:

Подробней о real и integer

Вступление

В одном из прошлых уроков, я уже рассказывал об особенностях числовых типов real и integer. В этом уроке рассмотрим всё более подробно, а также рассмотрим проблемные примеры из прошлых уроков.
Для полного понимания темы, необходимо знать двоичную систему счисления. На мой взгляд, будет сложно объяснить двоичную систему счисления новичку картоделу. Но скорее, не потому, что это какая-то сложная тема, а потому, что эти знания ему больше не пригодятся и лишь запутают его. Поэтому я не буду объяснять всё до мелочей, если сильно захотите, то сами найдете в интернете видео\статью о двоичной системе счисления и устройстве чисел с плавающей точкой.

Разница между числовыми типами

Вы уже знаете о некоторых различиях между real и integer, но я повторю их.
Integer – это любое целое число (-5, 0, 6).
Real – это число с плавающей точкой. Таким числом может быть как целое число, так и конечное дробное (-5, -2.554, 0, 1, 5.5).

Преобразование типов

Раньше я не акцентировал внимание на том, что значения типа real и значения типа integer это хоть и числа, но это разные типы данных. Компьютер хранит и вычисляет их по-разному, для него это вообще две разные вещи. А если они где-то используются вместе, например, в выражении, то в итоге все значения будут преобразованы к одному типу.
Посмотрите на такой код:
function Test takes nothing returns nothing
    local integer i = 2
    local real r = 2.5
    local integer a = r
    local real b = i
endfunction
Такой код не компилируется потому, что мы пытаемся присвоить значение типа real переменной "a" с типом integer. Надеюсь, вы помните, как преобразовывать в друг друга типы real и integer с помощью функций R2I и I2R. Исправим ситуацию, добавим преобразование типов:
function Test takes nothing returns nothing
    local integer i = 2
    local real r = 2.5
    local integer a = R2I(r)
    local real b = i
endfunction
Теперь код компилируется и работает, хоть он ничего полезного и не делает. Как вы заметили, я преобразовал только значение с типом real перед тем, как положить его в переменную "a" с типом integer, но ничего не сделал со значением типа integer перед тем, как положить его в переменную "b" с типом real. Это не значит, что там нет преобразования, просто преобразование integer в real происходит автоматически и использовать функцию I2R не всегда нужно.
Теперь посмотрим на то, что случается со значениями при преобразовании:
function Test takes nothing returns nothing
    local integer i = 2
    local real r = 2.5
    local integer a = R2I(r)
    local real b = i
    call BJDebugMsg("a = " + I2S(a))
    call BJDebugMsg("b = " + R2S(b))
endfunction
Вывод на экран:
a = 2
b = 2.000
Если преобразовать integer в real, то значение не изменится. А если преобразовать real в integer, то мы видим, что дробная часть исчезла. Обратите внимание на то, что при преобразовании real в integer, дробная часть именно исчезает, а не округляется. Например, если преобразовать в integer значение 2.88888 то в результате всё равно будет 2.
При преобразовании integer в real значение гарантировано не изменится, поэтому компьютер может не беспокоиться о последствиях и преобразовать тип автоматически. В случае преобразования real в integer значение может измениться (потерять дробную часть) поэтому используя функцию R2I, вы как будто даёте своё согласие на "преобразование с потерями".
Также необходимо знать о том, что арифметические операции ведут себя немного по-разному в зависимости от типов значений, над которыми они проводятся. Если оба значения имеют тип integer, то в результате получится число с типом integer и оно потеряет дробную часть. А если хотя бы одно значение имеет тип real, то в результате получится число с типом real и если у него есть дробная часть, то она останется.
Возьмем пример из прошлого урока:
function Test takes nothing returns nothing
    local integer a = 2 * 2 / 3
    local integer b = 2 / 3 * 2

    call BJDebugMsg("a = " + I2S(a))
    call BJDebugMsg("b = " + I2S(b))
endfunction
Вывод на экран:
a = 1
b = 0
С точки зрения математики, обе переменные должны быть равны 1.3333(3). Если учитывать то, что это целые числа, то обе переменные должны быть равны 1. Но в этом примере, значение переменной "b" по какой-то причине равно нулю. Давайте посчитаем всё вручную, чтобы понять, что тут происходит. Начнем с выражения "a = 2 * 2 / 3":
1. 2 * 2 = 4
2. 4/3 = 1.333(3) но это тип integer поэтому откинем дробную часть и у нас получится 1.
По итогу значение переменной "а" равно 1, всё сходится. Теперь посчитаем выражение "b = 2 / 3 * 2":
1. 2/3 = 0.666(6) но это тип integer поэтому откинем дробную часть и у нас получится 0.
2. 0 * 2 = 0
Всё сошлось, значение переменной "b" равно 0. Дело в том, что у литералов также есть тип, а все литералы в примере выше имеют тип integer. Если заменить тип переменных на real, то ничего не поменяется:
function Test takes nothing returns nothing
    local real a = 2 * 2 / 3
    local real b = 2 / 3 * 2

    call BJDebugMsg("a = " + R2S(a))
    call BJDebugMsg("b = " + R2S(b))
endfunction
Вывод на экран:
a = 1.000
b = 0.000
Чтобы числовой литерал имел тип real нужно, чтобы в числе была точка, например:
2.534
1.0
1. //писать ноль после точки необязательно, “1.” и “1.0” это одно и то же значение.
Сначала исправим пример с типом real, чтобы в результате обе переменные были примерно равны 1.333:
function Test takes nothing returns nothing
    local real a = 2. * 2. / 3.
    local real b = 2. / 3. * 2.

    call BJDebugMsg("a = " + R2S(a))
    call BJDebugMsg("b = " + R2S(b))
endfunction
Вообще-то можно было поставить точку только в первом числе, а остальные преобразовались бы по цепочке. Тип real можно сравнить с зомби, он "заражает" значения с типом integer через операции. Потому, что, как я уже писал, если при выполнении арифметической операции хотя бы одно значение имеет тип real, то в результате получится число с типом real.
Теперь исправим пример с типом integer, чтобы обе переменные были равны 1:
function Test takes nothing returns nothing
    local integer a = 2 * 2 / 3
    local integer b = R2I(2. / 3. * 2.)

    call BJDebugMsg("a = " + I2S(a))
    call BJDebugMsg("b = " + I2S(b))
endfunction
В общем, не то чтобы это какое-то важное знание, но иногда нужно быть внимательным к таким деталям.

Точность

Уточню, под точностью имеются в виду не случайные колебания или ошибки, никаких случайностей и ошибок в вычислениях нет. Под точностью имеется в виду доступное для вычислений количество знаков. Например, в математике выражение 1/3 + 1/3 + 1/3 равно 1. Три трети, это одно целое. Но теперь представьте, что вы должны использовать только десятичные дроби при расчетах и к тому же вам доступно только три знака после точки. Тогда 1/3 будет равно 0.333, а всё выражение 0.333 + 0.333 + 0.333 = 0.999, а это уже не единица. В джассе всё немного иначе, но смысл примерно тот же.
Подробно рассматривать тип integer мы не будем, у него всё очень точно, только у него нет дробной части. Лучше сосредоточимся на типе real и разберемся, почему его зовут числом с плавающей точкой.
Для начала узнаем, как устроен тип real. Компьютер не может хранить и вычислять бесконечные числа, все числа имеют ограниченное количество знаков. В нашем случае и integer, и real состоят из 32 битов. Я не хочу всё усложнять и рассказывать вам о том, что такое двоичная система счисления. Если хотите, ищите статьи сами, но это знание не сильно поможет вам в создании карт. Поэтому лучше представьте, что у вас есть по 6 знаков на число. Число 6 взято для удобства и в нем нет никакого смысла, а 32 бита это тоже 32 знака, но только в двоичной системе счисления. Возьмем, например число 356, запишем его используя 6 знаков и получим 000356. Это было целое число, а что если нам нужно дробное число, например, 3.5? Мы можем использовать три знака для целой части и три знака для дробной, получится 003.500. А еще мы можем передвигать точку в зависимости от нужд, при этом изменяя доступное число знаков для дробной и целой части. Например: 4355.55, 0.06243, 324323., .000001. Ну, вы поняли, точка как-бы “плавает”, а такое число называют числом с плавающей точкой.
Из всего этого следует, что чем больше целая часть, тем меньше места есть для дробной части и поэтому при проведении операций с очень большими числами будут ошибки в дробной части.
Прежде чем продолжить, познакомимся с такой нативной функцией:
native R2SW takes real r, integer width, integer precision returns string
где r – число, которое мы преобразовываем в строку,
width – минимальное количество знаков до точки (недостающие знаки будут пробелами),
precision – количество знаков после точки (недостающие знаки будут нулями)
Данная функция преобразовывает число типа real в строку (тип string). В отличие от похожей функции R2S, которая показывает только три знака после точки, функция R2SW принимает дополнительные аргументы и может показывать больше или меньше знаков после точки.
А теперь запустим такую функцию:
function TestReal takes nothing returns nothing
    local real a = 444444. + 0.22222222
    local real b = 4444. + 0.22222222
    local real c = 44. + 0.22222222
    call BJDebugMsg(R2SW(a, 0, 8))
    call BJDebugMsg(R2SW(b, 0, 8))
    call BJDebugMsg(R2SW(c, 0, 8))
endfunction
Вывод на экран:
444444.21875000
4444.22216796
44.22222136
Если к первому числу добавить еще пару цифр к целой части, то дробная часть будет полностью вытеснена целой частью. А эти непонятно откуда взявшиеся числа, которые мы видим там, где должны быть двойки, это наша следующая проблема. Она связана как раз таки с двоичной системой счисления, я не смогу объяснить причину простыми словами, поэтому и не буду. Просто нужно привыкнуть к тому, что при любых операциях с типом real могут получиться мелкие ошибочки. А еще эти ошибки способны накапливается в переменных. Ничего страшного в этом нет, но стоит запомнить лишь одну вещь, нельзя строго сравнивать значения с типом real. Это значит, что не стоит использовать операции равно (==) и не равно (!=) имея дело с типом real. Потому, что из-за этих мелких колебаний значения типа real не всегда бывают одинаковыми, когда вы этого ожидаете. Лучше напишите такую функцию для сравнения значений с типом real:
function IsEqualReal takes real a, real b, real eps returns boolean
    if (a - b >= 0) then
        return a - b <= eps
    else
        return -(a - b) <= eps
    endif
endfunction
где a – первое число,
b – второе число,
eps – максимальная разница между числами, при которой они считаются равными
Демонстрация:
function B2S takes boolean b returns string
    if (b) then
        return "true"
    else
        return "false"
    endif
endfunction

function IsEqualReal takes real a, real b, real eps returns boolean
    if (a - b >= 0) then
        return a - b <= eps
    else
        return -(a - b) <= eps
    endif
endfunction

function Test takes nothing returns nothing
    call BJDebugMsg("1.5 равно 2 при разнице 0.5 - " + B2S(IsEqualReal(1.5, 2, 0.5)))
    call BJDebugMsg("-0.5 равно 0.5 при разнице 1 - " + B2S(IsEqualReal(-0.5, 0.5, 1)))
    call BJDebugMsg("0.0005 равно 0 при разнице 0.01 - " + B2S(IsEqualReal(0.0005, 0, 0.01)))
    call BJDebugMsg("-2 равно -4 при разнице 2 - " + B2S(IsEqualReal(-2, -4, 2)))
    
    call BJDebugMsg("1.5 не равно 2 при разнице 0.25 - " + B2S(IsEqualReal(1.5, 2, 0.25)))
    call BJDebugMsg("-0.5 не равно 0.5 при разнице 0.5 - " + B2S(IsEqualReal(-0.5, 0.5, 0.5)))
    call BJDebugMsg("0.0005 не равно 0 при разнице 0.0001 - " + B2S(IsEqualReal(0.0005, 0, 0.0001)))
endfunction
После запуска функции Test на экране будет такой текст:
1.5 равно 2 при разнице 0.5 - true
-0.5 равно 0.5 при разнице 1 - true
0.0005 равно 0 при разнице 0.01 - true
-2 равно -4 при разнице 2 - true
1.5 не равно 2 при разнице 0.25 - false
-0.5 не равно 0.5 при разнице 0.5 - false
0.0005 не равно 0 при разнице 0.0001 - false
На всякий случай еще раз повторю, это не случайные ошибки! Сколько бы вы не проводили одни и те же операции с одними и теми же значениями, у них будет один и тот же результат. Просто их сложно предсказать из-за большого количества комбинаций.

`
ОЖИДАНИЕ РЕКЛАМЫ...
0
37
2 года назад
0
У меня в профиле наглядная картинка есть, как работает IEEE 754. Можешь забрать в статью. Чем дальше от нуля или от целого числа, тем меньше точность.
0
11
2 года назад
0
ScorpioT1000:
У меня в профиле наглядная картинка есть, как работает IEEE 754. Можешь забрать в статью. Чем дальше от нуля или от целого числа, тем меньше точность.
Если честно, я сам толком не разобрался в теме чисел с плавающей точкой. Поэтому написал, как сам всё понял. Если я написал где-то откровенную глупость, то укажите мне на нее, пожалуйста. А если вы хотите, чтобы я добавил больше информации, то у меня не хватит на это сил. Максимум, что я мог бы добавить, это объяснение, что такое двоичная система счисления и как устроены числа с плавающей точкой по битам. Но на мой взгляд, смысла в этом нет, я лично ни разу не стыкался с какими-то проблемами с точностью.
0
37
2 года назад
Отредактирован ScorpioT1000
0
Можно ссылку на вики хотябы дать ru.wikipedia.org/wiki/IEEE_754-2008
Там есть два нуля и две бесконечности, а ещё NaN, флоат можно записывать в scientific notation, короче, можно многое открыть для себя и читателей)
0
11
2 года назад
0
ScorpioT1000:
Можно ссылку на вики хотябы дать ru.wikipedia.org/wiki/IEEE_754-2008
Там есть два нуля и две бесконечности, а ещё NaN, флоат можно записывать в scientific notation, короче, можно многое открыть для себя и читателей)
А в джассе разве такое есть? Я то знаю, что такое есть во многих языках програмирования, я немного учил джаву, но зачем это картоделам на джассе и виджассе?
0
37
2 года назад
0
Такое есть, потому что дело не в жассе, а в том, как работают современные процессоры. Это не только тип данных языка, а тип данных в архитектуре современных компьютеров.
Чтобы оставить комментарий, пожалуйста, войдите на сайт.