Переменные
Содержание |
Понятие переменной, тип переменной
В классических книгах по программированию, под переменными понимают некоторую поименованную область памяти, либо регистр процессора. В нашем же случае, мы имеем дело с виртуальной машиной, соответственно ни регистров ни памяти в обычном понимании у нас нет. Все, с чем мы имеем дело, это с объектами в "сознании" виртуальной машины. Так или иначе, но и там и тут переменные служат для одной цели: для хранения некоторой информации. Ей могут быть и исходные данные программы, и некоторые промежуточные значения, или же результаты выполнения программы. Впрочем, некоторые программы могут не иметь исходных данных (например, генератор текстур), другие же могут не производить никакого вывода, так что все это довольно условно.
Поэтому, будем считать проще, что переменная это просто некоторая сущность, некоторый объект, способный хранить данные. Под данными мы будем понимать любую информацию, которую возможно представить в цифровом виде.
Тип переменной
Часто с понятием переменной связывают понятие ее типа. Стоит отметить что это понятие так же является математической абстракцией. С точки зрения самой машины все данные представляют собой одно и то же — совокупность нулей и едениц. А что под ними понимать в каждом конкретном случае — решает уже программист. Так вот, в некотором смысле тип переменной показывает что именно хранится в данной переменной.
Наиболее распространенные типы переменных это:
- Логический тип, принимающий только два значения ("истина" и "ложь")
- Целые числа различной длины (байт, слово, двойное слово и т. д.)
- Числа с плавающей точкой
- Строки (последовательность байтов в памяти, воспринимаемая как строка текста)
- Указатели (переменные содержащие в себе адрес другой переменной)
- Составные типы (некоторая совокупность из вышеперечисленных типов)
Как уже говорилось выше, первоначально о типе переменной знал только программист, пишущий программу. Естественно, вся ответственность за соблюдение соответствия типов выполняемым операциям была возложена на самого программиста. Постепенно, с развитием языков программирования высокого уровня была выдвинута идея явным образом сопоставлять переменной ее тип. Это обеспечивало большее удобство в написании программ, поскольку всю работу по контролю за типами выполнял сам компилятор, который выдавал сообщения об ошибках если типы переменных не соответствовали операциям, или была вероятность неверного их истолкования. Так появились языки со статической типизацией.
Статическая типизация на примере C++
Итак, статическая типизация это — приём, при котором переменная, параметр подпрограммы или возвращаемое значение функции связывается с типом в момент объявления и этот тип не может быть изменён позже (переменная или параметр будут принимать, а функция будет возвращать значения только этого типа).
То есть, если мы объявляем переменные целочисленного типа как int i, j; то мы можем выполнять над ними только операции, соответствующие понятиям этого типа, вроде операции сложения i + j или операции присваивания одной переменной другой i = j. Если же мы попытаемся присвоить такой переменной значение другого, неприводимого типа, например i = "hello world" то компилятор выдаст ошибку.
Рассмотрим кратко преимущества и недостатки статически типированных языков и сравним их впоследствии с другими подходами к типизации.
Преимущества статической типизации:
- Строгость программ
- Большая возможность оптимизации, а следовательно:
- Высокая скорость работы
- Минимальные требования к памяти
Недостатки:
- Необходимость объявления типов даже там где это очевидно
- Засорение кода программы дополнительными символами, а следовательно:
- Отвлечение внимания программиста от самой задачи в сторону рутины формализации
- Плохая читаемость кода
- Необходимость дополнительных ухищрений для передачи параметров разных типов
- Невозможность написания абстрактных алгоритмов
Вывод: Программы, написанные на языках со статической типизацией обладают большой эффективностью и производительностью, однако это дается ценой строгости и определенности. Программирование на таких языках это принятие их правил игры, при этом программист должен мыслить в жестко ограниченных рамках. Вот почему эти языки чаще всего применяются именно для системного программирования, где в первую очередь важна производительность полученного кода, и только потом удобство его написания и поддержки.
Динамическая типизация на примере Ruby
Впоследствии, в противовес статической типизации была разработана модель динамически типированных языков. При таком подходе, переменная не имеет заранее определенного типа, а принимает тип при инициализации и при присвоении ей некоторого значения. Таким образом, в различных участках программы одна и та же переменная может принимать значения разных типов.
На первый взгляд это может показать странным, особенно с точки зрения программиста, привыкшего к статически типированным языкам (таким как С++ или Паскаль). На самом деле здесь нет ничего страшного или нелогичного. Все мы в своей жизни, сами того не подозревая, оперируем понятиями динамических переменных. Один и тот же объект в разных ситуациях мы можем воспринимать с разных точек зрения. Задавая вопрос "как пройти в библиотеку", мы можем получить как конкретный ответ "в 3 часа ночи?! идиот!" так и целый набор: "на автобусе номер 23, на такси или пешком, тут не очень далеко". При этом, нас совершенно не смущает то, что мы заранее не знаем каков будет результат, наоборот — это дает нам большую гибкость и свободу действий; мы двигаемся дальше на основании того, каков был результат предыдущей операции. Подобно этому примеру, мы можем писать программы которые оперируют переменными неопределенного типа. В результате, код получается более лаконичным, более читаемым и, в конце концов, более близким к естественному восприятию человека (ведь именно этого мы ждем от языков программирования!).
Концепция динамической типизации в полной мере нашла свое применение в языке Ruby. В этом языке любая переменная может в разное время иметь различные типы. Вот пример кода на этом языке:
<source lang="ruby"> def get_data(need_array = false)
result = 0 # целое число if need_array result = [ 1, 2, 3, 4 ] # тип меняется на Array else result = { 1 => 2, 3 => 4} # тип меняется на Hash end result
end </source>
Преимущества динамической типизации:
- Легкость написания программ
- Лаконичность и хорошая читаемость
- Высокая гибкость языка. Возможность решения проблемы разными способами
- Большие возможности к повторному использованию кода (абстрактные алгоритмы)
Недостатки:
- Существенно меньшая производительность, по сравнению со статически типированными языками
- Сложность в написании оптимизаторов, их малая эффективность
- Как правило, динамически типированные языки являются интерпретаторами (компиляция неэффективна)
Вывод: Языки с динамической типизацией очень удобны для использования на прикладном уровне. Учитывая их высокую гибкость и лаконичность, программисту легче излагать свои мысли, при том что свое внимание он концентрирует на самой задаче, не отвлекаясь на частные проблемы реализации. Конечно, они уступают по эффективности статическим, компилируемым языкам, однако это уже вопрос требующий отдельного рассмотрения в рамках конкретно поставленной задачи.
Полудинамическая типизация
Рассмотрев преимущества и недостатки статической и динамической типизации, мы подходим к мысли объединения этих двух подходов в рамках одного языка. Языка, который бы сочетал в себе все преимущества динамической типизации, с возможностью указания типов там где это необходимо. В результате мы получили бы язык, достаточно гибкий и в то же время позволяющий эффективно работать с типами.
Язык К++ как раз и ставит перед собой задачу совмещения этих двух подходов. Переменные этого языка могут быть либо типированными, либо не типированными (динамическими). Первые обеспечивают производительность, вторые — гибкость. Достигается это прежде всего ценой того, что переменная не может изменять свой тип во время выполнения программы. Однако, это относится только к статически типированным переменным. Полностью динамические переменные этого недостатка лишены.
При разработке программы на языке К++, программист должен стараться держать баланс между удобством написания программы и ее эффективностью. В целом, рекомендуется указывать типы переменных везде где это возможно. Динамические же переменные следует применять в тех местах, где заранее неизвестно, с каким типом переменной придется работать. При этом, код получится достаточно производительным и одновременно гибким.
Типизация при объявлении
Язык К++ позволяет задавать тип переменной несколькими способами. Одним из них является типирование переменной при объявлении. При этом, необходимо написать ключевое слово var, за которым следует идентификатор типа переменной, а затем ее имя. Точка с запятой (;) завершает конструкцию:
<source lang="kpp"> var int i; </source>
Здесь мы объявили целочисленную переменную с именем i и неявно инициализировали ее значением 0. Такой синтаксис удобен в тех случаях, когда заранее неизвестно каким должно быть значение переменной. Видимо, в данном случае программист решил что оно будет присвоено явным образом впоследствии. Если же заранее ясно что на момент обявления переменная должна иметь определенное значение, следует применять синтаксис объявления с инициализацией.
Примечание: Неявная инициализация это причина многих трудноуловимых ошибок времени выполнения. В языке К++ это имеет менее разрушительные последствия, чем например в С++ (где значение неинициализированной переменной может быть абсолютно произвольным!), тем не менее настоятельно рекомендуется инициализировать переменные сразу при объявлении. Это избавит вас от опасности забыть присвоить значение переменной впоследствии.
Типизация при инициализации
Более предпочтительным способом объявления типированной переменной является объявление с инициализацией. В данном случае, под инициализацией понимается процесс начального задания значения переменной. Для этого следует так же написать ключевое слово var, после которого сразу идет идентификатор имени переменной, а затем выражение — инициализатор, присваивающее значение вновь созданной переменной. При этом, тип переменной будет определен как тип инициализатора:
<source lang="kpp"> var i = 0; var s = "hello world"; </source>
В приведенном выше примере, переменной i будет назначен целочисленный тип (int), в то время как переменная s будет иметь строковый тип (string).
Примечание: В некоторых случаях может сложиться ситуация, что инициализатор переменной это довольно сложное выражение. При этом программисту сложно определить какой же на самом деле будет тип переменной. Для ясности приведем пример из разряда "как не надо делать":
<source lang="kpp"> var x = 2+3*4+f(3)/3*0.2; </source>
Синтаксически здесь все верно. Переменная будет создана и ей будет назначен некоторый тип. Но вот какой? Опытный читатель заметит, что в выражении присутствуют константы с плавающей точкой и операция деления. Стало быть, в результате приведения типов, результирующий тип переменной так же будет числом с плавающей точкой. Внимательный же читатель заметит так же, что в выражении присутствует вызов функции f(3). И тут дело обстоит еще хуже. Функция может возвращать значение любого типа. Чтобы узнать какого именно, нужно обратиться к документации, либо к описанию самой функции. А что если она возвращает нетипированную переменную (см. ниже)? Тогда предсказать тип переменной будет еще сложнее (если забежать вперед, то можно сказать что результат выражения так же будет нетипирован).
Скорее всего Читатель уже согласился с мыслью, что подобных конструкций следует избегать. Однако что же делать, если все же требуется завести переменную, которая должна быть инициализирована подобным образом? Выхода тут может быть два. Либо отделить объявление переменной от ее инициализации, либо совместить типированное объявление с инициализацией.
Первый вариант: <source lang="kpp"> var real x; ... //где то по коду программы x = 2+3*4+f(3)/3*0.2; </source>
Второй, более удачный вариант: <source lang="kpp"> var real x = 2+3*4+f(3)/3*0.2; </source>
Стоит отметить, что во втором случае будет выполнена операция приведения типов, то есть будет сделана попытка преобразовать тип значения инициализатора к явно указаному типу переменной. Если тип инициализатора возможно привести к типу переменной, то все пройдет успешно. Если же типы неприводимы, то произойдет либо ошибка компиляции (при статическом типе инициализатора), либо ошибка времени выполнения.
Нетипированные (динамические) переменные
Иногда бывает необходимо работать с переменной, тип которой на момент компиляции неизвестен. В этом случае применяются нетипированные или динамические переменные. Такие переменные объявляются просто, путем написания ключевого слова var, за которым идет идентификатор имени переменной:
<source lang="kpp"> var x; </source>
В ходе работы программы, такая переменная может принимать значения любых типов. Она будет автоматически приводиться к нужным типам (и наоборот), что в конечном счете обеспечивает гибкость кода.
Примером применения динамических переменных могут послужить контейнеры — объекты которые могут хранить в себе другие объекты. Простейшими примерами контейнеров могут послужить массивы. В языке К++ массивы не являются типированными. Они могут хранить любые объекты, даже объекты различных типов. Разумеется, сам массив оперирует со своими элементами как с нетипированными переменными (ведь он не знает заранее, объекты каких типов будут в него класть).
Программист может написать некоторую подпрограмму для обработки элементов массива, например для их сортировки. При этом, все что ему нужно знать это способ перебора элементов контейнера и некоторую операцию сравнения двух элементов, для выяснения их очередности (эту операцию контейнеру предоставляет пользователь; как именно это делается, будет описано дальше). Этим достигается эффективность написания кода и открываются широкие возможности по повторному использованию кода. К примеру, в традиционных типированных языках вроде Паскаля, пришлось бы реализовывать отдельные методы сортировки строк, числовых массивов и т. д., здесь же мы обходимся одной реализацией.
Примечание: Применение динамических переменных негативно сказывается на эффективности работы программы и на скорости ее выполнения. Поэтому старайтесь применять динамические переменные только там где это действительно необходимо, либо там где не требуется высокая производительность.
О важности инициализации переменных
В заключение этой главы, приведем краткое обобщение материала относительно инициализации переменных. Как уже было сказано выше, инициализация переменных это важный момент, правильное понимание которого позволит программисту писать более эффективные программы, и в то же время избавить себя от многих типичных ошибок программирования. В ходе изучения языка постарайтесь выработать у себя некоторые шаблоны проектирования, следуя которым вы избавите себя от возможных проблем.
Вот некоторый список рекомендаций относительно объявления переменных:
- Старайтесь всегда объявлять переменные, инициализируя их
- Если выражение инициализатора достаточно сложное, либо содержит вызовы функций, применяйте явное типирование объявляемой переменной, либо используйте приведение типов в самом выражении
- Объявляйте переменные как можно ближе к месту их использования
- Нетипированные переменные применяйте только в тех случаях, где это действительно необходимо
Коненчо, этот список далеко не полный, да он и не претендует на полноту. То что написано выше ни в коем случае не является правилом, которому нужно следовать непременно, везде и всегда. Границы разумного определяете вы сами, просто старайтесь не упускать этих моментов из виду.