Нетипизированные константы. От ошибки в учебнике к глубокому пониманию Go

Всем привет!  

Сегодня расскажем, как пример с ошибкой из учебника по языку Go вызвал любопытство у бывшего стажера «Автомакона» (и нынешнего программиста) Олега Самсонова и к чему это привело.

df1eaa3cb2f911a2515ceecf269daff2.jpg

Путь в IT — это всегда погружение в огромный массив информации, изучение примеров и поиски ответов на вопросы «почему так» и «как это работает». Особенно на старте карьеры важно не только поглощать знания, но и ставить под сомнение их источники, ведь именно так мы лучше понимаем, как работает весь механизм. Обучение программированию — это не только освоение синтаксиса, но и умение анализировать и разбираться в сложных концепциях.

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

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

Когда ошибка — хороший учитель

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

Недавно, изучая тему «Константы», он наткнулся на такой пример:

const pi = 3.14  //нетипизированная константа
var w int = pi + 2  //компилятор должен преобразовать pi в int

Решил проверить этот пример и был удивлен, когда компилятор выдал ошибку:

cannot use pi + 2 (untyped float constant 5.14) as int value in variable 
declaration (truncated)

Невозможно использовать pi + 2 (нетипизированная константа с плавающей точкой 5.14) в качестве значения int в объявлении переменной.

Получается, что в учебном примере была ошибка.

Первым делом, как полагается современному начинающему программисту, я пошел читать  документацию  к чату GPT. Но он не смог мне дать правильный ответ:

f92f523663040496eb1ed177a9a29198.png

Нет, чат GPT, w не получит значение 5, а мы получим ошибку: cannot convert pi (untyped float constant 3.14) to type int.

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

Фрагмент из книги Джей Макгаврен «Head First. Изучаем Go»

Фрагмент из книги Джей Макгаврен «Head First. Изучаем Go»

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

Давайте разбираться.

С чего обычно начинают изучение любого языка программирования? С написания программы «Hello, World!».

На GO она может выглядеть вот так:

package main
import "fmt"
func main() {
       hello := "Hello, world!"
       fmt.Println(hello)
}

(в дальнейшем для примеров будем опускать объявление пакета, импорты и названия функций, оставляя только саму суть)

Что у нас тут происходит: объявили переменную hello, присвоили ей значение «Hello, world», вывели на печать. Мы знаем, что GO является статически типизированным языком, но где здесь явное указание типа для переменной hello? А тем не менее, если мы посмотрим на ее тип, то он окажется string. Компилятор как-то понимает это. А если мы сделаем так hello:= 3.14 — то он поймет, что это float64, хотя тип тоже нигде явно не указан.

А все дело кроется в нетипизированных константах. Встречайте, на сцене нетипизированная строковая константа «Hello, World»!

Можно не рассматривать понятие константы и прочие салатики (например IOTA, про которую много материала), а сразу перейти к главному блюду.

Существует шесть типов нетипизированных констант:

  • нетипизированная строка;

  • нетипизированное булевое значение;

  • нетипизированное число с плавающей точкой;

  • нетипизированное комплексное число;

  • нетипизированная руна;

  • нетипизированное целочисленное значение.

Раз уж затронули в начальном примере нетипизированные строковые константы, то начнем с них:

const str = "Hello, world!"

Если бы это была переменная, мы бы точно сказали, что ее тип — string. Но это константа, и у нее пока что нет фиксированного типа. Можно даже ее не именовать, согласно документации, просто текст, заключенный в двойные кавычки уже является не типизированной константой. «Hello» — уже является константой, поэтому когда мы выполняем что-то вроде этого:

hello := "Hello" + "world"

Мы используем нетипизированные строковые константы. Может возникнуть логичный вопрос: если »Привет» и »мир» нетипизированные константы, то почему hello будет иметь явный тип string?

Ответ звучит так: переменная всегда нуждается в типе, в go нет нетипизированных переменных, а нетипизированные константы, как бы это странно не звучало, имеют тип по умолчанию, который они и передают значению, если тип не указан, а он требуется. Для нетипизированных строковых констант типом по умолчанию является string, поэтому »Hello» и »world» как бы подсказывают, что у hello тоже будет тип string.

Для чего же они нужны? Допустим мы решили сделать свой тип данных и объявили переменную этого типа:

type StringType string
var st StringType

теперь st не может взаимодействовать с переменными типа string, , но с нашей константой str она неплохо себя чувствует:

const str = "Привет!"  //нетипизированная строковая константа
type StringType string  //наш кастомный тип на основе string
var st StringType //переменная нашего кастомного типа
st = str
fmt.Println(st) // Привет!
//а тут мы объявим переменную типа string и попробуем взаимодействовать с st
var strStr string = "Мир!"
newSt := st + strStr // ошибка: mismatched types StringType and string, 
не получилось, типы все-таки разные, но st может взаимодействовать с нашей константой str.

Получается, что наша константа подстроилась под тип StringType. Но стоит сделать замечание, что данное преобразование будет происходить, если тип совместим со строками. Если наш тип будет выглядеть так type StringType int, то чуда не произойдет и мы получим ошибку cannot use str (untyped string constant «Привет!») as StringType value in assignment.

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

Все, что было сказано выше про нетипизированные строковые константы, применимо и к нетипизированным булевым константам.

Значения true и false являются нетипизированными булевыми константами, которые могут быть присвоены любой булевой переменной, но после того, как переменная получит тип, провести операции над разными типами не получится:

const ConstBool = true  //нетипизированная булевая константа
type MyBool bool  //наш кастомный тип на основе bool
var boo MyBool //переменная нашего кастомного типа
boo = ConstBool //все в рамках закона
fmt.Println(boo) // true


//а тут мы объявим переменную типа bool и попробуем взаимодействовать с boo
var VarBool bool = false
boo = VarBool // ошибка: cannot use VarBool (variable of type bool) as 
MyBool value in assignment

В Go существует два типа чисел с плавающей точкой float32 и float64. Для нетипизированной константы с плавающей точкой типом по умолчанию будет float64, хотя она может быть присвоена и значению типа float32:

const f64 = 0.0 // тип по умолчанию float64
var f32 float32 f32 = f64  // тип float32, ошибки нет

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

const f64 = 3,41e38 // float64
var f32 float32 f32 = f64  // ошибка переполнения, так как максимальное число для float32 
3.4e38

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

const f64 = 3,41e38 // float64
var f32 float32 
f32 = f64/2  // тут нет ошибки, все скомпилируется

Константы в пакете math имеют гораздо больше знаков после запятой, чем доступно во float64, это позволяет выполнять арифметические операции с большей точностью.

const Pi = math.Pi   // 3.141592653589793238462643383279502884197169399...
fmt.Println(Pi)      // 3.141592653589793
fmt.Println(Pi * 2)  // 6.283185307179586
var f32 float32
f32 = Pi
fmt.Println(f32)     // 3.1415927
fmt.Println(f32 * 2) // 6.2831855

Для нетипизированных комплексных констант тип по умолчанию complex128. Они ведут себя примерно так же, как константы с плавающей точкой.

const I = (0.0 + 1.0i)   //нетипизироанная комплексная коснтанта
type MyCoplex complex128 //кастомный тип
const TypeI complex128 = (0.0 + 1.0i)  //явно указываем тип complex128
var m MyCoplex //объявляем переменную с кастомным типом
m = (0.0 + 1.0i) //присваиваме переменной значение complex128
m = I  //присваиваем переменной значение нетипизированной комплексной константы
m = TypeI  // тут уже ошибка, так как MyCoplex и complex128 разные типы
fmt.Println(m)

Стандартный пример с кастомным типом, который мы рассматривали и для строк, и для булевых значений.

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

const I = 2.0 + 0i //нетипизироанная комплексная коснтанта
fmt.Printf("%T", I) // complex128

Тип complex128, но мы можем объявить переменные типов float64 и int и присвоить им значение константы I, и никакой ошибки компиляции не возникнет:

const I = 2.0 + 0i //нетипизированная комплексная коснтанта
var f64 float64
var i int
f64 = I
i = I
fmt.Println(f64) // 2
fmt.Println(i) // 2

Это очень интересное свойство нетипизированных констант переходить между типами. Но оно работает, только если не будет потеряна информация или не будет переполнения типа. Для первого примера const I = (0.0 + 1.0i)  для float64 мы получим ошибку переполнения, а для int — невозможность обрезать мнимую часть.

Перейдем к нетипизированным целочисленным константам и нетипизированным рунам.

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

Старый пример с кастомным типом:

type MyInt int //кастомный тип
const Three = 3 //нетипизированная целочисленная константа
const TypedThree int = 3  // константа с явным указанием типа int
var mi MyInt
mi = Three // создаем переменную кастомного типа и присваиваем ей значение нетипизированной константы
mi = TypedThree // тут ошибка
fmt.Println(mi)

Этот же пример можно использовать с другими целочисленными типами (Int8, int16, uint8, uint16 и др.)

Мы знаем, что тип rune это псевдоним int32, поэтому для символов, заключенных в одинарные кавычки, тип по умолчанию будет int32.

Это позволяет нам проводить довольно интересные математические операции:

const A = 'a'  // руна, тип int32
const a1 = 123  // int
const a1A = a1 * A  // 11931  тип int32

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

var a uint = 10
var b = uint(10)
c := uint(10)
fmt.Println(a == b)  //true
fmt.Println(a == c)  //true
fmt.Println(c == b)  //true

Мы явно указали тип, к которому нужно привести константу.

Также не стоит забывать про переполнение типа, как это было в константах с плавающей точкой. Например var i8 int8 = 128 вызовет ошибку переполнения.

Рассмотрим некоторые математические операции, которые несмотря на разный тип, возможны:

При взаимодействии констант int и float, мы получим float

  • const a = 2 + 3.0  // 5 float64

  • const b = 2 / 3.0  // 0.6666666666666666  float64

  • const c = 2×3.0  // 6 float64

  • const d = 2 — 3.0  // -1 float64

При делении констант int на int мы получаем int, но результат будет обрезан до целого числа

const b = 15 / 4  // 3 int

Вывод

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

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

const pi = 3.14 
var w int = pi + 2

pi — нетипизированная константа с типом по умолчанию float64. Переменная w представляет из себя нашу константу с типом float64 и 2 — int. Так почему же не происходит чуда?

Как и для комплексных констант, когда мы пытались преобразовать их к типу int, так и для констант с плавающей точкой, действует правило, преобразование выполнится, если не будет потеряна информация. В тексте ошибки мы видим, что сложение двух нетипизированных констант выполнено (untyped float constant 5.14) , но преобразовать 5.14 к типу int уже не получится, так как теряется дробная часть.

Чат GPT пытался явно привести pi к типу int, но у него это не получилось, потому что значения типизированных констант всегда должны быть точно представлены значениями постоянного типа.

  • uint (-1) // -1 нельзя представить как uint

  • int (3.14) // 3.14 нельзя представить как целое число

  • int64(1 << 100) // 1267650600228229401496703205376 не может быть представлено как int64

По итогу, получить значение pi в виде int, мы можем, если объявим переменную с типом float64 и приравняем ей значение pi. А уже для переменной мы можем выполнить явное преобразование типа, с отсечением дробной части:

const pi = 3.14  //нетипизированная константа
var Pi = 3.14  //переменная типа float64
var w int = int(Pi) + 2  //переменные можно явно преобразовывать
fmt.Println(w) // 5

© Habrahabr.ru