Необходимо написать два метода которые делают следующее 1 создают одномерный длинный массив например
Перейти к содержимому

Необходимо написать два метода которые делают следующее 1 создают одномерный длинный массив например

  • автор:

Типы данных

QHB имеет богатый набор собственных типов данных, доступных пользователям. Пользователи могут добавлять новые типы в QHB с помощью команды CREATE TYPE.

В Таблице 1 показаны все встроенные типы данных общего назначения. Большинство альтернативных имен, перечисленных в столбце «Псевдонимы», являются именами, используемыми внутри QHB по историческим причинам. Кроме того, доступны некоторые используемые или устаревшие типы, но они не перечислены здесь.

Таблица 1. Типы данных

Имя Псевдонимы Описание
bigint int8 8-байтовое целое со знаком
bigserial serial8 автоинкрементное восьмибайтовое целое число
bit [ (n) ] битовая строка фиксированной длины
bit varying [ (n) ] varbit [ (n) ] битовая строка переменной длины
boolean bool логический логический (истина / ложь)
box прямоугольная коробка на плоскости
bytea двоичные данные («байтовый массив»)
character [ (n) ] char [ (n) ] символьная строка фиксированной длины
character varying [ (n) ] varchar [ (n) ] символьная строка переменной длины
cidr сетевой адрес IPv4 или IPv6
circle круг на плоскости
date календарная дата (год, месяц, день)
double precision float8 число с плавающей запятой двойной точности (8 байт)
inet адрес хоста IPv4 или IPv6
integer int, int4 четырехбайтовое целое со знаком
interval [ fields ] [ (p) ] промежуток времени
json текстовые данные JSON
jsonb двоичные данные JSON, разложенные
line бесконечная линия на плоскости
lseg отрезок на плоскости
macaddr MAC (Media Access Control) адрес
macaddr8 MAC (Media Access Control) адрес (формат EUI-64)
money сумма в валюте
numeric [ (p, s) ] decimal [ (p, s) ] точное число выбираемой точности
path геометрический путь на плоскости
pg_lsn порядковый номер журнала QHB
point геометрическая точка на плоскости
polygon замкнутый геометрический путь на плоскости
real float4 число с плавающей точкой одинарной точности (4 байта)
smallint int2 двухбайтовое целое со знаком
smallserial serial2 автоинкрементное двухбайтовое целое число
serial serial4 автоинкрементное четырехбайтовое целое число
text символьная строка переменной длины
time [ (p) ] [ without time zone ] время суток (без часового пояса)
time [ (p) ] with time zone timetz время суток, включая часовой пояс
timestamp [ (p) ] [ without time zone ] дата и время (без часового пояса)
timestamp [ (p) ] with time zone timestamptz дата и время, включая часовой пояс
tsquery запрос текстового поиска
tsvector документ текстового поиска
txid_snapshot снимок идентификатора транзакции на уровне пользователя
uuid универсально уникальный идентификатор
xml данные XML

Каждый тип данных имеет внешнее представление, определяемое его входными и выходными функциями. Многие из встроенных типов имеют очевидные внешние форматы. Однако несколько типов являются уникальными для QHB, например, геометрические пути, или имеют несколько возможных форматов, таких как типы даты и времени. Некоторые из функций ввода и вывода не являются обратимыми, т. е. результат функции вывода может потерять точность по сравнению с исходным вводом.

Числовые Типы

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

Таблица 2. Числовые Типы

Имя Размер хранилища Описание Ассортимент
smallint 2 байта целое число малого диапазона От -32768 до +32767
integer 4 байта типичный выбор для целого числа От -2147483648 до +2147483647
bigint 8 байт большое целое число От -9223372036854775808 до +9223372036854775807
decimal переменная указанная пользователем точность, точная до 131072 цифр перед десятичной точкой; до 16383 цифр после запятой
numeric переменная указанная пользователем точность, точная до 131072 цифр перед десятичной точкой; до 16383 цифр после запятой
real 4 байта переменная точность, неточная Точность 6 десятичных цифр
double precision 8 байт переменная точность, неточная Точность 15 десятичных цифр
smallserial 2 байта небольшое автоинкрементное целое число От 1 до 32767
serial 4 байта автоинкрементное целое число 1 до 2147483647
bigserial 8 байт большое автоинкрементное целое число 1 до 9223372036854775807

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

Целочисленные типы

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

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

В SQL определены только целочисленные типы integer (или int), smallint и bigint. Имена типов int2, int4 и int8 являются расширениями, которые также используются некоторыми другими системами баз данных SQL.

Числа произвольной точности

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

Следующие термины используются ниже: Точность (precision) числа — это общее количество значащих цифр во всем числе, то есть количество цифр по обеим сторонам десятичной точки. Масштаб (scale) числа — это количество десятичных цифр в дробной части, справа от десятичной точки. Таким образом, число 23,5141 имеет точность 6 и масштаб 4. Целые числа можно считать имеющими нулевой масштаб.

Можно настроить как максимальную точность, так и максимальный масштаб числового столбца. Чтобы объявить столбец числового типа, используйте синтаксис:

NUMERIC(precision, scale) 

Точность должна быть положительной, масштаб должен быть неотрицательным. Альтернативный вариант:

NUMERIC(precision) 

устанавливает масштаб 0. Форма:

NUMERIC 

без какой-либо точности или масштаба создает столбец, в котором могут быть сохранены числовые значения любой точности и масштаба, вплоть до предела реализации по точности. Столбец такого типа не будет приводить входные значения к какому-либо конкретному масштабу, тогда как numeric столбцы с объявленным масштабом будут приводить входные значения к этому масштабу. (Стандарт SQL требует, чтобы масштаб по умолчанию был равен 0, что соответствует приведению к целочисленной точности. Если вы беспокоитесь о переносимости, всегда указывайте точность и масштаб явно).

Заметка
Максимально допустимая точность, если она явно указана в объявлении типа, составляет 1000; NUMERIC без указанной точности подпадает под ограничения, описанные в Таблице 2.

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

Числовые значения физически сохраняются без каких-либо дополнительных начальных или конечных нулей. Таким образом, заявленная точность и масштаб столбца являются максимальными, а не фиксированными распределениями. (В этом смысле числовой тип больше похож на varchar(n) чем на char(n)). Фактическое требование к хранилищу составляет два байта для каждой группы из четырех десятичных разрядов, плюс три-восемь байтов служебных данных.

В дополнение к обычным числовым значениям, числовой тип допускает специальное значение NaN, означающее «не число». Любая операция с NaN приводит к другому NaN. При записи этого значения в качестве константы в команду SQL вы должны заключать в кавычки, например, UPDATE table SET x = ‘NaN’ . При вводе строка NaN распознается без учета регистра.

Заметка
В большинстве реализаций концепции «не число» NaN не считается равным любому другому числовому значению (включая NaN). Чтобы разрешить сортировку и использование числовых значений в древовидных индексах, QHB рассматривает значения NaN как равные и превышающие все значения, отличные от NaN.

Типы decimal и numeric эквивалентны. Оба типа являются частью стандарта SQL.

При округлении значений числовой тип округляет связи от нуля в то время как (на большинстве машин) типы real и double precision округляют до ближайшего четного числа. Например:

SELECT x, round(x::numeric) AS num_round, round(x::double precision) AS dbl_round FROM generate_series(-3.5, 3.5, 1) as x; x | num_round | dbl_round ------+-----------+----------- -3.5 | -4 | -4 -2.5 | -3 | -2 -1.5 | -2 | -2 -0.5 | -1 | -0 0.5 | 1 | 0 1.5 | 2 | 2 2.5 | 3 | 2 3.5 | 4 | 4 (8 rows) 

Типы с плавающей точкой

Типы данных real и double precision являются неточными числовыми типами переменной точности. На всех поддерживаемых в настоящее время платформах эти типы являются реализациями стандарта IEEE 754 для двоичной арифметики с плавающей запятой (одинарной и двойной точности соответственно) в той степени, в которой это поддерживается базовым процессором, операционной системой и компилятором.

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

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

На всех поддерживаемых в настоящее время платформах real тип имеет диапазон от 1E** -37 ** до 1E** +37 ** с точностью не менее 6 десятичных цифр. Тип double precision имеет диапазон от 1E** -307 ** до 1E** +308 ** с точностью не менее 15 цифр. Значения, которые являются слишком большими или слишком маленькими, вызовут ошибку. Округление может иметь место, если точность введенного числа слишком высока. Числа, слишком близкие к нулю, которые не могут быть представлены отличными от нуля, вызовут ошибку недостаточного значения.

По умолчанию значения с плавающей запятой выводятся в текстовой форме в самом кратком десятичном представлении; полученное десятичное значение ближе к истинному сохраненному двоичному значению, чем к любому другому значению, представляемому с той же двоичной точностью. (Однако выходное значение в настоящее время никогда не бывает точно посередине между двумя представимыми значениями, чтобы избежать широко распространенной ошибки, когда входные подпрограммы не соблюдают должным образом правило округления до ближайшего четного). Это значение будет использовать не более 17 значащих десятичных цифр для значения float8 и не более 9 цифр для значений float4.

Заметка
Этот формат вывода с наименьшей точностью вывода генерируется намного быстрее, чем исторически сложившийся формат округления.

Для снижения точности вывода, можно использовать параметр extra_float_digits для выбора округления десятичного числа. Установка значения 0 восстанавливает предыдущее значение округления по умолчанию до 6 (для float4) или 15 (для float8) значащих десятичных цифр. Установка отрицательного значения уменьшает количество цифр; например, -2 округляет вывод до 4 или 13 цифр соответственно.

Любое значение extra_float_digits больше 0 выбирает формат с наименьшей точностью.

В дополнение к обычным числовым значениям типы с плавающей точкой имеют несколько специальных значений:

Они представляют специальные значения IEEE 754 «бесконечность», «отрицательная бесконечность» и «не число» соответственно. При записи этих значений в качестве констант в SQL-команде необходимо заключить их в кавычки, например, UPDATE table SET x = ‘-Infinity’ . При вводе эти строки распознаются без учета регистра.

Заметка
IEEE754 указывает, что NaN не должен сравниваться с любым другим значением с плавающей запятой (включая NaN). Чтобы позволить значениям с плавающей точкой сортироваться и использоваться в древовидных индексах, QHB рассматривает значения NaN как равные и превышающие все значения, отличные от NaN.

QHB также поддерживает стандартные обозначения SQL float и float(p) для указания неточных (inexact) числовых типов. Здесь p указывает минимально допустимую точность в двоичных разрядах. QHB принимает значения от float(1) до float(24) как выбор типа real, в то время как значения от float(25) до float(53) как выбор типа double precision. Значения p вне допустимого диапазона формируют ошибку. float без заданной точности считается double precision.

Серийные типы

В этом разделе описывается специфичный для QHB способ создания столбца автоинкрементирования. Другой способ — использовать функцию столбца идентификаторов стандарта SQL, см. в описании CREATE TABLE.

Типы данных smallserial, serial и bigserial — это не настоящие типы, а просто удобство записи для создания столбцов уникальных идентификаторов (аналогично свойству AUTO_INCREMENT поддерживаемому некоторыми другими базами данных). В текущей реализации, указание:

CREATE TABLE tablename ( colname SERIAL ); 
CREATE SEQUENCE tablename_colname_seq AS integer; CREATE TABLE tablename ( colname integer NOT NULL DEFAULT nextval('tablename_colname_seq') ); ALTER SEQUENCE tablename_colname_seq OWNED BY tablename.colname; 

Таким образом, создаётся целочисленный столбец и его значения по умолчанию организованы для назначения из генератора последовательности. Ограничение NOT NULL применяется, чтобы гарантировать, что нулевое значение не может быть вставлено. (В большинстве случаев вы также хотели бы присоединить ограничение UNIQUE или PRIMARY KEY чтобы предотвратить случайную вставку дублирующихся значений, но это не происходит автоматически). Наконец, последовательность помечается как «принадлежащая» столбцу, так что она будет удалена, если столбец или таблица будут удалены.

Заметка
Поскольку smallserial, serial и bigserial реализованы с использованием последовательностей, в последовательности значений, которая появляется в столбце, могут быть «дыры» или пробелы, даже если строки никогда не удаляются. Значение, выделенное из последовательности, все равно будет «израсходовано», даже если строка, содержащая это значение, никогда не будет успешно вставлена в столбец таблицы. Это может произойти, например, если транзакция вставки откатывается. Подробности смотрите в nextval() в разделе Функции управления последовательностями.

Чтобы вставить следующее значение последовательности в столбец последовательности, укажите, что столбцу последовательности должно быть присвоено его значение по умолчанию. Это можно сделать либо путем исключения столбца из списка столбцов в инструкции INSERT, либо с помощью ключевого слова DEFAULT.

Имена типов serial и serial4 эквивалентны: оба создают столбцы типа integer. Имена типов bigserial и serial8 работают аналогично, за исключением того, что они создают столбец bigint. Следует использовать bigserial, если вы предполагаете использовать более 2** 31 ** идентификаторов за время существования таблицы. Имена типов smallserial и serial2 также работают аналогично, за исключением того, что они создают столбец smallint.

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

Денежные Типы

Тип money хранит сумму в валюте с фиксированной дробной точностью; см. таблицу 3. Дробная точность определяется настройкой базы данных lc_monetary. Диапазон, указанный в таблице, предполагает наличие двух дробных цифр. Ввод принимается в различных форматах, включая целочисленные литералы и литералы с плавающей запятой, а также типичное форматирование валюты, например, ’$1,000.00’. Вывод обычно в последней форме, но зависит от локали.

Таблица 3. Денежные Типы

Имя Размер хранилища Описание Ассортимент
money 8 байт сумма в валюте От -92233720368547758.08 до +92233720368547758.07

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

Значения типов данных numeric, int и bigint могут быть приведены к money. Преобразование из типов данных real и double precision можно выполнить, если сначала привести к типу numeric, например:

SELECT '12.34'::float8::numeric::money; 

Однако это не рекомендуется. Числа с плавающей запятой не должны использоваться для обработки денег из-за возможной ошибки округления.

Денежное значение может быть приведено к числовому без потери точности. Преобразование в другие типы потенциально может привести к потере точности и также должно выполняться в два этапа:

SELECT '52093.89'::money::numeric::float8; 

Деление денежной величины на целое число производится с усечением дробной части в сторону нуля. Чтобы получить округленный результат, разделите на значение с плавающей запятой или преобразуйте денежное значение в числовое перед делением и обратно в денежное после этого. (Последнее предпочтительнее, чтобы избежать риска потери точности). Когда денежная стоимость делится на другую денежную стоимость, результат получается double precision (т. е. чистое число, а не деньги); денежные единицы отменяют друг друга при делении.

Символьные типы

Таблица 4. Символьные типы

Имя Описание
character varying(n), varchar(n) переменная длина с ограничением
character(n), char(n) фиксированная длина, с дополнением
text переменная неограниченная длина

Таблица 4 показывает типы символов общего назначения, доступные в QHB.

SQL определяет два основных типа символов: character varying(n) и character(n), где n — положительное целое число. Оба этих типа могут хранить строки длиной до n символов (не байтов). Попытка сохранить более длинную строку в столбце этих типов приведет к ошибке, если только избыточные символы не являются пробелами, в этом случае строка будет усечена до максимальной длины. (Это несколько странное исключение требуется стандартом SQL). Если строка, которая должна быть сохранена, короче объявленной длины, значения character будут дополнены пробелами; Значения типа character varying просто сохранят более короткую строку.

Если кто-либо явно преобразует значение в character varying(n) или character(n), то значение чрезмерной длины будет усечено до n символов без возникновения ошибки. (Это также требуется стандартом SQL).

Обозначения varchar(n) и char(n) являются псевдонимами для character varying(n) и character(n), соответственно. character без спецификатора длины эквивалентен character(1). Если character varying используется без спецификатора длины, тип принимает строки любого размера. Последнее является расширением QHB.

Кроме того, QHB предоставляет тип text, в котором хранятся строки любой длины. Хотя тип text не соответствует стандарту SQL, он есть и в некоторых других системах управления базами данных SQL.

Значения типа character физически дополняются пробелами до указанной ширины n и сохраняются и отображаются таким образом. Однако конечные пробелы обрабатываются как семантически несущественные и не учитываются при сравнении двух значений типа character. В сопоставлениях, где пробел является значительным, такое поведение может привести к неожиданным результатам; например, SELECT ‘a ‘::CHAR(2) collate «C» < E'a\n'::CHAR(2) возвращает true, даже если в локали C пробел будет больше новой строки. Конечные пробелы удаляются при преобразовании character значения в один из других типов строк. Обратите внимание, что конечные пробелы семантически значимы в character varying и text значениях, а также при использовании сопоставления с шаблоном оператором LIKE и в регулярных выражениях.

Потребность в памяти для короткой строки (до 126 байт) составляет 1 байт плюс фактическая строка, которая включает заполнение пробелами в случае character. Более длинные строки имеют 4 байта служебной информации вместо 1. Длинные строки сжимаются системой автоматически, поэтому физические требования к диску могут быть меньше. Очень длинные значения также хранятся в фоновых таблицах, чтобы они не мешали быстрому доступу к более коротким значениям столбцов. В любом случае самая длинная строка символов, которую можно сохранить, составляет около 1 ГБ. (Максимальное значение, которое будет разрешено для n в объявлении типа данных, меньше этого. Менять это было бы бесполезно, поскольку в многобайтовых кодировках число символов и байтов может быть совершенно разным. Если вы хотите сохранить длинные строки без определенного верхнего предела, используйте text или character varying без спецификатора длины вместо того, чтобы создавать произвольный предел длины).

Заметка
Между этими тремя типами нет разницы в производительности, за исключением увеличения места для хранения при использовании типа с пробелом и нескольких дополнительных циклов ЦП для проверки длины при сохранении в столбце с ограниченной длиной. Хотя character(n) имеет преимущества в производительности в некоторых других системах баз данных, в QHB такого преимущества нет; на самом деле character(n) обычно самый медленный из трех из-за его дополнительных затрат на хранение. В большинстве случаев вместо этого следует использовать text или character varying.

Обратитесь к разделу Строковые константы за информацией о синтаксисе строковых литералов, а также к главе Функции и операторы за информацией о доступных операторах и функциях. Набор символов базы данных определяет набор символов, используемый для хранения текстовых значений; для получения дополнительной информации о поддержке набора символов обратитесь к разделу Поддержка набора символов.

Пример 7.1. Использование типов символов

CREATE TABLE test1 (a character(4)); INSERT INTO test1 VALUES ('ok'); SELECT a, char_length(a) FROM test1; -- (1) a | char_length ------+------------- ok | 2 CREATE TABLE test2 (b varchar(5)); INSERT INTO test2 VALUES ('ok'); INSERT INTO test2 VALUES ('good '); INSERT INTO test2 VALUES ('too long'); ERROR: value too long for type character varying(5) INSERT INTO test2 VALUES ('too long'::varchar(5)); -- explicit truncation SELECT b, char_length(b) FROM test2; b | char_length -------+------------- ok | 2 good | 5 too l | 5 

В QHB есть два других символьных типа фиксированной длины, показанных в Таблице 5. Тип name существует только для хранения идентификаторов во внутренних системных каталогах и не предназначен для использования обычными пользователями. Его длина в настоящее время определяется как 64 байта (63 используемых символа плюс терминатор), но на него следует ссылаться, используя константу NAMEDATALEN в исходном коде C/RUST. Длина устанавливается во время компиляции (и, следовательно, настраивается для специальных целей); максимальная длина по умолчанию может измениться в будущем выпуске. Тип «char» (обратите внимание на кавычки) отличается от char(1) тем, что он использует только один байт памяти. Он используется внутри системных каталогов как упрощенный тип перечисления.

Таблица 5. Специальные типы символов

Имя Размер хранилища Описание
«char» 1 байт однобайтовый внутренний тип
name 64 байта внутренний тип для имен объектов

Двоичные типы данных

Тип данных bytea позволяет хранить двоичные строки; см. таблицу 7.6.

Таблица 6. Двоичные типы данных

имя Размер хранилища Описание
bytea 1 или 4 байта плюс фактическая двоичная строка двоичная строка переменной длины

Бинарная строка — это последовательность октетов (или байтов). Двоичные строки отличаются от символьных строк двумя способами. Во-первых, двоичные строки специально позволяют хранить октеты с нулевым значением и другие «непечатные» октеты (как правило, октеты вне десятичного диапазона от 32 до 126). Строки символов запрещают нулевые октеты, а также запрещают любые другие значения октетов и последовательности значений октетов, которые являются недопустимыми в соответствии с выбранной кодировкой набора символов базы данных. Во-вторых, операции над двоичными строками обрабатывают фактические байты, тогда как обработка символьных строк зависит от настроек локали. Короче говоря, двоичные строки подходят для хранения данных, которые программист считает «необработанными байтами», тогда как символьные строки подходят для хранения текста.

Тип bytea поддерживает два формата для ввода и вывода: формат «hex» (шестнадцатеричный) и исторический формат QHB «escape» (экранированный). Они оба всегда принимаются на вход. Формат вывода зависит от параметра конфигурации bytea_output; по умолчанию используется «hex».

Стандарт SQL определяет другой тип двоичной строки, который называется BLOB или BINARY LARGE OBJECT. Формат ввода отличается от bytea, но предоставляемые функции и операторы в основном одинаковы.

«Шестнадцатеричный» формат bytea

«Шестнадцатеричный» формат кодирует двоичные данные в виде 2 шестнадцатеричных цифр на байт, наиболее значимые из которых являются первыми. Всей строке предшествует последовательность \x (чтобы отличить ее от escape-формата). В некоторых контекстах первоначальный обратный слеш, возможно, должен быть экранирован путем его удвоения (см. раздел Строковые константы). Для ввода шестнадцатеричные цифры могут быть в верхнем или нижнем регистре, и пробел допускается между парами цифр (но не внутри пары цифр и не в начальной последовательности \x ). Шестнадцатеричный формат совместим с широким спектром внешних приложений и протоколов, и он, как правило, быстрее преобразуется, чем escape-формат, поэтому его использование является предпочтительным.

SELECT '\xDEADBEEF'; 

Экранированный формат bytea

Экранированный формат является традиционным форматом QHB для типа bytea. В нем используется подход, представляющий двоичную строку как последовательность символов ASCII, при этом преобразовываются те байты, которые не могут быть представлены в виде символа ASCII, в специальные escape-последовательности. Если с точки зрения приложения представление байтов в виде символов имеет смысл, то такое представление может быть удобным. Но на практике это обычно сбивает с толку, поскольку стирает различия между двоичными строками и символьными строками, а также конкретный выбранный управляющий механизм несколько громоздок. Следовательно, этого формата, вероятно, следует избегать для большинства новых приложений.

При вводе значений bytea в escape-формате, октеты определенных значений должны быть экранированы, тогда как все октетные значения могут быть экранированы. В общем случае, чтобы экранировать октет, преобразуйте его в трехзначное восьмеричное значение и поставьте перед ним обратную косую черту. Сама обратная косая черта (десятичное значение октета 92) альтернативно может быть представлена двойной обратной косой чертой. Таблица 7 показывает символы, которые должны быть экранированы, и дает альтернативные escape-последовательности, где это применимо.

Таблица 7. Экранированные символы октета bytea

Десятичное значение октета Описание Экранированное входное представление Пример Шестнадцатеричное представление
0 нулевой октет ’\000’ SELECT ’\000’::bytea; \x00
39 одинарная кавычка ”” или ’\047’ SELECT ””::bytea; \x27
92 обратный слэш ’\\’ или ’\134’ SELECT ’\\’::bytea; \x5c
От 0 до 31 и от 127 до 255 «Непечатные» октеты ’\ xxx’ (восьмеричное значение) SELECT ’\001’::bytea; \x01

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

Причина, по которой одинарные кавычки должны быть удвоены, как показано в Таблице 7, заключается в том, что это верно для любого строкового литерала в команде SQL. Универсальный синтаксический анализатор строковых литералов использует самые крайние одинарные кавычки и сводит любую пару одинарных кавычек к одному символу данных. То, что функция ввода bytea видит, — это всего лишь одна одинарная кавычка, которую она обрабатывает как простой символ данных. Однако входная функция bytea обрабатывает обратную косую черту как спецсимвол, и другие действия, показанные в Таблице 7, реализуются этой функцией.

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

Октеты bytea выводятся в шестнадцатеричном формате по умолчанию. Если вы измените bytea_output на escape, «непечатные» октеты преобразуются в их эквивалентные трехзначные восьмеричные значения и им предшествует один обратный слеш. Большинство «печатаемых» октетов выводятся по их стандартному представлению в клиентском наборе символов, например:

SET bytea_output = 'escape'; SELECT 'abc \153\154\155 \052\251\124'::bytea; bytea ---------------- abc klm *\251T 

Октет с десятичным значением 92 (обратная косая черта) удваивается в выходных данных. Подробности в Таблице 8.

Таблица 8. Выходные экранированные октеты bytea

Десятичное значение октета Описание Выходное представление с выходом Пример Результат на выходе
92 обратный слэш \\ SELECT ’\134’::bytea; \\
От 0 до 31 и от 127 до 255 «Непечатные» октеты \ xxx (восьмеричное значение) SELECT ’\001’::bytea; \001
От 32 до 126 «Печатаемые» октеты представление набора символов клиента SELECT ’\176’::bytea; ~

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

Типы даты/времени

QHB поддерживает полный набор типов даты и времени SQL, показанных в Таблице 9. Операции, доступные для этих типов данных, описаны в разделе Функции и операторы даты/времени. Даты подсчитываются в соответствии с григорианским календарем, даже за годы до того, как этот календарь был введен .

Таблица 9. Типы даты / времени

Имя Размер хранилища Описание Низкая стоимость Высокое значение разрешение
timestamp [ (p) ] [ without time zone ] 8 байт дата и время (без часового пояса) 4713 г. до н.э. 294276 н.э. 1 микросекунда
timestamp [ (p) ] with time zone 8 байт дата и время с часовым поясом 4713 г. до н.э. 294276 н.э. 1 микросекунда
date 4 байта дата (без времени суток) 4713 г. до н.э. 5874897 н.э. 1 день
time [ (p) ] [ without time zone ] 8 байт время суток (без даты) 00:00:00 24:00:00 1 микросекунда
time [ (p) ] with time zone 12 байт время суток (без даты) с часовым поясом 00: 00: 00 + 1459 24: 00: 00-1459 1 микросекунда
interval [ fields ] [ (p) ] 16 байт интервал времени -178000000 лет 178000000 лет 1 микросекунда

Заметка
Стандарт SQL требует, чтобы запись только timestamp была эквивалентна timestamp without time zone, и QHB соблюдает это поведение. timestamptz принимается как сокращение от timestamp with time zone; это расширение QHB.

time, timestamp и interval принимают необязательное значение точности p которое указывает количество дробных цифр, сохраняемых в поле секунд. По умолчанию нет явного ограничения точности. Допустимый диапазон p составляет от 0 до 6.

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

YEAR MONTH DAY HOUR MINUTE SECOND YEAR TO MONTH DAY TO HOUR DAY TO MINUTE DAY TO SECOND HOUR TO MINUTE HOUR TO SECOND MINUTE TO SECOND 

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

Тип time with time zone определяется стандартом SQL, но определение обладает свойствами, которые приводят к сомнительной полезности. В большинстве случаев сочетание date, time, timestamp without time zone и timestamp with time zone должно обеспечивать полный диапазон функциональных возможностей даты/времени, требуемых для любого приложения.

Ввод даты / времени

Ввод даты и времени допускается практически в любом приемлемом формате, включая ISO 8601, SQL- совместимый, традиционный POSTGRES и другие. Для некоторых форматов порядок ввода даты, месяца и года в дате является неоднозначным, и существует поддержка для определения ожидаемого порядка этих полей. Установите для параметра DateStyle значение MDY чтобы выбрать интерпретацию месяц-день-год, DMY для выбора интерпретации день-месяц-год или YMD чтобы выбрать интерпретацию год-месяц-день.

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

type [ (p) ] 'value' 

где p — необязательная спецификация точности, дающая количество дробных цифр в поле секунд. Точность может быть указана для типов time, timestamp и interval и может варьироваться от 0 до 6. Если точность не указана в спецификации константы, по умолчанию используется точность литерального значения (но не более 6 цифр).

Даты

Таблица 10 показывает некоторые возможные входные данные для типа даты.

Таблица 10. Ввод даты

Пример Описание
1999-01-08 ISO 8601; 8 января в любом режиме (рекомендуемый формат)
January 8, 1999 однозначно в любом datestyle ввода datestyle
1/8/1999 8 января в режиме MDY; 1 августа в режиме DMY
1/18/1999 18 января в режиме MDY; отклонено в других режимах
01/02/03 2 января 2003 г. в режиме MDY; 1 февраля 2003 г. в режиме DMY; 3 февраля 2001 г. в режиме YMD
1999-Jan-08 8 января в любом режиме
Jan-08-1999 8 января в любом режиме
08-Jan-1999 8 января в любом режиме
99-Jan-08 8 января в режиме YMD, в других ошибка
08-Jan-99 8 января, кроме ошибки в режиме YMD
Jan-08-99 8 января, кроме ошибки в режиме YMD
19990108 ISO 8601; 8 января 1999 года в любом режиме
990108 ISO 8601; 8 января 1999 года в любом режиме
1999.008 год и день года
J2451187 Юлианская дата
January 8, 99 BC 99 год до нашей эры
Время

Типы времени суток -это time [ (p) ] without time zone и time [ (p) ] with time zone . «time» эквивалентно «time without time zone».

Допустимые входные данные для этих типов состоят из времени суток, за которым следует необязательный часовой пояс. (См. таблицу 11 и таблицу 12). Если часовой пояс указан во входных данных для time without time zone, он игнорируется. Вы также можете указать дату, но она будет игнорироваться, за исключением случаев, когда вы используете имя часового пояса, которое включает правило перехода на летнее время, например America/New_York. В этом случае указание даты требуется для определения того, применяется ли стандартное или летнее время. Соответствующее смещение часового пояса записывается в поле со значением time with time zone.

Таблица 11. Ввод времени

Пример Описание
04:05:06.789 ISO 8601
04:05:06 ISO 8601
04:05 ISO 8601
040506 ISO 8601
04:05 AM такой же, как 04:05; AM не влияет на значение
04:05 PM такой же, как 16:05; входной час должен быть
04:05:06.789-8 ISO 8601
04:05:06-08:00 ISO 8601
04:05-08:00 ISO 8601
040506-08 ISO 8601
04:05:06 PST часовой пояс указан аббревиатурой
2003-04-12 04:05:06 America/New_York часовой пояс указан полным именем

Таблица 12. Ввод часового пояса

Пример Описание
PST Сокращение (для тихоокеанского стандартного времени)
America/New_York Полное название часового пояса
PST8PDT Спецификация часового пояса в стиле POSIX
-8:00 Смещение ISO-8601 для PST
-800 Смещение ISO-8601 для PST
-8 Смещение ISO-8601 для PST
zulu Военная аббревиатура для UTC
z Короткая форма zulu

Обратитесь к разделу Часовые пояса за дополнительной информацией о том, как указать часовые пояса.

Отметки времени

Допустимые входные данные для типов отметок времени состоят из объединения даты и времени, за которым следует необязательный часовой пояс, за которым следует необязательный AD или BC. (В качестве альтернативы AD/BC могут появляться перед часовым поясом, но это не предпочтительный порядок). Таким образом:

1999-01-08 04:05:06 
1999-01-08 04:05:06 -8:00 

действительные значения, соответствующие стандарту ISO 8601. Кроме того, поддерживается общий формат:

January 8 04:05:06 1999 PST 

Стандарт SQL различает метку времени без часового пояса и метку времени с литералами часового пояса по наличию символа «+» или «-» и смещению часового пояса после времени. Следовательно, согласно стандарту,

TIMESTAMP '2004-10-19 10:23:54' 

это метка времени без часового пояса, в то время как

TIMESTAMP '2004-10-19 10:23:54+02' 

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

TIMESTAMP WITH TIME ZONE '2004-10-19 10:23:54+02' 

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

Для метки времени со значением часового пояса внутреннее сохраненное значение всегда указывается в формате UTC (универсальное координированное время, традиционно известное как среднее время по Гринвичу, GMT). Входное значение с указанным явным часовым поясом преобразуется в UTC с использованием соответствующего смещения для этого часового пояса. Если во входной строке не указан часовой пояс, предполагается, что он находится в часовом поясе, указанном системным параметром TimeZone, и преобразуется в UTC с использованием смещения для зоны часового пояса.

Когда выводится метка времени со значением часового пояса, она всегда преобразуется из UTC в текущую зону часового пояса и отображается как местное время в этой зоне. Чтобы увидеть время в другом часовом поясе, измените часовой пояс или используйте конструкцию AT TIME ZONE (см. раздел AT TIME ZONE).

Преобразования между меткой времени без часового пояса и меткой времени с часовым поясом обычно предполагают, что значение метки времени без часового пояса должно быть принято или задано как местное время часового пояса. Для преобразования можно указать другой часовой пояс, используя AT TIME ZONE.

Специальные значения

QHB для удобства поддерживает несколько специальных значений ввода даты/времени, как показано в Таблице 13. Значения infinity и -infinity специально представлены внутри системы и будут отображаться без изменений; другие же — просто сокращенные обозначения, которые при чтении будут преобразованы в обычные значения даты/времени. (В частности, now и связанные строки преобразуются в определенное значение времени, как только они будут прочитаны). Все эти значения должны быть заключены в одинарные кавычки при использовании в качестве констант в командах SQL.

Таблица 13. Специальные даты / времени

Строка ввода Действительные типы Описание
epoch date, timestamp 1970-01-01 00: 00: 00 + 00 (системное время Unix ноль)
infinity date, timestamp позже всех других отметок времени
-infinity date, timestamp раньше всех других отметок времени
now date, time, timestamp время начала текущей транзакции
today date, timestamp полночь (00:00) сегодня
tomorrow date, timestamp полночь (00:00) завтра
yesterday date, timestamp полночь (00:00) вчера
allballs time 00: 00: 00.00 UTC

Следующие SQL- совместимые функции также можно использовать для получения текущего значения времени для соответствующего типа данных: CURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMP, LOCALTIME, LOCALTIMESTAMP. Последние четыре принимают необязательную спецификацию с точностью до секунды. (См. раздел Текущая дата/время). Обратите внимание, что они являются функциями SQL и не распознаются в строках ввода данных.

Формат вывода типов дата/время

Формат вывода типов даты / времени может быть установлен в один из четырех стилей ISO 8601, SQL (Ingres), традиционный POSTGRES (формат даты Unix) или немецкий. По умолчанию используется формат ISO. (Стандарт SQL требует использования формата ISO 8601. Название формата вывода «SQL» является исторической случайностью). В Таблице 14 приведены примеры каждого стиля вывода. Выходные данные типов date и time как правило, представляет собой только часть даты или времени в соответствии с приведенными примерами. Однако стиль POSTGRES выводит значения только для даты в формате ISO.

Таблица 14. Стили вывода даты / времени

Спецификация стиля Описание пример
ISO ISO 8601, стандарт SQL 1997-12-17 07:37:16-08
SQL традиционный стиль 12/17/1997 07:37:16.00 PST
Postgres оригинальный стиль Wed Dec 17 07:37:16 1997 PST
German региональный стиль 17.12.1997 07:37:16.00 PST

Заметка
ISO 8601 определяет использование заглавной буквы T для разделения даты и времени. QHB принимает этот формат на входе, но на выходе он использует пробел, а не T, как показано выше. Это для удобочитаемости и для соответствия RFC 3339, а также некоторым другим системам баз данных.

В стилях SQL и POSTGRES день отображается перед месяцем, если указан порядок полей DMY, в противном случае месяц отображается перед днем. (См. раздел Формат вывода типов дата/время о том, как этот параметр также влияет на интерпретацию входных значений). В Таблице 15 приведены примеры.

Таблица 15. Соглашение о дате заказа

Установка datestyle Порядок ввода Пример вывода
SQL, DMY day/month/year 17/12/1997 15:37:16.00 CET
SQL, MDY month/day/year 12/17/1997 07:37:16.00 PST
Postgres, DMY day/month/year Wed 17 Dec 07:37:16 1997 PST

Стиль даты/времени может быть выбран пользователем с помощью команды SET datestyle, параметра DateStyle в файле конфигурации qhb.conf или используя переменную среды PGDATESTYLE на сервере или клиенте.

Функция форматирования to_char (см. раздел Функции форматирования типов данных) также доступна как более гибкий способ форматирования данных даты/времени.

Часовые пояса

Часовые пояса и условности часовых поясов зависят от политических решений, а не только от геометрии Земли. В 1900-х годах часовые пояса во всем мире стали несколько стандартизированы, но по-прежнему подвержены произвольным изменениям, особенно в отношении правил перехода на летнее время. QHB использует широко используемую базу данных часовых поясов IANA (Olson) для получения информации о исторических правилах часовых поясов. Что касается времени в будущем, предполагается, что последние известные правила для данного часового пояса будут продолжать соблюдаться в течение неопределенного времени в будущем.

QHB стремится быть совместимым со стандартными определениями SQL для типичного использования. Однако стандарт SQL имеет странное сочетание типов и возможностей даты и времени. Есть две очевидные проблемы:

  • Хотя тип date не может иметь связанный часовой пояс, тип time может. Часовые пояса в реальном мире не имеют большого значения, если они не связаны ни с датой, ни с временем, поскольку смещение может изменяться в течение года с переходом на летнее время.
  • Часовой пояс по умолчанию задается в виде постоянного числового смещения от UTC. Таким образом, невозможно адаптироваться к летнему времени при выполнении арифметических операций по датам и временам в пределах границ летнего времени.

Чтобы устранить эти трудности, мы рекомендуем использовать типы даты/времени, которые содержат дату и время при использовании часовых поясов. Мы не рекомендуем использовать тип time with time zone (хотя он поддерживается QHB для соответствия стандарту SQL). QHB предполагает ваш местный часовой пояс для любого типа, содержащего только дату или время.

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

QHB позволяет указывать часовые пояса в трех разных формах:

  • Полное название часового пояса, например, America/New_York. Распознанные имена часовых поясов перечислены в представлении pg_timezone_names (см. pg_timezone_names). Для этой цели QHB использует широко используемые данные часового пояса IANA, поэтому те же имена часовых поясов также распознаются другим программным обеспечением.
  • Сокращение часового пояса, например PST. Такая спецификация просто определяет конкретное смещение от UTC, в отличие от полных имен часовых поясов, которые также могут подразумевать набор правил перехода на летнее время. Распознанные сокращения перечислены в представлении pg_timezone_abbrevs (см. раздел pg_timezone_abbrevs). Вы не можете задать параметры конфигурации TimeZone или log_timezone для сокращения часового пояса, но вы можете использовать сокращения во входных значениях даты/времени и с оператором AT TIME ZONE.
  • В дополнение к названиям и аббревиатурам часовых поясов QHB будет принимать спецификации часовых поясов в стиле POSIX в виде STDoffset или STDoffsetDST, где STD — сокращение зоны, offset — числовое смещение в часах к западу от UTC, а DST — это необязательное сокращение зоны перехода на летнее время, предполагаемое на один час впереди заданного смещения. Например, если EST5EDT еще не является распознанным названием зоны, оно будет принято и будет функционально эквивалентно времени Восточного побережья США. В этом синтаксисе сокращение зоны может быть строкой букв или произвольной строкой, заключенной в угловые скобки (<>). Когда присутствует сокращение зоны перехода на летнее время, предполагается, что оно будет использоваться в соответствии с теми же правилами перехода на летнее время, которые используются в записи posixrules базы данных часовых поясов IANA. В стандартной установке QHB posixrules совпадает с US/Eastern, поэтому спецификации часовых поясов в стиле POSIX соответствуют правилам перехода на летнее время в США. При необходимости вы можете изменить это поведение, заменив файл posixrules.

Короче говоря, в этом разница между сокращениями и полными именами: сокращения представляют конкретное смещение от UTC, тогда как многие полные имена подразумевают локальное правило перехода на летнее время и поэтому имеют два возможных смещения UTC. Например, 2014-06-04 12:00 America/New_York представляет полуденное местное время в Нью-Йорке, которое для этой конкретной даты было восточным летним временем (UTC-4). Итак, 2014-06-04 12:00 EDT указывает тот же момент времени. Но 2014-06-04 12:00 EST указывает полдень по восточному поясному времени (UTC-5), независимо от того, было ли летнее время номинально действующим на эту дату.

Чтобы усложнить ситуацию, некоторые юрисдикции использовали одно и то же сокращение часового пояса для обозначения разных смещений UTC в разное время; например, в Москве MSK означало UTC + 3 в некоторые годы и UTC + 4 в другие. QHB интерпретирует такие сокращения в соответствии с тем, что они имели в виду (или имели в виду совсем недавно) в указанную дату; но, как и в приведенном выше примере EST, это не обязательно совпадает с местным гражданским временем этой даты.

Следует опасаться, что функция часового пояса в стиле POSIX может привести к молчаливому принятию фиктивного ввода, поскольку нет никакой проверки на правильность сокращений зон. Например, SET TIMEZONE TO FOOBAR0 будет работать, оставляя систему эффективно использующей довольно своеобразное сокращение для UTC. Другая проблема, о которой следует помнить, заключается в том, что в именах часовых поясов POSIX положительные смещения используются для местоположений к западу от Гринвича. В любом другом месте QHB следует соглашению ISO-8601, согласно которому положительные смещения часовых поясов находятся к востоку от Гринвича.

Во всех случаях названия и сокращения часовых поясов распознаются без учета регистра.

Ни имена часовых поясов, ни сокращения не встроены в сервер; они получены из файлов конфигурации, хранящихся в папках . /share/timezone/ и . /share/timezonesets/ установочного каталога .

Параметр конфигурации TimeZone можно установить в файле qhb.conf или любым другим стандартным способом, описанным в главе Конфигурация сервера. Есть также несколько специальных способов установить его:

  • SQL-команда SET TIME ZONE устанавливает часовой пояс для сеанса. Это альтернативное написание команды SET TIMEZONE TO с синтаксисом более совместимым со спецификацией SQL.
  • Переменная окружения PGTZ используется клиентами libpq для отправки команды SET TIME ZONE на сервер при подключении.

Формат ввода интервалов

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

[@] quantity unit [quantity unit. ] [direction] 

где quantity (количество) — это число (возможно, подписанное); unit (единица измерения) — microsecond, millisecond, second, minute, hour, day, week, month, year, decade, century, millennium или сокращения или множественные числа этих единиц; direction (направление) может быть ago (назад) или пустым. Знак (@) является необязательным. Суммы различных единиц неявно суммируются с помощью соответствующего знака учета. ago меняет знак всех полей. Этот синтаксис также используется для вывода интервала, если для IntervalStyle установлено значение postgres_verbose.

Количество дней, часов, минут и секунд может быть указано без явной маркировки единиц измерения. Например, ’1 12:59:10’ читается так же, как ’1 day 12 hours 59 min 10 sec’. Кроме того, комбинация лет и месяцев может быть указана с тире; например, ’200-10’ читается так же, как «200 years 10 months». (Эти более короткие формы фактически являются единственными, разрешенными стандартом SQL, и используются для вывода, когда для IntervalStyle установлено значение sql_standard).

Значения интервалов также можно записать в виде временных интервалов ISO 8601, используя либо «формат с обозначениями» раздела 4.4.3.2 стандарта, либо «альтернативный формат» раздела 4.4.3.3. Формат с обозначениями выглядит так:

P quantity unit [ quantity unit . ] [ T [ quantity unit . ]] 

Строка должна начинаться с буквы P и может содержать букву T которая вводит единицы времени дня. Доступные сокращения единиц приведены в Таблице 16. Единицы могут быть опущены и могут быть указаны в любом порядке, но после T должны появляться единицы меньше, чем день. В частности, значение M зависит от того, находится ли он до или после T

Таблица 16. Сокращения единиц интервала ISO 8601

Сокращенное название Смысл
Y Лет
M Месяцы (в части даты)
W Недели
D Дни
H Часы
M Минуты (во временной части)
S Секунд

В альтернативном формате:

P [ years-months-days ] [ T hours:minutes:seconds ] 

строка должна начинаться с P, а T разделяет части интервала даты и времени. Значения приведены в виде чисел, аналогичных датам ISO 8601.

При записи интервальной константы со спецификацией полей или при назначении строки интервальному столбцу, определенному спецификацией полей, интерпретация немаркированных величин зависит от полей. Например, INTERVAL ‘1’ YEAR читается как 1 год, тогда как INTERVAL ‘1’ означает 1 секунду. Кроме того, значения полей «справа» от наименее значимого поля, разрешенного спецификацией полей, молча отбрасываются. Например, запись INTERVAL ‘1 day 2:03:04’ HOUR TO MINUTE приводит к удалению поля секунд, но не поля дня.

Согласно стандарту SQL все поля интервального значения должны иметь одинаковый знак, поэтому ведущий отрицательный знак применяется ко всем полям; например, знак минус в интервальном литерале ’-1 2:03:04’ применяется как к дням, так и к часам/минутам/секундам. QHB позволяет полям иметь разные знаки и традиционно обрабатывает каждое поле в текстовом представлении как независимо подписанное, так что часть часа/минуты/секунды считается положительной в этом примере. Если для IntervalStyle установлено значение sql_standard то ведущий знак считается применимым ко всем полям (но только если дополнительные знаки не появляются). В противном случае используется традиционная интерпретация QHB. Чтобы избежать неоднозначности, рекомендуется прикреплять явный знак к каждому полю, если какое-либо поле является отрицательным.

В подробном формате ввода и в некоторых полях более компактных форматов ввода значения полей могут иметь дробные части; например, ’1.5 week’ или ’01:02:03.45’. Такой ввод преобразуется в соответствующее количество месяцев, дней и секунд для хранения. Когда это приводит к дробному числу месяцев или дней, дробь добавляется в поля нижнего порядка с использованием коэффициентов преобразования 1 месяц = 30 дней и 1 день = 24 часа. Например, ’1.5 month’ становится 1 месяц и 15 дней. Только секунды будут отображаться как дробные на выходе.

В Таблице 17 приведены некоторые примеры правильных interval ввода.

Таблица 17. Интервальный ввод

Пример Описание
1-2 Стандартный формат SQL: 1 год 2 месяца
3 4:05:06 Стандартный формат SQL: 3 дня 4 часа 5 минут 6 секунд
1 year 2 months 3 days 4 hours 5 minutes 6 seconds Традиционный формат Postgres: 1 год 2 месяца 3 дня 4 часа 5 минут 6 секунд
P1Y2M3DT4H5M6S ISO 8601 «формат с обозначениями»: то же значение, что и выше
P0001-02-03T04: 05: 06 ISO 8601 «альтернативный формат»: то же значение, что и выше

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

SELECT EXTRACT(hours from '80 minutes'::interval); date_part ----------- 1 SELECT EXTRACT(days from '80 hours'::interval); date_part ----------- 0 

Функции justify_days и justify_hours доступны для настройки дней и часов, которые выходят за пределы их нормальных диапазонов.

Формат вывода интервалов

Формат вывода типа интервала может быть установлен в один из четырех стилей sql_standard, postgres, postgres_verbose или iso_8601 с помощью команды SET intervalstyle. По умолчанию используется формат postgres. В Таблице 18 приведены примеры каждого стиля вывода.

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

Вывод стиля postgres совпадает с выводом выпусков PostgreSQL до 8.4, когда для параметра DateStyle было установлено значение ISO.

Вывод стиля postgres_verbose совпадает с выводом выпусков PostgreSQL до 8.4, когда для параметра DateStyle был установлен в значение, отличное от ISO.

Вывод стиля iso_8601 соответствует «формату с указателями», описанному в разделе 4.4.3.2 стандарта ISO 8601.

Таблица 18. Примеры стиля вывода интервала

Спецификация стиля Год-Месяц Интервал Дневной интервал Смешанный интервал
sql_standard 1-2 3 4:05:06 -1-2 +3 -4: 05: 06
postgres 1 year 2 mons 3 days 04:05:06 -1 year -2 mons +3 days -04:05:06
postgres_verbose @ 1 year 2 mons @ 3 days 4 hours 5 mins 6 secs @ 1 year 2 mons -3 days 4 hours 5 mins 6 secs ago
iso_8601 P1Y2M P3DT4H5M6S Р-1Y-2M3DT-4H-5М-6S

Логический тип

QHB предоставляет стандартный логический (boolean) тип SQL; см. таблицу 19. Логический тип может иметь несколько состояний: «истина» (TRUE), «ложь» (FALSE) и третье состояние «неизвестно», которое представлено NULL значением SQL.

Таблица 19. Логический тип данных

Имя Размер хранилища Описание
boolean 1 байт состояние истинно или ложно

Булевы константы могут быть представлены в запросах SQL ключевыми словами SQL TRUE, FALSE и NULL.

Функция ввода типа данных для логического типа принимает эти строковые представления для «истинного» состояния:

true yes on 1 

и эти представления для «ложного» состояния:

false no off 0 

Также допускаются уникальные префиксы этих строк, например, t или n. Начальные или конечные пробелы игнорируются, и регистр не имеет значения.

Функция вывода типа данных для логического типа всегда выдает либо t либо f, как показано в примере 7.2.

Пример 7.2. Использование логического типа

CREATE TABLE test1 (a boolean, b text); INSERT INTO test1 VALUES (TRUE, 'sic est'); INSERT INTO test1 VALUES (FALSE, 'non est'); SELECT * FROM test1; a | b ---+--------- t | sic est f | non est SELECT * FROM test1 WHERE a; a | b ---+--------- t | sic est 

Ключевые слова TRUE и FALSE являются предпочтительным (SQL- совместимым) методом для записи логических констант в запросах SQL. Но вы также можете использовать строковые представления, следуя общему синтаксису строковой литеральной константы, описанному в раздел Константы других типов, например, ’yes’::boolean.

Обратите внимание, что синтаксический анализатор автоматически понимает, что TRUE и FALSE имеют логический тип, но это не так для NULL, потому что он может иметь любой тип. Поэтому в некоторых контекстах вам, возможно, придется явным образом привести NULL к логическому значению, например NULL::boolean. И наоборот, приведение может быть опущено из строково-литерального логического значения в контекстах, где анализатор может сделать вывод, что литерал должен иметь логический тип.

Перечисляемые типы

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

Объявление перечислимых типов

Типы перечислений создаются с помощью команды CREATE TYPE, например:

CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy'); 

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

CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy'); CREATE TABLE person ( name text, current_mood mood ); INSERT INTO person VALUES ('Moe', 'happy'); SELECT * FROM person WHERE current_mood = 'happy'; name | current_mood ------+-------------- Moe | happy (1 row) 

Порядок

Порядок значений в перечисляемом типе— это порядок, в котором значения были перечислены при создании типа. Все стандартные операторы сравнения и связанные агрегатные функции поддерживаются для перечислений. Например:

INSERT INTO person VALUES ('Larry', 'sad'); INSERT INTO person VALUES ('Curly', 'ok'); SELECT * FROM person WHERE current_mood > 'sad'; name | current_mood -------+-------------- Moe | happy Curly | ok (2 rows) SELECT * FROM person WHERE current_mood > 'sad' ORDER BY current_mood; name | current_mood -------+-------------- Curly | ok Moe | happy (2 rows) SELECT name FROM person WHERE current_mood = (SELECT MIN(current_mood) FROM person); name ------- Larry (1 row) 

Надежность типа

Каждый перечисляемый тип данных является отдельным и не может сравниваться с другими перечисляемыми типами. Смотрите этот пример:

CREATE TYPE happiness AS ENUM ('happy', 'very happy', 'ecstatic'); CREATE TABLE holidays ( num_weeks integer, happiness happiness ); INSERT INTO holidays(num_weeks,happiness) VALUES (4, 'happy'); INSERT INTO holidays(num_weeks,happiness) VALUES (6, 'very happy'); INSERT INTO holidays(num_weeks,happiness) VALUES (8, 'ecstatic'); INSERT INTO holidays(num_weeks,happiness) VALUES (2, 'sad'); ERROR: invalid input value for enum happiness: "sad" SELECT person.name, holidays.num_weeks FROM person, holidays WHERE person.current_mood = holidays.happiness; ERROR: operator does not exist: mood = happiness 

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

SELECT person.name, holidays.num_weeks FROM person, holidays WHERE person.current_mood::text = holidays.happiness::text; name | num_weeks ------+----------- Moe | 4 (1 row) 

Детали реализации

Метки перечисления чувствительны к регистру, поэтому ’happy’ — это не то же самое, что ’HAPPY’. Пробелы в метках тоже значимы.

Хотя перечисляемые типы в основном предназначены для статических наборов значений, существует поддержка добавления новых значений в существующий тип перечисления и переименования значений (см. ALTER TYPE). Существующие значения нельзя удалить из типа перечисления, равно как нельзя изменить порядок сортировки таких значений, исключая удаление и повторное создание типа перечисления.

Значение перечисляемого типа занимает четыре байта на диске. Длина текстовой метки значения перечисления ограничена настройкой NAMEDATALEN скомпилированной в QHB; в стандартных сборках это означает максимум 63 байта.

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

Геометрические типы

Геометрические типы данных представляют собой двумерные пространственные объекты. Таблица 20 показывает геометрические типы, доступные в QHB.

Таблица 20. Геометрические типы

Имя Размер хранилища Описание Представление
point 16 байт Точка на плоскости (x,y)
line 32 байта Бесконечная линия ,
lseg 32 байта Конечный отрезок ((x1,y1),(x2,y2))
box 32 байта Прямоугольная коробка ((x1,y1),(x2,y2))
path 16 + 16n байт Закрытый путь (похож на полигон) ((x1,y1). )
path 16 + 16n байт Открытый путь [(x1,y1). ]
polygon 40 + 16n байт Полигон (похож на замкнутый путь) ((x1,y1). )
circle 24 байта Круг <(x, y), r>(центральная точка и радиус)

Богатый набор функций и операторов доступен для выполнения различных геометрических операций, таких как масштабирование, перемещение, вращение и определение пересечений. Они объяснены в раздел Геометрические функции и операторы.

Точки

Точки являются фундаментальным двумерным строительным блоком для геометрических типов. Значения типа точка указываются с использованием одного из следующих синтаксисов:

( x, y ) x, y 

где x и y — соответствующие координаты в виде чисел с плавающей точкой.

Точки выводятся с использованием первого синтаксиса.

Линии

Линии представлены линейным уравнением Ax + By + C = 0, где A и B не равны нулю одновременно. Значения типа line вводятся и выводятся в следующем виде:

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

[ ( x1, y1 ), ( x2, y2 ) ] ( ( x1, y1 ), ( x2, y2 ) ) ( x1, y1 ), ( x2, y2 ) x1, y1 , x2, y2 

где (x1, y1) и (x2, y2) две разные точки на линии.

Отрезки линии

Отрезки линии представлены парами точек, которые являются конечными точками отрезка. Значения типа lseg указываются с использованием любого из следующих синтаксисов:

[ ( x1, y1 ), ( x2, y2 ) ] ( ( x1, y1 ), ( x2, y2 ) ) ( x1, y1 ), ( x2, y2 ) x1, y1 , x2, y2 

где (x1, y1) и (x2, y2) — конечные точки отрезка линии.

Отрезки линии выводятся с использованием первого синтаксиса.

Рамки (boxes)

Рамки представлены парами точек, которые находятся в противоположных углах рамки. Значения типа box указываются с использованием любого из следующих синтаксисов:

( ( x1, y1 ), ( x2, y2 ) ) ( x1, y1 ), ( x2, y2 ) x1, y1 , x2, y2 

где (x1, y1) и (x2, y2) — любые два противоположных угла рамки.

Рамки выводятся с использованием второго синтаксиса.

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

Пути

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

Значения типа path указываются с использованием любого из следующих синтаксисов:

[ ( x1, y1 ), . ( xn, yn ) ] ( ( x1, y1 ), . ( xn, yn ) ) ( x1, y1 ), . ( xn, yn ) ( x1, y1 , . xn, yn ) x1, y1 , . xn, yn 

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

Пути выводятся с использованием первого или второго синтаксиса, в зависимости от ситуации.

Полигоны

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

Значения типа polygon указываются с использованием любого из следующих синтаксисов:

( ( x1, y1 ), . ( xn, yn ) ) ( x1, y1 ), . ( xn, yn ) ( x1, y1 , . xn, yn ) x1, y1 , . xn, yn 

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

Полигоны выводятся с использованием первого синтаксиса.

Круги

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

 < ( x, y ), r >( ( x, y ), r ) ( x, y ), r x, y , r 

где ( x , y ) — центральная точка, а r — радиус окружности.

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

Типы сетевых адресов

QHB предлагает типы данных для хранения адресов IPv4, IPv6 и MAC, как показано в Таблице 21. Лучше использовать эти типы вместо обычных текстовых типов для хранения сетевых адресов, потому что эти типы предлагают проверку ошибок ввода, а так же специализированные операторы и функции (см. раздел Функции и операторы сетевых адресов).

Таблица 21. Типы сетевых адресов

Имя Размер хранилища Описание
cidr 7 или 19 байт Сети IPv4 и IPv6
inet 7 или 19 байт IPv4 и IPv6 хосты и сети
macaddr 6 байт MAC-адреса
macaddr8 8 байт MAC-адреса (формат EUI-64)

При сортировке типов данных inet или cidr адреса IPv4 всегда будут сортироваться до адресов IPv6, включая адреса IPv4, инкапсулированные или сопоставленные с адресами IPv6, например ::10.2.3.4 или ::ffff:10.4.3.2.

inet

Тип inet содержит адрес хоста IPv4 или IPv6 и, возможно, его подсеть, все в одном поле. Подсеть представлена количеством битов сетевого адреса, присутствующих в адресе хоста («маска сети»). Если маска сети равна 32, а адрес — IPv4, то это значение не указывает на подсеть, только на один хост. В IPv6 длина адреса составляет 128 бит, поэтому 128 бит указывают уникальный адрес хоста. Обратите внимание, что, если вы хотите принимать только сети, вы должны использовать тип cidr а не inet.

Формат ввода для этого типа: address/y где address — это адрес IPv4 или IPv6, а y — количество бит в маске сети. Если часть /y отсутствует, маска сети равна 32 для IPv4 и 128 для IPv6, поэтому значение представляет только один хост. На дисплее часть /y подавляется, если маска сети указывает один хост.

cidr

Тип cidr содержит спецификацию сети IPv4 или IPv6. Форматы ввода и вывода соответствуют правилам маршрутизации бесклассового интернет-домена. Форматом для указания сетей является address/y где address — это сеть, представленная в виде адреса IPv4 или IPv6, а y — количество битов в маске сети. Если y опущен, он рассчитывается с использованием допущений из старой классовой системы нумерации сети, за исключением того, что он будет по крайней мере достаточно большим, чтобы включать все октеты, записанные во входных данных. Указывать сетевой адрес, биты которого установлены справа от указанной маски, является ошибкой.

Таблица 22 показывает несколько примеров.

Таблица 22. Примеры ввода типа cidr

ввод cidr вывод cidr abbrev(cidr)
192.168.100.128/25 192.168.100.128/25 192.168.100.128/25
192.168/24 192.168.0.0/24 192.168.0/24
192.168/25 192.168.0.0/25 192.168.0.0/25
192.168.1 192.168.1.0/24 192.168.1/24
192.168 192.168.0.0/24 192.168.0/24
128.1 128.1.0.0/16 128.1/16
128 128.0.0.0/16 128.0/16
128.1.2 128.1.2.0/24 128.1.2/24
10.1.2 10.1.2.0/24 10.1.2/24
10.1 10.1.0.0/16 10.1/16
10 10.0.0.0/8 10/8
10.1.2.3/32 10.1.2.3/32 10.1.2.3/32
2001:4f8:3:ba::/64 2001:4f8:3:ba::/64 2001:4f8:3:ba::/64
2001:4f8:3:ba:2e0:81ff:fe22:d1f1/128 2001:4f8:3:ba:2e0:81ff:fe22:d1f1/128 2001:4f8:3:ba:2e0:81ff:fe22:d1f1
::ffff:1.2.3.0/120 ::ffff:1.2.3.0/120 ::ffff:1.2.3/120
::ffff:1.2.3.0/128 ::ffff:1.2.3.0/128 ::ffff:1.2.3.0/128

inet и cidr

Существенное различие между типами данных inet и cidr заключается в том, что inet принимает значения с ненулевыми битами справа от маски сети, а cidr — нет. Например, 192.168.0.1/24 действителен для inet но не для cidr.

Если вас не устраивает формат вывода inet или cidr, попробуйте функции host, text и abbrev.

macaddr

Тип macaddr хранит MAC-адреса, известные, например, по аппаратным адресам карты Ethernet (хотя MAC-адреса используются и для других целей). Ввод принимается в следующих форматах:

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

IEEE Std 802-2001 определяет вторую показанную форму (с дефисами) в качестве канонической формы для MAC-адресов и определяет первую форму (с двоеточиями) в качестве обратной битовой нотации, так что 08-00-2b-01-02-03 = 01:00:4D:08:04:0C . В настоящее время это соглашение широко игнорируется и относится только к устаревшим сетевым протоколам (таким как Token Ring). QHB не содержит положений об обращении битов, и все принятые форматы используют канонический порядок LSB.

Остальные пять форматов ввода не являются частью какого-либо стандарта.

macaddr8

Тип macaddr8 хранит MAC-адреса в формате EUI-64, известном, например, по аппаратным адресам платы Ethernet (хотя MAC-адреса также используются и для других целей). Этот тип может принимать MAC-адреса длиной 6 и 8 байтов и сохранять их в формате длины 8 байтов. MAC-адреса, предоставленные в 6-байтовом формате, будут сохраняться в 8-байтовом формате с 4-м и 5-м байтами, установленными в FF и FE соответственно. Обратите внимание, что IPv6 использует модифицированный формат EUI-64, где 7-й бит должен быть установлен в единицу после преобразования из EUI-48. Для этого изменения предусмотрена функция macaddr8_set7bit. Вообще говоря, доступен любой ввод, который состоит из пар шестнадцатеричных цифр (на границах байтов), необязательно последовательно разделяемых одним из ‘:’ , ‘-‘ или ‘.’ . Количество шестнадцатеричных цифр должно быть 16 (8 байт) или 12 (6 байт). Начальные и конечные пробелы игнорируются. Ниже приведены примеры допустимых форматов ввода:

Все эти примеры будут указывать один и тот же адрес. Верхний и нижний регистр допускается для цифр от a до f. Вывод всегда в первой из показанных форм. Последние шесть форматов ввода, которые упомянуты выше, не являются частью какого-либо стандарта. Чтобы преобразовать традиционный 48-битный MAC-адрес в формате EUI-48 в модифицированный формат EUI-64, который будет включен в качестве части хоста адреса IPv6, используйте macaddr8_set7bit как показано ниже:

SELECT macaddr8_set7bit('08:00:2b:01:02:03'); macaddr8_set7bit ------------------------- 0a:00:2b:ff:fe:01:02:03 (1 row) 

Типы битовых строк

Битовые строки — это строки 1 и 0. Их можно использовать для хранения или визуализации битовых масок. Существует два типа битовых строк SQL: bit(n) и bit varying(n), где n — положительное целое число.

Данные типа bit должны точно соответствовать длине n; попытка сохранить более короткие или более длинные битовые строки является ошибкой. bit varying данные имеют переменную длину вплоть до максимальной длины n; более длинные строки будут отклонены. Запись bit без длины эквивалентна bit(1), тогда как bit varying без указания длины означает неограниченную длину.

Заметка
Если явно привести значение битовой строки в bit(n), оно будет усечено или дополнено нулями справа, чтобы быть равно точно n битами, без возникновения ошибки. Аналогично, если явно привести значение битовой строки в bit varying(n), оно будет усечено справа, если оно больше, чем n бит.

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

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

CREATE TABLE test (a BIT(3), b BIT VARYING(5)); INSERT INTO test VALUES (B'101', B'00'); INSERT INTO test VALUES (B'10', B'101'); ERROR: bit string length 2 does not match type bit(3) INSERT INTO test VALUES (B'10'::bit(3), B'101'); SELECT * FROM test; a | b -----+----- 101 | 00 100 | 101 

Значение битовой строки требует 1 байт для каждой группы из 8 битов, плюс 5 или 8 байтов служебной информации в зависимости от длины строки (но длинные значения могут быть сжаты или перемещены вне строки, как объяснено в разделе Символьные типы для символьных строк).

Типы текстового поиска

QHB предоставляет два типа данных, предназначенных для поддержки полнотекстового поиска, который представляет собой поиск по коллекции документов на естественном языке для поиска документов, которые лучше всего соответствуют запросу. Тип tsvector представляет документ в форме, оптимизированной для текстового поиска; тип tsquery аналогично представляет текстовый запрос. В разделе Функции текстового поиска и операторы обобщаются связанные функции и операторы.

tsvector

Значение tsvector — это упорядоченный список различных лексем, которые представляют собой слова, которые были нормализованы для объединения различных вариантов одного и того же слова . Сортировка и удаление дубликатов выполняются автоматически во время ввода, как показано в этом примере:

SELECT 'a fat cat sat on a mat and ate a fat rat'::tsvector; tsvector ---------------------------------------------------- 'a' 'and' 'ate' 'cat' 'fat' 'mat' 'on' 'rat' 'sat' 

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

SELECT $$the lexeme ' ' contains spaces$$::tsvector; tsvector ------------------------------------------- ' ' 'contains' 'lexeme' 'spaces' 'the' 

(Мы используем строковые литералы, заключенные в кавычки в этом и следующем примере, чтобы избежать путаницы с двойными кавычками внутри литералов). Встроенные кавычки и обратные слэши должны быть удвоены:

SELECT $$the lexeme 'Joe''s' contains a quote$$::tsvector; tsvector ------------------------------------------------ 'Joe''s' 'a' 'contains' 'lexeme' 'quote' 'the' 

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

SELECT 'a:1 fat:2 cat:3 sat:4 on:5 a:6 mat:7 and:8 ate:9 a:10 fat:11 rat:12'::tsvector; tsvector ------------------------------------------------------------------------------- 'a':1,6,10 'and':8 'ate':9 'cat':3 'fat':2,11 'mat':7 'on':5 'rat':12 'sat':4 

Позиция обычно указывает местоположение исходного слова в документе. Позиционная информация может использоваться для ранжирования близости. Значения положения могут варьироваться от 1 до 16383; большие числа автоматически устанавливаются на 16383. Дублирующие позиции для одной и той же лексемы отбрасываются.

Лексемы, имеющие позиции, могут быть дополнительно помечены весом, который может быть A , B , C или D . D является значением по умолчанию и, следовательно, не отображается на выходе:

SELECT 'a:1A fat:2B,4C cat:5D'::tsvector; tsvector ---------------------------- 'a':1A 'cat':5 'fat':2B,4C 

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

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

SELECT 'The Fat Rats'::tsvector; tsvector -------------------- 'Fat' 'Rats' 'The' 

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

SELECT to_tsvector('english', 'The Fat Rats'); to_tsvector ----------------- 'fat':2 'rat':3 

tsquery

Значение tsquery хранит лексемы, которые нужно искать, и может комбинировать их, используя логические операторы & (AND), | (OR) и ! (NOT), а также оператор поиска фразы ; (FOLLOWED BY). Существует также вариант ; оператора FOLLOWED BY, где N — целочисленная константа, которая определяет расстояние между двумя искомыми лексемами. эквивалентно ;.

Скобки могут быть использованы для принудительной группировки этих операторов. При отсутствии скобок, ! (NOT) связывается наиболее плотно, затем ; (FOLLOWED BY) следующий наиболее плотно, затем & (AND), с | (OR) связываются наименее плотно.

Вот некоторые примеры:

SELECT 'fat & rat'::tsquery; tsquery --------------- 'fat' & 'rat' SELECT 'fat & (rat | cat)'::tsquery; tsquery --------------------------- 'fat' & ( 'rat' | 'cat' ) SELECT 'fat & rat & ! cat'::tsquery; tsquery ------------------------ 'fat' & 'rat' & !'cat' 

При желании лексемы в tsquery могут быть помечены одной или несколькими весовыми буквами, что ограничивает их совпадением только с лексемами tsvector с одним из этих весов:

SELECT 'fat:ab & cat'::tsquery; tsquery ------------------ 'fat':AB & 'cat' 

Кроме того, лексемы в tsquery могут быть помечены * для указания соответствия префикса:

SELECT 'super:*'::tsquery; tsquery ----------- 'super':* 

Этот запрос будет соответствовать любому слову в tsvector который начинается с «super».

Правила цитирования для лексем такие же, как описано ранее для лексем в tsvector; и, как и в случае с tsvector, любая необходимая нормализация слов должна быть выполнена перед преобразованием в тип tsquery. Функция to_tsquery удобна для выполнения такой нормализации:

SELECT to_tsquery('Fat:ab & Cats'); to_tsquery ------------------ 'fat':AB & 'cat' 

Обратите внимание, что to_tsquery будет обрабатывать префиксы так же, как и другие слова, что означает, что это сравнение возвращает true:

SELECT to_tsvector( 'postgraduate' ) @@ to_tsquery( 'postgres:*' ); ?column? ---------- t 

потому что postgres будет связан с postgr:

SELECT to_tsvector( 'postgraduate' ), to_tsquery( 'postgres:*' ); to_tsvector | to_tsquery ---------------+------------ 'postgradu':1 | 'postgr':* 

который будет соответствовать основной форме postgraduate.

Тип UUID

Тип данных uuid хранит универсальные уникальные идентификаторы (UUID) в соответствии с RFC 4122, ISO / IEC 9834-8: 2005 и соответствующими стандартами. (Некоторые системы называют этот тип данных глобально уникальным идентификатором или GUID, вместо этого). Этот идентификатор является 128-битной величиной, которая генерируется алгоритмом, выбранным для того, чтобы сделать очень маловероятным, что этот же идентификатор будет сгенерирован кем-либо еще в известной вселенной с использованием того же алгоритма. Поэтому для распределенных систем эти идентификаторы обеспечивают лучшую гарантию уникальности, чем генераторы последовательностей, которые уникальны только в одной базе данных.

UUID записывается в виде последовательности шестнадцатеричных цифр в нижнем регистре, в нескольких группах, разделенных дефисами, в частности, в группе из 8 цифр, за которой следуют три группы из 4 цифр, за которыми следует группа из 12 цифр, всего 32 цифры, представляющие 128 бит Пример UUID в этой стандартной форме:

a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11 

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

A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11 a0eebc999c0b4ef8bb6d6bb9bd380a11 a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11

Вывод всегда в стандартной форме.

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

Тип XML

Тип данных xml может использоваться для хранения данных XML. Его преимущество перед хранением данных XML в текстовом поле состоит в том, что он проверяет входные значения на предмет корректности, и существуют вспомогательные функции для выполнения над ним безопасных операций; см. раздел Функции XML. Использование этого типа данных требует, чтобы установка была построена с помощью configure —with-libxml .

Тип xml может хранить правильно сформированные «документы», как определено стандартом XML, а также фрагменты «содержимого», которые определяются посредством ссылки на более разрешительный «document node» модели данных XQuery и XPath. Грубо говоря, это означает, что фрагменты контента могут иметь более одного элемента верхнего уровня или символьного узла. Выражение xmlvalue IS DOCUMENT можно использовать для оценки того, является ли конкретное значение xml полным документом или только фрагментом содержимого.

Создание значений XML

Чтобы получить значение типа xml из символьных данных, используйте функцию xmlparse:

XMLPARSE ( < DOCUMENT | CONTENT >value) 
XMLPARSE (DOCUMENT 'Manual. ') XMLPARSE (CONTENT 'abcbarfoo') 

Хотя это единственный способ преобразовать символьной строки в значения XML в соответствии со стандартом SQL, специфичные для QHB синтаксисы:

xml 'bar' 'bar'::xml 

также могут быть использованы.

Тип xml не проверяет входные значения по декларации типа документа (DTD), даже когда входное значение указывает DTD. В настоящее время также отсутствует встроенная поддержка проверки на соответствие другим языкам XML схем, например XML Schema.

Обратная операция, производящая символьное строковое значение из xml, использует функцию xmlserialize:

XMLSERIALIZE ( < DOCUMENT | CONTENT >value AS type ) 

type может быть character, character varying или text (или псевдонимом для одного из них). Опять же, согласно стандарту SQL, это единственный способ преобразования между типами xml и символьными типами, но QHB также позволяет просто приводить значение.

Когда значение символьной строки приводится к типу xml или из него без прохождения XMLPARSE или XMLSERIALIZE, соответственно, выбор DOCUMENT вместо CONTENT определяется параметром конфигурации сеанса «XML option», который можно установить с помощью стандартной команды:

SET XML OPTION < DOCUMENT | CONTENT >; 

или более похожий на QHB синтаксис:

SET xmloption TO < DOCUMENT | CONTENT >; 

По умолчанию используется CONTENT, поэтому разрешены все формы XML-данных.

Обработка кодировки

Необходимо соблюдать осторожность при работе с несколькими кодировками символов на клиенте, сервере и в данных XML, передаваемых через них. При использовании текстового режима для передачи запросов на сервер и запроса результатов к клиенту (что является нормальным режимом), QHB преобразует все символьные данные, передаваемые между клиентом и сервером, и наоборот, в кодировку символов соответствующей цели; см. раздел Поддержка набора символов. Это включает в себя строковые представления значений XML, как в приведенных выше примерах. Обычно это означает, что объявления кодировки, содержащиеся в данных XML, могут стать недействительными, поскольку символьные данные преобразуются в другие кодировки при перемещении между клиентом и сервером, поскольку объявление встроенной кодировки не изменяется. Чтобы справиться с этим поведением, объявления кодировки, содержащиеся в символьных строках, представленных для ввода в тип xml, игнорируются, и предполагается, что содержимое находится в текущей серверной кодировке. Следовательно, для правильной обработки символьные строки данных XML должны отправляться от клиента в текущей клиентской кодировке. В обязанности клиента входит либо преобразование документов в текущую клиентскую кодировку перед отправкой их на сервер, либо соответствующая настройка клиентской кодировки. На выходе значения типа xml не будут иметь объявления кодировки, и клиенты должны предполагать, что все данные находятся в текущей клиентской кодировке.

При использовании двоичного режима для передачи параметров запроса на сервер и запроса результатов обратно клиенту преобразование кодировки не выполняется, поэтому ситуация отличается. В этом случае будет соблюдаться объявление кодировки в данных XML, и если оно отсутствует, предполагается, что данные находятся в UTF-8 (как требуется стандартом XML; обратите внимание, что QHB не поддерживает UTF-16). На выходе данные будут иметь объявление кодировки, определяющее кодировку клиента, если только кодировка клиента не является UTF-8, в этом случае она будет опущена.

Нет необходимости говорить, что обработка данных XML с помощью QHB будет менее подвержена ошибкам и более эффективна, если кодировка данных XML, кодировка клиента и серверная кодировка совпадают. Поскольку данные XML обрабатываются внутри UTF-8, вычисления будут наиболее эффективными, если кодировка сервера также UTF-8.

Предупреждение.
Некоторые функции, связанные с XML, могут вообще не работать с данными, отличными от ASCII, если кодировка сервера не соответствует UTF-8. Известно, что это проблема, в частности, для xmltable() и xpath().

Доступ к значениям XML

Тип данных xml необычен тем, что не предоставляет операторов сравнения. Это потому, что не существует четко определенного и универсально полезного алгоритма сравнения для данных XML. Одним из следствий этого является то, что вы не можете получить строки, сравнивая столбец xml со значением поиска. Поэтому значения XML, как правило, должны сопровождаться отдельным ключевым полем, таким как идентификатор. Альтернативное решение для сравнения значений XML состоит в том, чтобы сначала преобразовать их в символьные строки, но обратите внимание, что сравнение символьных строк не имеет ничего общего с полезным методом сравнения XML.

Поскольку для типа данных xml нет операторов сравнения, невозможно создать индекс непосредственно для столбца этого типа. Если требуется быстрый поиск в данных XML, возможные обходные пути включают приведение выражения к типу символьной строки и его индексацию или индексирование выражения XPath. Конечно, фактический запрос должен быть скорректирован для поиска по индексируемому выражению.

Функциональность текстового поиска в QHB также может быть использована для ускорения полного поиска документов в XML-данных. Однако необходимая поддержка предварительной обработки еще не доступна в дистрибутиве QHB.

Типы JSON

Типы данных JSON предназначены для хранения данных JSON (нотации объектов JavaScript), как указано в RFC 7159. Такие данные также могут храниться в виде текста, но у типов данных JSON есть преимущество, заключающееся в том, что каждое сохраненное значение является действительным в соответствии с правилами JSON. Есть также различные JSON-специфические функции и операторы, доступные для данных, хранящихся в этих типах данных; см. раздел Функции и операторы JSON.

QHB предлагает два типа для хранения данных JSON: json и jsonb. Для реализации эффективных механизмов запросов для этих типов данных QHB также предоставляет тип данных jsonpath описанный в разделе Тип jsonpath.

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

Поскольку тип json хранит точную копию входного текста, он сохраняет семантически незначимые пробелы между токенами, а также порядок ключей в объектах JSON. Кроме того, если объект JSON внутри значения содержит один и тот же ключ более одного раза, то все пары ключ/значение сохраняются. (Функции обработки считают последнее значение как оперативное). В отличие от этого, jsonb не сохраняет пробел, не сохраняет порядок ключей объекта и не сохраняет дубликаты ключей объекта. Если во входных данных указаны дубликаты ключей, сохраняется только последнее значение.

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

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

RFC 7159 позволяет строкам JSON содержать escape-последовательности Unicode, обозначенные \uXXXX. В функции ввода для типа json экранирование Unicode разрешено независимо от кодировки базы данных и проверяется только на синтаксическую корректность (то есть, что четыре шестнадцатеричные цифры следуют за \u). Однако функция ввода для jsonb более строгая: она запрещает экранирование Unicode для символов, не относящихся к ASCII (те, что выше U+007F), если только кодировка базы данных не UTF8. Тип jsonb также отклоняет \u0000 (потому что это не может быть представлено в текстовом типе QHB) и настаивает на том, что любое использование суррогатных пар Unicode для обозначения символов вне базовой многоязычной плоскости Unicode является правильным. Допустимые экранированные символы Юникода преобразуются в эквивалентные символы ASCII или UTF8 для хранения; это включает в себя складывание суррогатных пар в один символ.

Заметка
Многие из функций обработки JSON, описанные в разделе Функции и операторы JSON, преобразуют экранирование Unicode в обычные символы и, следовательно, будут выдавать те же типы ошибок, которые только что были описаны, даже если их ввод имеет тип json не jsonb. Тот факт, что функция ввода json не выполняет эти проверки, может рассматриваться как исторический артефакт, хотя он допускает простое хранение (без обработки) экранирования JSON Unicode в кодировке базы данных отличной от UTF8. В общем, по возможности лучше избегать смешивания экранирования Unicode в JSON с кодировкой базы данных отличной от -UTF8, если это возможно.

При преобразовании текстового ввода JSON в jsonb примитивные типы, описанные в RFC 7159, эффективно отображаются на собственные типы QHB, как показано в Таблице 23. Следовательно, существуют некоторые незначительные дополнительные ограничения на то, что составляет допустимые данные jsonb которые не применяются ни к типу json, ни к JSON в абстрактном виде, что соответствует ограничениям на то, что может быть представлено базовым типом данных. В частности, jsonb будет отклонять числа, выходящие за пределы numeric типа данных QHB, а json — нет. Такие ограничения, определенные реализацией, разрешены RFC 7159. Однако на практике такие проблемы гораздо чаще встречаются в других реализациях, поскольку обычно тип примитива JSON представляется в виде типа с плавающей запятой двойной точности IEEE 754 (что явно предусмотрено и позволяет RFC 7159). При использовании JSON в качестве формата обмена с такими системами следует учитывать опасность потери числовой точности по сравнению с данными, изначально сохраняемыми в QHB.

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

Таблица 23. Примитивные типы JSON и соответствующие им типы QHB

Примитивный тип JSON Тип QHB Примечания
string text \u0000 не \u0000, как и не-ASCII Unicode, если кодировка базы данных не UTF8
number numeric Значения NaN и infinity не допускаются
boolean boolean Допускается только строчное true и false написание
null (none) SQL NULL — это другое понятие

Синтаксис ввода и вывода JSON

Синтаксис ввода/вывода для типов данных JSON соответствует RFC 7159.

Ниже приведены все допустимые выражения json (или jsonb):

-- Simple scalar/primitive value -- Primitive values can be numbers, quoted strings, true, false, or null SELECT '5'::json; -- Array of zero or more elements (elements need not be of same type) SELECT '[1, 2, "foo", null]'::json; -- Object containing pairs of keys and values -- Note that object keys must always be quoted strings SELECT ''::json; -- Arrays and objects can be nested arbitrarily SELECT '>'::json; 

Как указывалось ранее, когда значение JSON вводится, а затем печатается без какой-либо дополнительной обработки, json выводит тот же текст, который был введен, в то время как jsonb не сохраняет семантически незначимые детали, такие как пробелы. Например, обратите внимание на различия здесь:

SELECT ''::json; json ------------------------------------------------- (1 row) SELECT ''::jsonb; jsonb -------------------------------------------------- (1 row) 

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

SELECT ''::json, ''::jsonb; json | jsonb -----------------------+------------------------- | (1 row) 

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

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

Разработка документов JSON

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

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

Анализ вложений и наличия в jsonb

Анализ вложений является важной возможностью jsonb. Не существует параллельного набора средств для типа json. Анализ вложений определяет, содержится ли один документ jsonb внутри другого. Эти примеры возвращают значение true, за исключением отмеченных случаев:

-- Simple scalar/primitive values contain only the identical value: SELECT '"foo"'::jsonb @> '"foo"'::jsonb; -- The array on the right side is contained within the one on the left: SELECT '[1, 2, 3]'::jsonb @> '[1, 3]'::jsonb; -- Order of array elements is not significant, so this is also true: SELECT '[1, 2, 3]'::jsonb @> '[3, 1]'::jsonb; -- Duplicate array elements don't matter either: SELECT '[1, 2, 3]'::jsonb @> '[1, 2, 2]'::jsonb; -- The object with a single pair on the right side is contained -- within the object on the left side: SELECT ''::jsonb @> ''::jsonb; -- The array on the right side is not considered contained within the -- array on the left, even though a similar array is nested within it: SELECT '[1, 2, [1, 3]]'::jsonb @> '[1, 3]'::jsonb; -- yields false -- But with a layer of nesting, it is contained: SELECT '[1, 2, [1, 3]]'::jsonb @> '[[1, 3]]'::jsonb; -- Similarly, containment is not reported here: SELECT '>'::jsonb @> ''::jsonb; -- yields false -- A top-level key and an empty object is contained: SELECT '>'::jsonb @> '>'::jsonb; 

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

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

-- This array contains the primitive string value: SELECT '["foo", "bar"]'::jsonb @> '"bar"'::jsonb; -- This exception is not reciprocal -- non-containment is reported here: SELECT '"bar"'::jsonb @> '["bar"]'::jsonb; -- yields false 

В jsonb также есть оператор наличия (existence), который является вариацией темы вложения: он проверяет, отображается ли строка (заданная как текстовое значение) как ключ объекта или элемент массива на верхнем уровне значения jsonb. Эти примеры возвращают значение true, за исключением отмеченных случаев:

-- String exists as array element: SELECT '["foo", "bar", "baz"]'::jsonb ? 'bar'; -- String exists as object key: SELECT ''::jsonb ? 'foo'; -- Object values are not considered: SELECT ''::jsonb ? 'bar'; -- yields false -- As with containment, existence must match at the top level: SELECT '>'::jsonb ? 'bar'; -- yields false -- A string is considered to exist if it matches a primitive JSON string: SELECT '"foo"'::jsonb ? 'foo'; 

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

Заметка
Поскольку содержимое JSON является вложенным, соответствующий запрос может пропустить явный выбор подобъектов. В качестве примера предположим, что у нас есть столбец doc содержащий объекты на верхнем уровне, а большинство объектов содержат поля tags которые содержат массивы подобъектов. Этот запрос находит записи, в которых появляются «term»:»paris» содержащие как «term»:»paris» и «term»:»food», игнорируя любые такие ключи вне массива tags:

```sql SELECT doc->'site_name' FROM websites WHERE doc @> '<"tags":[, ]>'; ``` Можно сделать то же самое, скажем, ```sql SELECT doc->'site_name' FROM websites WHERE doc->'tags' @> '[, ]'; ``` но такой подход менее гибок, а зачастую и менее эффективен. 

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

Различные операторы вложений и наличия, а также все другие операторы и функции JSON описаны в разделе Функции и операторы JSON.

Индексация jsonb

Индексы GIN можно использовать для эффективного поиска ключей или пар ключ/значение, встречающихся в большом количестве документов jsonb (датумов). Предоставляются два «класса операторов» GIN, предлагающих различные компромиссы производительности и гибкости.

Класс операторов GIN по умолчанию для jsonb поддерживает запросы с операторами верхнего уровня наличия ключей: ? , ?& и ?| и оператор наличия пути/значения @> . (Подробнее о семантике, которую реализуют эти операторы, см. таблицу 45). Пример создания индекса с помощью этого класса операторов:

CREATE INDEX idxgin ON api USING GIN (jdoc); 

Нестандартный класс операторов GIN jsonb_path_ops поддерживает индексирование только оператора @> . Пример создания индекса с помощью этого класса операторов:

CREATE INDEX idxginp ON api USING GIN (jdoc jsonb_path_ops); 

Рассмотрим пример таблицы, в которой хранятся документы JSON, извлеченные из стороннего веб-сервиса, с документированным определением схемы. Типичный документ:

Мы храним эти документы в таблице с именем api, в столбце jsonb именем jdoc. Если для этого столбца создается индекс GIN, индекс может использовать такие запросы:

-- Find documents in which the key "company" has value "Magnafone" SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> ''; 

Тем не менее, индекс не может быть использован для запросов, подобных следующему, потому что хотя оператор ? индексируется, он не применяется непосредственно к индексируемому столбцу jdoc:

-- Find documents in which the key "tags" contains key or array element "qui" SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc -> 'tags' ? 'qui'; 

Тем не менее, при надлежащем использовании индексов выражений в приведенном выше запросе может использоваться индекс. Если запросы к определенным элементам в ключе «tags» распространены, определение индекса, подобного этому, может быть целесообразным:

CREATE INDEX idxgintags ON api USING GIN ((jdoc -> 'tags')); 

Теперь, WHERE предложение jdoc -> ‘tags’ ? ‘qui’ будет распознаваться как приложение индексируемого оператора ? к индексированному выражению jdoc -> ‘tags’ . (Более подробную информацию об индексах выражений можно найти в разделе Индексы по выражениям).

Кроме того, индекс GIN поддерживает @@ и @? операторы, которые выполняют сопоставление jsonpath.

SELECT jdoc->’guid’, jdoc->’name’ FROM api WHERE jdoc @@ ‘$.tags[*] == «qui»‘;

SELECT jdoc->’guid’, jdoc->’name’ FROM api WHERE jdoc @@ ‘$.tags[*] ? (@ == «qui»)’;

Индекс GIN извлекает из jsonpath операторы следующей формы: accessors_chain = const . Цепочка доступа может состоять из методов доступа .key , [*] и [index] . jsonb_ops дополнительно поддерживает методы доступа .* и .** .

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

-- Find documents in which the key "tags" contains array element "qui" SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> ''; 

Простой индекс GIN для столбца jdoc может поддержать этот запрос. Но обратите внимание, что такой индекс будет хранить копии каждого ключа и значения в столбце jdoc, в то время как индекс выражения предыдущего примера хранит только данные, найденные под ключом tags. Хотя подход с простым индексом является гораздо более гибким (поскольку он поддерживает запросы по любому ключу), целевые индексы выражений, вероятно, будут меньше и быстрее для поиска, чем простой индекс.

Хотя класс операторов jsonb_path_ops поддерживает только запросы с операторами @> , @@ и @? , он имеет заметные преимущества в производительности по сравнению с классом операторов по умолчанию jsonb_ops. Индекс jsonb_path_ops обычно намного меньше индекса jsonb_ops на тех же данных, и специфика поиска лучше, особенно когда запросы содержат ключи, которые часто появляются в данных. Поэтому поисковые операции с индексом jsonb_path_ops обычно выполняются лучше, чем с классом операторов по умолчанию.

Техническое различие между индексами GIN jsonb_ops и jsonb_path_ops состоит в том, что первый создает независимые элементы индекса для каждого ключа и значения в данных, а второй создает элементы индекса только для каждого значения в данных. 1 По сути, каждый элемент индекса jsonb_path_ops является хешем значения и ключа (ключей), ведущих к нему; например, чтобы индексировать > , будет создан один элемент индекса, включающий все три элемента foo, bar и baz в хеш- значение. Таким образом, запрос вложений, ищущий эту структуру, приведет к чрезвычайно специфичному поиску по индексу; но нет никакого способа узнать, является ли foo ключом. С другой стороны, индекс jsonb_ops будет создавать три элемента индекса, представляющих foo, bar и baz отдельно; затем, чтобы выполнить запрос вложений, он будет искать строки, содержащие все три этих элемента. Хотя индексы GIN могут выполнять такой поиск довольно эффективно, он все равно будет менее конкретным и более медленным, чем эквивалентный поиск jsonb_path_ops, особенно если существует очень большое количество строк, содержащих какой-либо один из трех элементов индекса.

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

jsonb также поддерживает индексы btree и hash. Обычно они полезны, только если важно проверить равенство полных документов JSON. Упорядочение btree для данных jsonb редко представляет большой интерес, но для полноты он является:

Object > Array > Boolean > Number > String > Null Object with n pairs > object with n - 1 pairs Array with n elements > array with n - 1 elements 

Объекты с одинаковым количеством пар сравниваются в следующем порядке:

key-1, value-1, key-2 .

Обратите внимание, что ключи объектов сравниваются в порядке их хранения; в частности, поскольку более короткие ключи хранятся перед более длинными ключами, это может привести к неинтуитивным результатам, таким как:

Аналогично, массивы с равным количеством элементов сравниваются в следующем порядке:

Примитивные значения JSON сравниваются с использованием тех же правил сравнения, что и для базового типа данных QHB. Строки сравниваются с использованием параметров сортировки базы данных по умолчанию.

Трансформации

Доступны дополнительные расширения, которые реализуют преобразования для типа jsonb для разных процедурных языков.

Тип jsonpath

Тип jsonpath реализует поддержку языка путей SQL/JSON в QHB для эффективного запроса данных JSON. Он предоставляет двоичное представление разобранного выражения пути SQL/JSON, которое указывает элементы, которые должны быть извлечены механизмом пути из данных JSON для дальнейшей обработки с помощью функций запроса SQL/JSON.

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

  • Точка ( . ) Используется для доступа членов.
  • Квадратные скобки ( [] ) используются для доступа к массиву.
  • Массивы SQL/JSON начинаются с 0, в отличие от обычных массивов SQL, которые начинаются с 1.

Выражение пути SQL/JSON обычно пишется в запросе SQL в виде строкового литерала SQL, поэтому его необходимо заключить в одинарные кавычки, а любые одинарные кавычки, требуемые в пределах значения, должны быть удвоены (см. раздел Строковые константы). Некоторые формы выражений пути требуют строковых литералов внутри них. Эти встроенные строковые литералы следуют соглашениям JavaScript/ECMAScript: они должны быть заключены в двойные кавычки, и в них могут использоваться экранированные символы обратной косой черты для представления символов, трудно поддающихся вводу. В частности, способ написать двойную кавычку во встроенном строковом литерале — это \» , а чтобы написать обратную косую черту, нужно написать \\ . Другие специальные последовательности обратной косой черты включают в себя те, которые распознаются в строках JSON: \b , \f , \n , \r , \t , \v для различных управляющих символов ASCII и \uNNNN для символа Unicode, идентифицируемого его кодовой точкой из 4 шестнадцатеричных цифр. Синтаксис обратной косой черты также включает два случая, которые не допускаются JSON: \xNN для символьного кода, написанного только с двумя шестнадцатеричными цифрами, и \u для символьного кода, записанного с помощью от 1 до 6 шестнадцатеричных цифр.

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

  • Литералы пути примитивных типов JSON:Unicode text, numeric, true, false или null.
  • Переменные пути, перечисленные в Таблице 24.
  • Операторы доступа перечислены в Таблице 25.
  • Операторы и методы jsonpath, перечисленные в разделе Операторы пути и методы SQL/JSON
  • Круглые скобки, которые можно использовать для предоставления выражений фильтра или определения порядка вычисления пути.

Подробнее об использовании выражений jsonpath с функциями запросов SQL/JSON см. раздел Язык путей SQL/JSON.

Таблица 24. Переменные jsonpath

Переменная Описание
$ Переменная, представляющая текст JSON для запроса (элемент контекста).
$varname Именованная переменная. Его значение может быть установлено с помощью параметра vars нескольких функций обработки JSON. См. таблицу 47 и ее примечания для деталей.
@ Переменная, представляющая результат оценки пути в выражениях фильтра.

Таблица 25. Операторы доступа jsonpath

Метод доступа к элементу, который возвращает элемент объекта с указанным ключом. Если имя ключа является именованной переменной, начинающейся с $ или не соответствует правилам JavaScript идентификатора, оно должно быть заключено в двойные кавычки в виде строкового литерала.

То же, что и .∗∗, но с фильтром по уровням вложенности иерархии JSON. Уровни вложенности указываются как целые числа. Нулевой уровень соответствует текущему объекту. Чтобы получить доступ к самому низкому уровню вложенности, вы можете использовать last ключевое слово. Это расширение QHB стандарта SQL/JSON.

Метод доступа к элементу массива. subscript может быть задан в двух формах: от index или start_index to end_index. Первая форма возвращает один элемент массива по его индексу. Вторая форма возвращает срез массива по диапазону индексов, включая элементы, которые соответствуют предоставленным start_index и end_index .

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

Массивы

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

Объявление типов массивов

Чтобы проиллюстрировать использование типов массивов, мы создадим эту таблицу:

CREATE TABLE sal_emp ( name text, pay_by_quarter integer[], schedule text[][] ); 

Как показано, тип данных массива именуется путем добавления квадратных скобок ( [] ) к имени типа данных элементов массива. Приведенная выше команда создаст таблицу с именем sal_emp со столбцом типа text (name), одномерным массивом типа integer (pay_by_quarter), который представляет квартальную зарплату сотрудника, и двумерным массивом text (schedule), который представляет недельный график сотрудника.

Синтаксис CREATE TABLE позволяет указывать точный размер массивов, например:

CREATE TABLE tictactoe ( squares integer[3][3] ); 

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

Текущая реализация также не применяет заявленное количество измерений. Все массивы определенного типа элементов считаются одинаковыми, независимо от размера или количества измерений. Итак, объявление размера массива или числа измерений в CREATE TABLE — это просто документация; это не влияет на поведение во время выполнения.

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

pay_by_quarter integer ARRAY[4],

Или, если не указан размер массива:

pay_by_quarter integer ARRAY,

Однако, как и прежде, QHB не налагает ограничения на размер в любом случае.

Ввод значений массива

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

где delim — символ разделителя для типа, как отмечено в записи pg_type. Среди стандартных типов данных, представленных в дистрибутиве QHB, все используют запятую (,), за исключением типа box, в котором используется точка с запятой (;). Каждый val является либо константой типа элемента массива, либо подмножеством. Пример константы массива:

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

Чтобы установить для элемента константы массива значение NULL, напишите NULL для значения элемента. (Подойдет любой вариант NULL верхнем или нижнем регистре). Если вам нужно фактическое строковое значение «NULL», вы должны заключить его в двойные кавычки.

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

Теперь мы можем показать некоторые операторы INSERT:

INSERT INTO sal_emp VALUES ('Bill', '', ', >'); INSERT INTO sal_emp VALUES ('Carol', '', ', >'); 

Результат двух предыдущих вставок выглядит так:

SELECT * FROM sal_emp; name | pay_by_quarter | schedule -------+---------------------------+------------------------------------------- Bill | | ,> Carol | | ,> (2 rows) 

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

INSERT INTO sal_emp VALUES ('Bill', '', ', >'); ERROR: multidimensional arrays must have array expressions with matching dimensions 

Синтаксис конструктора ARRAY также может быть использован:

INSERT INTO sal_emp VALUES ('Bill', ARRAY[10000, 10000, 10000, 10000], ARRAY[['meeting', 'lunch'], ['training', 'presentation']]); INSERT INTO sal_emp VALUES ('Carol', ARRAY[20000, 25000, 25000, 25000], ARRAY[['breakfast', 'consulting'], ['meeting', 'lunch']]); 

Обратите внимание, что элементы массива являются обычными константами или выражениями SQL; например, строковые литералы заключаются в одинарные кавычки, а не в двойные, как это было бы в литерале массива. Синтаксис конструктора ARRAY более подробно обсуждается в разделе Конструкторы массивов.

Доступ к массивам

Теперь мы можем выполнить несколько запросов к таблице. Сначала мы покажем, как получить доступ к одному элементу массива. Этот запрос извлекает имена сотрудников, зарплата которых изменилась во втором квартале:

SELECT name FROM sal_emp WHERE pay_by_quarter[1] <> pay_by_quarter[2]; name ------- Carol (1 row) 

Номера индексов массива пишутся в квадратных скобках. По умолчанию QHB использует для массивов соглашение об одинарной нумерации, то есть массив из n элементов начинается с array[1] и заканчивается array[n] .

Этот запрос возвращает заработную плату всех сотрудников за третий квартал:

SELECT pay_by_quarter[3] FROM sal_emp; pay_by_quarter ---------------- 10000 25000 (2 rows) 

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

SELECT schedule[1:2][1:1] FROM sal_emp WHERE name = 'Bill'; schedule ------------------------ ,> (1 row) 

Если какое-либо измерение записывается как срез, т. е. содержит двоеточие, то все измерения рассматриваются как срезы. Любое измерение, которое имеет только одно число (без двоеточия), рассматривается как значение от 1 до указанного числа. Например, [2] обрабатывается как [1:2] , как в этом примере:

SELECT schedule[1:2][2] FROM sal_emp WHERE name = 'Bill'; schedule ------------------------------------------- ,> (1 row) 

Чтобы избежать путаницы со случаем без срезов, лучше использовать синтаксис срезов для всех измерений, например, [1:2][1:1] , а не [2][1:1] .

Можно опустить нижнюю и/или верхнюю границу спецификатора слайса; отсутствующая граница заменяется нижним или верхним пределом индексов массива. Например:

SELECT schedule[:2][2:] FROM sal_emp WHERE name = 'Bill'; schedule ------------------------ ,> (1 row) SELECT schedule[:][1:1] FROM sal_emp WHERE name = 'Bill'; schedule ------------------------ ,> (1 row) 

Выражение индекса массива вернет значение NULL, если либо сам массив, либо любое из выражений индекса равно NULL. Кроме того, null возвращается, если индекс находится за пределами границ массива (в этом случае ошибка не возникает). Например, если schedule в настоящее время имеет размеры [1:3][1:2] то ссылка на schedule[3][3] выдает NULL. Точно так же ссылка на массив с неправильным числом индексов дает NULL значение, а не ошибку.

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

Текущие измерения любого значения массива можно получить с помощью функции array_dims:

SELECT array_dims(schedule) FROM sal_emp WHERE name = 'Carol'; array_dims ------------ [1:2][1:2] (1 row) 

array_dims дает текстовый результат, который удобен для чтения людьми, но, возможно, неудобен для программ. Измерения также можно получить с помощью array_upper и array_lower, которые возвращают верхнюю и нижнюю границу указанного размера массива соответственно:

SELECT array_lower(schedule, 1), array_upper(schedule, 1) FROM sal_emp WHERE name = 'Carol'; array_lower array_upper -------------------------- 1 2 (1 row) 

array_length вернет длину указанного размера массива:

SELECT array_length(schedule, 1) FROM sal_emp WHERE name = 'Carol'; array_length -------------- 2 (1 row) 

cardinality возвращает общее количество элементов в массиве по каждому измерению. Это фактическое число строк, которое возвращает вызов unnest:

SELECT cardinality(schedule) FROM sal_emp WHERE name = 'Carol'; cardinality ------------- 4 (1 row) 

SELECT unnest(schedule) FROM sal_emp WHERE name = ’Carol’; > unnest > ———- > breakfast > consulting > meeting > lunch > (4 row)—>

Модификация массивов

Значение массива можно полностью заменить:

UPDATE sal_emp SET pay_by_quarter = '' WHERE name = 'Carol'; 

или используя синтаксис выражения ARRAY:

UPDATE sal_emp SET pay_by_quarter = ARRAY[25000,25000,27000,27000] WHERE name = 'Carol'; 

Массив также можно обновить в одном элементе:

UPDATE sal_emp SET pay_by_quarter[4] = 15000 WHERE name = 'Bill'; 

или обновить в срезе:

UPDATE sal_emp SET pay_by_quarter[1:2] = '' WHERE name = 'Carol'; 

Синтаксис среза с опущенной нижней и/или верхней границами может быть использован, но только при обновлении значения массива, которое не является NULL или нульмерным (в противном случае не существует ограничения по нижнему индексу для замены).

Сохраненный массив можно расширять, добавляя элементы, которые еще не присутствуют. Любые позиции между ранее присутствующими и вновь назначенными элементами будут заполнены null. Например, если массив myarray в настоящее время имеет 4 элемента, он будет иметь шесть элементов после обновления, которое присваивается myarray[6] ; myarray[5] будет содержать null. В настоящее время расширение таким способом допускается только для одномерных, а не многомерных массивов.

Назначение индексов позволяет создавать массивы, в которых не используются одинарная нумерация. Например, можно назначить myarray[-2:7] для создания массива со значениями индекса от -2 до 7 .

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

SELECT ARRAY[1,2] || ARRAY[3,4]; ?column? ----------- (1 row) SELECT ARRAY[5,6] || ARRAY[[1,2],[3,4]]; ?column? --------------------- ,,> (1 row) 

Оператор конкатенации позволяет помещать отдельный элемент в начало или конец одномерного массива. Он также принимает два N -мерных массива или N -мерный и N+1 -мерный массив.

Когда отдельный элемент помещается в начало или конец одномерного массива, результатом является массив с тем же нижним индексом, что и у операнда массива. Например:

SELECT array_dims(1 || '[0:1]='::int[]); array_dims ------------ [0:2] (1 row) SELECT array_dims(ARRAY[1,2] || 3); array_dims ------------ [1:3] (1 row) 

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

SELECT array_dims(ARRAY[1,2] || ARRAY[3,4,5]); array_dims ------------ [1:5] (1 row) SELECT array_dims(ARRAY[[1,2],[3,4]] || ARRAY[[5,6],[7,8],[9,0]]); array_dims ------------ [1:5][1:2] (1 row) 

Когда N -мерный массив помещается в начало или конец N+1 -мерного массива, результат аналогичен описанному выше случаю массива элементов. Каждый N -мерный подмассив по сути является элементом внешнего измерения N+1 -мерного массива. Например:

SELECT array_dims(ARRAY[1,2] || ARRAY[[3,4],[5,6]]); array_dims ------------ [1:3][1:2] (1 row) 

Массив также может быть создан с использованием функций array_prepend, array_append или array_cat. Первые два поддерживают только одномерные массивы, но array_cat поддерживает многомерные массивы. Несколько примеров:

SELECT array_prepend(1, ARRAY[2,3]); array_prepend --------------- (1 row) SELECT array_append(ARRAY[1,2], 3); array_append -------------- (1 row) SELECT array_cat(ARRAY[1,2], ARRAY[3,4]); array_cat ----------- (1 row) SELECT array_cat(ARRAY[[1,2],[3,4]], ARRAY[5,6]); array_cat --------------------- ,,> (1 row) SELECT array_cat(ARRAY[5,6], ARRAY[[1,2],[3,4]]); array_cat --------------------- <,,> 

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

SELECT ARRAY[1, 2] || ''; -- the untyped literal is taken as an array ?column? ----------- SELECT ARRAY[1, 2] || '7'; -- so is this one ERROR: malformed array literal: "7" SELECT ARRAY[1, 2] || NULL; -- so is an undecorated NULL ?column? ---------- (1 row) SELECT array_append(ARRAY[1, 2], NULL); -- this might have been meant array_append --------------

В приведенных выше примерах анализатор видит целочисленный массив на одной стороне оператора конкатенации и константу неопределенного типа с другой. Эвристика, которую он использует для разрешения типа константы, предполагает, что она того же типа, что и другие входные данные оператора — в данном случае, целочисленный массив. Поэтому предполагается, что оператор конкатенации представляет array_cat, а не array_append. Если это неправильный выбор, его можно исправить, приведя константу к типу элемента массива; но явное использование array_append может быть предпочтительным решением.

Поиск в массивах

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

SELECT * FROM sal_emp WHERE pay_by_quarter[1] = 10000 OR pay_by_quarter[2] = 10000 OR pay_by_quarter[3] = 10000 OR pay_by_quarter[4] = 10000; 

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

SELECT * FROM sal_emp WHERE 10000 = ANY (pay_by_quarter); 

Кроме того, вы можете найти строки, где массив имеет все значения, равные 10000, с помощью:

SELECT * FROM sal_emp WHERE 10000 = ALL (pay_by_quarter); 

В качестве альтернативы можно использовать функцию generate_subscripts. Например:

SELECT * FROM (SELECT pay_by_quarter, generate_subscripts(pay_by_quarter, 1) AS s FROM sal_emp) AS foo WHERE pay_by_quarter[s] = 10000; 

Вы также можете искать в массиве с помощью оператора && , который проверяет, перекрывает ли левый операнд правый операнд. Например:

SELECT * FROM sal_emp WHERE pay_by_quarter && ARRAY[10000]; 

Этот и другие операторы массива более подробно описаны в разделе Функции и операторы массива. Его можно ускорить с помощью соответствующего индекса, как описано в разделе Типы индексов.

Вы также можете искать конкретные значения в массиве, используя функции array_position и array_positions. Первый возвращает индекс первого вхождения значения в массиве; последний возвращает массив с индексами всех вхождений значения в массиве. Например:

SELECT array_position(ARRAY['sun','mon','tue','wed','thu','fri','sat'], 'mon'); array_positions ----------------- 2 SELECT array_positions(ARRAY[1, 4, 3, 1, 3, 4, 2, 1], 1); array_positions -----------------

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

Синтаксис ввода и вывода массива

Внешнее текстовое представление значения массива состоит из элементов, которые интерпретируются в соответствии с правилами преобразования ввода-вывода для типа элемента массива, а также элементов оформления, которые указывают на структуру массива. Оформление состоит из фигурных скобок ( < и >) вокруг значения массива и символов-разделителей между смежными элементами. Символ-разделитель обычно представляет собой запятую ( , ), но может быть и другим: он определяется настройкой typdelim для типа элемента массива. Среди стандартных типов данных, представленных в дистрибутиве QHB, все используют запятую, за исключением типа box, в котором используется точка с запятой ( ; ). В многомерном массиве каждое измерение (строка, плоскость, куб и т. д.) получает свой собственный уровень фигурных скобок, и разделители должны быть записаны между смежными фигурными скобками сущностями одного уровня.

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

По умолчанию значение индекса нижней границы измерений массива равно единице. Для представления массивов с другими нижними границами диапазоны индексов массива могут быть указаны явно перед записью содержимого массива. Это оформление состоит из квадратных скобок ( [] ) вокруг нижней и верхней границ каждого измерения массива, между которыми стоит символ разделителя ( : ). Оформление размера массива сопровождается знаком равенства ( = ). Например:

SELECT f1[1][-2][3] AS e1, f1[1][-1][5] AS e2 FROM (SELECT '[1:1][-2:-1][3:5]=,>>'::int[] AS f1) AS ss; e1 | e2 ----+---- 1 | 6 (1 row) 

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

Если значение, записанное для элемента, равно NULL (в любом случае), элемент считается равным NULL. Наличие любых кавычек или обратной косой черты отключает это и позволяет вводить буквальное строковое значение литерала «NULL».

Как показано ранее, при записи значения массива вы можете использовать двойные кавычки вокруг любого отдельного элемента массива. Вы должны сделать это, если значение элемента в противном случае приведет к путанице синтаксического анализатора значений массива. Например, элементы, содержащие фигурные скобки, запятые (или символ-разделитель типа данных), двойные кавычки, обратные слэши или начальные или конечные пробелы должны быть заключены в двойные кавычки. Пустые строки и строки, соответствующие слову NULL, также должны быть заключены в кавычки. Чтобы поместить двойную кавычку или обратную косую черту в значение элемента массива заключенного в кавычки, поставьте перед ним обратную косую черту. Кроме того, можно избежать кавычек и использовать обратную косую черту для защиты всех символов данных, которые в противном случае были бы приняты за синтаксис массива.

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

Заметка
Синтаксис конструктора массива (см. раздел Конструкторы массивов) часто проще в работе, чем синтаксис массива-литерала при записи значений массива в командах SQL. В массиве значения отдельных элементов записываются так же, как если бы они не были членами массива.

Составные типы

Составной тип представляет структуру строки или записи; по существу, это просто список имен полей и их типов данных. QHB позволяет использовать составные типы во многом так же, как и простые типы. Например, столбец таблицы может быть объявлен как составной тип.

Декларация составных типов

Вот два простых примера определения составных типов:

CREATE TYPE complex AS ( r double precision, i double precision ); CREATE TYPE inventory_item AS ( name text, supplier_id integer, price numeric ); 

Синтаксис сопоставим с CREATE TABLE, за исключением того, что можно указывать только имена и типы полей; никакие ограничения (такие как NOT NULL) в настоящее время не могут быть включены. Обратите внимание, что ключевое слово AS имеет важное значение; без этого система будет думать, что подразумевается другого вид команды CREATE TYPE, и вы получите странные синтаксические ошибки.

Определив типы, мы можем использовать их для создания таблиц:

CREATE TABLE on_hand ( item inventory_item, count integer ); INSERT INTO on_hand VALUES (ROW('fuzzy dice', 42, 1.99), 1000); 
CREATE FUNCTION price_extension(inventory_item, integer) RETURNS numeric AS 'SELECT $1.price * $2' LANGUAGE SQL; SELECT price_extension(item, 10) FROM on_hand; 

Каждый раз, когда вы создаете таблицу, составной тип также автоматически создается с тем же именем, что и таблица, для представления типа строки таблицы. Например, если бы мы сказали:

CREATE TABLE inventory_item ( name text, supplier_id integer REFERENCES suppliers, price numeric CHECK (price > 0) ); 

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

Построение составных значений

Чтобы записать составное значение в виде литеральной константы, заключите значения полей в круглые скобки и разделите их запятыми. Вы можете поместить двойные кавычки вокруг любого значения поля, и это необходимо делать, если оно содержит запятые или скобки. (Подробнее см. ниже). Таким образом, общий формат составной константы следующий:

что будет допустимым значением типа inventory_item определенного выше. Чтобы сделать поле пустым, не пишите никаких символов в его позиции в списке. Например, эта константа задает третье поле NULL:

Если нужна пустая строка, а не NULL, напишите двойные кавычки:

Здесь первое поле является не-NULL пустой строкой, третье — NULL.

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

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

ROW('fuzzy dice', 42, 1.99) ROW('', 42, NULL) 

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

('fuzzy dice', 42, 1.99) ('', 42, NULL) 

Синтаксис конструктора строки более подробно обсуждается в разделе Конструкторы строк.

Доступ к составным типам

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

SELECT item.name FROM on_hand WHERE item.price > 9.99; 

Это не будет работать, так как элемент item считается именем таблицы, а не именем столбца on_hand, согласно правилам синтаксиса SQL. Вы должны написать это так:

SELECT (item).name FROM on_hand WHERE (item).price > 9.99; 

или если вам нужно использовать имя таблицы (например, в запросе с несколькими таблицами), например так:

SELECT (on_hand.item).name FROM on_hand WHERE (on_hand.item).price > 9.99; 

Теперь заключенный в скобки объект правильно интерпретируется как ссылка на столбец item, а затем из него можно выбрать субполе.

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

SELECT (my_func(. )).field FROM . 

Без лишних скобок это приведет к синтаксической ошибке.

Название специального поля * означает «все поля», как более подробно описано в разделе Использование составных типов в запросах.

Модификация составных типов

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

INSERT INTO mytab (complex_col) VALUES((1.1,2.2)); UPDATE mytab SET complex_col = ROW(1.1,2.2) WHERE . ; 

Первый пример пропускает ROW, второй использует его; мы могли бы сделать это в любом случае.

Мы можем обновить отдельное подполе составного столбца:

UPDATE mytab SET complex_col.r = (complex_col).r + 1 WHERE . ; 

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

И мы можем указать подполя как цели для INSERT:

INSERT INTO mytab (complex_col.r, complex_col.i) VALUES(1.1, 2.2); 

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

Использование составных типов в запросах

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

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

SELECT c FROM inventory_item c; 

Этот запрос создает один составной столбец, поэтому мы можем получить вывод, например:

c ------------------------ ("fuzzy dice",42,1.99) (1 row) 

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

Обычный синтаксис имени квалифицированного столбца table_name.column_name можно понимать как применение выбора поля к составному значению текущей строки таблицы. (Из соображений эффективности это не реализовано таким образом).

SELECT c.* FROM inventory_item c; 

тогда, согласно стандарту SQL, мы должны расширить содержимое таблицы на отдельные столбцы:

name | supplier_id | price -----------+-------------+------- fuzzy dice | 42 | 1.99 (1 row) 

как будто выполнялся запрос:

SELECT c.name, c.supplier_id, c.price FROM inventory_item c; 

QHB будет применять это поведение раскрытия к любому составному выражению, хотя, как показано выше, вам нужно писать круглые скобки вокруг значения, к которому применяется .* , когда это не простое имя таблицы. Например, если myfunc() — это функция, возвращающая составной тип со столбцами a, b и c, то эти два запроса имеют одинаковый результат:

SELECT (myfunc(x)).* FROM some_table; SELECT (myfunc(x)).a, (myfunc(x)).b, (myfunc(x)).c FROM some_table; 

Заметка
QHB обрабатывает расширение столбцов, фактически превращая первую форму во вторую. Таким образом, в этом примере myfunc() будет вызываться три раза в строке с любым синтаксисом. Если это дорогая функция, вы можете избежать этого, с помощью запроса:

`SELECT m.* FROM some_table, LATERAL myfunc(x) AS m;` Помещение функции в боковой элемент FROM позволяет вызывать ее более одного раза в строке. `m.*` все еще разворачивается в `m.a`, `m.b`, `m.c`, но теперь эти переменные являются просто ссылками на выходные данные элемента *FROM*. (Ключевое слово *LATERAL* здесь необязательно, но мы показываем его, чтобы уточнить, что функция получает `x` из *some_table*). 

Синтаксис composite_value.* приводит к расширению столбца такого типа, когда он появляется на верхнем уровне выходного списка SELECT, списка RETURNING в INSERT/UPDATE/DELETE, предложения VALUES или конструктора строки. Во всех других контекстах (в том числе, когда они вложены в одну из этих конструкций), присоединение .* к составному значению не меняет значение, поскольку означает «все столбцы», и поэтому то же составное значение создается снова. Например, если somefunc() принимает составной аргумент, эти запросы совпадают:

SELECT somefunc(c.*) FROM inventory_item c; SELECT somefunc(c) FROM inventory_item c; 

В обоих случаях текущая строка inventory_item передается функции в виде одного составного аргумента. Несмотря на то, что .* ничего не делает в таких случаях, это хороший стиль, поскольку он ясно показывает, что предполагается составное значение. В частности, синтаксический анализатор будет рассматривать c как c.* для ссылки на имя таблицы или псевдоним, а не на имя столбца, так что нет никакой двусмысленности; тогда как без .* неясно, означает ли c имя таблицы или имя столбца, и на самом деле интерпретация имени столбца будет предпочтительнее, если есть столбец с именем c .

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

SELECT * FROM inventory_item c ORDER BY c; SELECT * FROM inventory_item c ORDER BY c.*; SELECT * FROM inventory_item c ORDER BY ROW(c.*); 

Все эти предложения ORDER BY задают составное значение строки, в результате чего строки сортируются в соответствии с правилами, описанными в разделе Сравнение составных типов. Однако если inventory_item элемент содержит столбец с именем «c», то первый случай будет отличаться от других, поскольку это означало бы сортировку только по этому столбцу. Учитывая имена столбцов, показанные ранее, эти запросы также эквивалентны приведенным выше:

SELECT * FROM inventory_item c ORDER BY ROW(c.name, c.supplier_id, c.price); SELECT * FROM inventory_item c ORDER BY (c.name, c.supplier_id, c.price); 

(В последнем случае используется конструктор строки с опущенным ключевым словом ROW).

Другое специальное синтаксическое поведение, связанное с составными значениями, заключается в том, что мы можем использовать функциональную нотацию для извлечения поля составного значения. Простой способ объяснить это состоит в том, что нотации field(table) и table.field являются взаимозаменяемыми. Например, эти запросы эквивалентны:

SELECT c.name FROM inventory_item c WHERE c.price > 1000; SELECT name(c) FROM inventory_item c WHERE price(c) > 1000; 

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

SELECT somefunc(c) FROM inventory_item c; SELECT somefunc(c.*) FROM inventory_item c; SELECT c.somefunc FROM inventory_item c; 

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

Синтаксис ввода и вывода составного типа

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

пробел будет игнорироваться, если тип поля целочисленный, но не текстовый.

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

Абсолютно пустое значение поля (без запятых или круглых скобок) означает NULL. Чтобы записать значение, которое является пустой строкой, а не NULL, напишите «» .

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

Заметка
Помните, что то, что вы пишете в команде SQL, сначала будет интерпретироваться как строковый литерал, а затем как составной. Это удваивает количество обратных косых черт, которые вам нужны (при условии использования синтаксиса escape-строки). Например, чтобы вставить текстовое поле, содержащее двойную кавычку и обратную косую черту в составном значении, вам нужно написать:

```sql INSERT . VALUES ('("\"\\")'); ``` 

Обработчик строковых литералов удаляет один уровень обратной косой черты, так что то, что поступает в синтаксический анализатор составных значений, выглядит как ( «\»\\» ). В свою очередь, строка, переданная подпрограмме ввода типа данных text, становится «\ . (Если бы мы работали с типом данных, чья подпрограмма ввода также обрабатывала обратную косую черту, например, bytea, нам может потребоваться до восьми обратных косых черт в команде, чтобы получить одну обратную косую черту в сохраненном составном поле). Кавычки из «долларов» (см. раздел Строковые константы в долларовых кавычках) могут использоваться, чтобы избежать необходимости удваивать обратную косую черту.

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

Типы диапазонов

Типы диапазона — это типы данных, представляющие диапазон значений некоторого типа элемента (называемый подтипом диапазона). Например, диапазоны меток времени могут использоваться для представления диапазонов времени, зарезервированных для комнаты собраний. В этом случае тип данных — tsrange (сокращение от «timestamp range»), а timestamp — это подтип. Подтип должен иметь общий порядок, чтобы было четко определено, находятся ли значения элемента в пределах, до или после диапазона значений.

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

Типы встроенных диапазонов

QHB поставляется со следующими встроенными типами диапазонов:

  • int4range — диапазон integer
  • int8range — диапазон от bigint
  • numrange — диапазон numeric значений
  • tsrange — диапазон timestamp without time zone
  • tstzrange — диапазон timestamp with time zone
  • daterange — диапазон date

Кроме того, вы можете определить свои собственные типы диапазонов; см. CREATE TYPE для получения дополнительной информации.

Примеры

CREATE TABLE reservation (room int, during tsrange); INSERT INTO reservation VALUES (1108, '[2010-01-01 14:30, 2010-01-01 15:30)'); -- Containment SELECT int4range(10, 20) @> 3; -- Overlaps SELECT numrange(11.1, 22.2) && numrange(20.0, 30.0); -- Extract the upper bound SELECT upper(int8range(15, 25)); -- Compute the intersection SELECT int4range(10, 20) * int4range(15, 25); -- Is the range empty? SELECT isempty(numrange(1, 5)); 

См. раздел Функции диапазона и операторы для получения полного списка операторов и функций для типов диапазонов.

Инклюзивные и эксклюзивные границы

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

В текстовой форме диапазона включающая нижняя граница представлена как » [ «, в то время как исключительная нижняя граница представлена как » ( «. Аналогично, включающая верхняя граница представлена как » ] «, тогда как исключительная верхняя граница представлена на » ) «. (Подробнее см. раздел Диапазон ввода/вывода).

Функции lower_inc и upper_inc проверяют включенность нижней и верхней границ значения диапазона, соответственно.

Бесконечные (неограниченные) диапазоны

Нижняя граница диапазона может быть опущена, что означает, что все точки, меньшие верхней границы, включены в диапазон, например, (,3] . Аналогично, если верхняя граница диапазона опущена, то все точки, превышающие нижнюю границу, включаются в диапазон. Если нижняя и верхняя границы опущены, все значения типа элемента считаются находящимися в диапазоне. Указание отсутствующей границы как включающей автоматически преобразуется в исключительную, например, [,] преобразуется в (,) . Можно думать об этих пропущенных значениях как +/-infinity, но они представляют собой специальные значения типа диапазона и считаются выходящими за пределы значений +/-infinity любого типа элемента диапазона.

Типы элементов, имеющие понятие «бесконечность», могут использовать их в качестве явных связанных значений. Например, в диапазонах меток времени [today,infinity) исключает специальное значение метки времени бесконечность, тогда как [today,infinity] включает его, аналогично [today,) and [today,] .

Функции lower_inf и upper_inf проверяют бесконечную нижнюю и верхнюю границы диапазона соответственно.

Диапазон ввода/вывода

Входные данные для значения диапазона должны соответствовать одному из следующих шаблонов:

(lower-bound,upper-bound) (lower-bound,upper-bound] [lower-bound,upper-bound) [lower-bound,upper-bound] empty 

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

Нижняя граница (lower-bound) может быть либо строкой, которая является допустимым вводом для подтипа, либо пустой, чтобы указать отсутствие нижней границы. Аналогично, верхняя граница (upper-bound) может быть либо строкой, которая является допустимым вводом для подтипа, либо пустой, чтобы указать отсутствие верхней границы.

Каждое связанное значение может быть заключено в кавычки с использованием символов » (двойная кавычка). Это необходимо, если связанное значение содержит круглые скобки, скобки, запятые, двойные кавычки или обратную косую черту, поскольку в противном случае эти символы были бы приняты как часть синтаксиса диапазона. Чтобы поместить двойную кавычку или обратную косую черту в указанное значение в кавычках, поставьте перед ним обратную косую черту. (Кроме того, пара двойных кавычек внутри связанного значения в двойных кавычках используется для представления символа двойной кавычки, аналогично правилам для одинарных кавычек в строках литералов SQL.). Кроме того, вы можете избежать кавычек и использовать экранирование обратной косой черты для защиты всех символов данных, которые в противном случае были бы приняты как синтаксис диапазона. Кроме того, чтобы написать связанное значение, которое является пустой строкой, напишите «», так как запись «ничего» означает бесконечная граница.

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

Заметка
Эти правила очень похожи на правила записи значений полей в литералах составного типа. См. раздел Синтаксис ввода и вывода составного типа для дополнительных комментариев.

-- includes 3, does not include 7, and does include all points in between SELECT '[3,7)'::int4range; -- does not include either 3 or 7, but includes all points in between SELECT '(3,7)'::int4range; -- includes only the single point 4 SELECT '[4,4]'::int4range; -- includes no points (and will be normalized to 'empty') SELECT '[4,4)'::int4range; 

Построение рядов

Каждый тип диапазона имеет функцию-конструктор с тем же именем, что и тип диапазона. Использование функции конструктора часто более удобно, чем запись литеральной константы диапазона, поскольку она устраняет необходимость в дополнительных кавычках связанных значений. Функция конструктора принимает два или три аргумента. Форма с двумя аргументами создает диапазон в стандартной форме (включая нижнюю границу, исключающую верхнюю границу), тогда как форма с тремя аргументами создает диапазон с границами формы, указанной в третьем аргументе. Третий аргумент должен быть одной из строк » () «, » (] «, » [) » или » [] «. Например:

-- The full form is: lower bound, upper bound, and text argument indicating -- inclusivity/exclusivity of bounds. SELECT numrange(1.0, 14.0, '(]'); -- If the third argument is omitted, '[)' is assumed. SELECT numrange(1.0, 14.0); -- Although '(]' is specified here, on display the value will be converted to -- canonical form, since int8range is a discrete range type (see below). SELECT int8range(1, 14, '(]'); -- Using NULL for either bound causes the range to be unbounded on that side. SELECT numrange(NULL, 2.2); 

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

Дискретный диапазон — это тот, чей тип элемента имеет четко определенный «шаг», такой как integer или date. В этих типах можно сказать, что два элемента являются смежными, когда между ними нет допустимых значений. Это отличается от непрерывных диапазонов, где всегда (или почти всегда) можно идентифицировать другие значения элемента между двумя заданными значениями. Например, диапазон по типу numeric является непрерывным, как и диапазон по timestamp. (Хотя метка времени (timestamp) имеет ограниченную точность и поэтому теоретически может рассматриваться как дискретная, лучше считать ее непрерывной, поскольку размер шага обычно не представляет интереса).

Другой способ думать о типе дискретного диапазона состоит в том, что существует четкое представление о «следующем» или «предыдущем» значении для каждого значения элемента. Зная это, можно выполнить преобразование между инклюзивным и эксклюзивным представлениями границ диапазона, выбрав следующее или предыдущее значение элемента вместо заданного первоначально. Например, в целочисленном диапазоне типов [4,8] и (3,9) обозначают одинаковый набор значений; но это не так для числового диапазона.

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

Все встроенные типы диапазонов int4range, int8range и daterange используют каноническую форму, которая включает нижнюю границу и исключает верхнюю границу; то есть [) . Однако определяемые пользователем типы диапазонов могут использовать другие соглашения.

Определение новых типов диапазона

Пользователи могут определять свои собственные типы диапазонов. Наиболее распространенная причина для этого — использовать диапазоны по подтипам, не предусмотренным среди встроенных типов диапазонов. Например, чтобы определить новый тип диапазона подтипа float8:

CREATE TYPE floatrange AS RANGE ( subtype = float8, subtype_diff = float8mi ); SELECT '[1.234, 5.678]'::floatrange; 

Поскольку float8 не имеет значимого «шага», мы не определяем функцию канонизации в этом примере.

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

Если считается, что подтип имеет дискретные, а не непрерывные значения, команда CREATE TYPE должна указывать функцию канонизации. Функция канонизации принимает значение входного диапазона и должна возвращать эквивалентное значение диапазона, которое может иметь различные границы и форматирование. Канонический вывод для двух диапазонов, представляющих один и тот же набор значений, например целочисленные диапазоны [1, 7] и [1, 8) , должен быть одинаковым. Неважно, какое представление вы выберете как каноническое, если два эквивалентных значения с разными форматами всегда отображаются на одно и то же значение с одинаковым форматированием. В дополнение к настройке формата включающих/исключающих границ функция канонизации может округлять граничные значения в случае, если желаемый размер шага больше, чем тот, который способен хранить подтип. Например, тип диапазона по метке времени может быть определен так, чтобы иметь размер шага в час, и в этом случае функция канонизации должна будет округлить границы, не кратные часу, или, возможно, вместо этого выдавать ошибку.

Кроме того, любой тип диапазона, который предназначен для использования с индексами GiST или SP-GiST, должен определять разность подтипов или функцию subtype_diff. (Индекс по-прежнему будет работать без subtype_diff, но он, вероятно, будет значительно менее эффективным, чем если бы была предусмотрена разностная функция). Разностная функция подтипа принимает два входных значения подтипа и возвращает их разность (то есть X минус Y ) представляется как значение float8. В примере выше может использоваться функция float8mi которая лежит в основе обычного оператора float8 minus; но для любого другого подтипа необходимо преобразование некоторых типов. Кроме того, может потребоваться творческая мысль о том, как представить различия в виде чисел. В максимально возможной степени функция subtype_diff должна согласовываться с порядком сортировки, подразумеваемым выбранным классом оператора и параметрами сортировки; то есть её результат должен быть положительным всякий раз, когда первый аргумент больше второго в соответствии с порядком сортировки.

Менее упрощенный пример функции subtype_diff:

CREATE FUNCTION time_subtype_diff(x time, y time) RETURNS float8 AS 'SELECT EXTRACT(EPOCH FROM (x - y))' LANGUAGE sql STRICT IMMUTABLE; CREATE TYPE timerange AS RANGE ( subtype = time, subtype_diff = time_subtype_diff ); SELECT '[11:10, 23:00]'::timerange; 

См. CREATE TYPE для получения дополнительной информации о создании типов диапазона.

Индексирование

Индексы GiST и SP-GiST могут быть созданы для столбцов таблицы типов диапазона. Например, чтобы создать индекс GiST:

CREATE INDEX reservation_idx ON reservation USING GIST (during);

Индекс GiST или SP-GiST может ускорять запросы с участием следующих операторов диапазона: = , && , , > , -|- , & < и &>(дополнительную информацию см. в разделе Функции диапазона и операторы).

Кроме того, B-дерев и хэш-индексы могут быть созданы для столбцов таблицы типов диапазона. Для этих типов индексов единственная полезная операция диапазона — это равенство. Для значений диапазона определен порядок сортировки B-деревьев с соответствующими операторами < и >, но этот порядок довольно произвольный и обычно не используется в реальном мире. Поддержка B-деревьев и хеш-типов диапазонов в первую очередь предназначена для внутренней сортировки и хеширования в запросах, а не для создания фактических индексов.

Ограничения для диапазонов

В то время как UNIQUE является естественным ограничением для скалярных значений, обычно он не подходит для типов диапазона. Вместо этого, часто более подходящим является ограничение исключения (см. CREATE TABLE . CONSTRAINT . EXCLUDE). Ограничения исключения позволяют задавать такие ограничения, как «неперекрывающиеся» для типа диапазона. Например:

CREATE TABLE reservation ( during tsrange, EXCLUDE USING GIST (during WITH &&) ); 

Это ограничение предотвратит одновременное существование любых перекрывающихся значений в таблице:

INSERT INTO reservation VALUES ('[2010-01-01 11:30, 2010-01-01 15:00)'); INSERT 0 1 INSERT INTO reservation VALUES ('[2010-01-01 14:45, 2010-01-01 15:45)'); ERROR: conflicting key value violates exclusion constraint "reservation_during_excl" DETAIL: Key (during)=(["2010-01-01 14:45:00","2010-01-01 15:45:00")) conflicts with existing key (during)=(["2010-01-01 11:30:00","2010-01-01 15:00:00")). 

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

CREATE EXTENSION btree_gist; CREATE TABLE room_reservation ( room text, during tsrange, EXCLUDE USING GIST (room WITH =, during WITH &&) ); INSERT INTO room_reservation VALUES ('123A', '[2010-01-01 14:00, 2010-01-01 15:00)'); INSERT 0 1 INSERT INTO room_reservation VALUES ('123A', '[2010-01-01 14:30, 2010-01-01 15:30)'); ERROR: conflicting key value violates exclusion constraint "room_reservation_room_during_excl" DETAIL: Key (room, during)=(123A, ["2010-01-01 14:30:00","2010-01-01 15:30:00")) conflicts with existing key (room, during)=(123A, ["2010-01-01 14:00:00","2010-01-01 15:00:00")). INSERT INTO room_reservation VALUES ('123B', '[2010-01-01 14:30, 2010-01-01 15:30)'); INSERT 0 1 

Типы доменов

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

Например, мы могли бы создать домен над целыми числами, который принимает только положительные целые числа:

CREATE DOMAIN posint AS integer CHECK (VALUE > 0); CREATE TABLE mytable (id posint); INSERT INTO mytable VALUES(1); -- works INSERT INTO mytable VALUES(-1); -- fails 

Когда оператор или функция базового типа применяется к значению домена, домен автоматически преобразуется в базовый тип. Так, например, считается, что результат mytable.id — 1 имеет тип integer, а не posint. Мы могли бы написать (mytable.id — 1)::posint чтобы привести результат обратно к posint, в результате чего ограничения домена будут перепроверены. В этом случае это приведет к ошибке, если выражение было применено к значению идентификатора 1. Присвоение значения базового типа полю или переменной типа домена допускается без написания явного приведения, но ограничения домена будут проверены.

Для получения дополнительной информации см. CREATE DOMAIN.

Типы идентификаторов объектов

Идентификаторы объектов (OID) используются внутри QHB в качестве первичных ключей для различных системных таблиц. Тип oid представляет идентификатор объекта. Существует также несколько типов псевдонимов для oid: regproc, regprocedure, regoper, regoperator, regclass, regtype, regrole, regnamespace, regconfig и regdictionary. В Таблице 26 приведена общая информация по типам идентификаторов объектов.

Тип oid в настоящее время реализован как беззнаковое четырехбайтовое целое число. Следовательно, он недостаточно велик, чтобы обеспечить уникальность всей базы данных в больших базах данных или даже в больших отдельных таблицах.

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

Типы псевдонимов OID не имеют собственных операций, кроме специализированных процедур ввода и вывода. Эти подпрограммы могут принимать и отображать символические имена для системных объектов, а не исходное числовое значение, которое будет использовать тип oid. Типы псевдонимов позволяют упростить поиск значений OID для объектов. Например, чтобы проверить строки pg_attribute относящиеся к таблице mytable, можно написать:

SELECT * FROM pg_attribute WHERE attrelid = 'mytable'::regclass; 
SELECT * FROM pg_attribute WHERE attrelid = (SELECT oid FROM pg_class WHERE relname = 'mytable'); 

Хотя это само по себе выглядит не так уж и плохо, все же упрощенно. Для выбора правильного OID потребуется гораздо более сложный суб-выбор, если в разных схемах есть несколько таблиц с именем mytable. Преобразователь ввода regclass обрабатывает поиск в таблице в соответствии с настройкой пути к схеме, поэтому автоматически выполняет «правильные действия». Точно так же приведение OID таблицы к regclass удобно для символического отображения числового OID.

Таблица 26. Типы идентификаторов объектов

Имя Ссылки Описание Пример значения
oid Любые числовой идентификатор объекта 564182
regproc pg_proc имя функции sum
regprocedure pg_proc функция с типами аргументов sum(int4)
regoper pg_operator имя оператора +
regoperator pg_operator оператор с типами аргументов *(integer,integer) или -(NONE,integer)
regclass pg_class имя отношения pg_type
regtype pg_type имя типа данных integer
regrole pg_authid название роли smithee
regnamespace pg_namespace имя пространства имен pg_catalog
regconfig pg_ts_config конфигурация текстового поиска english
regdictionary pg_ts_dict словарь текстового поиска simple

Все типы псевдонимов OID для объектов, сгруппированных по пространству имен, принимают имена, соответствующие схеме, и будут отображать имена, соответствующие схеме, при выводе, если объект не будет найден в текущем пути поиска без проверки. Типы псевдонимов regproc и regoper будут принимать только входные имена, которые являются уникальными (не перегруженными), поэтому они имеют ограниченное использование; для большинства применений более подходящими являются regprocedure или regoperator. Для regoperator унарные операторы идентифицируются путем написания NONE для неиспользованного операнда.

Дополнительным свойством большинства типов псевдонимов OID является создание зависимостей. Если константа одного из этих типов появляется в сохраненном выражении (например, в выражении по умолчанию для столбца или в представлении), она создает зависимость от ссылочного объекта. Например, если столбец имеет выражение по умолчанию nextval(’my_seq’::regclass), QHB понимает, что выражение по умолчанию зависит от последовательности my_seq; система не позволит удалить последовательность без предварительного удаления выражения по умолчанию. regrole является единственным исключением для свойства. Константы этого типа не допускаются в таких выражениях.

Заметка
Типы псевдонимов OID не полностью соответствуют правилам изоляции транзакций. Планировщик также рассматривает их как простые константы, что может привести к неоптимальному планированию.

Другим типом идентификатора, используемым системой, является xid или идентификатор транзакции (сокращенно xact). Это тип данных системных столбцов xmin и xmax. Идентификаторы транзакции являются 32-битными величинами.

Третий тип идентификатора, используемый системой — cid, или идентификатор команды. Это тип данных системных столбцов cmin и cmax. Идентификаторы команд также являются 32-битными величинами.

Последний тип идентификатора, используемый системой — это tid, или идентификатор кортежа (идентификатор строки). Это тип данных системного столбца ctid. Идентификатор кортежа — это пара (номер блока, индекс кортежа в блоке), который идентифицирует физическое местоположение строки в своей таблице.

(Системные столбцы более подробно описаны в разделе Системные столбцы).

Тип pg_lsn

Тип данных pg_lsn может использоваться для хранения данных LSN (порядкового номера журнала — Log Sequence Number), которые являются указателем на местоположение в WAL. Этот тип является представлением XLogRecPtr и внутренним системным типом QHB.

Внутренне LSN представляет собой 64-разрядное целое число, представляющее позицию байта в потоке журнала упреждающей записи. Он печатается в виде двух шестнадцатеричных чисел длиной до 8 цифр, разделенных косой чертой; например, 16/B374D848. Тип pg_lsn поддерживает стандартные операторы сравнения, такие как = и > . Два номера LSN могут быть вычтены с помощью оператора — ; результат — число байтов, разделяющих эти местоположения в wal-журнале.

Псевдо-типы

Система типов QHB содержит ряд записей специального назначения, которые в совокупности называются псевдотипами. Псевдотип нельзя использовать в качестве типа данных столбца, но его можно использовать для объявления аргумента функции или типа результата. Каждый из доступных псевдотипов полезен в ситуациях, когда поведение функции не соответствует простому взятию или возвращению значения определенного типа данных SQL. В таблице 7.27 перечислены существующие псевдотипы.

Таблица 27. Псевдо-типы

Имя Описание
any Указывает, что функция принимает любой тип входных данных.
anyelement Указывает, что функция принимает любой тип данных (см. раздел Полиморфные типы).
anyarray Указывает, что функция принимает любой тип данных массива (см. раздел Полиморфные типы).
anynonarray Указывает, что функция принимает любой тип данных, отличный от массива (см. раздел Полиморфные типы).
anyenum Указывает, что функция принимает любой тип данных enum (см. разделы Полиморфные типы и Перечисляемые типы).
anyrange Указывает, что функция принимает любой тип данных диапазона (см. разделы Полиморфные типы и Типы диапазонов).
cstring Указывает, что функция принимает или возвращает строку C с нулевым символом в конце.
internal Указывает, что функция принимает или возвращает внутренний тип данных сервера.
language_handler Обработчик вызова процедурного языка объявляется как возвращающий language_handler.
fdw_handler Обработчик обёртки fdw_handler данных объявляется как возвращающий fdw_handler.
index_am_handler Обработчик метода доступа к индексу объявлен как возвращающий index_am_handler.
tsm_handler Обработчик метода tableample объявлен как возвращающий tsm_handler.
record Определяет функцию, принимающую или возвращающую неопределенный тип строки.
trigger Объявлена триггерную функция для возврата trigger.
event_trigger Объявлена функция запуска события, которая возвращает event_trigger.
pg_ddl_command Определяет представление команд DDL, доступных для триггеров событий.
void Указывает, что функция не возвращает значения.
unknown Определяет еще не разрешенный тип, например, недекорированный строковый литерал.
opaque Устаревшее имя типа, которое раньше служило многим из вышеперечисленных целей.

Функции, закодированные в C/Rust (встроенные или динамически загружаемые), могут быть объявлены для принятия или возврата любого из этих псевдо-типов данных. Автор функции должен гарантировать, что функция будет вести себя безопасно, когда псевдотип используется в качестве типа аргумента.

Функции, закодированные в процедурных языках, могут использовать псевдотипы только в соответствии с их языками реализации. В настоящее время большинство процедурных языков запрещают использование псевдотипа в качестве типа аргумента и разрешают только void и record в качестве типа результата (плюс trigger или event_trigger когда функция используется в качестве триггера или триггера события). Некоторые также поддерживают полиморфные функции, используя типы anyelement, anyarray, anynonarray, anyenum и anyrange.

Псевдотип internal используется для объявления функций, предназначенных только для внутреннего вызова системой базы данных, а не путем прямого вызова в запросе SQL. Если у функции есть хотя бы один аргумент типа internal, ее нельзя вызвать из SQL. Чтобы сохранить безопасность типов этого ограничения, важно следовать этому правилу кодирования: не создавайте никакую функцию, которая объявляется возвращающей internal если у нее нет хотя бы одного internal аргумента.

Для этой цели термин « значение » включает элементы массива, хотя в терминологии JSON иногда элементы массива отличаются от значений внутри объектов.

Ruby/Для начинающих

Изначально числа представлены тремя типами: два целых типа (классы Fixnum и Bignum ) и один с плавающей запятой (класс Float ). Возможно подключение дополнительных типов, например, комплексных и рациональных чисел, но пока ограничимся тремя.

Целые числа [ править ]

Целые числа в Ruby не ограничены по величине, то есть могут хранить сколь угодно большие значения. Для обеспечения такого волшебного свойства было создано два класса. Один из них хранит числа меньше 2 30 > (по модулю), а второй — всё, что больше. По сути, для больших чисел создаётся массив из маленьких, а раз массив не имеет ограничений по длине, то и число получается неограниченным по значению.

Если, например, написать

puts 54308428790203478762340052723346983453487023489987231275412390872348475 ** 54308428790203478762340052723346983453487023489987231275412390872348475 

то интерпретатор начинает ругаться и выдаёт Infinity . Можно подумать, что он не может обработать такое большое число. Конечно же это не так, что становится ясно после прочтения выдаваемого интерпретатором предупреждения. Там написано, что показатель степени (то есть второе число) не может быть типа Bignum (чтобы не пришлось слишком много считать).

Как ни странно, 2 30 > определяется как Bignum

(2**30).class #=> Bignum 

Однако, целое число, меньшее (по модулю) 2 30 > определяется как Fixnum

((2**30)-1).class #=> Fixnum (-(2**30)+1).class #=> Fixnum 

Как только число типа Fixnum становится больше или равным 2 30 > (по модулю), то оно преобразуется к классу Bignum . Если число типа Bignum становится меньше 2 30 > , то оно преобразуется к типу Fixnum .

При записи целых чисел сначала указывается знак числа (знак + обычно не пишется). Далее идёт основание системы счисления, в которой задаётся число (если оно отлично от десятичной): 0 — для восьмеричной, 0x — для шестнадцатеричной, 0b — для двоичной. Затем идёт последовательность цифр, выражающих число в данной системе счисления. При записи чисел можно использовать символ подчёркивания, который игнорируется при обработке. Чтобы закрепить вышесказанное, посмотрим примеры целых чисел:

# тип Fixnum 123_456 # подчёркивание игнорируется -567 # отрицательное число 0xbad # шестнадцатеричное число 0377 # восьмеричное -0b101010 # отрицательное двоичное 0b0101_0101 # подчёркивание игнорируется # тип Bignum 123_456_789_123_456 # подчёркивание игнорируется -123_456_789_123_456 # отрицательное 07777777777777777777 # восьмеричное большое 

Как видно из примеров, маленькие целые ( Fixnum ) и больши́е целые ( Bignum ) отличаются только значением.

Числа с плавающей запятой [ править ]

Числа с плавающей запятой задаются только в десятичной системе счисления, при этом для отделения дробной части используется символ . (точка). Для задания чисел с плавающей запятой может быть применена и экспоненциальная форма записи: два различных представления 0.1234e2 и 1234e-2 задают одно и то же число 12.34 .

# тип Float -12.34 # отрицательное число с плавающей запятой 0.1234е2 # экспоненциальная форма для числа 12.34 1234е-2 # экспоненциальная форма для числа 12.34 

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

Семейный портрет чисел [ править ]

В отличие от большинства элементарных типов данных, числа обладают своей иерархией. Все числа в Ruby наследованы от класса Numeric (числовой). Поэтому, если хотите добавить новый метод ко всем числам, то нужно расширять именно этот класс. Далее идёт деление чисел: Integer (целое), Float (число с плавающей запятой) и Complex (комплексное). При желании можно добавить и Rational (рациональное), но на данном семейном портрете оно отсутствует.

От класса Integer наследуются два класса: Fixnum (фиксированное целое) и Bignum (большое целое). К первому относятся все числа, по модулю меньшие 2 30 > , а ко второму — все остальные.

  • Fixnum автоматически становится Bignum по превышении 2 30 > по модулю. И наоборот, падая ниже, Bignum преобразуется в Fixnum .
  • Из отрицательного числа можно получить корень, когда подключена библиотека mathn . Он будет типа Complex .
  • Как только число типа Complex лишается мнимой части, то оно становится либо Integer ( Fixnum или Bignum ), либо Float (в зависимости от типа действительной части). Если подключена библиотека mathn , может получиться число типа Rational .
  • Если в результате арифметических действий в числе типа Rational знаменатель приравнивается 1 , то оно преобразуется к числу Integer .

Арифметические операции [ править ]

Арифметические операции в Ruby обычны: сложение ( + ), вычитание ( — ), умножение ( * ), деление ( / ), получение остатка от деления ( % ), возведение в степень ( ** ).

6 + 4 #=> 10 6 - 4 #=> 2 6 * 4 #=> 24 6 / 4 #=> 1 6 % 4 #=> 2 6 ** 4 #=> 1296 

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

Порядок вычисления обычный. Для изменения приоритета применяются круглые скобки:

2 + 2 * 2 #=> 6 (2 + 2) * 2 #=> 8 

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

1/3 #=> 0 2/3 #=> 0 3/3 #=> 1 

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

Одна вторая в Ruby ноль,
А три вторые — единица.
Запомнить надо эту соль,
Чтоб результату не дивиться.

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

6.0 + 4 #=> 10.0 6 - 4.0 #=> 2.0 6.0 * 4.0 #=> 24.0 6.0 / 4 #=> 1.5 (одно из чисел с плавающей запятой, значит результат с плавающей запятой) 6.0 % 4 #=> 2.0 6 ** 4.0 #=> 1296.0 

Лучше проверить эти сведения самостоятельно.

Поразрядная арифметика [ править ]
Знак операции Название
& Побитовое «и»
| Побитовое «или»
^ Побитовое «исключающее или»
Побитовый сдвиг влево
>> Побитовый сдвиг вправо
~ Побитовая инверсия

Операции побитовой арифметики заимствованы из языка Си. На этот раз без всяких экзотических особенностей.

6 & 4 #=> 4 6 | 4 #=> 6 6 ^ 4 #=> 2 6  4 #=> 96 6 >> 4 #=> 0 (чересчур намного сдвинули) ~4 #=> -5 (операция только над одним аргументом) 

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

Операции с присваиванием [ править ]

Часто можно встретить выражения вида:

number_one += number_two 

Это выполнение операции сразу с присваиванием. Вышеуказанная запись равнозначна следующей:

number_one = number_one + number_two 

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

string = "едем" string += ", " string *= 3 string #=> "едем, едем, едем, " array = [1, 2, 3] array += [4, 5] array #=> [1, 2, 3, 4, 5] 

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

Методы явного преобразования типов [ править ]

Метод Операция
to_f Преобразовать в число с плавающей запятой
to_i Преобразовать в целое число
to_s Преобразовать в строку
to_a Преобразовать в массив (до версии 1.9+)

Методы преобразования типов в Ruby традиционно начинаются с приставки to_ . Последующая буква — это сокращение от названия класса, в который происходит преобразование ( f — Float — число с плавающей запятой, i — Integer — целое, s — String — строка, a — Array — массив). Посмотрим их действие на примере:

7.to_f #=> 7.0 7.9.to_i #=> 7 7.to_s #=> "7" "7".to_a #=> ["7"] 

Случайное число [ править ]

Часто требуется получить случайное число. Пример:

rand(100) #=> 86 rand #=> 0.599794231588021 

В первом случае метод rand возвращает целое число в диапазоне от 0 до 99 (на единицу меньше 100). Во втором случае метод rand возвращает число с плавающей запятой в диапазоне от 0.0 до 1.0 включительно. Различие в результате обусловлено передаваемым параметром:

  • если передаётся параметр (в данном случае 100 ), то генерируется целое случайное число (в диапазоне 0..N-1 , где N — передаваемый аргумент);
  • если параметр отсутствует, то генерируется число с плавающей запятой в диапазоне от 0.0 до 1.0 .

Есть способ предсказать весь ряд «случайных» чисел. Делается это при помощи метода srand . Ему передаётся целое число (идентификатор «случайной» последовательности). После этого весь случайный ряд можно предугадать. Проведём опыт: берусь угадать массив, который будет создан следующей программой.

srand 123 Array.new(5) rand(100) > #=> [66, 92, 98, 17, 83] 

Если вы выполните данную программу у себя, то получите тот же самый массив. 123 — номер «случайной» последовательности. Измените его и массив изменится!

Если вызвать srand без параметра или не вызывать его вообще, то номер «случайной» последовательности выбирается случайным образом.

Хитрости [ править ]

start_number = 1234 puts sprintf("%b", start_number) # метод sprintf заимствован из Си puts start_number.to_s(2) # современный метод — означает «по основанию», # аргументом может служить не только 8 и 16, но и 5, 30… # На самом деле, основание не может превышать 36, # что вполне объяснимо — 10 цифр и 26 букв латинского алфавита. 

Поменять порядок цифр данного числа на обратный:

start_number = 1234 puts start_number.to_s.reverse # метод reverse переворачивает строку 

Получить значение N-го двоичного разряда данного целого числа:

start_number, N = 1234, 5 puts start_number[N] 

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

number_one, number_two = 134, 234 number_one, number_two = number_two, number_one 

Округлить число с плавающей запятой до двух разрядов:

float_integer = 3.1415926535 puts (float_integer * 100).to_i.to_f / 100 puts ((float_integer + 0.005) * 100).to_i / 100.0 puts sprintf("%.2f", float_integer).to_f # полуСишный способ =) 

На самом деле во второй строке оставляются два знака после запятой, а остальные просто отбрасываются безо всяких округлений, в то время как в третьей строке действительно происходит округление до двух знаков после запятой. Это легко проверить попытавшись округлить до трёх знаков после запятой:

float_integer = 3.1415926535 puts (float_integer * 1000).to_i.to_f / 1000 #=>3.141 puts ((float_integer + 0.0005) * 1000).to_i / 1000.0 #=>3.142 puts sprintf("%.3f", float_integer).to_f #=>3.142 

Но всё же лучше:

float_integer = 3.1415926535 float_integer.round 3 #=>3.142 (возможно, что round округляет только до целых) 

Подробнее о массивах [ править ]

Массивы — это тип данных, с которым вам придётся работать постоянно. Облик большинства программ зависит именно от правильного (читай: «изящного») использования массивов.

Способы создания массива [ править ]

Массив создаётся как минимум тремя способами. Первый способ:

[1, 2, 3, 4, 5, 6] 

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

Array.new(6) |index| index + 1 > #=> [1, 2, 3, 4, 5, 6] 

Параметром метода .new является количество элементов будущего массива (в данном случае это число 6). В фигурных скобках указано, как мы будем заполнять массив. В данном случае значение элемента массива будет больше на единицу его индекса. Третий способ заключается в создании объекта типа Range (диапазон) и вызове метода .to_a :

(1..6).to_a #=> [1, 2, 3, 4, 5, 6] 

Есть ещё много способов, но эти три используются чаще всего.

Диапазоны [ править ]

Методом to_a очень удобно создавать из диапазона массив, содержащий упорядоченные элементы данного диапазона.

(1..10).to_a #=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] ("a".."d").to_a #=> ["a", "b", "c", "d"] 

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

["a", "b", "c", "d", "e"] 

Традиционно нумерация массива начинается с нуля и возрастает по одному:

Такая нумерация называется в Ruby положительной индексацией. «Хм, — скажете вы, — а есть ещё и отрицательная?» Да, есть!

Плюсы расставлены лишь для красоты. Но вернёмся к отрицательной индексации. Каков её смысл? Чтобы его пояснить, давайте решим задачку: дан массив, требуется получить предпоследний элемент.

array = ["a", "b", "c", "d", "e"] array[array.size - 2] #=> "d" 

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

array = ["a", "b", "c", "d", "e"] array[-2] #=> "d" 

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

array = ["a", "b", "c", "d", "e"] array[1..-2] #=> ["b", "c", "d"] 
array = ["a", "b", "c", "d", "e"] array[1. -1] #=> ["b", "c", "d"] 

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

О двумерных массивах [ править ]

Для Ruby двумерный массив — это не более чем массив, содержащий одномерные массивы. Вот несколько примеров двумерных массивов:

[[1], [2, 3], [4]] # разная длина элементов-массивов [[1, 2], [3, 4]] # одинаковая длина [["прива", "Привет"], ["пока", "Всего хорошего"]] # двумерный массив (классика) [["прива", "Привет"], [1, ["пока", "Всего хорошего"]]] # гибрид двух-трёх-мерного массива 
  • Двумерность массива средствами языка не отслеживается. Вполне могут возникнуть гибриды разномерных массивов.
  • Подмассивы внутри двумерного массива могут иметь произвольную длину.
  • Элементы из двумерного массива достаются последовательно: сначала элемент-массив, потом элемент.

Методы работы с массивами [ править ]

Разнообразие и полезность методов у массивов создаёт впечатление, что все сложные алгоритмы уже реализованы. Это не так, но программистам Ruby дана действительно обширная библиотека методов. Здесь мы рассмотрим лишь самые употребимые, остальные ищите в справочнике.

Получение размера массива [ править ]

В Ruby массивы динамические: в каждый конкретный момент времени неизвестно, сколько в нём элементов. Чтобы не плодить тайн подобного рода и был реализован метод .size :

[1, "считайте", 3, "количество", 5, 6, "запятых", 2, 5].size #=> 9 

Мы явно указали массив, но на его месте могла стоять переменная:

array = [1, "считайте", 3, "количество", 5, 6, "запятых", 2, 5] array.size #=> 9 

Метод .size есть у многих классов. Например, у ассоциативных массивов и строк. И даже у целых чисел.

Поиск максимального/минимального элемента [ править ]

Вспомните сколько усилий вам приходилось прилагать, чтобы найти максимальный элемент? А сколько раз вы повторяли этот кусок кода в своих программах? Ну а в Ruby поиск максимального элемента осуществляется при помощи метода .max , а в более сложных случаях при помощи метода .max_by . Вот как это выглядит:

["у", "попа", "была", "собака"].max #=> "у" максимальный по значению ["у", "попа", "была", "собака"].max_by |elem| elem.size > #=> "собака" максимальный по размеру строки 

Методы .min и .min_by работают аналогично:

["у", "попа", "была", "собака"].min #=> "была" минимальный по значению ["у", "попа", "была", "собака"].min_by |elem| elem.size > #=> "у" минимальный по размеру строки 

Ну как? А в Ruby эти методы уже давно. Подробнее о том, как работает хеш «под капотом» можно дополнительно прочесть в статье.

Упорядочение [ править ]

Чтобы упорядочить массив, нужно вызвать метод .sort или .sort_by (начиная с версии 1.8).

["у", "попа", "была", "собака"].sort #=> ["была", "попа", "собака", "у"] сортировка по значению ["у", "попа", "была", "собака"].sort_by |elem| elem.size > #=> ["у", "попа", "была", "собака"] сортировка по размеру строки 

Для двумерных массивов:

[[1,0], [16,6], [2,1], [4,5],[4,0],[5,6]].sort_by |elem| elem[1]> #=> [[1, 0], [4, 0], [2, 1], [4, 5], [16, 6], [5, 6]] сортировка "внешних" элементов по значению "внутренних" [[1,0], [16,6], [2,1], [4,5],[4,0],[5,6]].sort_by |elem| elem[0]> #=> [[1, 0], [2, 1], [4, 0], [4, 5], [5, 6], [16, 6]] 

Остается только добавить, что массивы упорядочиваются по возрастанию. Если вам надо по убыванию, то придётся писать собственный метод сортировки пузырьком. Шутка! По правде же, есть много способов выстроить массив по убыванию. Пока мы будем использовать метод .reverse , обращающий массив.

Обращение массива [ править ]

Обращение массива — это изменение порядка элементов на обратный, то есть первый элемент становится последним, второй элемент — предпоследним и так далее.

Для обращения массива существует метод .reverse . Применим его к предыдущим примерам, чтобы получить сортировку по убыванию:

["у", "попа", "была", "собака"].sort.reverse #=> ["у", "собака", "попа", "была"] ["у", "попа", "была", "собака"].sort_by |elem| elem.size >.reverse #=> ["собака", "была", "попа", "у"] 

Метод .reverse мы просто прицепили в конец предыдущего примера. Так можно выстроить произвольную цепочку допустимых методов; выполняться они будут по очереди, начиная с самого левого, то есть самого первого в цепочке.

Сложение/вычитание массивов [ править ]

Для сложения массивов, строк и чисел используется метод + :

[1, 2, 3, 4] + [5, 6, 7] + [8, 9] #=> [1, 2, 3, 4, 5, 6, 7, 8, 9] 

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

Вычитаются массивы методом — , но происходит это сложнее, чем расцепление вагонов:

[1, 1, 2, 2, 3, 3, 3, 4, 5] - [1, 2, 4] #=> [3, 3, 3, 5] 

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

Объединение и пересечение массивов (как множеств) [ править ]

Очень часто приходится решать задачи, в которых нужно оперировать множествами. У массивов припасено для этих целей два метода: | (объединение) и & (пересечение).

Рассмотрим объединение множеств в действии:

[1, 2, 3, 4, 5, 5, 6] | [0, 1, 2, 3, 4, 5, 7] #=> [1, 2, 3, 4, 5, 6, 0, 7] 

Объединение получается вот так. Сначала массивы сцепляются:

[1, 2, 3, 4, 5, 5, 6, 0, 1, 2, 3, 4, 5, 7] 

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

На деле это выглядит так:

[1, 2, 3, 4, 5, -5-, 6, 0, -1-, -2-, -3-, -4-, -5-, 7] 

Зачёркнутые числа — это удаленные элементы (дубликаты). Переходим от слов к делу:

[1, 2, 3, 4, 5, 5, 6] & [0, 2, 1, 3, 5, 4, 7] #=> [1, 2, 3, 4, 5] 

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

[1, 2, 3, 4, 5, -5-, -6-] & [-0-, 2, 1, 3, 5, 4, -7-] 

В итоге пересечения или объединения множеств получается массив, не содержащий дубликатов.

Удаление дубликатов [ править ]

Для удаления дубликатов (повторяющихся элементов массива) в Ruby используется метод .uniq :

[1, 2, 3, 4, 5, 5, 6, 0, 1, 2, 3, 4, 5, 7].uniq #=> [1, 2, 3, 4, 5, 6, 0, 7] 

Процесс зачистки массива от дубликатов такой же, как и в объединении.

[1, 2, 3, 4, 5, -5-, 6, 0, -1-, -2-, -3-, -4-, -5-, 7] 

Поэтому объединение массивов можно записать как

(array1 + array2).uniq 

Но проще, конечно, объединять палкой.

Сплющивание массивов [ править ]

Метод .flatten делает из многомерного массива простой, длинный одномерный массив. Он как бы расплющивает его. Например, чтобы найти в двумерном массиве наибольший элемент, мы сперва расплющим массив, а потом найдём максимум методом .max :

array = [[1, 2], [3, 4]] array.flatten.max #=> 4 

Расплющивание происходит в несколько этапов. Сначала происходит удаление всех квадратных скобок.

-[[- 1, 2 -]-, -[- 3, 4 -]]- 

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

[1, 2, 3, 4] 

Вот и всё! У нас они разбежаться не успели. Повторите данное упражнение на других массивах (двумерных, трёхмерных и так далее).

Удаление неопределённых (nil) элементов [ править ]

Функцию удаления элементов nil массива выполняет метод .compact например:

array = [1, nil, 2, nil, 3] array.compact #=> [1, 2, 3] 
Транспонирование двумерного массива [ править ]

Задача: дан двумерный массив. Вывести одномерный массив с максимумами каждого из столбцов. Хм… посмотрим сперва, как эта задача решается для строчек, а не столбцов:

array2D = [[1, 2], [3, 4]] array2D.map |array| array.max > #=> [2, 4] 

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

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

array2D = [[1, 2], [3, 4]] array2D.transpose.map |array| array.max > #=> [3, 4] 

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

Размножение массивов [ править ]

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

["много", "денег", "прячет", "тёща"] * 2 #=> ["много", "денег", "прячет", "тёща", "много", "денег", "прячет", "тёща"] 

Того же самого эффекта можно добиться сцепив массив необходимое количество раз:

array = ["много", "денег", "прячет", "тёща"] array + array #=> ["много", "денег", "прячет", "тёща", "много", "денег", "прячет", "тёща"] 

Заметили, что есть некоторая параллель с целыми числами? Умножение можно заменить сложением и наоборот!

Функциональность стека [ править ]

Часто и во многих алгоритмах надо добавить элемент в конец массива:

array = [1, 2, 3, 4, 5] array[array.size] = 6 array #=> [1, 2, 3, 4, 5, 6] 

И если уж добавили, то надо как-то его и удалить. Делается это примерно так:

array = [1, 2, 3, 4, 5, 6] array[0. -1] #=> [1, 2, 3, 4, 5] 

Но как всегда, эти задачи возникали слишком часто и их решили реализовать в виде методов. Методы назвали .push («втолкнуть» в конец массива) и .pop («вытолкнуть» элемент из массива):

array = [1, 2, 3, 4, 5] array.push(6) array #=> [1, 2, 3, 4, 5, 6] array  7 #=> [1, 2, 3, 4, 5, 6, 7], другой синтаксис array.pop #=> 7 array #=> [1, 2, 3, 4, 5, 6] 
Функциональность очереди и списка [ править ]

Чтобы можно было использовать массив в качестве очереди и/или списка, потребуется сделать всего лишь пару методов. Первый из них добавляет элемент в начало массива, а второй удаляет элемент из начала. Давайте посмотрим, как это делается универсальными методами [] , []= и + :

array = [1, 2, 3, 4, 5] # добавим элемент в начало массива # способ № 1 array = [6] + array array #=> [6, 1, 2, 3, 4, 5] array[0] #=> 6 # способ № 2 array[1..array.size] = array[0..-1] #=> [1, 1, 2, 3, 4, 5] array[0] = 6 array #=> [6, 1, 2, 3, 4, 5] # удалим элемент из начала массива array = array[1..-1] array #=> [1, 2, 3, 4, 5] 

Теперь посмотрим, какие методы реализуют точно такую же функциональность:

array = [1, 2, 3, 4, 5] # добавляем элемент в начало массива array.unshift(6) #=> [6, 1, 2, 3, 4, 5] # удаляем из начала массива array.shift #=> 6 array #=> [1, 2, 3, 4, 5] 

Удаляющий метод — .shift («сдвинуть»), а метод, добавляющий элемент в начало массива, называется .unshift (непереводимое слово, означающее нечто противоположное «сдвинуть обратно»).

Мнемограмма для методов стека/очереди/списка [ править ]

Мнемограмма для методов .shift , .unshift , .pop и .push :

.unshift(0) .push(6) [1, 2, 3, 4, 5] .shift .pop 

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

Метод .shift сдвигает влево,
Метод .pop — направо.
Метод .push к концу цепляет,
А .unshift — к началу.

Замечание. Имена методов unshift / shift неинтуитивны. Во-первых, они не напоминают о том, что работа идёт с головой массива, а не с хвостом, во-вторых ничего не говорят о том, идёт заполнение или опустошение стека. Можно создать для этих методов псевдонимы с говорящими именами, например, feed / spit (кормить/выплевывать):

class Array alias feed :unshift alias spit :shift end 
Создание своих классов, работающих как массивы [ править ]

Если потребуется написание своего класса, который работает, как описанные массивы, то возникают некоторые тонкости. Дело в том, что реализация всех описанных методов займёт жуткое количество времени и сил. На самом деле, для реализации большинства описанных методов, достаточно реализовать .each .

Где это может понадобиться? Например, вы реализуете класс, который умеет читать из файла записи определённой структуры. Основную его логику занимает именно чтение нужного формата, кеширование, разбор, десериализация и тому подобное.

Просто реализуйте .each и включите в ваш класс примесь Enumerable . В нём находится реализация методов, таких как .inject , .each_with_index и тому подобные.

Логические методы [ править ]

Логический метод — это метод, результатом которого является логическое выражение ( true или false ).

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

array = [1, 2, 2, 3] puts array.methods.grep(/\?$/) 

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

array = [1, 2, 2, 3] puts array.methods.grep(/\?$/).sort 
Есть ли элемент в массиве? [ править ]

Как узнать, есть ли некоторый элемент в массиве? Попробуем решить эту задачу при помощи метода .size и итератора .find_all :

array = [1, 2, 3, 4, 5, 6, 7] required = 5 # число, которое мы будем искать array.find_all |elem| elem == required >.size != 0 #=> true # это значит, что такое число есть 

Использование связки из трёх методов ( != , .find_all и .size ) для такой задачи — возмутительно! Разработчики не могли с этим долго мириться и реализовали метод специально для этой задачи. Имя ему — .include? . Перепишем нашу задачу, но на этот раз будем использовать правильный метод:

array = [1, 2, 3, 4, 5, 6, 7] required = 5 # число, которое мы будем искать array.include?(required) #=> true # что бы это значило? 

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

lake = ["правый берег", "ветер", "вода", "вода", "вода", "окунь", "вода", "вода", "левый берег"] lake.include?("акула") #=> false 

Опытным путём мы доказали, что акулы в озере не водятся.

Массив пустой? [ править ]

Если вы хотите задать массиву вопрос «пуст ли ты?», но боитесь обидеть, то можете пойти окружным путём. Например, спросить у него: ты равен пустому массиву?

empty_array = [] filled_array = [1, 2, 2, 3] empty_array == [] #=> true filled_array == [] #=> false 

Ещё можно задать вопрос: твой размер равен нулю?

empty_array = [] filled_array = [1, 2, 2, 3] empty_array.size == 0 #=> true filled_array.size == 0 #=> false 

Но наш вам совет: не стоит искать обходных путей. Спросите его напрямую: .empty? («пуст?»):

empty_array = [] filled_array = [1, 2, 2, 3] empty_array.empty? #=> true filled_array.empty? #=> false 
И наоборот [ править ]

В Ruby принято избегать отрицания условия. Например, если вам нужно сделать что-то, если массив не пуст, можно воспользоваться методом, обратным empty? . Этот метод называется any? .

array = [1, 2, 4] array.length > 0 #=> true array.empty? #=> false array.any? #=> true 

Итераторы [ править ]

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

В старину люди делали это циклами. В Ruby у списочных структур данных есть встроенные методы, которые проходят весь ряд поэлементно, но, в отличие от циклов:

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

Имя им — итераторы.

Изменение всех элементов массива [ править ]

Изменить все элементы массива можно по-всякому. Начнём с обнуления:

array = ["шифровка", "Штирлица", "в", "Центр", "секретно"] array.map 0 > #=> [0, 0, 0, 0, 0] 

В приведенном примере каждый элемент массива будет заменён нулем независимо от того, чем является этот элемент. Например, при попытке обнулить таким образом двумерный массив [[1, 2], [3, 4]] , в результате получим [0, 0] .

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

Можно дать элементу .map иное задание. Для этого зажимаем в фигурные скобы замыкания иной код:

array = [1, 2, 3, 4, 5] array.map |elem| elem ** 2 > #=> [1, 4, 9, 16, 25] 

Прежде, чем замыканию выдать квадрат очередного элемента, ему нужно знать этот элемент. Итератор .map даёт ему значение элемента, словно фотографию, обрамлённую слева и справа вертикальными чертами | . Чтобы замыкание смогло взять эту фотографию, обязательно нужно дать ей имя. В нашем случае это elem , но подходят и такие названия:

Но недопустимы названия вроде Like_This или 3This . Правила именования тут такие же, как и для обычных переменных.

Вы уже, наверное, хорошо поняли, что в итераторах массивы обрабатываются по очереди; двери вагона расходятся, появляется элемент. Замыкание даёт ему прозвище, выполняет код в лапках-фигурных скобках. Затем переходит к следующему вагону, и там всё сначала.

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

То имя, что показывается в раздвижных дверях, — это не сам элемент, это лишь его копия. Фотография. Голограмма. Это даже не другая переменная, это не переменная вообще. Бессмысленно присваивать новое значение фотографии:

array = [1, 2, 3, 4, 5] array.map |elem| elem = elem**2 > # присваивание не имеет смысла: elem несёт лишь значение элемента, не являясь им 

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

array = [1, 2, 3, 4, 5] array.map |elem| elem**2 > #=> [1, 4, 9, 16, 25] array #=> [1, 2, 3, 4, 5] — неизменный первый поезд 

Можно присвоить его новой переменной array_of_squares . А можно заместить им существующую переменную array :

array = [1, 2, 3, 4, 5] array = array.map |elem| elem**2 > #=> [1, 4, 9, 16, 25] array #=> [1, 4, 9, 16, 25] 

Это общее явление Ruby: методы (здесь — итераторы) не меняют объект (массив), с которым работают. Они лишь выдают результат, который потом можно использовать как аргумент или присвоить переменной.

Явление было воспето в фольклоре:

Метод .map всё изменяет,
Как кто пожелает
И обижается на тех,
Кто результат не сохраняет.

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

array = [1, 2, 3, 4, 5] array.map! |elem| elem**2 > #=> [1, 4, 9, 16, 25] array #=> [1, 4, 9, 16, 25] 

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

array = [1, 2, 3, 4, 5, nil] array.compact.map! |elem| elem**2 > #=> [1, 4, 9, 16, 25] array #=> [1, 2, 3, 4, 5, nil] array.compact!.map! |elem| elem**2 > #=> [1, 4, 9, 16, 25] array #=> [1, 4, 9, 16, 25] 

В первом случае операция .compact создала копию массива, тогда как .compact! заменила первоначальные значения результатами, полученными от .map!

Отбор элементов по признаку [ править ]

Вот как итератор .find_all выберет из массива все чётные элементы:

array = [1, 2, 3, 4, 5] array.find_all |elem| elem % 2 == 0 > #=> [2, 4] 

elem % 2 == 0 — это вопрос «чётен ли elem ?». Ответом, как всегда, будет true или false . Ведь «чётность» — это равенство нулю ( == 0 ) остатка деления ( % ) на 2 .

Кстати, равенство нулю можно проверять и при помощи метода .zero? . А чётность тоже можно проверить разными способами:

(elem % 2).zero? (elem & 1).zero? (elem[0]).zero? # Этот вариант круче всех 

Если на вопрос, заданный в замыкании, ответ true , то |elem| (значение очередного элемента исходного массива), заносится в новый массив, который в итоге будет выводом из итератора .find_all .

Выражение в замыкании для .find_all должно быть логическим, то есть принимать значение true или false .

Если нужно элементы
По условию искать,
То полезнее .find_all
Метод вам не отыскать!

Также есть методы .odd? и .even? для более наглядной реализации таких вещей, нечетный и четный соответственно.

array = [1, 2, 3, 4, 5] array.find_all |elem| elem.even? > #=> [2, 4] 
Суммирование/произведение/агрегация элементов [ править ]

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

array = [1, 2, 3, 4, 5] array.inject(0) |result, elem| result + elem > #=> 15 

Рассмотрим все по порядку. Начнём с нуля. Его следует расшифровывать как result = 0 перед началом работы итератора, то есть это начальное значение переменной result (переменной промежуточного результата).

Далее идёт объявление двух переменных. Первая из них ( result ) будет хранить промежуточный результат. Вторая ( elem ) — фотография текущего элемента массива (или последовательности), мы такую уже видели.

После объявления описан алгоритм работы итератора. В данном случае ему предписано каждый элемент массива складывать с промежуточной суммой: result + elem .

Учитывая эти два замечания, напишем код, который является неправильным:

array = [1, 2, 3, 4, 5] array.inject(0) |result, elem| result = result + elem > #=> 15 

Имена переменных result и elem созданы богатым воображением автора. В ваших программах они могут называться иначе.

Невероятно, но от изменения имён переменных результат не меняется. Помните это!

Для полноты картины решим ещё одну задачку. На этот раз будем искать произведение всех элементов массива:

array = [1, 2, 3, 4, 5] array.inject(1) |result, elem| result * elem > #=> 120 

А вот так можем вычислить факториал для заданного числа (n!):

n = 9 (1..n).to_a.inject() |one, two| one * two > #=> 362880 

Чтобы закрепить материал, решите задачу: найти произведение всех положительных элементов массива. Подсказка: используйте метод .find_all .

Элементов надо кучу
Перемножить иль сложить?
Есть .inject на этот случай,
Его бы вам употребить.

Разбиение надвое [ править ]

Итератор .partition делит массив на две части по некоторому бинарному признаку (чётности, положительности, наличию высшего образования и тому подобным). Вот как разделить массив на две части по признаку кратности трём:

array = [1, 2, 3, 4, 5, 6, 7, 8, 9] array.partition |x| (x % 3).zero? > #=> [[3, 6, 9], [1, 2, 4, 5, 7, 8]] 

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

Есть интересная хитрость, позволяющая разместить массив, полученный .partition , в две разные переменные:

array = [1, 2, 3, 4, 5, 6, 7, 8, 9] one, two = array.partition |x| (x % 3).zero? > one #=> [3, 6, 9] two #=> [1, 2, 4, 5, 7, 8] 

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

Логические итераторы [ править ]

В версии 1.8 появилось несколько логических методов: .all? и .any? . Они положили начало такому классу методов, как логические итераторы.

Логический итератор — это итератор (метод, обрабатывающий все элементы последовательности), возвращающий значение логического типа — true или false .

Конечно же, идея логических итераторов долгое время летала в ноосфере. Существовали итераторы, которые являлись условно-логическими: они возвращали nil в случае неудачи и какой-либо объект — в случае удачи. В логическом контексте поведение таких итераторов можно было посчитать логическим ( false → nil , а true → число/строка/любой объект). Примером условно-логического итератора служит метод .detect .

Все ли элементы удовлетворяют условию? [ править ]

В математической логике такой «итератор» называется квантором общности, обозначается символом ∀ . На языке Ruby он называется .all? . По сложившейся традиции, давайте посмотрим, как решалась бы эта задача до версии 1.8, то есть до появления логических итераторов:

array = [1, 2, 2, 3] array.inject(true) |result, elem| result && (elem > 2) > #=> false 

В примере используется так называемый механизм презумпции виновности. Переменной result присваивается значение true . Логическое умножение изменяет значение переменной result на false .

Данная программа проверяет, все ли элементы array больше двух.

Ещё один вариант решения той же задачи:

array = [1, 2, 2, 3] array.find_all |elem| elem  2 >.size.zero? #=> false 

Давайте решим эту же задачу при помощи новоявленного логического итератора:

array = [1, 2, 2, 3] array.all? |elem| elem > 2 > #=> false 

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

Хотя бы один элемент удовлетворяет условию? [ править ]

Вслед за квантором общности (он же — логический итератор .all? ), из математической логики был перенесён и квантор существования — ∃ . На языке Ruby он называется .any? . Чтобы оценить его по достоинству, посмотрим решение задачи без его участия. Проверим, содержит ли array хотя бы один элемент больший двух:

array = [1, 2, 2, 3] array.inject(false) |result, elem| result || (elem > 2) > #=> true 

В данном примере используется так называемый механизм презумпции невиновности. Переменной result присваивается значение false . В результате логического сложения, происходит изменение значения переменной result на true .

Теперь тоже самое, но через логический итератор .any? :

array = [1, 2, 2, 3] array.any? |elem| elem > 2 > #=> true 

Естественно, что с появлением логических итераторов, реализация задач математической логики (в рамках языка Ruby) стала удобней.

Хитрости [ править ]

Вот так можно сгенерировать «хороший пароль» — произвольную последовательность из чисел или латинских букв, общей длиной в 8 символов.

symbols = ["a".."z", "A".."Z", "0".."9"].map |range| range.to_a >.flatten puts (0. 8).map symbols.sample >.join 

Метод sample возвращает случайный элемент из массива.

Перемешать упорядоченный массив:

array = [1, 2, 3, 4, 5, 6, 7] array.sort_by rand > #=> перемешанный массив 

Выстроить элементы массива по убыванию без использования .reverse :

array = [2, 1, 3, 5, 6, 7, 4] array.sort |x, y| y x > #=> [7, 6, 5, 4, 3, 2, 1] 

Задачи про массивы [ править ]

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

Одномерные [ править ]
  1. Вывести индексы массива в том порядке, в котором соответствующие им элементы образуют возрастающую последовательность.
  2. В численном массиве найти сумму отрицательных элементов.
  3. Найти все индексы, по которым располагается максимальный элемент.
  4. В массиве переставить в начало элементы, стоящие на чётной позиции, а в конец — стоящие на нечётной.
Двумерные [ править ]
  1. Поменять первый и последний столбец массива местами.
  2. Упорядочить N-ый столбец.
  3. Упорядочить строки, содержащие максимальный элемент.
  4. Упорядочить строки, если они не отсортированы и перемешать, если они отсортированы.
  5. Упорядочить строки массива по значению элемента главной диагонали в каждой из строк (в исходном массиве).
  6. Найти номера строк, элементы которых упорядочены по возрастанию.
Двумерные целочисленные [ править ]
  1. Найти максимальный элемент для каждого столбца, а затем получить произведение этих элементов.
  2. Найти минимум в двумерном массиве.
  3. Найти произведение положительных элементов.
  4. Найти сумму положительных элементов, больших К.
  5. Вычислить сумму и среднее арифметическое элементов главной диагонали.
  6. Найти номера строк, все элементы которых — нули.

Подробнее об ассоциативных массивах [ править ]

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

Индексные массивы чаще всего называют просто «массивами», а ассоциативные массивы — «хешами» или «словарями».

Хеши можно представить как массив пар: ключ=>значение . Но в отличие от массива, хеш неупорядочен: нельзя заранее сказать, какая пара будет первой, а какая последней. Правда, удобство использования массива это не умаляет. Более того, поскольку в Ruby переменные не типизированы и методам с похожей функциональностью дают похожие имена, то использование хеша чаще всего равносильно использованию массива.

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

Давайте создадим хеш, где в качестве ключа будем использовать целое число:

hash = 5=>3, 1=>6, 3=>2> hash[5] #=> 3 hash[2] #=> nil это значит, что объект отсутствует hash[3] #=> 2 

А вот так будет выглядеть та же самая программа, если мы будем использовать массив:

array = [nil, 6, nil, 2, nil, 3] array[5] #=> 3 array[2] #=> nil array[3] #=> 2 

Первый случай применимости хеша: если в массиве намечаются обширные незаполненные (то есть заполненные nil ) области, то целесообразнее использовать хеш с целочисленным индексом.

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

Продолжим поиски случаев применимости хеша и на этот раз подсчитаем, сколько раз каждое число повторяется в данном целочисленном массиве. Решение массивом:

array = [1, 2, 1, 2, 3, 2, 1, 2, 4, 5] array.uniq.map |i| [i, array.find_all |j| j == i >.size] > #=> [[1, 3], [2, 4], [3, 1], [4, 1], [5, 1]] 

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

Теперь рассмотрим решение этой же задачи, но с применением хеша:

array = [1, 2, 1, 2, 3, 2, 1, 2, 4, 5] array.inject(Hash.new 0 >) |result, i| result[i] += 1 result > #=> 1, 1=>3, 2=>4, 3=>1, 4=>1> 

Удалось избавиться от лишних методов и обойтись лишь одной «пробежкой» итератора по массиву.

Начальный хеш был создан хитроумной комбинацией Hash.new , что в переводе на русский означает примерно следующее: «создадим пустой хеш, в котором любому несуществующему ключу будет соответствовать 0 ». Это нужно, чтобы суммирование (метод + ) не выдавало ошибку вида: «не могу сложить nil и число типа Fixnum ». В качестве упражнения, предлагаю вам заменить комбинацию Hash.new < 0 >на <> и посмотреть, чем это чревато.

Зачем нужно дописывать result ? Дело в том, что комбинация result[i] += 1 имеет в качестве результата целое число (учитывая, что массив целочисленный), а не хеш. Следовательно, параметру result автоматически будет присвоено целое число (см. описание итератора .inject ). На следующей итерации мы будем обращаться к result , как к хешу, хотя там уже будет храниться число. Хорошо, если программа выдаст ошибку, а если нет? Проверьте это самостоятельно.

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

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

Теперь представим, что мы работаем системными администраторами. У нас есть список DNS-имён и IP-адреса. Каждому DNS-имени соответствует только один IP-адрес. Как нам это соответствие записать в виде программы? Попробуем это сделать при помощи массива:

array = [["comp1.mydomen.ru", "192.168.0.3"], ["comp2.mydomen.ru", "192.168.0.1"], ["comp3.mydomen.ru", "192.168.0.2"]] 

Всё бы ничего, но чтобы найти IP-адрес по DNS имени, придётся перелопатить весь массив в поиске нужного DNS:

dns_name = "comp1.mydomen.ru" array.find_all |key, value| key == dns_name >[0][-1] #=> "192.168.0.3" 

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

  • Если в двумерном массиве заранее известное количество столбцов (в нашем случае — два), то каждому из столбцов (в рамках любого итератора) можно дать своё имя (в нашем случае: key и value ). Если бы мы такого имени не давали, то вышеописанное решение выглядело бы так:
array.find_all |array| array[0] == dns_name >[0][-1] #=> "192.168.0.3" 

Без именования столбцов, внутри итератора вы будете работать с массивом (в двумерном массиве каждый элемент — массив, а любой итератор «пробегает» массив поэлементно). Это высказывание действительно, когда «пробежка» осуществляется по двумерному массиву.

  • Метод .find_all возвращает двумерный массив примерно следующего вида: [[«comp1.mydomen.ru», «192.168.0.3»]] , чтобы получить строку «192.168.0.3» необходимо избавиться от двумерности. Делается это при помощи метода [] , который вызывается два раза (понижает размерность c двух до нуля). Метод [0] возвращает в результате — [«comp1.mydomen.ru», «192.168.0.3»] , а метод [-1] — «192.168.0.3» . Как раз это нам и было нужно.

Теперь ту же самую задачу решим, используя хеш:

hash = "comp1.mydomen.ru"=>"192.168.0.3", "comp2.mydomen.ru"=>"192.168.0.1", "comp3.mydomen.ru"=>"192.168.0.2"> hash["comp1.mydomen.ru"] #=> "192.168.0.3" 

Нет ни одного итератора и, следовательно, не сделано ни одной «пробежки» по массиву.

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

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

В заключении, как и было обещано, приводится решение задачи с использованием метода .update :

array = [1, 2, 1, 2, 3, 2, 1, 2, 4, 5] array.inject(<>) |result, i| result.update( i=>1 >) |key, old, new| old+new >> #=> 1, 1=>3, 2=>4, 3=>1, 4=>1> 

Описание метода .update будет дано ниже. На данном этапе попытайтесь угадать принцип работы метода .update .

Что используется в качестве ключей? [ править ]

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

Если состояние объектов-ключей изменилось, то хешу необходимо вызвать метод .rehash .

array1 = ["а", "б"] array2 = ["в", "г"] hash = array1=>100, array2=>300> hash[array1] #=> 100 array1[0] = "я" hash[array1] #=> nil hash.rehash #=> <["я", "б"]=>100, ["в", "г"]=>300> hash[array1] #=> 100 

В данном примере ключами хеша ( hash ) являются два массива ( array1 и array2 ). Одному из них ( array1 ) мы изменили нулевой элемент (с «а» на «я» ). После этого доступ к значению был потерян. После выполнения метода .rehash всё встало на свои места.

Как Ruby отслеживает изменение ключа в ассоциативном массиве? Очень просто: с помощью метода .hash , который генерирует «контрольную сумму» объекта в виде целого числа. Например:

[1, 2, 3].hash #=> 25 

Способы создания ассоциативного массива [ править ]

При создании ассоциативного массива важно ответить на несколько вопросов:

  • Какие данные имеются?
  • Какого типа эти данные?
  • Что будет ключом, а что — значением?

Ответы определят способ создания хеша.

Из одномерного массива [ править ]

Положим, что у нас в наличии индексный массив, где ключ и значение записаны последовательно. Тогда мы можем использовать связку методов * и Hash[] :

array = [1, 4, 5, 3, 2, 2] Hash[*array] #=> 4, 5=>3, 2=>2> 

Элементы, стоящие на нечётной позиции (в данном случае: 1, 5 и 2) стали ключами, а элементы, стоящие на чётной позиции (то есть: 4, 3 и 2), стали значениями.

Из двумерного массива [ править ]

Если повезло меньше и нам достался двумерный массив с элементами вида [[«ключ_1», «значение_1»], [«ключ_2», «значение_2»], [«ключ_3», «значение_3»], …] , то его надо сплющить ( .flatten ) и тем задача будет сведена к предыдущей:

array = [[1, 4], [5, 3], [2, 2]] Hash[*array.flatten] #=> 4, 5=>3, 2=>2> 

Каждый нулевой элемент подмассива станет ключом, а каждый первый — значением.

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

[["ключ_1", "ключ_2", "ключ_3", ], ["значение_1", "значение_2", "значение_3", ]] 

Вспоминаем методы работы с массивами. Там был метод .transpose (транспонирование массива), вызов которого сведёт задачу к предыдущей.

array = [[1, 5, 2], [4, 3, 2]] Hash[*array.transpose.flatten] #=> 4, 5=>3, 2=>2> 
Нет данных [ править ]

Если нет данных, то лучше записать хеш как пару фигурных скобок:

hash = <> hash[1] = 4 hash[5] = 3 hash[2] = 2 hash #=> 4, 5=>3, 2=>2> 

И уже по ходу дела разобраться, что к чему.

Известен только тип значений [ править ]

Сведения о типе значений использовать следует так: создать хеш, в котором будет определён элемент по умолчанию. Элементом по умолчанию должен быть нулевой элемент соответствующего типа, то есть для строки это будет пустая строка ( «» ), для массива — пустой массив ( [] ), а для числа — нуль ( 0 или 0.0 ). Это делается, чтобы к пустому элементу можно было что-то добавить и при этом не получить ошибку.

hash = Hash.new("") hash["песенка про зайцев"] += "В тёмно-синем лесу, " hash["песенка про зайцев"] += "где трепещут осины" hash #=> "В темно-синем лесу, где трепещут осины"> 
hash = Hash.new(0) hash["зарплата"] += 60 hash["зарплата"] *= 21 hash #=> 1260> 

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

Всё известно и дано [ править ]

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

"март"=>400, "январь"=>350, "февраль"=>200> #=> на выходе такой же текст fox: 1, wolf: 2, dragon: 3> #=> 1, :wolf=>2, :dragon=>3> обратите внимание на знак ':', он говорит что fox - это не строка, # а чтото вроде перечисления (Enum), как в языке Си. 

Не изобретайте велосипед и поступайте как можно проще.

Методы работы с ассоциативными массивами [ править ]

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

Получение массива значений и массива ключей [ править ]

Для получения отдельно массива ключей или значений существуют методы .keys и .values .

1=>4, 5=>3, 2=>2>.keys #=> [1, 2, 5] 1=>4, 5=>3, 2=>2>.values #=> [4, 3, 2] 

Ассоциативные массивы в Ruby неупорядоченны: массивы могут иметь любой порядок элементов.

Замена ключей на значения [ править ]

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

hash = "первый ключ"=>4, "второй ключ"=>5> hash.invert #=> "первый ключ", 5=>"второй ключ"> 

Поскольку ключи в ассоциативных массивах уникальны, то ключи с одинаковыми значениями будут отброшены:

hash = "первый ключ"=>10, "второй ключ"=>10> hash.invert #=> "второй ключ"> 

Небольшая хитрость: hash.invert.invert возвратит нам хеш с уникальными значениями.

Обновление пары [ править ]

Что вы делаете, если хотите обновить какую-то программу или игру? Правильно, устанавливаете апдейт. Вы не поверите, но для обновления значения в ассоциативном массиве используется метод .update . Странно, да? Пример использования этого метода в «боевых» условиях мы уже приводили в начале раздела. Если вы помните, то мы считали, сколько раз повторяется каждое число. Наверняка, вы немного подзабыли его решение (у программистов есть привычка не помнить константы). Позволю себе его вам напомнить:

array = [1, 2, 1, 2, 3, 2, 1, 2, 4, 5] array.inject(<>) |result, i| result.update(i=>1>) |key, old, new| old + new > > #=> 1, 1=>3, 2=>4, 3=>1, 4=>1> 

Страшноватая запись. Поэтому будем разбирать её по частям.

result.update(i=>1>) |key, old, new| old + new > 

Сразу после названия метода (в нашем случае .update ) идёт передача параметра. Страшная запись 1> — это не что иное, как ещё один хеш. Ключ его хранится в переменной i (счётчик итератора .inject ), а в качестве значения выбрана единица. Зачем? Расскажу чуть позже.

Не обязательно писать именно 1> . Можно «сократить» фигурные скобки и записать i=>1 .

Счётчик итератора — это переменная в которую итератор записывает текущий элемент последовательности.

Здесь вроде бы все понятно. Запись стала менее страшной, но всё равно вызывает дрожь. Будем это исправлять!

  |key, old, new|  >  

Раньше мы не встречались с такой записью. Но ничего страшного в ней нет. Это что-то вроде по́ля боя. Нам выдали вооружение и необходимо провести некий манёвр. В нашем случае, арсенал у нас внушительный: key , old и new . Бой начинается при некоторых условиях. Наш бой начнется, когда при добавлении очередной пары (переданной в предыдущей части страшной записи) обнаружится, что такой ключ уже есть в хеше. Нам предлагается описать наши действия именно в таком случае. Что же это за действия?

   old + new >  

Всего лишь сложение old и new . Ничего не говорит? Тогда расскажу, что значат переменные key , old и new . В переменную key передаётся текущий ключ, в old — старое значение по ключу (англ. old — старый), а в переменную new — добавляемое значение по ключу (англ. new — новый).

Теперь переведём запись old + new на русский: в случае обнаружения ключа в хеше, нам необходимо сложить старое значение с новым. Если помните, то новое значение равняется единице, то есть в случае когда ключ, хранимый в i уже есть в хеше result , то к старому значению просто добавляется единица. Вот и всё… а вы боялись.

Рекомендуется перечитать данную главу ещё раз, так как вы её немного не поняли.

Интересно, сколько читателей сможет прочитать эту строку и не зациклиться на предыдущей?

Слияние двух массивов [ править ]

Для слияния двух массивов можно использовать тот же метод .update или его алиас .merge или .merge! :

hash1 = 3 => "a", 4 => "c"> hash2 = 5 => "r", 7 => "t"> hash1.merge!(hash2) #=> "r", 7=>"t", 3=>"a", 4=>"c"> 

Если во втором массиве ключ будет совпадать с каким-либо ключем из первого массива, значение будет заменено на значение из второго массива.

Размер ассоциативного массива [ править ]

Ну вот, с новичками мы познакомились, теперь можно переходить к старым знакомым. Помните, как мы находили размер массива? Вот и с хешами точно также:

hash = 5=>1, 1=>3, 2=>4, 3=>1, 4=>1> hash.size #=> 5 

Стоит уточнить, что если в индексных массивах под размером понимается количество элементов, то в ассоциативном массиве это количество пар вида ключ=>значение . В остальном же это наш старый добрый .size .

Удаление пары по ключу [ править ]

О том, как добавлять элементы в массив мы знаем, а вот про удаление — нет. Необходимо это исправить. Чем мы сейчас и займёмся.

hash = 5=>1, 1=>3, 2=>4, 3=>1, 4=>1> hash.delete(5) #=> 1 hash #=> 3, 2=>4, 3=>1, 4=>1> hash.delete(5) #=> nil 

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

Метод .delete возвращает значение, которое соответствовало ключу в удаляемой паре. Если в хеше отсутствует пара с передаваемым ключом, то метод .delete возвращает nil . Напоминаем, что nil — это символ пустоты.

Удаление произвольной пары [ править ]

Многие программисты удивляются, когда узнаю́т, что ассоциативные массивы имеют метод .shift . Связано это удивление с тем, что у индексных массивов он удаляет первый элемент, возвращая его во время удаления. А вот как понять, какая пара является первой? И что такое первый в неупорядоченной последовательности пар?

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

Давайте посмотрим его в действии:

hash = 5=>3, 1=>6, 3=>2> hash.shift #=> [5, 3] hash #=> 6, 3=>2> 

Обратите внимание, что метод .shift возвращает удаляемую пару в виде индексного массива [ключ, значение] .

Не стоит обольщаться по поводу того, что метод .shift возвращает первую пару. Помните, что ассоциативные массивы неупорядоченны.

Преобразовать в индексный массив [ править ]

Чуть ранее уже говорилось, что в большинстве случаев индексные массивы удобней ассоциативных.

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

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

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

hash = "гаечный ключ"=>10, "разводной ключ"=>22> hash.to_a #=> [["гаечный ключ", 10], ["разводной ключ", 22]] 

Способ преобразования таков. Сперва пары (ключ=>значение) преобразуются в массив:

["гаечный ключ"=>10], ["разводной ключ"=>22]> 

Затем «стрелку» заменяем на запятую:

["гаечный ключ", 10], ["разводной ключ", 22]> 

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

[["гаечный ключ", 10], ["разводной ключ", 22]] 
Упорядочение хеша [ править ]

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

hash = "гаечный ключ"=>4, "разводной ключ"=>10> hash.sort #=> [["гаечный ключ", 4], ["разводной ключ", 10]] 

В методе .sort_by передаются два значения:

hash = "гаечный ключ"=>4, "разводной ключ"=>10> hash.sort_by |key, value| value > #=> [["гаечный ключ", 4], ["разводной ключ", 10]] 

Здесь мы упорядочили хеш по значению.

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

Поиск максимальной/минимальной пары [ править ]

Максимальная пара в хеше ищется точно также, как и максимальный элемент в массиве

hash = "гаечный ключ"=>10, "разводной ключ"=>22> hash.max #=> ["разводной ключ", 22] hash.min #=> ["гаечный ключ" , 10] 

но с небольшими особенностями:

  • результат поиска — массив из двух элементов вида [ключ, значение] ,
  • сначала поиск происходит по ключу, а в случае равноправных ключей при использовании max_by и min_by — по значению.

Несколько больше возможностей приобрели методы max_by и min_by :

hash = "гаечный ключ"=>10, "разводной ключ"=>22> hash.max_by |key, value| value > #=> ["разводной ключ", 22] hash.min_by |array| array[0] > #=> ["гаечный ключ" , 10] 

Также, как и в методе sort_by есть возможность по разному получать текущую пару: в виде массива или двух переменных.

Логические методы [ править ]

Работа логических методов похожа на допрос с пристрастием. Помните, как в детективах во время теста на детекторе лжи, главный герой восклицал: «Отвечать только „да“ или „нет“!» Если перевести это на язык Ruby, то это будет звучать примерно так: «Отвечать только true или false !»

В детективах набор вопросов стандартен:

  • Знали ли вы мистера X?
  • Вы были на месте преступления?
  • Убивали ли мистера X?

На Ruby примерно тоже самое:

  • Ты пустой?
  • Есть ли такой элемент?
  • Ты массив?
  • Уверен, что не строка?

Но давайте рассмотрим их подробней.

Хеш пустой? [ править ]

Зададим вопрос «Хеш пустой?», но используя известный нам лексикон. Для начала спросим «Пустой хеш тебе не брат-близнец?»

empty_hash = <> filled_hash = "гаечный"=>20, "замочный"=>"английский", "разводной"=>34> empty_hash == <> #=> true filled_hash == <> #=> false 

Можно спросить по другому: «Размер у тебя не нулевой?»

empty_hash = <> filled_hash = "гаечный"=>20, "замочный"=>"английский", "разводной"=>34> empty_hash .size.zero? #=> true filled_hash.size.zero? #=> false 

Но давайте будем задавать правильные вопросы

empty_hash = <> filled_hash = "гаечный"=>20, "замочный"=>"английский", "разводной"=>34> empty_hash .empty? #=> true filled_hash.empty? #=> false 

а то ещё примут нас за приезжих…

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

Есть такой ключ? [ править ]

Если вам нужно узнать у хеша ответ на вопрос «есть у тебя такой ключ?», но вы не знаете как это правильно спросить, то скорее всего вы зададите вопрос в два этапа: «какие ключи у тебя есть?» и «есть среди них такой ключ?»

pocket = "гаечный"=>20, "замочный"=>"английский", "разводной"=>34> pocket.keys.include?("гаечный") #=> true 

В данном примере у нас в pocket нашёлся «гаечный» ключ.

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

pocket = "гаечный"=>20, "замочный"=>"английский", "разводной"=>34> pocket.key?("гаечный") #=> true 

или в стиле индексных массивов

pocket = "гаечный"=>20, "замочный"=>"английский", "разводной"=>34> pocket.include?("гаечный") #=> true 

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

Этот же вопрос можно задать методами: .member? и .has_key? .

Есть такое значение? [ править ]

Давайте подумаем, как задать вопрос «есть такое значение?» хешу. Скорее всего, мы опять зададим вопрос в два этапа: «какие значения есть?» и «есть ли среди них нужное нам?»

pocket = "гаечный"=>20, "замочный"=>"английский", "разводной"=>34> pocket.values.include?("гаечный") #=> false — ой, забыл сменить pocket.values.include?("английский") #=> true 

Но аборигены говорят иначе и задают вопрос напрямую:

pocket = "гаечный"=>20, "замочный"=>"английский", "разводной"=>34> pocket.value?("английский") #=> true 

Задать вопрос «Есть такое значение?» можно не только при помощи метода .value? , но и при помощи более длинного .has_value? .

Итераторы [ править ]

У ассоциативных массивов есть следующие итераторы:

  • .find_all — поиск всех элементов, которые удовлетворяют логическому условию;
  • .map — изменение всех элементов по некоторому алгоритму;
  • .inject — сложение, перемножение и агрегация элементов массива.

Набор итераторов точно такой же, как и у индексных массивов — сказывается их родство. Вот только ведут себя они несколько иначе:

  • Результатом является двумерный массив (как после метода .to_a ).
  • В качестве счётчика (переменной в фотографии) передаётся массив вида [ключ, значение] .
  • Можно развернуть массив вида [ключ, значение] в две переменные.
  • В итераторе .inject развернуть массив можно используя запись .inject <|result, (key, value)| >.

Рассматривать заново работу каждого итератора в отдельности скучно. Поэтому мы будем рассматривать работу всех итераторов сразу.

hash = "гаечный ключ"=>4, "разводной ключ"=>10> hash.find_all |array| array[1]  5 > #=> [["гаечный ключ", 4]] hash.map  |array| "#array[0]> на #array[1]>" > #=> ["гаечный ключ на 4", "разводной ключ на 10"] hash.inject(0) |result, array| result + array[1] > #=> 14 

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

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

Теперь посмотрим, как можно развернуть array в две переменные. Делается это простой заменой array на key, value :

hash = "гаечный ключ"=>4, "разводной ключ"=>10> hash.find_all |key, value| value  5 > #=> [["гаечный ключ", 4]] hash.map |key, value| "#key> на #value>" > #=> ["гаечный ключ на 4", "разводной ключ на 10"] hash.inject(0) |result, key, value| result + value > #=> Ошибка в методе "+": невозможно сложить nil и число типа Fixnum 

Обратите внимание, что развёртка массива прошла успешно только в первых двух итераторах. В третьем возникла ошибка. Давайте выясним, откуда там взялся nil . Дело в том, что развернуть массив не удалось, и теперь он стал называться не array , а key . Переменная value осталась «не у дел», и ей присвоилось значение nil . Чтобы это исправить, достаточно поставить круглые скобки:

hash.inject(0) |result, (key, value)| result + value > #=> 14 

Ассоциативный массив, как и индексный массив, имеет метод .map , который передаёт замыканию ключ и соответствующее ему значение. При этом в замыкание на самом деле передаётся массив с ключом и значением, но Ruby «разворачивает» их в две переменные при передаче замыканию.

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

hash = "гаечный ключ"=>4, "разводной ключ"=>10> hash.map  | key, value | "#key> на #value>" > #=> ["гаечный ключ на 4", "разводной ключ на 10"] hash.map #=> [["гаечный ключ", 4], ["разводной ключ", 10]] 

Итератор .map , вызванный без аргументов, аналогичен методу .to_a : просто раскладывает хеш в двумерный массив.

Хитрости [ править ]

Одному программисту надоело писать hash[«key»] и он захотел сделать так, чтобы можно было написать hash.key .

class Hash def method_missing(id) self[id.id2name] end end hash = "hello"=>"привет", "bye"=>"пока"> hash.hello #=> "привет" hash.bye #=> "пока" 

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

Задачи [ править ]

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

Подробнее о строках [ править ]

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

Строковый тип является самым популярным в любом языке программирования. Ведь без него невозможно написать любую программу (особенно учитывая, что любая программа — это строка). При выводе на экран или записи в файл, любой тип данных преобразуется к строке (явно или неявно). Это значит, что в конечном итоге всё сводится к строковому типу. Кстати, и ввод данных тоже осуществляется в виде строки (и только потом преобразуется в другие типы).

Студенты четвёртого курса МЭТТ ГАИ поступили на подготовительные курсы в МГИУ. Там им начали преподавать основы программирования на Ruby. И одна из заданных им задач была: «Дано число, необходимо поменять порядок цифр на обратный». Задача сложная, но наши студенты об этом не знали и решили её преобразованием к строке: given.to_s.reverse . Преподаватели были поражены и впредь запретили им использовать преобразования к строке в своих программах. И всё потому, что это сильно упрощало решение и давало студентам огромное преимущество перед остальными слушателями курсов.

Язык Ruby унаследовал работу со строками из языка Perl (признанного лидера по работе со строками). В частности такой мощный инструмент как правила (rules).

Но наследование не подразумевает бездумного копирования. В частности, правила, в рамках Ruby, получили объектно-ориентированную реализацию, что позволяет применять к ним различные методы. Помимо правил, присутствует великое множество методов работы со строками. Причём некоторые из них являются нашими старыми знакомыми ( + , * , [] и так далее). Работают они несколько иначе, но некоторая параллель с массивами все же присутствует. Следует упомянуть два очень интересных момента:

  • Cтроки — это универсальный тип данных, так как в строку можно преобразовать любой другой тип данных. Также строку можно преобразовать в любой другой тип данных (ведь изначально любой код программы — это строка).
  • Cтроки очень удобно преобразовывать в массив и обратно (методы .join и .split ). Поэтому работа со строками практически столь же удобная, как и с массивами.

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

Способы создания строки [ править ]

Строка создаётся при помощи ограничительных символов. Для этих целей чаще всего используются » (программистская кавычка) и ‘ (машинописный апостроф). Их смысл различен. Строка в апострофах гарантирует, что в ней будет содержаться тот же текст, что и в коде программы, без изменений. Строка в кавычках будет проходить предварительное преобразование. Будут раскрыты конструкции «вставка» и «специальный символ».

Давайте будем называть строки в апострофах «ленивыми», а строки в кавычках — «работящими».

Вставка — это хитрая конструкция, которая вставляется между ограничительными символами внутри строки. Она состоит из комбинации октоторпа и двух фигурных скобок ( # ‘здесь был Вася’ > ). Внутри неё можно писать не только ‘Здесь был Вася’ , но и любой программный код. Результат программного кода будет преобразован к строке и вставлен на место вставки.

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

«Вставка» работает только в момент создания строки́. После создания придётся придумывать другие способы подстановки данных в стро́ки.

Специальный символ начинается со знака \ (обратная косая черта). Самые популярные из них: \n (переход на новую строку), \t (табуляция), \\ (обратная косая черта) и \» (двойная кавычка).

Хотя специальный символ и пишется, как два знака, но на деле это всего один символ. Доказать это можно выполнением простенького кода: «\n».size #=> 1 .

Для чего нужны работящие и ленивые строки? [ править ]

Скорее всего вы будете редко вспоминать про то, что существуют работящие и ленивые строки. Тем более, что это различие действительно только на момент создания строки. Рядовой программист пользуется либо работящими, либо ленивыми строками. Давайте посмотрим, как выглядит код программиста, который использует только ленивые строки:

my_number = 18 my_array = [1, 2, 3, 4] puts 'Моё число = ' + my_number.to_s + ', а мой массив длины ' + my_array.size.to_s 

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

my_number = 18 my_array = [1, 2, 3, 4] puts "Моё число = #my_number>, а мой массив длины #my_array.size>" 

Программа стала не только меньше, но и лучше читаться. Исчезли многочисленные сцепления.

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

my_array = [1, 2, 3, 4] puts "Повторенье — мать ученья. Мой массив = #my_array.join(\", \")>" 

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

my_array = [1, 2, 3, 4] puts "Повторенье — мать ученья. Мой массив = #my_array.join(", ")>" 

Нет необходимости в экранировании символов внутри вставки.

Методы работы со строками [ править ]

Методы строк умеют:

  • преобразовывать входные данные в красивый вид,
  • красиво оформить выходные данные,
  • дезертировать в массивы.

Допустим, вы нашли максимальный элемент массива. И вам надо вывести результат на экран. Вы можете поступить вот так:

array = [4, 4, 2, 5, 2, 7] puts array.max #=> 7 

Несмотря на правильность решения, вывод результата выглядит некрасиво. Гораздо профессиональней будет написать вот такую программу:

array = [4, 4, 2, 5, 2, 7] puts "Максимальный элемент array = #array.max>" #=> Максимальный элемент array = 7 

Заметили, насколько привлекательней стал вывод результата, когда мы задействовали стро́ки?

Заниматься оформлением выходных данных сто́ит только непосредственно перед выводом результата. В остальное время использование оформительских элементов будет только мешать.

Всем известно, что чи́сла внутри компьютера хранятся в двоичной системе. Но вот парадокс: получить двоичное представление числа иногда очень сложно. В частности, для того, чтобы получить двоичную запись числа, необходимо создать строку и записывать результат в неё. Это значит, что результатом преобразования в другую систему счисления (из десятичной) будет строка. Давайте посмотрим, как эта задача решается:

my_number = 123 puts "В двоичном виде: %b" % my_number #=> В двоичном виде: 1111011 

Мы задействовали метод % (аналог sprintf в Си), который осуществляет форматирование строки́. Эту же задачу можно решить несколько иначе.

my_number = 123 puts "В двоичном виде: #my_number.to_s(2)>" #=> В двоичном виде: 1111011 

Со вторым вариантом вы могли уже́ встречаться в разделе, посвящённом числам (подраздел Хитрости).

Методы sprintf и printf в Ruby также присутствуют, но используются крайне редко. Чаще всего они заменяются на методы % и puts .

Арифметика строк [ править ]

Кто бы мог подумать, но строки можно складывать, умножать и, если постараться, то и делить. Естественно, что это не арифметические операции чисел, а методы со своим особенным поведением. Рассмотрим сложение, которое в строках работает как сцепление (конкатенация):

boy = "Саша" girl = "Маша" puts boy + " + " + girl + " = любовь!" #=> "Саша + Маша = любовь!" # или так: puts "#boy> + #girl> = любовь!" #=> "Саша + Маша = любовь!" 

Вот такое вот романтическое сцепление сердец!

Переходим к умножению. Умножают стро́ки только на целое число. Строка, которую умножают просто копируется указанное число раз. Давайте напишем речь для кукушки, чтобы она не сбилась и не накукукала нам слишком мало.

years_left = 100 "Ку-ку! " * years_left #=> "Ку-ку! Ку-ку! Ку-ку! Ку-ку! Ку-ку! Ку-ку! Ку-ку! … Ку-ку! Ку-ку! " 

Теперь за своё будущее можно не беспокоиться! Хотя нам может попасться неграмотная кукушка…

С делением совсем другая история. Сейчас его в языке просто нет, но этот недостаток уже ощущают многие. Смысл деления состоит в том, чтобы преобразовать строку в массив, разбив её по разделителю. Давайте преобразуем речь кукушки в массив и посмотрим, сколько же лет она нам насчитала:

cuckoo_talk = "Ку-ку! Ку-ку! Ку-ку! Ку-ку! Ку-ку! Ку-ку! Ку-ку! … Ку-ку! Ку-ку! " (cuckoo_talk / " ").size #=> 10 

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

Скобки нужны для того, чтобы вызвать .size от результата деления, а не от » » .

Деление в примере работает также как и метод .split , о котором речь пойдёт чуть позже. А чтобы оно заработало у вас, необходимо добавить небольшой код в начало программы:

class String alias / :split end 

Этот код как раз и говорит о том, что деление и .split — одно и тоже.

Деление стало нужно, когда метод * для массивов получил возможность работать, как .join (преобразовать массив в строку, расположив элементы через разделитель). В виду того, что .join и .split работают вместе точно также, как умножение и деление, то появилась идея заставить работать деление как .split .

Преобразование в массив или путешествие туда и обратно [ править ]

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

"Ку-ку".split('-') #=> ["Ку", "ку"] "Ку-ку".split('y') #=> ["К", "-к"] 

Обратите внимание, что разделитель из результата исчезает.

За преобразование строки́ в массив отвечает метод .split . Ему передаётся разделитель, по которому будет происходить деление строки на элементы. В нашем случае это ‘-‘ . Теперь вернём всё на место. Для этого мы будем использовать метод .join из арсенала массивов:

["Ку", "ку"].join('-') #=> "Ку-ку" ["Ку", "ку"].join(', ') #=> "Ку, ку" ["Ку", "ку"].join #=> "Куку" ["Ку", "ку"].to_s #=> "Куку" 

Кукушка кукует,
строку генерит.
Из строчки массив
Получу через .split .

  • Разделитель будет вставлен между элементами исходного массива.
  • Метод .join , вызванный без разделителя, даёт такой же результат, как и метод .to_s .

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

["Ку", "ку"] * '-' #=> "Ку-ку" ["Ку", "ку"] * 3 #=> ["Ку", "ку", "Ку", "ку", "Ку", "ку"] 

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

Длина строки [ править ]

Длина строки определяется точно также, как и длина массива или хеша, то есть методом .size :

"Во дворе дрова, а в дровах трава!".size #=> 33 "Три".size #=> 3 

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

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

Получение подстрок [ править ]

Получение подстрок работает точно также, как и получение подмассива. С тем лишь отличием, что нумерация идёт не по элементам, а по символам. Это логично, особенно если учесть, что для строки элементом является символ.

string = "Во дворе дрова, а в дровах трава!" string[27..-1] #=> "трава!" string[27. -1] #=> "трава" string[9. 14] #=> "дрова" string[9..13] #=> "дрова" 

Отрицательная нумерация тоже работает, то есть последний элемент имеет индекс -1 , предпоследний -2 и так далее.

Помните, что три точки в диапазоне дают подстроку без крайнего правого элемента. Кто-то шлагбаумом балуется!

Чтобы получить один единственный символ, необходимо получить подстроку длины 1.

string = "Во дворе дрова, а в дровах трава!" string[3..3] #=> "д" string[3] #=> 190 

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

Если мы указываем не диапазон, а число, то метод [] выдаёт нам не символ, а его целочисленный код.

string = "Во дворе дрова, а в дровах трава!" string[3] #=> 190 string[3].chr #=> "д" 

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

В последних версиях Ruby код string[3] уже выдает искомую «д» и без использования метода .chr . Но — зачем рисковать, если можно сразу указать точный диапазон?

Строка-перевёртыш [ править ]

Иногда хочется перевернуть строку задом наперед. Причины могут быть разные. Например, вы ищете палиндром (число, которое можно перевернуть без ущерба для его значения). Занимается этим благородным делом метод .reverse . Узнаем настоящее имя Алукарда из аниме Hellsing:

"Алукард.".reverse #=> ".дракулА" 

Не стоит путать метод .reverse для массивов с методом .reverse для строк. В массивах меняется порядок элементов, а в строках — символов.

Меняю шило на мыло! [ править ]

Для того, чтобы заменить «шило» на «мыло» используется не газета «Из рук в руки», а методы .sub и .gsub :

"шило в мешке не утаишь".sub("шило", "мыло") #=> "мыло в мешке не утаишь" 

Естественно, что менять можно не только шило и мыло, но и другие данные. Например, возраст. Девушка Ирина утверждает, что ей 18, но мы-то знаем, что ей 26. Давайте восстановим истину, возможно — в ущерб своему здоровью:

"Ирине 18 лет.".sub("18", "26") #=> "Ирине 26 лет." 

Заметили, что мы используем только метод .sub ? Давайте теперь рассмотрим работу метода .gsub и его отличие от .sub . На этот раз мы будем исправлять текст, авторы которого — недоучки ( или белорусы ), забывшие про правило «Жи, Ши — пиши через ‘и’»

string = "жыло-было шыбко шыпящее жывотное" string.sub("жы", "жи") #=> "жило-было шыбко шыпящее жывотное" string.gsub("жы", "жи") #=> "жило-было шыбко шыпящее животное" string.gsub("жы", "жи").gsub("шы", "ши") #=> "жило-было шибко шипящее животное" 

Метод .sub производит только одну замену, а .gsub — все возможные.

Название метода .sub произошло от английского «substitute» — «замена», созвучное же название метода .gsub отличается от него только словом «global».

Сканируем текст на ошибки [ править ]

Давайте найдем и посчитаем ошибки. Искать мы будем методом .scan .

string = "жыло-было шыбко шыпящее жывотное" string.scan("шы") #=> ["шы", "шы"] string.scan("шы").size #=> 2 string.scan("жы").size #=> 2 string.scan("жы").size + string.scan("шы").size #=> 4 

Метод .scan находит все указанные подстроки и возвращает их в виде массива строк. В данном примере, метод .size считает количество элементов массива, возвращаемого .scan .

Ужас, в одном предложении целых четыре ошибки. Будем отчислять!

Нашел ошибку метод .scan ,
В массив её запомнил.
Учителям он свыше дан!
Зачем его я вспомнил?!

Правила (они же регулярные выражения) работы со строками. [ править ]

Правила — это образцы к которым можно примерять строки. Правила обладают своим собственным языком, который позволяет описывать одну, две, сотню и вообще любое количество строк. Это своеобразная упаковка для множества строк в одну компактную запись.

Правила в Ruby ограничиваются символами / (косая черта). Примеры правил:

/(жы|шы)/ /\w+@[\w\.]+\w+/i 

Страшно? А зря. На самом деле работа с правилами очень проста. Главное привыкнуть и попрактиковаться.

Составные части правил:

Символьные классы Перечисление символов, которые может содержать строка. Квантификаторы Количество символов. Альтернативы Перечисление всевозможных вариантов. Группировки Возможность выделить несколько групп, которые могут обрабатываться отдельно. Модификаторы Изменение поведения правила. Например, игнорирование регистра символов.

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

Символьный класс. [ править ]

Символьный класс — просто конечный набор символов. Он ограничивается квадратными скобками и содержит перечисление символов, которые можно вместо него подставить. Заменяется он всего на один символ, входящий в этот класс. Примеры символьных классов:

/[абвгде]/ #=> простое перечисление символов /[а-яА-ЯЁё]/ #=> все русские буквы /[0-9a-z]/ #=> цифры и строчная латиница /[^0-9]/ #=> все символы, кроме цифр 

Замечание: В таблице Unicode символы ‘Ё’ и ‘ё’ стоят немного отдельно от остальных символов русского алфавита.

  • Можно использовать — (дефис) для указания диапазонов символов.
  • Если первый символ класса (идущий сразу после открывающейся квардратной скобки) — ^ (циркумфлекс), то это означает символ, который отсутствует в данном классе.
  • Некоторые популярные классы имеют короткую запись.

Взгляните на примеры правил! Правда, они стали понятней? По крайней мере второе…

Квантификатор [ править ]

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

/\w/ #=> три латинских буквы или цифры /\d/ #=> одна, две или три цифры /[а-яА-ЯЁё]/ #=> русское слово длиной три символа и больше 
  • Квантификатор с одним параметром называется точным и указывает точное количество повторений.
  • Квантификатор с двумя агрументами называется конечным и указывает конечный диапазон, в котором варьируется количество повторений.
  • Квантификатор без второго параметра (но с запятой) называется бесконечным и ограничивает количество повторений лишь снизу.
  • Некоторые популярные квантификаторы имеют короткую запись.

Снова посмотрите на примеры правил. Теперь вам они понятны? Если нет, то перечитайте две предыдущие главы — в них основа правил.

Альтернатива [ править ]

Альтернатива нужна, когда необходимо объединить несколько правил в одно. При этом совпадение засчитывается, когда есть совпадение хотя бы с одним правилом. Желательно альтернативу заключать внутрь группировки (круглые скобки). Правила, входящие в альтернативу, разделяются | (вертикальной чертой, которая и является альтернативой). Примеры альтернатив:

/(жы|шы)/ #=> или "жы", или "шы" /(\w+|[а-яА-Я]+)/ #=> или слово на латинице, или русское 

Вместо альтернативы можно задействовать логические итераторы .any? и .all? внутри .inject . Получается более гибкая конструкция.

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

Группировка [ править ]

Группировка используется, когда необходимо обрабатывать результат частями. Например, при обработке ссылок в HTML-документе удобно отдельно обрабатывать текст ссылки и URL. Группировка также как и альтернатива, заключается в круглые скобки. Более того, альтернатива обрабатывается как группировка. Доступ к результату совпадения каждой группировки осуществляется посредством специальных переменных $1 , $2 , …, $9 . Подробнее группировки будут рассмотрены в подразделе «Правильная замена». Пример использования группировки:

"2+7*3".gsub(/(\d+)\*(\d+)/) $1.to_i * $2.to_i > #=> "2+21" 

Существует много видов группировок. Например, (?:…) — группировка без сохранения результата в «долларовую переменную» или (?!…) — негативная группировка. В любом случае они ограничиваются парой круглых скобок.

Фиксирующая директива [ править ]

Фиксирующие директивы — это символы, которые привязывают правило к некоторому признаку. Например, к концу или началу строки.

/^\d+/ #=> строка начинается с числа /\w+$/ #=> последнее слово на латинице или число /^$/ #=> пустая строка 

Насколько видно из примеров,

  • ^ — привязка к началу строки,
  • $ — привязка к концу строки.

Фиксирующих директив гораздо больше двух. Об остальных читайте в специализированной литературе.

Модификатор [ править ]

Модификатор предназначен для изменения поведения правила. Он размещается сразу же после правила (после последней наклонной черты). Пример использования модификатора:

/(hello|world)/i #=> или "hello", или "world". Причём независимо от регистра /\s+/mix #=> несколько подряд идущих пробельных символов 

Бывают следующие модификаторы:

  • multiline — перенос строки считается простым символом,
  • ignorcase — поиск без учёта регистра,
  • extended — игнорировать пробельные символы.

Игнорирование регистра работает только для латиницы.

  • Можно применять любое количество модификаторов и в любом порядке.
  • Обратите внимание, что модификаторы образуют слово mix.

Теперь можно не бояться страшных правил.

Правильное разбиение [ править ]

Разбиение называется «правильным» тогда, когда в качестве аргумента метода .split используется правило. Например, можно разбить текст по знакам препинания. Для этого необходимо выполнить следующий код.

"Раз, два, три!".split(/[, \. ]+/) #=> ["Раз", "два", "три"] 

Обратите внимание, что в результирующем массиве знаки препинания отсутствуют.

Правильная замена [ править ]

С правильной заменой не всё так просто. Дело в том, что методы .sub и .gsub совместно с правилами становятся итераторами, которые последовательно обрабатывают каждое совпадение с правилом. Чтобы это увидеть в действии, давайте решим задачу исправления ошибок:

"Жыло-было шыбко шыпящее жывотное".gsub(/(ж|ш)ы/) $1 + "и" > #=> "Жыло-было шибко шипящее животное" 

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

"Жыло-было шыбко шыпящее жывотное".gsub(/(Ж|Ш|ж|ш)ы/) $1 + "и" > #=> "Жило-было шибко шипящее животное" 

Вот, теперь гораздо лучше. Как мы этого добились? Давайте разберёмся. Начнём с регулярного выражения:

/(Ж|Ш|ж|ш)ы/ 

Оно состоит из двух частей:

  • альтернативы с группировкой — (Ж|Ш|ж|ш) ,
  • символа — ы .

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

Зачем была использована группировка? Для пояснения причины, рассмотрим код в фигурных скобках:

 $1 + "и" > 

Вот для того, чтобы можно было использовать переменную $1 (результат первой группировки) мы и задействовали группировку. В данном случае, в $1 сохраняется первая буква слога, которая в результате исправления оШЫбки не меняется.

  • Для того, чтобы получить доступ к результату первой группировки, надо обратиться к переменной $1 (один доллар), ко второй — $2 (два доллара) и так далее до переменной $9 (девять долларов).
  • Переменные $1 — $9 заимствованы из языка Perl.

Можно ли было решить эту же задачу иначе? Конечно можно!

"Жыло-было шыбко шыпящее жывотное".gsub(/([ЖШжш])ы/) $1 + "и" > #=> "Жило-было шибко шипящее животное" 

На этот раз мы просто задействовали символьный класс вместо альтернативы, который описывает первую букву слога с оШЫбкой.

Есть ещё пару интересных моментов, которые вам необходимо знать. Во время предыдущего примера вас могли посетить следующие вопрос: а как получить весь текст, который совпал с правилом? Неужели необходимо делать всеобщую группировку?

Ответ на этот вопрос однозначный — нет! Достаточно придумать название переменной (которая будет содержать совпавший текст) и правильно описать внутри ушек:

"Раз, два, три!".gsub(/[а-я]+/) |word| word.reverse > #=> "заР, авд, ирт!" 
Правильный поиск [ править ]

Вот здесь метод .scan может развернуться в полную силу. Хотите получить массив всех русских слов в тексте? Запросто:

"Раз, два, три!".scan(/[А-Яа-я]+/) #=> ["Раз", "два", "три"] 

Хотите получить все знаки препинания? Нет ничего проще:

"Раз, два, три!".scan(/[, \.;:!]+/) #=> [", ", ", ", "!"] 

Если необходимо в метод .scan передавать правило с группировкой, то желательно использовать группировку без сохранения результата, то есть (?:…) . Иначе результатом метода .scan будет совпадение с группировкой, а не с правилом.

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

string = "495-506-13 56 nata@rambler.ru(34) 1.5.1232 12.14.56 31.декабря.9999" string.scan(/(?:[-a-z_\d])+@(?:[-a-z])*(?:\.[a-z])+/) #=> ["nata@rambler.ru"] 

Выполните её, посмотрите результат, а потом замените любую из группировок (?:…) на (…) и снова взгляните на результат.

Ну со .scan должно быть всё понятно. А вот то, что метод [] начинает тоже правильно искать — пока нет.

"Раз, два, три!"[/[А-Яа-я]+/] #=> "Раз" 

Если методу [] передать в качестве параметра правило, то он вернёт либо совпадение с правилом, либо nil .

Очень полезно использовать [] в ситуациях, когда надо узнать ответ на вопрос «есть хотя бы одна подстрока, которая удовлетворяет правилу?» или получить первое (или единственное) совпадение с правилом.

Существует древнее поверье, что если использовать одно и тоже правило для .scan и .split , то получаются две части текста, из которых реально получить исходный.

Text.scan(rule) + Text.split(rule) = Text 

Это значит, что если метод .split использует правило, описывающие все знаки припинания, то результатом будет текст без знаков припинания. А вот если это же правило будет использовать метод .scan , то в результате мы получим все знаки препинания без текста.

Рекомендуется использовать метод [] вместо метода =~ (заимствованного из Perl), так как [] более функционален.

Жадность [ править ]

Речь пойдёт о жадности среди квантификаторов. Возьмем некоторый квантификатор и посмотрим как он работает.

Нежадные квантификаторы иногда называют щедрыми.

Сперва он начинает искать последовательность длины m (вот так жадность), и если правило не срабатывает, он начинает уменьшать длину последовательности вплоть до n . Так работают обычные жадные кванторы.

Для решения вышеописанной проблемы и был придуман так называемый щедрый квантификатор. От жадного он отличается обратным ходом обработки, то есть длину последовательности он не уменьшает от m к n , а наоборот, увеличивает от n до m . Научить щедрости квантификатор можно знаком вопроса ? после любого жадного квантификатора.

"Раз, два, три!".scan(/[А-Яа-я]+?/) #=> ["Р", "а", "з", "д", "в", "а", "т", "р", "и"] "Жуй жвачку, жывотное!".gsub(/([жЖшШ]??)ы/) $1 + 'и' > #=> "Жуй жвачку, животное!" 

С рождения квантификаторы жадные. Щедрость — обретаемый признак.

На самом деле, жадный квантификатор называется жадным (или — максимальным) потому, что он пытается забрать все себе, а щедрый (минимальный) — стремится отдать все другим. И потому с рождения квантификаторы жадны, а щедрость — обретаемый признак. Подробнее вы можете узнать об этом в книге Джона Фридла «Регулярные выражения»

Хитрости [ править ]

Перенос по словам [ править ]

Несколько лет назад (ещё при жизни http://ruby-forum.ru) решали мы интересную задачу: как реализовать автоматический перенос на новую строку (wrap). Для тех, кто не застал те времена, уточню задание: дан текст, необходимо, вставить переносы таким образом, чтобы каждая из полученных строк была меньше n (для определённости n = 80 ). Недавно я осознал, что не могу решить эту задачу тем способом, который был нами тогда найден. Я его просто не помнил… Решение нашлось быстро, достаточно было вспомнить, что на английском языке эта задача именуется коротким и ёмким словом wrap.

class String def wrap(col = 80) gsub(/(.#col>>)( +|$\n?)|(.#col>>)/, "\\1\\3\n") end end 

Немного о структуре кода. Метод .wrap реализован для экземпляров класса String . Также стоит обратить внимание на то, что внутри правила (регулярного выражения) возможна «вставка» (как в «рабочих строках»). Используется сей метод следующим образом:

p "wrapping text with regular expressions".wrap(10) #=> "wrapping\ntext with\nregular\nexpression\ns\n" 

Теперь давайте разберёмся с правилом. Чтобы не смущать неокрепшие умы, заменим вставку на 80. Правило станет короче и менее страшным.

(.1, 80>)( +|$\n?)|(.1, 80>) 

Очевидно, что оно состоит из четырёх частей:

  • (.) — строка длиной от 1 до 80 символов (любых). Результат группировки записывается в $1 (один доллар) или «\\1» .
  • ( +|$\n?) — пробелы или конец строки. Результат группировки записывается в $2 (два доллара) или «\\2» . Обратите внимание на запись $\n? , которая означает «конец строки ( $ ), после которого может идти перенос ( \n )». Обратите внимание, что $2 мы не используем и поэтому можно использовать (?:) (группировку без сохранения результата).
  • | — или.
  • (.) — строка длиной от 1 до 80 символов (любых). Результат группировки записывается в $3 (три доллара) или «\\3» .

В результате работы этого правила произойдёт сопоставление с группировками 1 и 2 или 3. В первом случае будет обрабатываться строка, слова в которой по длине не превышают 80. Во втором случае строка будет принудительно усечена до 80 символов. Другими словами, мы пытаемся сделать перенос по словам, но если у нас не получается, то мы будем делать перенос так, как у нас получится.

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

Методы преобразования к строке [ править ]

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

class Container def to_s "контейнер" end end cont = Container.new p "Это #cont>" #=> "Это контейнер" 

Если нужно, чтобы ваши объекты упорядочивались и сравнивались с обычными строками, следует применять примесь Comparable и единственный специальный метод to_str . Наличие этого метода у вашего объекта — знак для Ruby, что для сравнения следует применять не встроенный в String метод, а ваш.

class Container include Comparable def to_str "контейнер" end def to_s "контейнер" end def (other) to_s other.to_s end end cont = Container.new "контейнер" == cont #=> true 

Задачи [ править ]

  1. Дана строка слов, разделёных пробелами. Вывести длиннейшее слово.
  2. Дана строка, содержащая кириллицу, латиницу и цифры. Вывести все слова, длина которых равна средней.
  3. Найти в строке первое целиком кириллическое слово.
  4. Дан текст (строка с переносами). Найти все слова, содержащие лишь три буквы «о».
    • Только для русских слов.
    • Для французских и русских слов.
    • Для любого регистра буквы «о».
  5. Найти в тексте время в формате «часы:минуты:секунды».
  6. Найти все слова без повторяющихся букв (например, «Лисп» или «Ruby», но не «Паскаль» или «Java»).
    • Только для русскоязычных слов.
    • Не учитывайте цифры в словах.
  7. Найти в тексте слова́, содержащие две прописные буквы, и исправить.
    • Решите задачу для слов и в кириллице, и в латинице.
  8. Найти в тексте даты формата «день.месяц.год».
    • Найдите дату, где день ограничен числом 31, а месяц 12. Год ограничивайте четырёхзначными числами.
    • Распознавайте месяц в виде «31.марта.2001».
  9. Дан текст. Найдите все URL адреса и вычлените из них ссылку на корневую страницу сайта (например, из http://ru.wikibooks.org/wiki/Ruby сделайте http://ru.wikibooks.org).

Ruby для романтиков от новичка до профессионала

Ruby: Learn Ruby in 24 Hours or Less - A Beginner’s Guide To Learning Ruby Programming Now (Ruby, Ruby Programming, Ruby Course) 1834672456, 1213994239

Table of contents :
Оглавление
Вместо предисловия
Руби против ибур
Для фана
Что мы будем изучать
Веб-программирование или что-то другое?
Сколько зарабатывают программисты?
Ваше преимущество
Среда исполнения
Настройка Windows для запуска первой программы
Здравствуйте, я ваш REPL
Запуск программы из файла
Я ваш файловый менеджер
Основы работы с файловой системой
Навигация
Создание файла
Консольный ниндзя
Текстовые редакторы
Первая программа
Переменные в языке Руби
Сложение и умножение строк
Типы данных
Докажем, что все в руби — объект
Приведение типов
Дробные числа
Интерполяция строк
Bang!
Блоки
Блоки и параметры
Любопытные методы класса Integer
Сравнение переменных и ветвление
Комбинирование условий
Некоторые полезные функции языка руби
Генерация случайных чисел
Угадай число
Тернарный оператор
Индикатор загрузки
Методы
Эмулятор судного дня
Переменные экземпляра и локальные переменные
Однорукий бандит (слот-машина)
Массивы
Немного про each
Инициализация массива
Обращение к массиву
Битва роботов
Массивы массивов (двумерные массивы)
Установка gem’ов
Обращение к массиву массивов
Многомерные массивы
Наиболее часто встречающиеся методы класса Array
Метод empty?
Методы length, size, count
Метод include?
Добавление элементов
Выбор элементов по критерию (select)
Отсечение элементов по критерию (reject)
Метод take
Есть ли хотя бы одно совпадение (any?)
Все элементы должны удовлетворять критерию (all?)
Несколько слов о популярных методах класса Array
Символы
Структура данных “Хеш” (Hash)
Другие объекты в качестве значений
Пример JSON-структуры, описывающей приложение
Англо-русский словарь
Наиболее часто используемые методы класса Hash
Установка значения по-умолчанию
Передача опций в методы
Набор ключей (HashSet)
Итерация по хешу
Метод dig
Проверка наличия ключа
Введение в ООП
Классы и объекты
Состояние
Состояние, пример программы
Полиморфизм и duck typing
Наследование
Модули
Subtyping (субтипирование) против наследования
Статические методы
Вся правда про ООП
Отладка программ
Отладка с использованием консольного отладчика
Отладка с использованием графического отладчика
Практическое занятие: подбор пароля и спасение планеты
Немного про виртуализацию, Docker, основные команды Docker
Ruby Version Manager (RVM)
Тестирование
RSpec
Заключение

Citation preview

Роман Пушкин Ruby для романтиков от новичка до профессионала Оглавление Вместо предисловия Руби против ибур Для фана Что мы будем изучать Веб-программирование или что-то другое? Сколько зарабатывают программисты? Ваше преимущество Среда исполнения Настройка Windows для запуска первой программы Здравствуйте, я ваш REPL Запуск программы из файла Я ваш файловый менеджер Основы работы с файловой системой Навигация Создание файла Консольный ниндзя Текстовые редакторы Первая программа Переменные Сложение и умножение строк Типы данных Докажем, что все в руби — объект Приведение типов Дробные числа

3 5 7 8 9 10 11 13 14 17 18 19 22 23 24 25 29 31 34 36 37 39 40 44

Интерполяция строк Bang! Блоки Блоки и параметры Любопытные методы класса Integer Сравнение переменных и ветвление Комбинирование условий Некоторые полезные функции языка руби Генерация случайных чисел Угадай число Тернарный оператор Индикатор загрузки Методы. Эмулятор судного дня Глобальные и локальные переменные Однорукий бандит (слот-машина) Массивы Немного про each Инициализация массива Обращение к массиву Битва роботов Массивы массивов (двумерные массивы) Установка gem’овов Обращение к массиву массивов Многомерные массивы Наиболее часто встречающиеся методы класса Array Метод empty? Методы length, size, count Метод include? Добавление элементов Выбор элементов по критерию (select) Отсечение элементов по критерию (reject) Метод take Есть ли хотя бы одно совпадение (any?) Все элементы должны удовлетворять критерию (all?) Несколько слов о популярных методах класса Array Символы Структура данных “Хеш” (Hash) Другие объекты в качестве значений Пример JSON-структуры, описывающей приложение Англо-русский словарь

45 48 51 53 56 58 62 65 67 70 72 75 76 78 84 86 90 93 94 96 98 103 112 117 120 121 122 124 125 125 126 127 127 128 128 129 129 132 136 139 143

Наиболее часто используемые методы класса Hash Установка значения по-умолчанию Передача опций в методы Список ключей Итерация по хешу Метод dig Проверка наличия ключа

146 148 150 157 159 161 164

Введение в ООП Классы и объекты Состояние Состояние, пример программы Полиморфизм и duck typing Наследование Модули Subtyping (субтипирование) против наследования Статические методы Вся правда про ООП Отладка программ Отладка с использованием консольного отладчика Отладка с использованием графического отладчика Практическое занятие: подбор пароля и спасение планеты Немного про виртуализацию, Docker, основные команды Docker Ruby Version Manager (RVM) Тестирование RSpec

166 166 168 180 185 192 199 201 205 209 211 214 220 222 237 240 254 256

Вместо предисловия В 21 веке программирование стало одной из важнейших наук в любой экономике. Процессы, которые происходили раньше без помощи компьютеров, были полностью или частично оптимизированы. Бизнес и простые люди увидели пользу электронных машин, и началась эпоха расцвета IT-индустрии. Во всем многообразии технологий образовались отдельные направления. Определились наиболее удобные инструменты для выполнения той или иной задачи. Языки программирования претерпели существенные изменения. Разобраться во всех языках и технологиях обычному читателю не так просто, как это может показаться на первый взгляд. В какой-то момент стало очевидно, что программист — одна из профессий 21-ого века. Но как стать программистом? В каком направлении приложить усилия? Что нужно изучать, а что не нужно? Как наиболее эффективно использовать время, чтобы освоить какую-либо технологию? Прежде, чем дать ответ на эти вопросы, нужно ответить на самый главный вопрос: а зачем нужно становиться программистом? Какой в этом смысл? Кто-то захочет стать программистом, чтобы разрабатывать микропрограммы для межконтинентальных баллистических ракет и космической индустрии. Кто-то хочет стать программистом для того, чтобы создавать свои собственные игры. Кто-то хочет освоить программирование в электронных таблицах, чтобы эффективнее считать налоги. Но задача именно этой книги более бытовая. Автор подразумевает, что читатель на вопрос “зачем нужно становиться программистом?” даст ответ “чтобы быть программистом и зарабатывать деньги”. Обычно такой ответ дают люди, которые уже попробовали себя в какой-либо профессии и хотят более эффективно использовать свое время и получать за это деньги. Также это могут быть молодые люди, которые вынуждены идти в ногу со временем и осваивать технологии как можно быстрее, и как можно быстрее получать результат от своих знаний. Причем, результат не только в виде самих знаний — как написать ту или иную программу — а результат в денежном эквиваленте. Знание какого-либо направления в программировании подразумевает знакомство с основами языка, с элементарной теорией (которая отличается для каждого направления), с основными понятиями и определениями, а также знакомство с не основными инструментами (такими как операционная система, утилиты и дополнительные программы).

Направлений существует огромное множество. Это и разработка игр, и научные исследования, и обработка и анализ данных, и веб-программирование, и программирование для мобильных устройств, и т.д. Быть специалистом по всем направлениям сразу невозможно. Поэтому человек, начинающий или желающий изучать программирование, стоит перед выбором — куда податься? Что учить? Если вы являетесь научным сотрудником НИИ, то выбор, скорее всего, падет на язык python или c++, так как для этих языков накоплено большое количество библиотек для анализа и обработки данных. Если вы, например, работаете сторожем и полностью довольны своей работой, то можно изучить какой-нибудь экзотический, маловостребованный на рынке язык программирования просто для того, чтобы не было скучно. Если вы живете в обществе, где каждый месяц нужно оплачивать счета, которые каждый месяц становятся все больше и больше, где нужно думать не только про сегодня, но и про завтра — выбор уже будет другим. Нужно будет изучить что-нибудь быстро, очень востребованное, чтобы скорее найти работу. Язык руби (ruby — англ.) и веб-программирование — это нечто среднее между “поскорее найти работу”, “выучить что-нибудь несложное и интересное” и “чтобы также пригодилось в будущем”. Руби не только позволяет составлять скучные программы, работая на кого-то в офисе, но также может быть полезен дома, в быту (одна из моих последних программ обучение игре на гитаре). Также философия самого языка подразумевает, что обучение и использование не будет скучным. К примеру, один из принципов языка — принцип наименьшего сюрприза (principle of a least surprise), который говорит буквально следующее: “что бы вы ни делали — скорее всего у вас получится”. Согласитесь, что это уже вдохновляет! Существуют также и другие языки программирования. Автор ни в коем случае не утверждает, что они плохие. Каждый язык хорош для определенной задачи. Но вспомним про нашу задачу и сравним с некоторыми другими языками.

Руби против ибур Язык “ибур” это “руби” наоборот. Это экзотический язык программирования, который кроме меня никто не знает. Я его сам только что придумал и я сам не знаю что он делает. Давайте сравним ибур с руби по трем параметрам, которые я описал выше:

Поскорее найти работу: Руби — очень популярный язык, легко найти работу Ибур — никто о нем не знает, работу найти невозможно Остальные параметры можно не сравнивать. Другими словами, если вам важно не только программирование в себе (что тоже неплохо), но и возможность заработать в обозримом будущем, то руби — неплохой выбор. Язык довольно популярен. Конечно, существуют и другие популярные языки программирования. Скажем, JavaScript, возможно, более популярен, но давайте сравним JavaScript и руби. Выучить что-нибудь несложное и интересное: Руби — principle of a least surprise, что уже довольно неплохо. JavaScript — изначально не создавался с идеей “принципа наименьшего сюрприза”. Сложнее, чем руби, так как является полностью асинхронным (пока поверьте мне на слово). Докажем, что JavaScript не такой уж и простой, как может показаться на первый взгляд. Рассмотрим программу на руби, которая сортирует числа: [11, 3, 2, 1].sort() Программа выше должна отсортировать числа 11, 3, 2, 1 в возрастающем порядке (пока не важно, если этот синтаксис вам непонятен, мы еще будем проходить эту тему). Результат работы программы на руби: 1, 2, 3, 11. Без сюрпризов! Но напишем ту же самую программу на JavaScript: [11, 3, 2, 1].sort(); Синтаксис в этом случае очень похож и отличается лишь точкой с запятой (semicolon) в конце. Но каков будет результат? Не всегда JavaScript программисты с опытом могут дать правильный ответ, ведь результат работы программы довольно неожиданный: 1, 11, 2, 3. Почему это так — это вопрос уже к истории. Но чтобы отсортировать числа в JavaScript, надо написать: [11, 3, 2, 1].sort((a, b) => a — b); Если разобраться, то это несложно. Но вопрос в другом. Нужно ли вам на начальном этапе тратить время на такие тонкости? JavaScript вполне востребован, и каждый рубипрограммист должен знать его на минимальном уровне. Но, признаться, быть full-time JavaScript разработчиком я бы хотел только за очень большие деньги. К тому же “чтобы также пригодилось в будущем” не очень подходит в случае с JavaScript. Язык очень динамично развивается. Знания полученные 10 лет назад уже не

актуальны (в данном случае я говорю про популярные фреймворки — наборы инструментов). В случае с руби фреймворк rails существует уже более 10 лет. Знания, полученные 10 лет назад, до сих пор применимы. К слову, про применимость знаний стоит сделать отдельное замечание. Знания языков shell-скриптинга до сих пор применимы, через более чем 30 лет мало что изменилось. Знания основ Computer Science — до сих пор применимо, на интервью и не только, эти знания практически не устаревают. Про применимость какого-либо языка в будущем никто не может дать точных прогнозов. Однако, можно посмотреть на статистику последних лет. На момент написания этой книги компания Microsoft купила за 7.5 миллиардов долларов GitHub, который был написан как раз на языке ruby. Другими словами, язык на сегодняшний день находится в прекрасной форме. Выпускаются обновления, улучшается скорость и синтаксис. А количество доступных библиотек позволяет быстро решить практически любую задачу (в рамках направления, которое называется веб-программирование).

Для фана На наш взгляд, язык программирования должен не только решать какие-то бизнес-задачи, но и быть приятным в использовании настолько, чтобы его хотелось использовать каждый день. К примеру, язык Java является отличным инструментом для решения бизнес-задач. Но требует к себе уважения — язык является типизированным (мы еще коснемся этой темы), необходимо указывать точный тип данных с которыми производятся различные операции. Это требует времени и полностью оправдано в бизнес-среде, где лучше потратить в несколько раз больше времени на разработку, чем платить потом за ошибки. В случае с руби можно написать программу быстро, “на коленке”. Нет очень большой надежности (что тоже является проблемой), но многие компании, особенно стартапы, пришли к выводу, что надежность является “достаточной”, а относительно невысокая скорость выполнения не является проблемой. Всё это с лихвой компенсируется скоростью разработки. Ведь в современном мире часто требуется сделать что-то быстро, чтобы быстро получить инвестиции, привлечь первых пользователей пока другие долго думают. С личной точки зрения автора, руби является хорошим инструментом для того, чтобы сделать что-то своё. Какой-то свой проект, программу, которой можно поделиться с окружающими, привлечь к себе внимание или заработать денег.

Другими словами, руби это эффективный, нескучный язык не только для работы, но и для себя лично — язык для романтиков.

Что мы будем изучать Как уже было замечено ранее, существует множество направлений программирования. Каждое направление уникально, и требует своих собственных навыков. На взгляд авторов на данный момент существует два (возможно и больше) “проверенных” направления в программировании, которые дают максимальный результат за минимальный срок. Под результатом тут понимается как денежная компенсация, так и само умение что-то сделать своими руками. Первое направление — это мобильная разработка: программы для мобильных телефонов (Android, iPhone), планшетов (iPad) и других устройств. Второе направление — веб-программирование. Если выбирать между мобильной разработкой и веб-программированием, то “быстрота освоения” любой из этих двух технологий по количеству вложенных усилий примерно одинакова. Однако, мобильная разработка обладает своими минусами. Например, Java язык для составления программ для Android — был уже упомянут выше. Нельзя сказать, что он является “достаточно простым” для новичка. Если честно, то с этим можно жить. В Java нет ничего такого, что является непостижимым или очень сложным. Однако, сама мобильная разработка часто подразумевает оптимизацию кода под мобильные устройства любыми средствами. Языки программирования и SDK (software development kit — набор разработчика для определенной платформы) очень часто навязывают определенный стиль разработки. И этот стиль сильно отличается от классического, объектно-ориентированного, программирования в сторону процедурного программирования. Процедурное программирование не всегда позволяет полностью использовать возможности языка, хотя это и не всегда важно, особенно если ваша задача — получить зарплату. Второй момент в разработке программ для мобильных устройств заключается в том, что на данный момент существуют две основных мобильных платформы. Одна платформа принадлежит корпорации Apple, другая — Google. Как именно будут развиваться эти платформы в будущем целиком зависит от политики этих компаний. В случае с веб-программированием на языке руби все выглядит немного иначе. Сам язык разрабатывается и поддерживается сообществом программистов. Веб-фреймворк rails, о котором мы еще поговорим, также поддерживается исключительно сообществом. Это

позволяет программистам со всего света создавать удобный инструмент именно таким, каким хочется, не оглядываясь на политику какой-либо компании. Более того, программы на языке руби редко исполняются на мобильных устройствах, поэтому “специально” оптимизировать их практически никогда не требуется. Ну и основное отличие руби от языков для мобильной разработки состоит в том, что руби это динамический язык — не в том смысле, что он динамично развивается (и это тоже) — а в том, что в нем присутствует так называемая динамическая типизация данных, о которой было уже упомянуто выше. Основное преимущество динамической типизации по сравнению со статической — меньше правил и меньше строгости, что дает более высокую скорость разработки приложений программистом (за счет более медленного исполнения написанных программ и “достаточной” надежности. Но скорость исполнения нас не особо интересует, ведь руби не используется для разработки мобильных приложений, хотя может работать ключевым звеном на сервере и обеспечивать функционирование мобильных приложений для iOS, Android, и т.д.). Несомненно, существуют и другие направления в программировании, которые не были проверены авторами этой книги. Например, разработка компьютерных игр. Наверное, для того, чтобы “проверить” все направления не хватит жизни, поэтому мы оставим эту затею для пытливых умов, и займемся тем, что точно востребовано на рынке, дает возможность “быстрого входа”, и является более или менее интересным и нескучным.

Веб-программирование или что-то другое? Книга “Руби для романтиков” разделена на две части. В первой части (вы её сейчас читаете) мы рассмотрим основы языка руби и использование языка из т.н. командной строки. Во второй части (планируется) будет непосредственно веб-программирование и фреймворк rails. Подождите, — скажет наблюдательный читатель, — ведь мы только что говорили про вебпрограммирование, а оно будет только во второй части? Всё верно. Дело в том, что сам по себе язык руби является довольно мощным инструментом. Студенты руби-школы находили работу и без знания вебпрограммирования. Основы языка, умение находить и использовать нужные библиотеки уже дают возможность создавать вполне полезные приложения, которые могут использоваться для обработки данных (например, веб-скрейпинг), для создания конфигурационных скриптов и управлением операционной системой (что обязательно пригодится любому системному администратору), для работы с файлами различного формата и так далее. Умение использовать язык для разного рода задач, не связанных с вебпрограммированием, дает неоспоримое преимущество перед тем, как вы начнете

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

Сколько зарабатывают программисты? Этот вопрос очень важен для тех, кто в программировании совершенно не разбирается. Но прежде, чем на него ответить, я хочу сделать отступление. Так как руби это, в основном, язык для веб-программирования, именно руби программисты положили начало удаленной (remote, на расстоянии) работе. Культура работать над одним проектом удаленно больше всего выражена именно в вебпрограммировании. Оно и понятно — для создания программного обеспечения, например, для самолетов, наверное, полезнее находиться именно в научном центре и работать рука об руку со своими коллегами из научного центра. Но в случае с веб-проектами, часто не важно где именно находится разработчик. Вклад в культуру удаленной разработки сделала и команда “37 signals”, разработчики которой находятся в разных частях света, и даже в разных временных зонах. Именно в “37 signals” появилась первая версия, пожалуй, самого популярного фреймворка для веб-разработки (rails). За последние 10 лет было доказано, что удаленная разработка возможна, что не всегда нужно держать команду программистов в одном офисе. Для любого руби программиста это огромный плюс. Ведь это означает, что руби программист не привязан к какой-то конкретной местности: можно работать на компанию в США из небольшого города, например, в Казахстане. При этом получать зарплату сильно выше любой зарплаты, которую можно получить “на месте”. Если взглянуть на статистику удаленных работ, то язык руби занимает второе место по количеству доступных вакансий https://remoteok.io/stats.php. Первое место удерживает JavaScript, но только лишь из-за того, что минимальные знания JavaScript являются необходимостью и он требуется в совокупности с остальными языками: Java, PHP, Ruby и т.д. А вот “чистый JavaScript” для full-stack программирования находится уже на третьем месте (Node.js). Хочется заметить, что количество работ по определенному языку не является самым важным показателем, и вообще не может быть никаких важных показателей, с помощью которых можно сделать “точный выбор” на всю оставшуюся жизнь. Мы лишь говорим о том, что мы знаем сейчас. Прогнозировать на несколько лет вперед в IT-индустрии очень сложно. Но, несомненно, хорошая новость заключается в том, что вам не нужны тысячи

работ — достаточно найти одну. Также обычно не очень важно сколько именно времени вы потратите на поиск работы — одну неделю, две недели или два месяца. Тут мы подходим к статистике, которая была собрана студентами руби-школы. Так сколько же зарабатывают руби программисты? Прежде чем ответить, сделаем оговорку, что речь будет идти только про удаленную работу. Рынок удаленных зарплат более стабилен, он был уравновешен программистами из разных стран, и на нем сформировалась определенная цена. Нет смысла сравнивать зарплату “на месте”, так как руби программист может (и даже обязан) работать удаленно, и в большинстве случаев это более выгодно. Также подразумевается, что программист имеет минимальные знания английского языка, которые позволяют ему общаться по переписке с заказчиками из других стран. Категории зарплат можно условно разделить на три части. В настоящее время стоимость часа работы программиста с 1 годом опыта составляет не более 10 долларов в час. От 1 года до 3 лет — примерно от 10 до 25 долларов в час. От 3 до 7 лет — примерно от 25 до 40 долларов в час. При достижении цифры в 40 долларов в час все становится очень индивидуально. К слову, стандартное количество часов в месяц — 160. Из нашего опыта, вполне реально без особых навыков за 1 год освоить программирование на руби и найти первую удаленную работу. Возможно, потребуется предрасположенность (этот факт не был доказан) и знание или желание выучить английский. Этот путь прошли многие студенты руби-школы, и подтверждение этим словам можно найти в нашем чате https://t.me/rubyschool

Ваше преимущество Прежде чем мы приступим к созданию вашей первой программы, важно будет упомянуть о том, что к программированию не относится. Любой человек имеет разный набор жизненного опыта. Возможно кто-то пришел в программирование из музыки, кто-то из финансов. Любому музыканту будет в разы проще написать программу для обучения людей нотной грамоте. Финансисту будет проще написать программу для учета торгового баланса. В чем заключается ваше преимущество? По мере изучения языка руби постоянно будет возникать вопрос о создании вашей собственной программы или серии программ по вашим идеям. Это необходимо по следующим причинам. Во-первых, любая программа обычно решает какую-то бизнес-задачу. Программистам платят деньги за то, что они оптимизируют бизнес-процессы, упрощают реальную жизнь, сокращают время, которые люди тратят на какие-либо действия. Например, представьте себе очередь в каком-нибудь государственном учреждении в 1986 году. Много людей

собрались в зале ожидания и ждут своей очереди. А теперь представим что есть программист, который написал программу “электронная очередь”. Через сеть Интернет любой человек может записаться на прием, прийти ровно к назначенному времени, а время, которое он провел в очереди, он потратит, например, преподавая урок математики школьникам. Экономическая выгода очевидна: время, проведенное в очереди, теперь тратится с пользой. А все из-за того, что был создан какой-то полезный сайт. То же самое и с вашими знаниями. Знания какой-либо предметной области уже являются ценным активом. Попробуйте увидеть ваше преимущество, подумать о том, каким образом вы могли бы улучшить мир. Хорошо если у вас будет несколько идей, записанных на бумаге. По мере работы с этой книгой, вы можете к ним возвращаться и задавать себе вопрос: а могу ли я это реализовать с помощью руби? Во-вторых, используя свое преимущество в какой-либо области, вы сможете создавать программы просто для демонстрации своих знаний. Даже самая простейшая программа, которую может написать профессиональный музыкант, будет вызывать восторг у программистов с большим опытом, которые музыкантами не являются. Не выбрасывайте свои программы, даже самые наивные из них можно будет в будущем улучшить. Они также пригодятся когда вы будете искать работу, иметь на руках хоть какой-то образец кода намного лучше, чем вообще его не иметь. Ваши программы могут казаться незначительными, но при приеме на работу играет роль не отдельная программа, а совокупность всего, что вами было продемонстрировано: знания программирования, написанные программы, резюме, знания предметной области, активный GitHub аккаунт, активный блог по программированию в Интернете. В-третьих, если вы не работаете над своим проектом, то ваш успех зависит от случайности. Сложно предсказать в какой именно коллектив вы попадете, какие стандарты качества создания программных продуктов будут в вашей компании. Человеку свойственно надеяться на лучшее, но практика показывает, что в реальной жизни все немного иначе, и успех часто зависит от случайности. Досадно попасть в компанию с бюрократическими сложностями, досадно попасть в коллектив с низкой технической квалификацией. Более того, начинающий программист может даже не распознать эти признаки, и как следствие — депрессия и разочарование в выбранном пути. Но на самом деле программирование должно доставлять удовольствие. И свой собственный проект — это ваш ориентир, показатель роста вашего уровня и страховка от случайности. В любой сложной ситуации на вашей новой работе вы сможете сказать себе «да, может быть я не очень продуктивен на этой работе, но вот мой проект, и вот демонстрация моей технической квалификации. Скорее всего дело не во мне, а в чём-то другом». Более того, этот аргумент можно всегда использовать для диалога с вашим менеджером, а сам

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

Среда исполнения Среда исполнения — важное понятие. В дальнейшем вводится понятие среда/окружение (environment), но это не одно и то же. Среда исполнения — это где и “кем” будут запускаться ваши программы на языке руби. Скажем, ученый-химик может делать эксперимент в пробирке, в большой стеклянной банке, и даже в собственной ванной. То же самое справедливо и для программы на руби. Она может быть исполнена разным “интерпретатором” (программой для запуска программ), в разных условиях — на операционной системе Windows, Mac, Linux. Когда автор этих строк впервые познакомился с компьютером, среда исполнения была одна — не было никакого выбора. При включении компьютера был виден курсор и надпись “ОК”, которая означала что можно вводить программу. Сейчас компьютеры стали более умными и новичку еще предстоит разобраться, как запускать программу, где вводить текст программы, “чем” запускать написанную программу, какая среда исполнения лучше. Кстати, в какой именно операционной системе запускается программа, для нас не очень важно. На сегодняшний день программу, написанную на любом из популярных языков программирования, можно запустить на трех ОС: Windows, MacOS, Linux. Обычно не требуется никаких изменений в самой программе или эти изменения минимальны. Статистика использования операционных систем показывает, что наиболее популярной ОС на сегодняшний день является ОС Windows. Именно с Windows мы и начнем, хотя это и не является лучшим выбором. Причина нашего решения в том, чтобы максимально быстро ввести вас в курс дела, и любой начинающий программист максимально быстро мог написать нужную программу. Ведь настройка среды исполнения обычно не очень простое дело для начинающих, и кажущаяся “сложность” может отпугнуть студента на первом этапе. Несмотря на то, что мы начнем запускать наши программы в ОС Windows, в будущем настоятельно рекомендуется не использовать ОС Windows для запуска программ на языке руби. Однако, эту ОС при желании можно использовать для написания программ. В любом случае, авторы рекомендуют как можно быстрее установить Linux (Mint Cinnamon edition, как наиболее простой дистрибутив) и использовать его. Если вы используете Mac, то нет необходимости устанавливать Linux.

Настройка Windows для запуска первой программы Терминал (который также называют словами “консоль”, “оболочка”, “шелл”, “командная строка”) — друг любого руби-хакера. Чтобы запускать программы, которые мы с вами напишем, нужен какой-то центральный пульт, откуда мы будем руководить процессом. Этим пультом и служит терминал. Ради точности следует заметить, что терминал — не совсем правильное слово. Но оно часто используется. Программисты говорят “запустить в терминале”, но если копнуть глубже, то терминал — особая программа, которая запускает оболочку (shell). И на самом деле мы отправляем команды в оболочку, где терминал служит лишь транзитным звеном, удобной программой для соединения с оболочкой. Забегая вперед, хочется заметить, что существуют разные типы оболочек. Стандартной оболочкой в индустрии является bash. Однако, авторы рекомендуют использовать zsh (читается как “зи-шелл”), в вариации “Oh My Zsh”. Эта оболочка немного отличается от стандарта, но дает более широкие возможности и является более удобной. Но в ОС Windows стандартная оболочка это cmd.exe. Если вы нажмете Пуск — Выполнить — cmd.exe, то увидите черный экран и “приглашение” командной строки:

“Приглашение” заканчивается символом >, который означает, что оболочка ожидает вашего ввода. Стоит сразу запомнить неочевидный момент — если что-то не получается, необходимо попробовать перезапустить оболочку. Это справедливо и для других операционных систем и за свою карьеру авторы наблюдали “магическое действие” этого

трюка на уже, казалось бы, очень опытных программистах. Выйти из оболочки можно словом exit или просто нажав на крестик вверху окна. В ОС Linux и Mac терминал обычно доступен по-умолчанию среди программ и можно запустить его щелкнув по невзрачной иконке, скорее всего в виде прямоугольника. В этих операционных системах приглашение командной строки принято обозначать символом доллара $. Это не всегда правда, но на будущее стоит запомнить — если вы видите знак доллара где-нибудь в документации и после этого знака идет команда (например $ ls), то знак доллара обычно вводить не надо. Это просто индикатор того, что команду надо выполнять в оболочке bash (или частично совместимой с ней zsh). Неважно, в какой оболочке вы сейчас находитесь, введите команду ruby и нажмите Enter. В случае с Linux и MacOS ошибки не будет, команда запустится и тихо будет ожидать окончания ввода программы. В Windows должна быть ошибка, ведь язык руби поумолчанию не установлен, а это значит, что нам надо его установить. Тут следует сделать отступление. Сейчас и в будущем — если вы не знаете, что делать, задайте вопрос гуглу. Например, в нашем случае — “how to run ruby program on windows”. Умение задавать вопрос и искать ответ — половина дела. Если честно, то только благодаря этому умению можно научиться программировать. Главное — мыслить последовательно и логически. Если не получается, всегда можно обратиться за помощью в чат (https://t.me/rubyschool). Для запуска программ на руби из ОС Windows нужно запустить Ruby Installer https://rubyinstaller.org/. После того, как программа установлена, можно вводить команду `ruby` в терминале (не запускается? Может, попробовать перезапустить терминал?). TODO про пути. Ruby запустится “тихо”, и будет ожидать вашего ввода. Введите `puts 1+1`, затем нажмите Enter, а потом Ctrl+D (иногда Ctrl+D приходится нажимать два раза): «` $ ruby puts 1+1 (нажмите Ctrl+D в этом месте) 2 $ «` Что мы видим на экране выше? Приглашение командной строки ($), вводим ruby, потом “puts 1+1”, потом Enter, который переводит нас на следующую строку, на которой мы нажимаем Ctrl+D. После этого “сама появляется” цифра 2. Что же тут произошло? Во-первых, вы запустили программу для запуска программ. Ruby — это программа (интерпретатор), которая позволяет запускать ваши, человечески написанные, программы. Компьютер говорит на языке нулей и единиц, и чтобы вас понять, ему надо считать человеческий язык — “puts 1+1”.

Комбинация Ctrl+D (обозначается также ^D) пригодится вам во всей вашей дальнейшей жизни, она передает сигнал о том, что “ввод закончен” (конец ввода, end of input, end of file, EOF). Это байт (его значение равно 4 — это запоминать не надо), который говорит о том, что наступил конец текстового потока данных, данных больше не будет. Интерпретатору ruby ничего больше не остается — только запустить то, что вы написали, что и было сделано. Набранная вами команда “puts 1+1” — это ваша первая программа. Но мы не сохраняли ее в файле, мы ввели эту программу с клавиатуры и она “пропала” после того, как была выполнена. Сожалеем, что вы не сохранили свою первую программу. Но ничего страшного, она занимала всего лишь 8 байт, и восстановить ее — небольшая проблема. Что же такое “puts 1+1”? Прежде чем ответить на этот вопрос, выполните задание. Запустите программу “1+1” (без puts). Мы увидим, что ничего не происходит. На самом деле результат был посчитан, но просто не выведен на экран. Возможен вариант, когда вы зададите компьютеру какую-нибудь сложную задачу и он будет считать ее очень долго. Но если вы не написали puts, то результат мы не узнаем. Другими словами, puts выводит результат. Это сокращение от двух английских слов: put string (вывести строку). В других языках были приняты другие сокращения для вывода строки, например в языке Бейсик это “print”. Так почему же надо писать puts вначале, а не в конце? Ведь сначала надо посчитать, а потом уже выводить. Почему puts стоит вначале? Все просто, в этом случае говорят: “метод (функция) принимает параметр”. Т.е. сначала мы говорим, что мы будем делать выводить, а потом — что именно мы хотим выводить. Нашу программу можно также записать как “puts(1+1)”. В этом случае видно, что в скобках — параметр. Ведь в математике мы сначала считаем то, что в скобках, а потом уже выполняем остальные действия. Кстати, наши поздравления! Вы написали свою первую программу. Задание: остановитесь тут и попробуйте написать программу, которая считает количество миллисекунд в сутках. Следующий абзац содержит ответ: «` $ ruby puts 60 * 60 * 24 * 1000 (нажмите Ctrl + D) «`

Задача чисто математическая, количество секунд в минуте умножаем на количество минут в часе, умножаем на количество часов в сутках. И, чтобы получились миллисекунды, а не секунды, умножаем на 1000. Далее, попробуйте запустить следующую программу: puts 5**5 * 4**4 * 3**3 * 2**2 * 1**1 Запись ** означает возведение в степень. Например, 3 ** 2 = 3 * 3 = 9. Удивительно, но результат работы программы (5 в пятой степени умноженное на 4 в четвертой и т.д.) выше будет равен количеству миллисекунд в сутках! Этому нет какого-то объяснения. Совпадение? Может быть. Если вы знаете объяснение, напишите нам на email. В качестве упражнения попробуйте написать такую конструкцию (подумайте, что произойдет): puts 60 * 60 * 24 * 1000 == 5**5 * 4**4 * 3**3 * 2**2 * 1**1

Здравствуйте, я ваш REPL В случае с “1+1” выше наш интерпретатор выполняет два действия: read (прочитать), evaluate (выполнить). Так как не было третьего действия “print” (puts в нашем случае), то не было и результата на экране. То есть, чтобы мы видели результат, надо выполнить read (R), evaluate (E), print (P). Хорошо бы еще и не запускать ruby каждый раз, чтобы программа в бесконечном цикле (loop — L) спрашивала нас “что хотите выполнить?”, т.е. сразу принимала бы ввод без лишних разговоров. Из начальных букв у нас получилось REPL — read evaluate print loop. То есть REPL это такая программа, которая сначала читает, потом исполняет, потом печатает результат, и потом начинает все сначала. Это понятие широко известно и используется не только в ruby. А в руби REPL-программа называется irb (interactive ruby). Попробуйте ввести irb и посмотрите что произойдет: $ irb 2.5.1 :001 > Непонятные цифры вначале — это версия руби. В нашем случае 2.5.1 (то же самое покажет команда ruby -v). 001 это номер строки — первая строка. То есть если REPL уже содержит “P” (print), то можно вводить “1+1” без puts.

Задание: посчитайте количество секунд в сутках, не выводя результат на экран с помощью puts. Принцип наименьшего сюрприза говорит нам о том, что выход из REPL должен быть командой exit. Вводим exit — получилось! Тут хочется заметить, что авторы редко используют именно irb в роли REPL. Существуют более удобные инструменты (pry — http://pryrepl.org/), которые выполняют ту же самую функцию, но имеют больше настроек. Этот инструмент рассматривается дальше в этой книге.

Запуск программы из файла Запуск программы из файла ненамного сложнее. Достаточно передать аргумент интерпретатору руби с именем файла: $ ruby app.rb В этом случае интерпретатор считает программу из файла app.rb и запустит ее так же, как если бы вы ввели эту программу и нажали ^D. Но возникает вопрос — как и где сохранить эту программу, в чем ее набрать, какой редактор кода использовать? Для начала ответим на первый вопрос — “где” сохранить программу, так как этот вопрос подразумевает знакомство с файловой системой и в нем есть некоторые подводные камни. Для Windows, операционной системы, с которой вам нужно как можно скорее уходить на Linux, необходимо создать директорию (каталог, папку) в разделе C: и назвать ее, например, projects. После этого нужно перейти в директорию, создать там файл и запустить его. Другими словами, нужно уже уметь делать как минимум четыре вещи: 1) Создавать директорию 2) Переходить в директорию 3) Создавать файл в директории и сохранять что-то в этот файл 4) Запускать файл (это мы уже умеем: ruby app.rb) Тут можно было бы дать основные команды ОС Linux для этих целей и не завязываться на ОС Windows. Однако, рынок диктует свои условия — большинство пользователей сейчас работают на Windows, а значит с большой долей вероятности и у вас установлена эта операционная система. Но не стоит отчаиваться, мы исправим этот досадный факт, а пока постараемся как можно быстрее настроить нашу среду исполнения, чтобы мы могли писать и запускать программы, а исправлением займемся потом.

Умение ориентироваться в файловой системе — ключевой навык любого программиста. Как библиотекарь должен знать, где какая книга лежит, так и программист должен знать (или уметь разобраться, найти), где лежит тот или иной файл. Нужно всегда иметь в голове примерную “картинку” файловой системы. Но из практики обучения студентов этому, казалось бы, простому делу выяснилось, что не все представляют себе, что такое файловая система и как эффективно работать с файлами (создавать, находить, переносить, переименовывать). Можно было бы написать список команд и дать задание запомнить эти команды. Но мы пойдем более гуманным и проверенным путем — мы познакомимся с файловым менеджером.

Я ваш файловый менеджер Если вы занимаетесь программированием более 20 лет, то вряд ли существует много инструментов, которые были актуальны тогда и сейчас. Именно поэтому мы изучаем руби, т.к. знаем, что знания, полученные 10 лет назад, до сих пор не теряют свою ценность. Но существуют также и другие инструменты, которые используются и сейчас, и пережили при этом не одно поколение операционных систем.

Один из таких инструментов — файловый менеджер:

Как было уже замечено ранее, работа с файлами — это ключевой навык программиста, системного администратора или даже любого эникейщика. В большинстве книг по программированию (и не только) работа с файлами не освещается достаточно хорошо. Дается набор шелл-команд, но никто не говорит, как работать с файлами эффективно, быстро и просто. Последнее — немаловажно для любого начинающего, ведь наша задача как можно эффективнее потратить наше время на наиболее значимые вопросы программирования, получить работу, а потом уже “дотачивать” навык. Поэтому запоминать команды оболочки, приведенные ниже, не стоит, они запомнятся сами. Более того, команды будут даны для ОС Linux (точнее, для оболочки, совместимой со стандартной bash). А комбинации клавиш для ОС Windows, т.к. Far работает только в Windows. Подождите, — скажет внимательный читатель, — мы хотим уйти от Windows, но также хотим научиться работать в Far? Дело в том, что файловый менеджер — вещь универсальная. Еще во времена DOS (уже малоизвестная операционная система от Microsoft) появился один из самых первых (самый первый?) файловых менеджеров — Norton Commander. Под операционной системой Linux (а также и MacOS) существует Midnight Commander:

Да и кроме “синих экранов” существуют различные варианты файловых менеджеров на любой вкус и цвет. Однако, популярность Far’овa настолько высока (из-за удобства прежде

всего), что некоторые программисты нашли способ запустить его на Linux и Mac без использования эмулятора. Способ установки Far на Linux и MacOS описан по ссылке https://github.com/elfmz/far2l. Начинающие программисты могут столкнуться с трудностями, следуя инструкциям по этой ссылке, но если у вас есть опыт или время, мы настоятельно рекомендуем установить Far на Linux/MacOS. На MacOS этот файловый менеджер устанавливается одной командой: «` $ brew install yurikoles/yurikoles/far2l «` Если на вашей MacOS не установлен HomeBrew, то потребуется установить сначала эту программу. Подробности см. по ссылке https://brew.sh/. После установки вы сможете запустить файловый менеджер командой `far2l`.

Рис. Far Manager запущен на MacOS Задание: если вы используете MacOS или Linux, найдите и установите файловый менеджер. Пример запроса в гугле: “file manager for mac os”. Для Linux семейства Ubuntu установка обычно сводится к двум командам в терминале: $ sudo apt-get update

$ sudo apt-get install mc (после этого можно вводить “mc”, чтобы запустился Midnight Commander). Не отчаивайтесь, если у вас ничего не получилось. Имейте в виду, что если что-то не работает локально, можно всегда воспользоваться облаком. Например, сайт https://repl.it/ предлагает на выбор множество языков программирования, которые можно запустить прямо в вашем браузере. Среди этих языков есть и Руби. Конечно, это не путь настоящего джедая, но как бэкап-план — отличное решение!

Основы работы с файловой системой Говорят, что файловая система “древовидная”, то есть её можно представить в виде дерева. Каждая ветвь — это директория, в которой может быть одна или несколько других директорий (ветвей) или файлов (листьев). Также директория может быть пустой. Самую главную директорию называют “корневой” (root directory — не надо путать с root home directory — это директория пользователя с именем root). Уже тут начинаются разногласия. Почему структура древовидная, а главная директория корневая, а не стволовая? Ведь ветви растут от главного ствола! Также, когда мы представляем дерево — мы подразумеваем, что дерево растет вверх. Хотя во всех файловых менеджерах “корни” растут вниз — надо нажать кнопку вниз, чтобы поставить курсор на 1 директорию ниже. Может тогда лучше говорить, что структура не древовидная, а корневидная?

Рис. Детское творчество в одном из детских садов в Кремниевой Долине. Любопытный программист задаст вопрос — а где корень у этого дерева? Дело в том, что корневой, самый главный, узел (обычно обозначается как root — корень) находится в самом верху. Или мы все-таки говорим про ветви, которые растут снизу вверх? В этом вопросе есть неопределенность, пусть она вас не пугает. В любом случае, корневидная она или древовидная, у дерева вверху намного меньше ветвей, а внизу намного больше. В файловой системе такого нет, все директории и файлы по-умолчанию отсортированы в алфавитном порядке. Наверное, у человека не нашлось более точной аналогии и было принято называть файловую структуру “древовидной”. Кстати, одна из моих любимых сортировок файлов и директорий в любом файловом менеджере — по дате обновления в убывающем порядке. Такой порядок позволяет в самом верху видеть файлы, которые были обновлены недавно. А человек обычно всегда работает с самыми “свежими” файлами. Как только вы прочувствуете это преимущество, вам не захочется сортировать по-другому.

Рис. Сортировка по дате обновления в убывающем порядке в Finder (MacOS) Те программисты, которые постоянно используют консоль (не пользуются файловыми менеджерами) упускают это очевидное преимущество. Нам достаточно скачать проект любой сложности и мы уже будем видеть, какие директории и файлы были изменены недавно — над чем идет работа, в курсе чего нужно быть. Так что, пока не узнав ни одной команды, вы уже получили знание, которое будет полезно и которое также приобретается и осознается не сразу даже опытными программистами — озарение может наступить через несколько лет.

Навигация Навигация в файловой системе — это просто переход из одного каталога в другой, чтобы посмотреть, что там находится, какие файлы. В Far для навигации используются кнопки вверх, вниз, Enter (войти в директорию), tab (для перехода на соседнюю панель). В bash для навигации существуют следующие команды (cd работает в Windows, а вот ls уже не работает): $ cd my_dir # войти в директорию my_dir $ cd .. # подняться на уровень выше $ ls # листинг (список файлов) С cd вроде бы все понятно, но листинг обычно не выдает полный список файлов. Оказывается, что есть еще скрытые файлы (в Linux и MacOS они начинаются с точки)! Поэтому команду нужно изменить на `ls -a` чтобы вывести все файлы. В Far’ове тоже есть такая настройка (в верхнем меню Options-Panel Settings-Show hidden and system files). Авторы редко используют `ls` или `ls -a`. Наиболее удобная команда консоли для вывода всех файлов это `ls -lah`:

Флаг `l` указывает на то, что нам нужен вывод в виде расширенного списка (который содержит права на доступ к файлу, имя владельца, размер в байтах, дату обновления). Флаг `a` говорит о том, что надо выводить информацию обо всех файлах (all), в т.ч. скрытых. Флаг `h` говорит о том, что нужно выводить размера файла не в байтах, а в humanreadable формате, т.е. в формате, который понятен человеку (килобайты, мегабайты, гигабайты и т.д.)

Кстати, флаг `h` очень полезный и часто используется для других команд. Например, `df h` (disk filesystem in human-readable format) выводит статистику свободного места на разделах вашего диска в гигабайтах. А в Far’овe нужно для этого нажать Ctrl+L (^L). Чтобы скрыть нужно еще раз нажать ^L. Вообще программисты довольно ленивые люди, поэтому чтобы что-то включить, а потом выключить (toggle), иногда нужно нажать одну и ту же комбинацию клавиш. Например, просмотр файла в Far и Midnight commander это клавиша F3, а выход из просмотра тоже F3 (кто не знает — тот обычно использует Escape и тянется в конец клавиатуры). Мы изучили пару шелл-команд (на самом деле их не надо запоминать, просто сделайте пометку в книге), но в файл-менеджере это всего лишь несколько правильных кнопок, которые позволяют не только легче понять, как выглядит ваш проект или файловая система целиком, но и дают более наглядный результат. Авторы книги, несмотря на большой опыт программирования и привычку делать все из “черного экрана” — консоли, время от времени все-таки запускают файловый менеджер. Особенно это полезно делать на новом проекте, который содержит много файлов. Задание: “походите” по вашей файловой системе и посмотрите, какие файлы и директории в ней существуют. Директории из корневого каталога (== “директории из корневой директории”) будут часто встречаться в будущем.

Создание файла Один программист из нашей компании написал самый маленький в мире вирус, он занимал 0 байт и даже его создатель не знал, что он делает (шутка). Комбинация для создания файла в Far и MC (Midnight Commander) — Shift+F4. Разница между двумя менеджерами в том, что первый спросит имя файла вначале (перед созданием) а второй — в конце (перед сохранением). В ОС Linux и MacOS (далее мы будем говорить “линукс-совместимые”, хотя это и не всегда правда и говорят “юникссовместимые”, или просто *nix) существует команда для создания пустого файла:

$ touch app.rb Команда выше создает пустой файл app.rb (если файл уже есть, команда меняет время обновления файла на текущее). По-умолчанию файловый менеджер откроет встроенный редактор, где вам будет предложено ввести любой текст. Команда touch редактор не открывает, и, если вы выполняете свои действия из консоли, то вам потребуется запустить текстовый редактор самостоятельно. Пока тему текстовых редакторов кода опустим, ведь для простейших программ мощный инструмент не нужен. В текстовом редакторе введите `puts “hello”` и нажмите Esc. Вам будет предложено сохранить файл. Сохранить можно также с помощью F2 (в редакторах кода это почти всегда ^S). У вас появилась программа в текущем каталоге среди остальных файлов, но мы забыли создать директорию! Тут можно сделать две вещи — удалить файл F8, создать директорию F7 и повторить то же самое там. Или создать директорию и скопировать, нажав клавишу F5, туда наш файл. Копирование производится с одной панели на другую, поэтому на одной панели нужно создать директорию, потом переключиться на соседнюю (Tab) и оттуда уже скопировать. Можно было переместить файл, нажав клавишу F6, чтобы скопированный файл потом не удалять. Задание: разберитесь с созданием, копированием, переносом файлов и директорий. Попробуйте скопировать несколько файлов, предварительно выделив их (Ins). Ведь это потребуется нам в дальнейшем, а команды для копирования файлов и директорий из консоли не такие очевидные.

Консольный ниндзя Новичку на первых порах лучше всего хорошо разобраться с файловым менеджером. Мы бы могли дать курс по консольным командам, но сколько человек бы мы потеряли в бою, если бы нужно было овладеть искусством манипулирования файлов в консоли, прежде чем написать первую программу? Ниже мы разберем основные команды. Не все программисты с опытом с ними знакомы, поэтому запоминать их не стоит, но сделать пометку в книге нужно обязательно. Эти команды могут вам потребоваться через год, два и более лет. Создать директорию (make directory) “one”: $ mkdir one

Создать одну директорию “one”, в ней другую “two”, и и в ней третью “three”. Без флага `p` (path) не обойдешься: $ mkdir -p one/two/three Вывести файл на экран: $ cat file.txt Трюк: существует альтернатива команде `cat` (кошка), которая называется `bat` (летучая мышь). На официальном сайте https://github.com/sharkdp/bat говорится, что летучая мышь это кошка с крыльями “A cat with wings”. Требуется установить bat перед использованием. Из коробки команда позволяет выводить файлы с подсветкой синтаксиса и номерами строк. Обычно вывод файла осуществляется другой командой, ведь файл может быть большой. Вывести первые 10 строк на экран: $ head -10 file.txt Вывести последние 10 строк на экран: $ tail -10 file.txt Иногда существует какой-то большой текстовый файл, в который постоянно добавляются данные. И вы хотите выводить на экран обновления без перезапуска команды tail. В этом случае поможет флаг `f` (follow — следовать): $ tail -f file.txt Выход из этой команды осуществляется стандартной комбинацией Ctrl+C. Для переименования файла используется команда `mv` (в файл-менеджере F6), от слова `move`. Для компьютера переименовать и переместить файл это одно и то же. Дело в том, что в таблице размещения файлов (практически в любой стандартной файловой системе), содержатся только структуры с метаданными о файле (имя, размер, атрибуты и т.д.). Содержимое размещено на диске. При переносе или переименовании мы изменяем только таблицу, хотя содержимое остается на том же месте. Именно поэтому перенос больших файлов (гигабайты) занимает доли секунды, если операция выполняется на том же диске. И минуты и часы, когда операция выполняется на разных дисках — ведь нужно “перенести” (на самом деле скопировать и удалить) содержимое: $ mv file1.txt file2.txt # переименовать первый файл во второй

Скопировать файл (copy): $ cp file1.txt file2.txt Скопировать файл в директорию (попробуйте самостоятельно перенести, move, файл в директорию): $ cp file1.txt my_directory Скопировать файл в директорию на 1 уровень выше: $ cp file1.txt .. Скопировать файл в директорию на 2 уровня выше (то же самое можно сделать и в файлменеджере, если указать в качестве назначения директорию `../..`): $ cp file1.txt ../.. Скопировать несколько файлов в директорию. К слову, тут уже у многих т.н. высокомерных программистов, которые любят давать советы, наступает клин. Можете использовать этот вопрос “для проверки” — “а знаешь ли ты какой командой можно скопировать несколько файлов в директорию?”: $ cp my_dir В Far Manager для копирования нескольких файлов необходимо их сначала выбрать. Это можно сделать с помощью клавиши Insert (Ins). Если клавиши Insert на вашем компьютере нет (существует только на расширенных клавиатурах), то выбрать можно с помощью Shift + “стрелка вверх” или Shift + “стрелка вниз”. После этого для копирования с одной панели на другую нажать F5. Стоит заметить, что если вы установили “Oh My Zsh” вместо bash, то у вас доступна клавиша Tab, которая очень помогает набирать имена файлов. Например, вводите `cp

Поиск файлов (без директорий) с расширением `rb`: $ find . -name ‘*.rb’ -type f Как вы могли заметить, существует разные способы поиска файлов в текущей директории. Текущая директория обозначается точкой. Двумя точками обозначается директория уровнем выше. Директория двумя уровнями выше обозначается как `../..`. Небольшая справка по разным обозначениям и примеры использования find: ● . — текущая директория. Пример команды (ищет все файлы с расширением log в текущей директории): find . -name ‘*.log’ ● .. — директория уровнем выше. Пример команды (ищет все файлы с расширением log в директории уровнем выше): find .. -name ‘*.log’ ● ../.. — директория двумя уровнями выше. Пример команды (ищет все файлы с расширением log в директории уровнем выше): find ../.. -name ‘*.log’ ● ~ — домашняя (home) директория, т.е. личная директория текущего пользователя. Пример команды (ищет все файлы с расширением log в домашней директории): find ~ -name ‘*.log’ ● / — корневая (root) директория. Пример команды (ищет все файлы с расширением log в корневой директории): find / -name ‘*.log’ В Far Manager можно искать файлы с помощью специального диалога, который можно вызвать комбинацией Alt+F7. Визуально этот диалог более наглядный и с ним проще работать. По-умолчанию маска файла задана как *.* (все файлы, по аналогии с *.log файлы с расширением log). В этом диалоге можно также искать файлы с определенной строкой (например, когда требуется найти все файлы, в которых встречается ваше имя). Поиск по всем файлам определенной строки (в нашем случае something): $ find . -name ‘*.rb’ -type f | xargs grep something Команда выше делает поиск, а потом перенаправляет результат в команду xargs, которая для каждой полученной строки запускает программу grep с аргументами: `grep something file1.rb`. Не стоит переживать, если эта конструкция непонятна — со временем все встанет на свои места. Иногда полезно что-то быстро сохранить в файл прямо из консоли. Когда ввод окончен, нужно нажать Ctrl+D. Будьте осторожны, эта команда затрёт предыдущее содержимое файла: $ cat > file.txt Добавить в конец файла:

$ cat >> file.txt Немного про саму файловую систему. Корневой каталог обозначается как `/`. Есть также такое понятие как “домашний каталог” — это личный каталог текущего пользователя. Узнать имя текущего пользователя можно с помощью команды “кто я”: $ whoami ninja Любопытно, что в pry (отладчик/дебаггер и REPL, рассматривается ниже) есть команда `whereami` (где я). Она показывает, где вы находитесь в текущем коде (разбирается далее в книге). Вывести текущую директорию на экран (PWD — Print Working Directory — напечатать рабочую директорию): $ pwd /home/ninja Домашний каталог обозначается тильдой `~`. Можно вывести его на экран: $ echo ~ /home/ninja Или совершить другие манипуляции: $ mkdir ~/tmp # создать директорию tmp в домашнем каталоге $ cp file.txt ~/tmp # скопировать файл в созданную директорию Кстати, создайте директорию ~/tmp — это удобно для хранения временных файлов. Существует системная директория /tmp, но все данные оттуда удаляются после перезапуска компьютера (по-умолчанию). Удаление файла, будьте осторожны (remove): $ rm file.txt Удаление директории: $ rm -r my_dir Надо заметить, что параметр `r` универсальный для многих команд — он указывает на то, что работа будет производиться с директорией, рекурсивно (recursive).

ВНИМАНИЕ! Будьте осторожны с командой “rf”. Существует самая опасная команда, которую вы можете ввести: `rm -rf /`. Эта команда удалит содержимое корневой директории на вашем диске без какого-либо подтверждения. Иногда в сети существуют злые шутники, которые могут попросить вас что-нибудь ввести. Всегда проверяйте, что именно вы вводите. Выше мы рассмотрели команду копирования, но есть еще одна, менее известная, команда копирования, которая вам может пригодиться: `scp`. Это команда копирует файлы с удаленного сервера на локальный компьютер и обратно. Например, на вашем сайте произошла какая-то ошибка и вы хотите скачать файл с описанием ошибок через SSH-доступ. Это можно сделать с помощью “scp”. Останавливаться подробно пока на этом не будем, при желании вы всегда можете найти справку в Интернете. На этом тренировка для настоящих ниндзя окончена, время выпить чаю, да съесть ещё этих французских булок.

Текстовые редакторы Существует много текстовых редакторов, но мы будем говорить только про редакторы кода. Они отличаются от текстовых редакторов тем, что редакторы типа Word сохраняют файлы не в plain (чистом) формате. Нам нужен текстовый редактор, который позволит сохранять файлы as is (так, как они есть, ну или почти): т.е. если мы вводим 1 символ и нажимаем “Сохранить”, то размер файла будет ровно 1 байт. Если редактор очень простой, то он может быть отнесен как к текстовым, так и к редакторам кода. Все редакторы кода можно разделить на два вида: консольные и графические. Самый простой консольный редактор это nano: $ nano

Подсказка внизу — это основные команды. Существуют и другие, более продвинутые редакторы (vim, emacs). К сожалению, для овладения консольными инструментами требуется больше времени. Существует множество холиваров (holy wars — святые войны) на тему редакторов кода. Авторы пришли к выводу, что не стоит придавать выбору редактора очень большое значение, т.к. редактор сам по себе не имеет смысла без наличия знаний по программированию. Из графических редакторов для руби следует выделить четыре (в порядке преференций авторов): ● ● ● ●

VsCode (также известный как Visual Studio Code, не путайте со средой разработки Visual Studio) RubyMine (платный) Atom Sublime Text (платный)

RubyMine относится не к редактору, а к IDE — Interactive Development Environment, это улучшенная версия редактора кода, которую называют “среда разработки”. Начинающему можно порекомендовать любой из вышеперечисленных. Возможно, кому-то понравится RubyMine, в котором наличие широких возможностей облегчает отладку и написание программ, особенно на первых порах. Однако, в этой книге работа с тем или иным редактором рассматриваться не будет. Вначале мы будем использовать редактор,

встроенный в ваш файловый менеджер (Shift + F4), а в дальнейшем выбор редактора будет только за вами. Обычно любой редактор при установке создает команду для запуска редактора из консоли. С помощью консольной команды можно открыть редактор для текущей директории: $ code . # команда откроет редактор VsCode Или Atom: $ atom . Если запустить команду без точки, то откроется каталог по-умолчанию. На практике редко приходится запускать редактор без параметра. Задание: установить текстовый редактор. Попробовать создать несколько файлов в текстовом редакторе. В каждый файл запишите имя человека, которого вы знаете. Удалите файлы.

Первая программа На самом деле нашей первой программой была программа сложения двух чисел: “puts 1+1”. Давайте создадим новый файл с именем app.rb и запишем в него следующий код: puts «I would hug you, but I’m just a text» Когда файл создан и сохранен, из терминала можно запустить программу: $ ruby app.rb I would hug you, but I’m just a text В файл-менеджере тоже можно ввести “ruby app.rb”. Но что такое, если запустить программу через файл-менеджер, то все пропадёт! Тонкость в том, что программа запускается, “отрабатывает” и управление переходит обратно — в терминал или в нашем случае в файловый менеджер. Поэтому чтобы посмотреть “что же там было” после того, как мы нажали Enter, надо нажать ^O. Ура! У нас получилась первая осмысленная программа. Давайте её немного улучшим: puts «I would hug you, but I’m just a text» gets

Теперь мы выводим на экран строку и вместо того, чтобы выходить из программы ожидаем ввода. Но не просто ввода, а ввода строки. Инструкция “gets” это по сути “get string” — получить строку. Вот мы и пробуем получить строку. Заметьте, что строка может состоять из множества символов, поэтому руби понимает окончание строки только в том случае, если вы нажмете Enter. Разумеется, можно просто нажать Enter, тогда строка будет пустая (если честно, то не совсем, но будет “казаться”, что она пустая). Запустите программу выше и попробуйте нажать Enter. Если вы запускаете программу из файл-менеджера, то результат не “пропадет” и программа будет ждать вашего ввода. Давайте составим простейшую программу для изучения иностранного языка. Возьмем три слова: ball, door, peace. Представим, что нам нужно выучить эти слова. Мы напишем программу, которая будет спрашивать — “Как переводится слово peace?”. В этот момент подразумевается, что пользователь должен дать ответ вслух: мяч, дверь, мир. Т.к. с остальными операторами языка мы не знакомы, то обойдемся тем, что есть: puts «How to translate ball?» gets puts «How to translate door?» gets puts «How to translate peace?» gets Попробуем запустить — работает! Это не очень удобное, но рабочее и полезное приложение. Оно не выводит ответы, но уже задает вопросы. Другими словами, с помощью двух операторов puts и gets мы смогли написать что-то интересное. Что же будет дальше! Для играющих на гитаре предлагаем программу для изучения нот на первой струне: puts gets puts gets puts gets puts gets

«Say a note on a 0 fret?» # Подразумевается, что мы скажем E «Say a note on a 1st fret?» # Подразумевается, что мы скажем F «Say a note on a 2nd fret?» # На втором ладу находится нота F# «Say a note on a 3rd fret?» # G

И так далее, до 12 лада (E F F# G G# A A# B C C# D D# E). Напишите программу самостоятельно, если тема музыки вам не интересна, сделайте программу для изучения 10 слов.

По поводу листинга выше можно сделать несколько замечаний. Во-первых, вы наверное уже заметили, что после строки можно оставить любой комментарий, достаточно ввести # (решётка, hash, иногда говорят pound sign). Можно оставлять комментарий и на новой строке. Можно оставлять сколько угодно комментариев и пустых строк, на работу программы это не влияет. Задание: попробуйте оставить комментарии к своей программе и добавить пустые строки после gets, чтобы визуально программа выглядела “легче”. Второе замечание — поддержка русского языка, а точнее правильной кодировки. В ОС Windows скорее всего возникнут проблемы с русской кодировкой. Это одна из причин почему не стоит использовать Windows и нужно переходить на MacOS или Linux — на этих операционных системах проблем с кодировкой нет. К счастью, проблема кодировки очень просто исправляется, если в самое начало файла добавить: # encoding: cp866 Разумеется, файл должен быть тоже сохранен в этой кодировке в текстовом редакторе. Другими словами, мы “дружим” руби и текстовый редактор. Интерпретатору руби говорим в какой кодировке будет этот файл, а в редакторе выбираем эту самую кодировку CP866 (также она может называться DOS кодировкой). После этого можно писать по-русски. В “нормальных” операционных системах этих трюков проделывать не нужно. Если можете, переключайтесь на них как можно скорее. В дальнейшем таких сложных трюков быть не должно, но помните — если что-то не получается, то ошибка может заключаться в том, что вы используете неправильную операционную систему. Несмотря на то, что руби должен без проблем работать в Windows, для этой операционной системы он не предназначался. А авторы популярных библиотек не тестируют свои программы на Windows. Задание: если у вас установлена ОС Windows, попробуйте скачать VMWare Workstation (платная программа) или VirtualBox (бесплатная). Это виртуальная машина — программа для запуска операционных систем внутри вашей ОС. Попробуйте запустить виртуальную машину и установить в ней Linux Mint Cinnamon edition. Попробуйте написать первую программу в Linux! Если не получится — ничего страшного, продолжайте обучение дальше, можно будет вернуться к этому позднее.

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

того, чтобы его потом изменить. В этом и заключается суть переменных — это ячейки памяти, куда мы можем что-то записать и, при желании, изменить. Но не обязательно менять значения переменных, можно создавать переменные для удобства. Правда, в этом случае переменные часто называют константами — ведь они не меняются! Поэтому в современном языке JavaScript для создания переменных есть два ключевых слова: let для создания переменной, и const для создания константы. Но в руби все проще. Попробуем “объявить” (создать, define, declare, create, make) простую переменную: puts «Your age?» age = gets puts «Your age is» puts age В программе выше мы спрашиваем возраст. После того, как возраст указан, программа выведет на экран ответ: Your age? 20 Your age is 20 Возраст, который мы вводим сохраняется в переменную `age`. Мы бы могли назвать ее другим именем (например, `a`), но в этом случае и на четвертой строке пришлось бы писать «puts a». Существуют т.н. naming conventions — соглашения о наименовании — их достаточно просто найти: ввести в поисковой системе запрос “naming conventions variables ruby”. В языках программирования Ruby и JavaScript мы столкнемся с тремя основными naming conventions: ●

Snake case (snake — змея), между словами ставится знак подчеркивания underscore (`_`). Переменные именуются следующим образом: client_age, user_password, user_password_expiration_date. Используется в руби, а также в базах данных. Camel case (camel — верблюд), слово начинается с маленькой буквы, слова разделяются с помощью больших букв: clientAge, userPassword, userPasswordExpirationDate. Используется в JavaScript. Kebab case (kebab — шашлык), слова разделяются дефисом: client-age, userpassword, user-password-expiration-date. Иногда используется в HTML, в т.н. dataатрибутах. Например: «.

Пока запомним только первый вариант, для Ruby. Если переменная имеет длинное название, то слова разделяем нижним подчеркиванием. Нужно заметить, что чем короче названия переменных, тем лучше. Всегда нужно стремиться писать код так, чтобы названия переменных не были слишком длинными. Однако, для начинающих эта задача не всегда по силам. Придумать хорошее название для переменной не всегда просто, среди программистов ходит даже такая шутка: > There are only two hard things in Computer Science: cache invalidation and naming things. > (Дословно: существуют две сложные проблемы в Компьютерной Науке: инвалидация кеша и именование вещей). Если название переменной получается слишком длинным, не стоит его “искусственно” занижать (например, переименовав `client_password_expiration_date` в `cped`). Обычно это свидетельство того, что контекст решаемой проблемы слишком широкий, и пришла пора разбить функциональность на малозависимые друг от друга классы/объекты. Однако, это задача для другой книги. На данном этапе можете называть переменные так, как вам хочется. Кроме naming conventions существуют правила: в руби переменные должны начинаться всегда с буквы, переменные могут содержать цифры и/или знак подчеркивания. Задание: написать программу, которая подряд спрашивает год рождения, место рождения, номер телефона трех клиентов, после чего выводит полученную информацию полностью в виде “карточек” (в англ.языке это бы называлось baseball card, аналогия в русском языке — карточка из картотеки). Так как придумать названия на первом этапе на английском языке может быть сложно (а для переменных желательно, но не обязательно использовать английский язык, а не траснлит), приведем перевод. Год рождения — year of birth, место рождения — place of birth, телефонный номер — phone number. На будущее при возникновении вопросов об именовании переменных, рекомендуется заглянуть в словарь: ● ● ● ●

https://www.multitran.ru/ — русско-английский и англо-русский словарь http://context.reverso.net/ — альтернативный словарь с контекстом http://www.thesaurus.com/ — поиск синонимов англ.языка https://translate.google.com/ — переводчик на все случаи жизни

Сложение и умножение строк Давайте посмотрим на нашу программу, что мы можем в ней улучшить?

puts «Your age?» age = gets puts «Your age is» puts age Две последние строки можно сократить до одной: puts «Your age?» age = gets puts «Your age is» + age Результат работы программы: Your age? 30 Your age is30 Чего-то не хватает? Правильно, пробела после слова “is”. Как вы уже увидели из примера выше, мы можем складывать строки. С точки зрения математики это не имеет никакого смысла, зато строки в памяти компьютера объединяются. Запустите такой код в REPL или в виде программы: «My name is » + «Roman» + » and my age is » + «30» Результат: «My name is Roman and my age is 30» Попробуйте теперь сложить два числа в виде строк следующим образом, постарайтесь понять каким будет ответ: «100» + «500» Спойлер: ответ будет “100500”. Другими словами, если число представлено в виде строки (взято в кавычки), Руби будет понимать это число как строку. Если бы напишем `100 + 500` (не берем в двойные кавычки каждое число), то результат будет 600. Оказывается, что строки можно не только складывать, но и умножать. Только строку нужно умножать на число, в примере ниже нельзя взять второе число в кавычки: «10» * 5 => «1010101010» Получили число 10 повторенное 5 раз. Если мы поставим после 10 пробел, результат будет более наглядным: 38

«10 » * 5 => «10 10 10 10 10 » Как было уже замечено, `10 ` это всего лишь строка, можно подставить любую строку: «Я молодец! » * 10 => «Я молодец! Я молодец! Я молодец! Я молодец! Я молодец! Я молодец! Я молодец! Я молодец! Я молодец! Я молодец! » На практике приходится часто умножать `=` или `-` на 80 (стандартная ширина экрана в символах, принятая за стандарт), чтобы визуально отличить одну часть от другой. Например: puts «Your age?» age = gets puts «=» * 80 puts «Your age is » + age Результат: Your age? 30 ====================================================================== Your age is 30

Типы данных Мы уже разобрались, что две строки можно складывать с помощью `+`. Также мы знаем, что строку можно умножить на число. С помощью этих экспериментов мы выяснили, что существует как минимум два типа данных: строка и число. Причем, само число, взятое в кавычки это строка. Давайте посмотрим на то, как руби понимает что такое число, а что такое строка: $ irb > «blabla».class => String > «123».class => String > 123.class => Integer

Говорят, что все в руби — объект (Object). В результате любой операции получается объект. Каждый объект “реализует метод” `class`. Выражение “реализует метод” означает, что какой-то программист, разработчик языка руби, сделал специальную небольшую подпрограмму, которую мы с вами можем запускать, если знаем имя этой подпрограммы. Чтобы вызвать подпрограмму для какого-либо объекта, нужно ввести точку и написать имя этой подпрограммы. В нашем случае имя этой подпрограммы (говорят “имя метода” или “имя функции”, метод и функция — синонимы) это `class`. Кстати, не надо путать имя метода `class` с ключевым словом `class`, которое определяет т.н. класс — мы будем проходить это позднее. Если бы в реальной жизни у объектов были методы, то мы бы с вами могли увидеть следующее: Яблоко.разрезать Яблоко.количество_семян Яблоко.количество_червей Река.температура_воды Река.количество_рыбы И так далее. Так вот, в каждом объекте определен метод `class`: Object.class В нашем случае `123` (без кавычек) и `»blabla»` это объекты. Тип объекта `123` — Integer (целое число). Тип объекта `»blabla»` — String (строка). Тип любого объекта можно получить добавив в конце `.class`. Конечно, для каждого объекта существует документация о том, какие методы поддерживаются. Настоятельно рекомендуется смотреть документацию для каждого типа с которым вы работаете. Пример документации для разных типов: ● ● ●

https://ruby-doc.org/core-2.5.1/Object.html — тип Object https://ruby-doc.org/core-2.5.1/String.html — тип String https://ruby-doc.org/core-2.5.1/Integer.html — тип Integer

Документацию легко найти по поисковому запросу, например “ruby object docs” или “ruby string docs”. В документации описано все, что мы можем делать с объектом. Это настоящая кладезь информации, документация должна стать вашим лучшим другом. Программист, который не поглядывает в официальную документацию по мере разработки и обучения, вряд ли добьется успеха. В документации указаны все возможные операции, которые можно выполнять с тем или иным объектом. Пример документации к `Object.class`: https://ruby-doc.org/core-2.5.1/Object.html#method-iclass

А вот про пример умножения строки на число: https://ruby-doc.org/core2.5.1/String.html#method-i-2A — в документации дан любопытный пример умножения строки на ноль (возвращается пустая строка). Существуют и другие типы данных, мы рассмотрим их в этой книге в следующих главах. Задание: Узнайте какой тип данных у `»»`. А какой тип данных у `0` (ноль)? Какой тип данных у минус единицы? Какой тип данных у округленного числа “Пи” `3.14`? Задание: Известно, что метод `.class` для любого объекта возвращает результат. REPL читает (read), выполняет (evaluate) и печатает (print) этот результат на экран. Но если все в руби объект, то какого типа возвращается сам результат, когда мы пишем `.class`? Вот этот метод `.class` — результат какого типа он возвращает? Видно ли это из документации? Проверьте. Попробуйте написать `123.class.class` — первое выражение `123.class` вернет результат, а следующий `.class` вычислит тип этого результата.

Докажем, что все в руби — объект Известно, что `123.class` возвращает Integer, `»blabla».class` возвращает String. Но у объекта (Object) существует также метод `is_a?`, который возвращает истину или ложь, если передать определенный параметр в этот метод: $ irb > 123.is_a?(Integer) => true В примере выше для объекта `123` мы вызвали метод `is_a?` с параметром Integer. Метод вернул результат true (истина). Т.е. 123 является типом Integer (целое число). Если мы проверим, является ли `123` строкой, то ответ будет “ложь”: $ irb > 123.is_a?(String) => false Но для строки ответ будет “истина”: $ irb > «blabla».is_a?(String) => true Кстати, “is_a?” не какое-то магическое выражение, а “калька” с английского языка. Мы как бы спрашиваем “Is this object a string?” (является ли этот объект строкой?).

Выше мы убедились, что 123 это число, а “blabla” это строка. Но является ли число и строка объектом? Давайте проверим: $ irb > 123.is_a?(Object) => true > «blabla».is_a?(Object) => true Оказывается, что да! Число и строка являются объектами. 123 это одновременно число и объект. “blabla” это одновременно строка и объект. Что такое объект — мы разберем дальше. На этом этапе нет необходимости запоминать метод “is_a?”, принцип его работы, как правильно его вызывать и что он возвращает (говорят — “сигнатуру” или “API”). Наверное, стоит в уме держать только `.class` возможность проверить, какого типа результат выполнения того или иного действия может пригодится в будущем.

Приведение типов (англ: converting types или type casting) Давайте попробуем написать программу, которая считает, сколько вам месяцев. Мы будем вводить возраст человека, а программа будет считать этот возраст в месяцах. Учитывая то, что мы прошли в предыдущих главах, вырисовывается такой код: puts «Your age?» age = gets age_months = age * 12 puts «Your age is » + age_months Выше мы объявили переменную age_months, в которую записываем значение переменной age, умноженное на 12. Сможете ли вы заметить, что в этой программе не так? Результат работы программы: Your age? 30 Your age is 30 30 30

30 30 30 30 30 30 30 30 30 О-оу! В программу закралась ошибка. Оказывается, что мы умножаем строку на число. Попробуйте запустить программу еще раз и ввести blabla: Your age? blabla Your age is blabla blabla blabla blabla blabla blabla blabla blabla blabla blabla blabla blabla Переменная age имеет тип String. И когда мы умножаем String на Integer, мы получаем длинную строку, которую мы повторили с помощью нашей программы 12 раз. Чтобы программа работала правильно, нам нужно умножать Integer на Integer (число на число). Мы уже делали это, когда считали количество миллисекунд в сутках — тогда у нас все работало правильно. Чтобы программа работала правильно в этот раз, нужно чтобы вместо String был тип Integer. Что мы можем тут сделать? Если посмотреть документацию к функции (или методу, не забыли что функция и метод это синонимы?) gets, то мы увидим, что gets возвращает тип String. Оно и понятно, gets это сокращение от “get string”. Все что нам нужно — это функция “get integer”, если мы верим в принцип наименьшего сюрприза и предсказуемость языка руби, то это будет “geti”: $ irb geti NameError (undefined local variable or method `geti’ for main:Object Did you mean? gets)

Упс! Не получилось. Но у нас была честная попытка. Такого метода не существует, но что-то нам подсказывает, что он может появиться в будущем. Будем думать дальше, как же нам исправить нашу программу? В языке JavaScript (про который каждый руби-программист должен немного думать) существует способ “превратить” строку в число путем умножения строки на единицу (node ниже это интерпретатор JavaScript, работает если у вас установлен Node.js): $ node > «123» * 1 123 Получится ли это проделать в руби? > «123» * 1 => «123» > («123» * 1).class => String Не получилось. Значит должны быть другие способы. Открываем документацию класса String и видим целую серию методов, которые начинаются со слова “to” (от англ. convert to — конвертировать в. ). Среди этих методов есть прекрасный метод “to_i”, который означает “to Integer”, “в число”. Если бы мы записывали методы по-русски, то название было бы “в_ч”. Не очень очевидно, но видимо программистам хотелось дать короткое название, ведь функция конвертации строки в число встречается довольно часто, и теперь мы имеем `to_i` вместо `to_integer`. Т.е. для преобразования строки в число будем использовать функцию to_i: > «123».to_i => 123 > «123».to_i.class => Integer Кстати, существует аналогичная функция у класса Integer для преобразования числа (и других типов) в строку: to_s (to string). Попробуем переписать нашу программу для подсчета возраста в месяцах: puts «Your age?» age = gets age_months = age.to_i * 12 puts «Your age is » + age_months

Снова получаем ошибку, да что же это такое! app.rb:4:in `+’: no implicit conversion of Integer into String (TypeError) В этот раз ошибка на четвертой строке. Но ошибка уже нам понятна — не можем преобразовать число в строку. Т.е. в четвертой строке мы складываем строку и число. Умножать строку на число можно, а складывать почему-то нельзя. Ну ничего страшного, попробуем сделать “приведение типов” еще раз: puts «Your age?» age = gets age_months = age.to_i * 12 puts «Your age is » + age_months.to_s Попробуем запустить: Your age? 30 Your age is 360 Заработало! Существует несколько других способов написать эту программу и все они правильные. Например, можно “привести к типу Integer” на второй строке (третью оставить без изменений): puts «Your age?» age = gets.to_i age_months = age * 12 puts «Your age is » + age_months.to_s Или можно переопределить значение переменной age, добавив одну строку: puts «Your age?» age = gets age = age.to_i age_months = age * 12 puts «Your age is » + age_months.to_s Или можно вообще обойтись без переменной age_months. Попробуйте написать такую программу самостоятельно.

Дробные числа Рассмотрим некоторые популярные приведения типов, с которыми мы уже столкнулись. Тот или иной объект может реализовывать один или несколько следующих методов: ● ● ●

`.to_i` — перевод чего-либо в число (например, строки) `.to_s` — перевод чего-либо в строку (например, числа) `.to_f` — перевод чего-либо в дробь (например, перевод строки в дробь)

Запустим REPL чтобы посмотреть что такое дробь: $ irb > 3.14.class => Float Мы ввели число 3,14 (обратите внимание — через точку). А тип, который представляет дробь, называется Float. Мы также “имеем право” представить любое целое число не только в виде Integer, но и в виде Float: $ irb > 123.class => Integer > 123.0.class => Float Так зачем нужен тип Float? Затем же, зачем нужна и сама дробь — в основном для приблизительных математических расчетов (для более точных есть тип BigDecimal, альтернативное “более точное” представление дроби, которое работает несколько медленнее, но точнее, чем Float). Посчитаем 30% налог на вводимую зарплату: «` puts «Your salary?» salary = gets.to_i tax_rate = 0.3 puts «Tax:» puts salary * tax_rate «` Запустите эту программу и проверьте как она работает.

Читаемость программы можно значительно улучшить, добавив интерполяцию строк: puts «Your age?» age = gets.to_i age_months = age * 12 puts «Your age is #» В последней строке нам не пришлось заниматься приведением типов. Каждый объект в руби может быть преобразован в строку (см.метод to_s у класса Object). Поэтому существует универсальный синтаксис для любого типа — интерполяция. Хитрость интерполяции в том, что вычисляется выражение внутри фигурных скобок и результат вычисления приводится к строке. Мы попробовали одно выражение `age_months`, результат этого выражения — значение переменной. Но мы можем изменить нашу программу и попробовать интерполяцию поинтереснее: puts «Your age?» age = gets.to_i puts «Your age is #» Нет необходимости в создании еще одной переменной, мы можем посчитать выражение прямо внутри фигурных скобок. Результат работы программы будет одинаковым. На первый взгляд может показаться, что интерполяция — незначительное улучшение. Даже в старых версиях языка JavaScript можно было пользоваться простым знаком плюс: $ node > «Your age is » + 30 * 12 ‘Your age is 360’ Но в новой версии JavaScript (ES 6 и выше) тоже появилась интерполяция строк, несмотря на то, что она в общем-то и не нужна. Просто эта функциональность значительно облегчает работу программиста: $ node > `Your age is $` ‘Your age is 360’ > Обратите внимание, что в JavaScript для интерполяции используются обратные кавычки (backticks), а в руби — двойные. Интерполяция строк полезна, когда нам приходится иметь дело с несколькими переменными. Рассмотрим программу:

«` puts «Your name?» name = gets puts «Your age?» age = gets.to_i puts «Your city?» city = gets puts «=» * 80 puts «You are #, your age in months is #, and you are from #» «` Результат работы программы: «` Your name? Roman Your age? 30 Your city? San Francisco ====================================================================== You are Roman , your age in months is 360, and you are from San Francisco «` Почти получилось. Мы использовали интерполяцию строк и после визуального разделителя вывели все с помощью одной строки. Однако, что-то пошло не так. Мы видим, что после слова “Roman” идет перенос строки. В чем же дело? Дело в том, что функция gets возвращает строку с символом “\n”. На самом деле это один символ с порядковым номером 10 в стандартной таблице всех символов. Была договоренность, что если этот символ выводится на консоль, то последующий вывод будет начинаться с новой строки. Давайте докажем, что gets возвращает не просто строку. Выполним в REPL: «` $ irb > x = gets Hi => «Hi\n» > x.class

=> String > x.size => 3 «` Мы попробовали присвоить переменной x значение gets. Т.к. REPL печатает результат выражения, то мы видим “Hi\n”. Т.е. REPL уже нам говорит о том, что в конце стоит управляющий символ. Далее мы проверили тип с помощью `.class` — строка. И потом обратились к методу `.size`, который возвращает длину строки. Несмотря на то, что мы ввели строку из двух символов, размер строки равен трем. Потому что оператор gets “записал” в строку еще управляющий символ перевода строки. Когда мы делали интерполяцию выше, этот перевод никуда не делся и добавился в результат вычисления строки. Поэтому в нас произошел переход на следующую строку и вывод получился неаккуратным. Исправим это недоразумение: «` puts «Your name?» name = gets.chomp puts «Your age?» age = gets.to_i puts «Your city?» city = gets.chomp puts «=» * 80 puts «You are #, your age in months is #, and you are from #» «` Проверим работу программы: «` $ ruby app.rb Your name? Roman Your age? 30 Your city? San Francisco ====================================================================== You are Roman, your age in months is 360, and you are from San Francisco «` 49

Заработало! Метод `chomp` класса String отрезает ненужный нам перевод строки. Важно отметить, что интерполяция строк работает только с двойными кавычками. Одинарные кавычки могут использоваться наравне с двойными за тем исключением, что интерполяция строк в них намеренно не поддерживается. Более того, инструменты статического анализа кода (например, Rubocop) выводят предупреждение, если вы используете двойные кавычки и не используете интерполяцию. В дальнейшем мы будем использовать одинарные кавычки, если интерполяция строк не нужна. Задание 1: посмотрите документацию к методу “chomp” и “size” класса String. Задание 2: напишите программу для подсчета годовой зарплаты. Пользователь вводит размер заработной платы в месяц, а программа выводит размер заработной платы в год. Допустим, что пользователь каждый месяц хочет откладывать 15% своей зарплаты. Измените программу, чтобы она выводила не только размер заработной платы, но и размер отложенных за год средств. Измените программу, чтобы она выводила размер отложенных средств за 5 лет.

Bang! Есть одна любопытная деталь в языке руби на которой стоит остановиться отдельно, это bang, exclamation mark, восклицательный знак или просто `!` в конце какого-либо метода. Рассмотрим программу (некоторые фразы в программе могут быть на русском языке, который по-умолчанию плохо поддерживается в Windows, мы еще раз рекомендуем вам переходить на Linux Mint Cinnamon или MacOS): «` x = ‘Я МОЛОДЕЦ’ x = x.downcase puts x «` Вывод программы: $ ruby app.rb я молодец Мы объявили переменную и присвоили ей значение “Я МОЛОДЕЦ”, заглавными буквами. На второй строчке мы переопределили переменную, присвоив ей значение “x.downcase”. Т.к. переменная “x” имеет тип String (тип “строка”, этот тип приобретают все переменные, когда мы присваиваем им значение в кавычках), то мы имеем право вызвать метод “downcase” для типа String (документация https://ruby-doc.org/core-

2.5.1/String.html#method-i-downcase). Этот метод преобразует заглавные буквы в строчные, и мы видим на экране вывод маленькими буквами. Больше всего нас интересует вторая строка “x = x.downcase”. В языке руби было принято соглашение для удобства, если требуется изменить значение самой переменной, не обязательно ее “переопределять” вот таким образом. Можно написать “x.downcase!” и руби будет знать, что операцию downcase нужно проделать не “просто так” и вернуть результат, а проделать заменить значение самой переменной. Не для каждого метода существует эта функциональность, в каждом отдельном случае требуется смотреть документацию. В руби вызов метода с восклицательным знаком считается “опасным”, т.к. меняется состояние (значение) объекта. Что же тут опасного скажет читатель, ведь мы просто меняем значение! Но не все так просто. Рассмотрим такую программу (без каких-либо хитрых трюков, просто попробуйте догадаться, что будет на экране): «` a = ‘HI’ b = a a = ‘xxx’ puts b «` У нас две переменных: a и b. На второй строке переменной b присваиваем значение a. Т.е. переменная b приобретает значение “HI”. Далее мы “забиваем” значение переменной a иксами (потому что можем, далее будет понятно почему). Что будет на экране? Да ничего необычного, переменную b мы не трогали и мы увидим “HI”. Теперь перепишем программу немного иначе: «` a = ‘HI’ b = a a.downcase! puts b «` Почти то же самое, отличается только третья строка. С переменной b мы ничего не делали. Но зато сделали с переменной a “опасную операцию”. Что будет выведено на экран? Оказывается, что “опасная операция” поменяет значение b. Попробуйте сами, вы увидите “hi”.

Объяснение этому кроется в том, как именно работает язык руби. Для начинающего вряд ли есть большой смысл вдаваться в эти детали. Вкратце лишь заметим, что каждая переменная — это просто адрес (число от 1 до какого-то большого значения, например 123456789). А вот само значение находится где-то далеко в памяти по этому адресу. Аналогия может быть с квартирным домом. В многоквартирном доме висит несколько звонков, у каждого звонка свой номер. Когда мы создаем новую переменную, то мы создаем новый звонок, который ведет к какой-то новой квартире. Когда присваиваем “b = a”, то новый звонок b ведет к той же самой квартире и все работает. Но когда мы выполняем “опасную операцию”, то мы меняем не звонки, а содержимое самой квартиры. В методе с восклицательным знаком нет ничего магического. Когда мы научимся создавать свои классы и объекты, вы сами сможете написать свой bang-метод. В некоторых популярных фреймворках эти методы также присутствуют. Например, в rails (веб-фреймворк, который мы будем изучать) существует популярный метод “save!”, который сохраняет объект. Восклицательный знак “намекает” на то, что 1) операция опасная, меняется внутреннее состояние объекта 2) если что-то пойдет не так, то может возникнуть исключение (об исключениях мы еще поговорим ниже). Задание: посмотрите какие еще существуют bang-методы у класса String.

Блоки В руби существует свое собственное понятие блока кода. Обычно, когда мы видим какойлибо код, то мы можем визуально разделить его на блоки или участки. Например: первые три строки отвечают за ввод информации, следующие пять строк за вывод и так далее. Несмотря на то, что мы можем называть эти участки кода блоками кода с чисто визуальной точки зрения, понятие “блок кода” в руби имеет свое собственное значение. Блок кода (block, code block) в руби это какая-то часть программы, которую мы куда-то передаем для последующего исполнения. Возникает вопрос — а зачем передавать, когда блок может исполниться вот тут сразу? На самом деле передача блока кода может иметь смысл в следующих случаях: ●

Код должен исполниться какое-то определенное количество раз. Скажем, мы хотим вывести “Спартак- чемпион!” 10 раз подряд. Вместо того, чтобы 10 раз писать puts, мы можем написать puts в одном блоке и передать этот блок для исполнения (далее вы узнаете как это делать). В этом случае программа может занимать одну строку вместо десяти. Код может исполниться, а может и не исполниться при каких-либо обстоятельствах. Причем, решение об этом часто принимаем не мы, а “кто-нибудь

еще”. Другими словами, если мы видим блок, то это еще не означает, что он будет обязательно исполнен. Записать блок в руби можно двумя способами: ● ●

В несколько строк, между ключевыми словами `do` и `end` В одну строку, между фигурными скобками: «

Результат выполнения блока не зависит от того, как вы записали блок. Фигурные скобки предназначены для записи простых конструкций. Между `do` и `end` мы можем записать подпрограммы (блоки кода) в несколько строк. На самом деле размер блока в строках кода неограничен. Но обычно 1 файл в языках ruby и JavaScript не должен быть более 250 строк. Если больше, то это индикатор того, что вы что-то делаете не так. Попробуем записать простой блок и посмотрим на результат выполнения: «` $ irb > 10.times < puts 'Спартак - чемпион!' >Спартак — чемпион! Спартак — чемпион! Спартак — чемпион! Спартак — чемпион! Спартак — чемпион! Спартак — чемпион! Спартак — чемпион! Спартак — чемпион! Спартак — чемпион! Спартак — чемпион! «` Давайте разберемся, что же тут произошло. Что такое 10? С каким классом мы имеем дело? Правильно, Integer. Смотрим документацию по Integer (запрос в гугле “ruby Integer docs”). Далее ищем метод “times” https://ruby-doc.org/core-2.5.1/Integer.html#method-i-times. Из документации видно, что метод “принимает блок”. На самом деле блок можно передать любому методу, даже тому, который “не принимает блок”. Вопрос лишь в том, будет ли этот блок запущен. Метод times запускает блок. Что же мы имеем? Мы имеем объект “10”, который знает о том, что он 10. Существует метод times, который написал какой-то программист (разработчик языка), и этот метод запускает переданный ему блок 10 раз.

Запомните, что блок можно передать любому методу. Вопрос лишь в том, что будет делать этот метод с блоком. А что он будет делать — нужно смотреть в документации. Например, следующая конструкция полностью валидна: «` $ irb gets < puts 'OK' >«` Ошибки не будет, но программа не имеет смысла. “gets” не знает что делать с блоком и просто его проигнорирует. Попробуем записать блок в несколько строк: «` 10.times do puts «Спартак — чемпион!» puts «(и Динамо тоже)» end «` Запустите программу и посмотрите что будет. Что происходит в программе выше: ● ● ●

Есть объект “10” типа Integer. Мы вызываем метод “times” у этого объекта. Мы передаем методу “times” блок кода, который состоит из двух строк.

История от автора: когда мне было около 8 лет, на советском компьютере Корвет мой отец показал мне первую программу на языке Бейсик: 10 PRINT «Рома «; 20 GOTO 10 Эта программа в бесконечном цикле выводила мое имя. Но из-за того, что не происходило перехода на новую строку, возникал любопытный визуальный эффект экран наполнялся словом “Рома” и “ехал вбок”. Можете попробовать сделать то же самое на языке руби: loop do print ‘Рома ‘ end Программа выше выполняет операцию в бесконечном цикле. Функция “print” отличается от “puts” тем, что не переводит курсор на следующую строку.

Блоки и параметры Тот объект (ниже это объект “24”, класс Integer), который запускает ваш блок, может передать в ваш блок параметр. Что делать с параметром — зависит уже от вас, т.е. от вашего блока. Параметр в блоке — это обычно какая-то полезная информация. Вы можете использовать этот параметр или игнорировать его. До сих пор мы игнорировали параметр. Это происходило неявно, параметр на самом деле передавался. Давайте теперь сделаем с параметром что-нибудь интересное. Напишем программу “бабушка”. Бабушка каждый месяц будет принимать от нас определенную сумму денег и складывать в свой сундучок (надеемся, что бабушка потом отдаст нам накопленные средства). Программа должна выводить сколько денег бабушка накопит в течение следующих 24 месяцев. «` sum = 0 24.times do |n| sum = sum + 500 puts «Месяц #, у бабушки в сундуке #» end «` Результат: «` Месяц Месяц Месяц Месяц . Месяц Месяц Месяц «`

бабушки бабушки бабушки бабушки

сундуке сундуке сундуке сундуке

500 1000 1500 2000

21, у бабушки в сундуке 11000 22, у бабушки в сундуке 11500 23, у бабушки в сундуке 12000

Мы получили не совсем предсказуемый результат, хотя в общем-то программа верна. По какой-то причине отсчет начался с нуля! На самом деле все в порядке, за исключением одного момента. То, что отсчет начинается с нуля — ожидаемо. Это описано в документации, и в этом нет ничего плохого. Однако то, как мы назвали переменную, может смутить некоторых опытных программистов.

Дело в том, что для натуральных чисел обычно используют переменные n, m и т.д. Если речь идет об индексе (а индекс начинается с нуля), используют переменные i, j и т.д. Нет большой ошибки если вы назвали переменную неправильно — ведь это не повлияет на результат. Однако, у кода есть два читателя — компьютер и человек. Человек — не только вы, но и кто-то другой, и он будет смотреть на ваш код (если вы пишете только для себя, то этот человек — вы в будущем). Поэтому нужно писать как можно более предсказуемый код. Тем более в языке руби, который исповедует принцип наименьшего сюрприза. Мы можем переписать нашу программу следующим образом: «` sum = 0 24.times do |i| sum = sum + 500 puts «Месяц #, у бабушки в сундуке #» end «` Т.е. просто переименовать переменную. Также с практической точки зрения, “нулевой месяц” не имеет смысла. Мы же не считаем количество яблок, начиная с нуля? Поэтому можно добавить “+1” и наш вывод примет более человеческий вид: «` sum = 0 24.times do |i| sum = sum + 500 puts «Месяц #, у бабушки в сундуке #» end «` Ради эксперимента давайте представим, что нам досталась не просто бабушка, а очень заботливая бабушка, которая все наши сбережения решила отнести в АО “МММ” (авторы книги настоятельно не рекомендуют относить туда свои сбережения). Посчитаем, сколько денег у нас будет через 24 месяца, если АО “МММ” будет начислять еще 10% ежемесячно: «` sum = 0 24.times do |i| sum = sum + 500 + sum * 0.1 puts «Месяц #, у бабушки в сундуке #» 56

end «` В нашу программу мы добавили только `+ sum * 0.1`. Давайте посмотрим на результат: «` Месяц Месяц Месяц . Месяц Месяц Месяц «`

1, у бабушки в сундуке 500.0 2, у бабушки в сундуке 1050.0 3, у бабушки в сундуке 1655.0 22, у бабушки в сундуке 35701.37469341988 23, у бабушки в сундуке 39771.512162761865 24, у бабушки в сундуке 44248.66337903805

Другими словами, если мы отдаем бабушке 500 рублей ежемесячно, а она кладет их под 10% ежемесячно в АО “МММ”, к концу 24 месяца мы будем иметь в сундуке чуть более 44 тысяч рублей. Задание: известно, что стоимость дома — 500 тысяч долларов. Человек берет дом в рассрочку на 30 лет. Чтобы выплатить дом за 30 лет, нужно платить 16666 долларов в год (это легко посчитать, разделив 500 тысяч на 30). Написать программу, которая для каждого года выводит сумму, которую осталось выплатить. Задание: измените программу из предыдущего задания со следующими условиями: человек берет дом не в рассрочку, а в кредит по ставке 4% годовых на оставшуюся сумму. Для каждого года посчитайте, сколько денег нужно заплатить за этот год за использование кредита. Задание: посчитайте количество денег (total), которые мы заплатим только в виде процентов по кредиту за 30 лет.

Любопытные методы класса Integer Методов для класса Integer не так много, и стоит посмотреть документацию, чтобы иметь понятие о том, что там вообще есть. На некоторых из них мы остановимся подробно. ●

`even?` и `odd?` — четный или нечетный

Мы можем проверить любое целое число на четность (делится ли оно на два без остатка) с помощью этих двух методов. Так как знак вопроса в конце метода встречается нам впервые, то остановимся на нем подробнее.

Знак вопроса в конце метода говорит лишь о том, что метод возвращает значение типа Boolean (в языке руби нет отдельного типа для Boolean, поэтому это либо TrueClass тип, либо FalseClass тип). Другими словами, значение либо true, либо false. Например, метод, который определяет, беременна ли девушка, можно записать только со знаком вопроса в конце, потому что результат или true (истина), или ложь (false). Часто такие методы начинаются со слова “is”: girl.is_little_bit_pregnant? Знак вопроса опционален и остается на совести программиста. Когда мы научимся объявлять свои собственные методы, вы сможете создать метод со знаком вопроса или без него. Но правила хорошего тона говорят о том, что если результат или true или false, надо ставить знак вопроса. Посмотрим, как это работает на числах: «` $ irb > 1.even? false > 1.odd? true > 2.even? true > 2.odd? false > 10 % 2 == 0 # наша собственная реализация even? true «` ●

`upto` — вверх до, `downto` — вниз до

Эти методы принимают параметр, и вызывают блок определенное количество раз. До этого мы использовали times, который вел отсчет с нуля. Чтобы посчитать от нуля до 10 можно использовать или `times` или `upto`: «` > 3.times < |i| puts "Я робот #" > Я робот 0 Я робот 1 Я робот 2 . > 0.upto(2) < |i| puts "Я робот #" > Я робот 0 Я робот 1

Я робот 2 «` Вывод идентичный, но конструкция “upto” более гибкая. Можно задавать интервал “от” и “до”. Например: «` > 1000.upto(1002) < |i| puts "Я робот #" > Я робот 1000 Я робот 1001 Я робот 1002 «` Конструкция “downto” аналогичная, но отсчет ведется в обратную сторону: «` puts «Запускаем ракеты. » 5.downto(1) < |i| puts "Осталось #секунд" > puts «Ба-бах!» «` Результат работы программы: «` $ ruby app.rb Запускаем ракеты. Осталось 5 секунд Осталось 4 секунд Осталось 3 секунд Осталось 2 секунд Осталось 1 секунд Ба-бах! «` Разумеется, блок можно написать с помощью do. end, результат от этого не изменится: «` puts «Запускаем ракеты. » 5.downto(0) do |i| puts «Осталось # секунд» end puts «Ба-бах!» «` Задание: вывести на экран числа от 50 до 100. 59

Задание: вывести на экран числа от 50 до 100, и если число четное — рядом с ним написать `true`, если нечетное — `false`.

Сравнение переменных и ветвление Одна из основ программирования — сравнение переменных (или значений). В зависимости от результата сравнения мы можем выполнять ту или иную часть программы. Например: если возраст пользователя меньше 18, то ограничить доступ к этому интересному сайту и не показывать содержимое. Когда сравнивают переменные, употребляют такие выражения как: ●

“Бранчинг”, “ветвление” — от англ.слова branch — ветвь. Подразумевается, что существует одна или более “ветвей” — участков кода, которые выполняются в зависимости от результата какого-либо сравнения. Примечание: в дальнейшем мы будем изучать работу с git, системой контроля версий, там тоже есть свои ветки, которые называют “бранчи”. Это немного другое. “Ветка”, “блок”, “бранч” — участок кода, который, возможно, будет исполнен при соблюдении некоторого условия. “Сравнение”, “тест” — непосредственно сама процедура сравнения. От программистов с опытом можно услышать слово “тест”: “тестирование” переменной на определенное значение. В *nix-оболочках можно ввести команду получения мануала (руководства) по тестированию переменных (это документация по тестированию переменных непосредственно для вашей оболочки, а не для языка руби): $ man test … test — check file types and compare values Примечание: в дальнейшем мы затронем тему тестирования наших программ и написание тестов. Это тоже будут тесты, но в другом смысле. Отличить одни тесты от других очень просто. Если речь идет об одной строке — значит это тест в смысле “сравнение”, “тестирование переменной на определенное условие”. Если речь идет о файле с тестом, значит это тестирование какойто функциональности большой программы.

Давайте напишем простейшее сравнение: «` puts ‘Your age?’

age = gets.to_i if age > 18 puts ‘Access granted’ end «` Результат работы программы: «` $ ruby app.rb Your age? 20 Access granted $ ruby app.rb Your age? 10 «` Для сравнения мы использовали оператор “if” (если), после которого мы пишем выражение, в нашем случае “age > 18”. Если это выражение является истиной (true), то мы исполняем блок — все то, что следует до слова end. Если выражение является ложью (false), то блок внутри не исполняется. Блоки принято делать с отступами (indentation), 2 пробела для одного уровня вложенности являются стандартом в руби. Сами по себе отступы обычно не влияют на работу программы, однако инструменты статического анализа типа Rubocop могут выдавать предупреждения, если вы не соблюдаете правильный indentation. Тут мы плавно подходим к следующему типу данных. Чтобы узнать какой это будет тип данных, давайте сделаем эксперимент в REPL: «` $ irb > true.class => TrueClass > false.class => FalseClass > true.is_a?(Boolean) [ERROR] «` У-у-упс! Оказывается, что нет единого типа данных Boolean! Есть тип TrueClass и есть FalseClass. Однако, полезно держать в голове мысль о том, что true и false это почти одно и то же. В языке программирования Си true и false это просто значения типа int.

Сравнивать переменные или значения можно по-разному. Существует несколько операторов сравнения: ● ● ● ● ● ● ●

`>` — больше `=` — больше или равно ` ‘5’ == 5 true В случае строгого сравнения в JavaScript мы получим “более предсказуемый” результат “слонов и мух” сравнивать нельзя: $ node > ‘5’ === 5 false В руби трюк со “слонами и мухами” не сработает. Если вы сравниваете переменные разных типов, то результат всегда будет ложь: $ irb > ‘5’ == 5 => false Кстати, в нашей программе вначале этой главы была допущена ошибка при сравнении возраста. Сможете ли вы ее увидеть? Наше условие было “age > 18”, когда на самом деле мы хотим проверить “age >= 18”, ведь восемнадцатилетие — это возраст совершеннолетия, после которого можно пускать пользователя на интересные сайты.

Если условие простое, из него можно также сделать one-liner (условие в 1 строку): «` exit if age 1

Условия после оператора if можно комбинировать. Иногда в одной строке необходимо делать несколько сравнений: «` if есть_в_кармане_пачка_сигарет and билет_на_самолет_с_серебристым_крылом puts ‘Всё не так уж плохо на сегодняшний день’ end

«` (Минздрав предупреждает — курение опасно для вашего здоровья). Существуют два варианта комбинации условий: И и ИЛИ. Каждый вариант может выражаться или словом (and и or соответственно) или в виде специальных символов: && и ||. Последний символ называется pipe (труба) operator, т.к. он двойной, то можно сказать double pipe operator. Пример в REPL: «` $ irb > 1 == 1 && 2 == 2 => true > 1 == 5 && 2 == 2 => false > 1 == 5 || 2 == 2 => true «` Существует также возможность использовать `and` вместо `&&` и `or` вместо `||`. Несмотря на то, что при этом читаемость программы улучшается, утилита статического анализа кода Rubocop “ругается” на такой синтаксис. Мы рекомендуем использовать общепринятые `&&` и `||`. См. https://github.com/rubocop-hq/ruby-style-guide#no-and-or-or Первый пример понятен: мы проверяем “1 == 1 И 2 == 2”. Единица равна единице, а двойка равна двойке. Во втором примере мы проверяем “1 == 5 И 2 == 2”. Двойка равна двойке как и в предыдущем примере, но единица пяти не равна. Т.к. мы комбинируем условие с помощью “И”, то мы и получаем результат “ложь”. Если бы мы комбинировали результат с помощью “ИЛИ”, то это была бы правда — должно выполняться только одно из условий (что и демонстрирует третий пример). Рассмотрим комбинирование условий на практике: «`

puts ‘Сколько вам лет?’ age = gets.to_i puts ‘Являетесь ли вы членом партии Единая Россия? (y/n)’ answer = gets.chomp.downcase if age >= 18 && answer == ‘y’ puts ‘Вход на сайт разрешен’ end «` Запустим программу: «` $ ruby app.rb Сколько вам лет? 19 Являетесь ли вы членом партии Единая Россия? (y/n) n $ ruby app.rb Сколько вам лет? 19 Являетесь ли вы членом партии Единая Россия? (y/n) y Вход на сайт разрешен «` Т.е. для посещения (воображаемого) сайта пользователь должен ввести свой возраст. Далее мы выполняем проверку: если возраст больше или равен 18 и если пользователь член партии Единая Россия, то разрешить доступ. Заметьте, что “больше или равен” мы указываем с помощью “>=”. Мы также могли бы написать: «` . if (age > 18 || age == 18) && answer == ‘y’ . «` Задание: попробуйте написать следующие сравнения в REPL и догадаться, каков будет результат для языка руби. Заполните таблицы: Выражение:

Задание: напишите программу, которая спрашивает логин и пароль пользователя в консоли. Если имя “admin” и пароль “12345”, программа должна выводить на экран “Доступ к банковской ячейке разрешен”. Задание: известно, что на Луне продают участки. Любой участок менее 50 квадратных метров стоит 1000 долларов. Участок площадью от 50 до 100 квадратных метров стоит 1500 долларов. От 100 и выше — по 25 долларов за квадратный метр. Напишите программу, которая запрашивает длину и ширину участка и выводит на экран его стоимость. Задание: напишите программу “иммигрант”. Программа должна задавать следующие вопросы: “У вас есть высшее образование? (y/n)”, “У вас есть опыт работы программистом? (y/n)”, “У вас более трех лет опыта? (y/n)”. За каждый положительный ответ начисляется 1 балл (переменную можно назвать score). Если набралось 2 или более баллов программа должна выводить на экран “Добро пожаловать в США”.

Некоторые полезные функции языка руби В предыдущих главах мы рассматривали программу: «` puts «Запускаем ракеты. » 5.downto(0) do |i| puts «Осталось # секунд» end puts «Ба-бах!» «` Однако, эта программа исполняется моментально, вывод на экран происходит мгновенно. Давайте исправим программу, чтобы в ней была настоящая задержка: «` puts «Запускаем ракеты. » 5.downto(1) do |i| puts «Осталось # секунд» sleep 1 end puts «Ба-бах!» «`

Т.е. “sleep” принимает параметр — количество секунд, которые программа должна “спать”. Можно также задавать дробное значение. Например `0.5` для половины секунды (500 мсек). В реальных программах “sleep” используется не часто — ведь программы должны исполняться как можно быстрее. Но иногда эта конструкция может использоваться при тестировании веб-приложений. Например “ввести логин, пароль, нажать на кнопку и подождать 10 секунд”. Но и тут существует много мнений. Некоторые программисты утверждают, что если в тестах нужен “sleep”, то тест написан неправильно. Но за многолетнюю практику автора, от “sleep” абсолютно во всех местах избавиться не удалось. Любопытная деталь заключается в том, что в JavaScript не существует “sleep”, т.к. этот язык является асинхронным по своей природе. Другими словами, нельзя приостановить программу. Несмотря на то, что для этого есть решение, это добавляет определенной сложности. Если программа в JavaScript не может прерываться, то это справедливо не только для “sleep”, а вообще для всего. Например, нужно прочитать в память большой файл. Но прерываться нельзя. На практике понятно, что чтение больших файлов занимает время. Поэтому в JavaScript было введено понятие callback’овов (обратных вызовов) и потом уже Promises. Пример неправильной программы на JavaScript: «` console.log(‘Запуск ракеты!’); setTimeout(function() < console.log('Прошла одна секунда, запускаем'); >, 1000); console.log(‘Ба-бах!’); «` Вывод: «` Запуск ракеты! Ба-бах! Прошла одна секунда, запускаем «`

Т.е. предупреждаем о запуске, ракета уже взорвалась, а через секунду мы ее хотим запустить. Непорядок! Поэтому в JavaScript следует мыслить асинхронно. Это несложно, и этот концепт понимается легко. Например, для правильного запуска ракеты нужно перенести последнюю строку внутрь setTimeout. Тогда все будет работать верно. Но в этом случае весь остальной код нам нужно будет писать с отступами и внутри setTimeout, ведь мы хотим сначала подождать, а потом делать все остальное. Если подождать 2 раза, то будет двойной уровень вложенности. На помощь пришло ключевое слово “await”, которое частично решает проблему. Но и в этом случае необходимо иметь в голове представление о том, как работает асинхронный код. JavaScript неплохой язык с декларативным уклоном. Если бы браузеры создавались сегодня, то этого языка бы не было. Но сейчас мы вынуждены работать с тем, что есть, история диктует свои правила. Для руби-программиста язык JavaScript не является большой проблемой. Освоить JS в минимальном варианте, который необходим для работы, можно за относительно короткое время. Хорошая новость в том, что вместе с руби JavaScript используется только на клиентской части (т.е. в браузере пользователя, а не на сервере). Поэтому клиентские скрипты обычно небольшие. А если большие, то для этого часто нанимают отдельного front-end разработчика. Из практики разработки авторы книги пришли к выводу, что человеку проще создавать программы не на асинхронных языках типа JavaScript, а на языках “обычных”, синхронных: ruby, go, python и т.д. Несмотря на то, что ничего сложного в асинхронных языках нет, начинающим программистам бывает сложно понять асинхронные конструкции, не зная синхронных.

Генерация случайных чисел Про генерацию достоверно-случайных чисел написано много научных трудов. Ведь компьютер — это что-то математическое и точное, каким образом в нем может быть случайность? На более ранних компьютерах случайные числа генерировались совсем не случайно — после каждого перезапуска компьютер выдавал одну и ту же последовательность. Поэтому в игру “Морской бой” начинающие программисты научились выигрывать после нескольких попыток — было заранее известно, где компьютер расположит свои корабли. Объяснение этому простое — нужно было где-то взять случайные данные, а взять их было негде. В современных операционных системах генератор случайных чисел учитывает множество параметров: паузы между нажатиями клавиш, движения мыши, сетевые события и так далее — вся эта информация, собранная из реального мира, помогает компьютеру генерировать случайные числа.

Но что, если этой информации недостаточно? Что, если мы только что включили компьютер, сделали несколько движений мышью и нажали несколько кнопок, и хотим получить комбинацию из миллиардов случайных чисел? Конечно, на основе полученной информации из реального мира алгоритм задает вектор, но какое количество векторов в этом случае возможно? Кажется, что много, пока дело не доходит до реальных проблем программирования. История из жизни: на одном сайте был опубликован алгоритм перемешивания карт в игре “Онлайн Покер”. Алгоритм выглядел следующим образом: «` for i := 1 to 52 do begin r := random(51) + 1; swap := card[r]; card[r] := card[i]; card[i] := swap; end; «` В общем-то ничего необычного на первый взгляд, но программа содержит четыре ошибки. Первая ошибка — значение индекса на второй строке никогда не будет равно нулю. Вторая ошибка — выбранный алгоритм не гарантирует равномерного распределения карт — эту ошибку сложнее всего заметить. Кстати, в руби имеется встроенный метод shuffle для массивов данных, который перемешивает правильным алгоритмом. Но основная ошибка в том, что random() использует 32-битное посевное значение (seed), которое может гарантировать “всего” 2 в 32 степени (примерно 4 миллиарда) уникальных комбинаций. Тогда как настоящее количество комбинаций это факториал 52 (намного больше 2^32). Т.к. в качестве seed используется количество миллисекунд после полуночи, то мы имеем всего 86.4 миллиона возможных комбинаций. Получается, что после пяти карт и синхронизации времени с сервером можно предсказать все карты в реальной игре. Пример выше лишь демонстрирует уязвимость алгоритмов для генерации случайных чисел. Если вы разрабатываете что-то важное, то стоит всерьез задуматься о “надежной” генерации случайных чисел (например, с помощью специальных устройств, которые можно подключить к компьютеру). Но для учебных целей нам подойдут встроенные функции руби — эти функции используют ядро вашей операционной системы для генерации “достаточно” случайных чисел: «` $ irb > rand(1..5)

4 > rand(1..5) 1 «` В функцию rand можно “хитрым образом” передать параметр, который задает диапазон (range) значений — в нашем случае от одного до пяти. При каждом вызове мы получаем случайное число из этого диапазона. Хитрость состоит в том, что мы передаем не два параметра, а один (хотя кажется, что два). Если передать два параметра, то будет ошибка: «` $ irb > rand(1, 5) [ERROR — функция не принимает 2 значения] «` Так что же такое `1..5` ? Давайте проверим: «` $ irb > (1..5).class => Range «` Так вот оно что! Это определенный класс в языке руби, который отвечает за диапазон, и называется этот класс Range. На самом деле этот класс довольно полезный. Документация https://ruby-doc.org/core-2.2.0/Range.html по этому классу выдает много интересного, но давайте для начала убедимся, что это никакая не магия, и этот объект можно инициализировать, как и любую другую переменную: «` $ irb > x = 1..5 => 1..5 > rand(x) => 4 «` Теперь понятно, что “rand” принимает один параметр. Попробуем скомбинировать rand и sleep: «` $ irb 70

> sleep rand(1..5) «` Программа будет ждать какое-то случайное количество секунд, от 1 до 5. Кстати, передать параметр в любой метод в языке руби можно как со скобками, так и без. Вот эти конструкции будут идентичны: «` $ irb > sleep rand(1..5) > sleep rand 1..5 > sleep(rand(1..5)) «` Последняя строка наиболее наглядно демонстрирует, что желает получить программист от языка руби: 1) сначала выполняется конструкция `1..5`, с помощью которой создается объект Range; 2) затем вычисляется случайное значение в диапазоне `rand(. )`; 3) затем ожидаем определенное количество секунд — т.е. то количество секунд, которое вернула функция rand. Использовать скобки или нет — личное предпочтение программиста. Чтобы не возникало путаницы, статические анализаторы кода (напр. Rubocop) выдают предупреждения, если ваш стиль сильно отличается от общепринятого стандарта. Отдельно хочется отметить возможность вычислять случайные дробные значения: «` $ irb > rand(0.03..0.09) => 0.03920647825951599 > rand(0.03..0.09) => 0.06772359081051581 «` Задание: посмотрите документацию по классу Range https://ruby-doc.org/core2.5.1/Range.html Задание: напишите программу, которая будет выводить случайное число от 500 до 510. Задание: напишите программу, которая будет выводить случайное число с дробью от 0 до 1. Например, 0.54321 или 0.123456 Задание: напишите программу, которая будет выводить случайное число с дробью от 2 до 4.

Угадай число Давайте закрепим наши знания на практике и напишем что-нибудь интересное например, программу “угадай число”. Компьютер загадывает число, а пользователю нужно это число угадать. В дальнейшем улучшим эту программу: «` number = rand(1..10) print ‘Привет! Я загадал число от 1 до 10, попробуйте угадать: ‘ loop do input = gets.to_i if input == number puts ‘Правильно!’ exit end if input != number print ‘Неправильно, попробуйте еще раз: ‘ end end «` На этом этапе у вас должно быть достаточно знаний для того, чтобы понять что здесь происходит. Попробуйте догадаться, как работает эта программа. Результат работы программы: «` Привет! Я загадал число Неправильно, попробуйте Неправильно, попробуйте Неправильно, попробуйте Неправильно, попробуйте Правильно! «`

от 1 до 10, попробуйте угадать: 2 еще раз: 7 еще раз: 8 еще раз: 9 еще раз: 10

Первая строка “загадывает” число и сохраняет значение в переменную `number`. Чуть ниже мы объявляем бесконечный цикл с помощью конструкции `loop do… end`. Сразу внутри “loop” мы объявляем переменную “input”, в которой мы сохраняем ввод пользователя.

Ввод пользователя имеет тип Integer, как и загаданное компьютером число. Поэтому в первом блоке мы “имеем право” произвести сравнение (в руби не будет ошибки, если вы будете сравнивать переменные разных типов, просто они никогда не будут равны). Несмотря на то, что цикл бесконечный — мы из него все равно выходим, но только при одном условии — когда угадали число. Это проверяется условием “input == number”. Т.к. мы пока не умеем объявлять собственные методы (функции), то мы используем exit для того, чтобы выйти из программы. С более глубокими знаниями Руби мы бы могли, например, спросить пользователя — хочет ли он сыграть еще раз? Следующий блок “if” содержит тест “если загаданное число НЕ равно вводу пользователя”. Обратите внимание, что мы используем “print”, а не “puts”, т.к. “puts” переводит строку, а нам этого не надо (если это непонятно, попробуйте заменить print на puts). В этой простой программе можно кое-что улучшить: «` number = rand(1..10) print ‘Привет! Я загадал число от 1 до 10, попробуйте угадать: ‘ loop do input = gets.to_i if input == number puts ‘Правильно!’ exit else print ‘Неправильно, попробуйте еще раз: ‘ end end «` Мы объединили два блока “if” в один с помощью ключевого слова “else” (иначе). В самом деле — зачем делать дополнительную проверку, если у нас всего два возможных варианта развития: или угадал число, или (иначе) не угадал. Задание: измените программу, чтобы она загадывала число от 1 до 1_000_000 (1 миллиона). Чтобы можно было угадать это число, программа должна сравнивать текущий ответ пользователя и искомое число: 1) если ответ пользователя больше, то программа должна выводить на экран “Искомое число меньше вашего ответа”; 2) иначе “Искомое число больше вашего ответа”. Может показаться, что угадать это число невозможно,

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

Тернарный оператор Тернарный оператор (ternary operator) встречается довольно часто и обычно является однострочной альтернативой (иногда говорят «one-liner») конструкции «if. else». Многие программисты успешно применяют этот оператор, но не знают как он называется. Мы рекомендуем запомнить это название, потому что всегда приятнее сказать коллеге: «уважаемый коллега, давайте заменим это прекрасное ветвление на тернарный оператор!». Несмотря на страшное название, синтаксис у тернарного оператора очень простой: «` something_is_truthy ? do_this() : else_this() «` Например: «` is_it_raining? ? stay_home() : go_party() «` Что аналогично такой же записи, но с использованием «if. else»: «` if is_it_raining? stay_home() else go_party() end «` Пустые скобки в том и другом случае можно опустить. Обратите внимание на двойной знак вопроса. Он появился из-за того, что авторы предполагают, что «is_it_raining?» это метод, который возвращает тип Boolean (TrueClass или FalseClass). А правило хорошего тона говорит о том, что все методы, возвращающие этот тип должны заканчиваться знаком вопроса. Если бы результат зависел от какой-либо переменной, то запись имела бы более «понятный» вид: «`

x ? stay_home() : go_party() «` Или: «` x ? stay_home : go_party «` Как видно из примера, тернарный оператор имеет более компактный вид и позволяет сэкономить несколько строк на экране. Недостаток (и одновременно преимущество) тернарного оператора в том, что он выглядит хорошо только тогда, когда нужно выполнить только одну инструкцию. Для нескольких методов подряд лучше использовать конструкцию «if. else». Результат выражения с тернарным оператором можно также записать в переменную. Например: «` x = is_it_raining? result = x ? stay_home : go_party «` `result` будет содержать результат выполнения операции `stay_home` или `go_party`. Это также справедливо и для конструкции «if. else»: «` x = is_it_raining? result = if x stay_home else go_party end «` В примерах выше результат выполнения метода `stay_home` или `go_party` будет записан в переменную `result`. Задание: запишите следующие примеры при помощи тернарного оператора: Пример 1: «` if friends_are_also_coming?

go_party else stay_home end «` Пример 2: «` if friends_are_also_coming? && !is_it_raining go_party else stay_home end «`

Индикатор загрузки Индикатор загрузки (который также называют “Progress bar”) это просто один из самых простых визуальных способов показать пользователю, что выполняется какое-то действие. Например, скачивание файла или форматирование жесткого диска занимает определенное время, но для того, чтобы пользователь знал, что компьютер не завис используют Progress Bar. Для закрепления полученных знаний напишем программу, которая будет выводить на экран сообщение о форматировании диска (не переживайте, самого форматирования диска не будет — только визуальная часть): «` print ‘Formatting hard drive’ 100_000.times do print ‘.’ sleep rand(0.05..0.5) end «` Из-за случайной задержки от 0,05 до 0,5 секунд визуальный эффект выглядит довольно правдоподобно. Как было замечено ранее, функция “print” в отличие от “puts” не переводит курсор на следующую строку. А теперь загадка: что напечатает программа ниже: «`

print «one\rtwo» «` (заметьте, что используются двойные кавычки). Правильный ответ: “two”. Что же тут произошло? Все просто: сначала компьютер вывел на экран слово “one”, потом курсор переместился в начало строки, и потом на экране появилось слово “two”. Говорят, что “\r” (от слова return — возврат) — управляющий символ. Задание: с помощью символов “/”, “-”, “\”, “|” сделайте анимацию — индикатор загрузки. Если выводить эти символы по-очереди на одном и том же месте, возникает ощущение вращающегося символа.

Методы Методы (или функции, реже — подпрограммы) — это небольшие участки программы, которые можно использовать повторно. До сих пор мы не использовали написанный код повторно (за исключением случаев, когда код находился внутри, например, loop), но методы позволяют существенно упростить вашу программу, разбив ее на несколько логических блоков. Методы не обязательно “должны” сделать программу меньше в размере. Основная задача — выделить какие-то логические блоки и сделать программу более читаемой для человека. Часто такой процесс называется рефакторингом (а эту технику рефакторинга “extract method”, выделить метод): есть большая программа и вот эта часть делает определенную функциональность, которую можно выделить отдельно, давайте ее выделим. В результате рефакторинга большой участок программы разбивается на два маленьких. Но методы можно писать и просто для удобства. Чуть выше мы использовали такую конструкцию: «` age = gets.to_i «` Назначение этого кода в том, чтобы считать ввод пользователя и сконвертировать String в Integer (с помощью “to_i”). Конструкция не очень понятна тем, кто смотрит на код впервые. Чтобы она стала более понятной, сделаем рефакторинг и выделим метод: «` def get_number gets.to_i

end age = get_number «` С помощью “def. end” мы “объявили” метод. Теперь мы можем смело писать “age = get_number”, с точки зрения программиста это выглядит более понятно, особенно когда речь идет про несколько переменных: «` age = get_number salary = get_number rockets = get_number «` Методы в руби всегда возвращают значение, даже если кажется, что они его не возвращают. Результат выполнения последней строки метода (в примере выше она же и первая) это и есть возвращаемое значение. Если мы по какой-то причине хотим вернуть значение в середине метода (и прекратить дальнейшее выполнение), мы можем использовать ключевое слово return: «` def check_if_world_is_crazy? if 2 + 2 == 4 return false end puts «Jesus, I can’t believe that» true end «` Последнюю строку можно записать как “return true”, но это необязательно. Метод, как и любой блок, может содержать несколько строк подряд. Также метод может принимать параметры: «` def get_number(what) print «Введите #: » gets.to_i end age = get_number(‘возраст’) salary = get_number(‘зарплату’) rockets = get_number(‘количество ракет для запуска’) 78

«` Результат работы программы: «` Введите возраст: 10 Введите зарплату: 3000 Введите количество ракет для запуска: 5 «` Согласитесь, что программа выше выглядит намного проще, чем она могла бы выглядеть без метода get_number: «` print ‘Введите возраст:’ age = gets.to_i print ‘Введите зарплату:’ salary = gets.to_i print ‘Введите количество ракет для запуска:’ rockets = gets.to_i «` Более того, представьте, что мы решили задать вопрос немного иначе: «Введите, пожалуйста» вместо «Введите». В случае с методом нам нужно сделать исправление только в одном месте. А если программа не разделена на логические блоки и «идет сплошной простынёй», исправления надо сделать сразу в трех местах. Начинающему может показаться что это совсем незначительные улучшения. Однако, на практике следует выполнять рефакторинг постоянно. Когда код хорошо организован, писать программы одно удовольствие! К сожалению, организация кода не такая простая задача, как может показаться на первый взгляд. Существует много техник рефакторинга, шаблонов проектирования и так далее. Но главное, конечно — желание программиста поддерживать порядок. Задание: напишите метод, который выводит на экран пароль, но в виде звездочек. Например, если пароль “secret”, метод должен вывести “Ваш пароль: ******”.

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

Программа будет выводить на экран поток случайных сообщений, которые будут представлять какие-либо события в мире. Если бы это была графическая программа, было бы интереснее. Но в текстовом виде степень интересности зависит лишь от воображения зрителя. Возможно, кому-то понравится наша программа и пользователи поставят ее как screen saver. Важное примечание: написать программу можно и проще, и лучше. Но не весь язык руби мы пока изучили. Поэтому ограничимся тем, что есть. Для начала условимся, что людей и машин осталось поровну: по 10000 с каждой стороны. В каждом цикле программы будет происходить одно случайное событие. И с одинаковой долей вероятности число людей или машин будет убавляться. Победа наступает в том случае, когда или людей, или машин не осталось. Приступим. Во-первых, сформулируем правило победы. У нас будет главный цикл и две переменных: «` humans = 10_000 machines = 10_000 loop do if check_victory? exit end . end «` Две переменные `humans` и `machines` будут хранить значение о количестве выживших. Метод `check_victory?` будет возвращать значение типа Boolean и если наступила победа одной из сторон (не важно какой), то производится выход из программы. Если победы не наступило, борьба продолжается. Пусть этот метод также выводит сообщение о том, кто в итоге выиграл. Теперь нужно определить несколько событий, которые могут случится. Назовем их `event1`, `event2` и `event3`. В зависимости от случайного значения будет вызываться тот или иной метод. Будем подбрасывать игральную кость (dice), которая пока будет принимать значение от 1 до 3: «` def event1 # . end

def event2 # . end def event3 # . end # . dice = rand(1..3) if dice == 1 event1 elsif dice == 2 event2 elsif dice == 3 event3 end «` Мы применили новое ключевое слово «elsif» (слово else нам уже знакомо). Elsif это, пожалуй, самое неочевидное сокращение в языке руби, которое означает «else if» (иначе если. ). Ну и завершим цикл конструкцией sleep, которая будет ждать случайное количество секунд (от 0.3 до 1.5): «` sleep rand(0.3..1.5) «` Готовая программа: «` ######################################## # ОПРЕДЕЛЯЕМ ПЕРЕМЕННЫЕ ######################################## @humans = 10_000 @machines = 10_000 ######################################## # ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ 81

######################################## # Метод возвращает случайное значение: true или false def luck? rand(0..1) == 1 end def boom diff = rand(1..5) if luck? @machines -= diff puts «# машин уничтожено» else @humans -= diff puts «# людей погибло» end end # Метод возвращает случайное название города def random_city dice = rand(1..5) if dice == 1 ‘Москва’ elsif dice == 2 ‘Лос-Анджелес’ elsif dice == 3 ‘Пекин’ elsif dice == 4 ‘Лондон’ else ‘Сеул’ end end def random_sleep sleep rand(0.3..1.5) end def stats puts «Осталось # людей и # машин» end ######################################## # СОБЫТИЯ

######################################## def event1 puts «Запущена ракета по городу #» random_sleep boom end def event2 puts «Применено радиоактивное оружие в городе #» random_sleep boom end def event3 puts «Группа солдат прорывает оборону противника в городе #» random_sleep boom end ######################################## # ПРОВЕРКА ПОБЕДЫ ######################################## def check_victory? false end ######################################## # ГЛАВНЫЙ ЦИКЛ ######################################## loop do if check_victory? exit end dice = rand(1..3) if dice == 1 event1 elsif dice == 2 event2 elsif dice == 3

event3 end stats random_sleep end «` Результат работы: «` Запущена ракета по городу Сеул 1 машин уничтожено Осталось 10000 людей и 9999 машин Применено радиоактивное оружие в городе Пекин 4 людей погибло Осталось 9996 людей и 9999 машин Применено радиоактивное оружие в городе Лос-Анджелес 4 машин уничтожено Осталось 9996 людей и 9995 машин Группа солдат прорывает оборону противника в городе Лондон . «` Задание: реализуйте метод “check_victory?” (сейчас он просто возвращает значение false). В случае победы или поражения необходимо выводить полученный результат на экран. Измените 10_000 на 10 чтобы легче было отлаживать программу. Задание: посмотрите документацию к «ruby case statements» и замените конструкцию «if. elsif» на «case. when». Задание: сделать так, чтобы цикл был теоретически бесконечным. Т.е. чтобы равновероятно на свет появлялись люди и машины. Количество появившихся людей или машин должно равняться количеству погибших людей или машин. Несмотря на то, что теоретически борьба может быть бесконечной, на практике может наступить ситуация, в которой та или иная сторона выигрывает. Проверьте программу на практике, попробуйте разные значения `humans` и `machines` (1000, 100, 10). Задание: улучшите программу, добавьте как минимум еще 3 события, которые могут влиять на результат судного дня.

Переменные экземпляра и локальные переменные Внимательный читатель уже обратил внимание на странный префикс `@` перед именем переменной. В языке руби нельзя получить доступ к переменным, объявленным вне метода. Исключение составляют лишь переменные экземпляра класса (что такое “экземпляр” рассказывается позднее, пока можете представлять их как “почти глобальные”). Например, следующий код не будет исполнен и интерпретатор руби выдаст ошибку: «` x = 123 def print_x puts x end print_x «` Текст ошибки “undefined local variable or method `x’ for main:Object (NameError)”. Но что же такое main? Оказывается, любая программа в руби “оборачивается” в класс “main”. Это легко доказать, достаточно запустить вот такую программу: «` puts self puts self.class «` Вывод: «` main Object «` Другими словами, это top-level scope в языке руби. Не стоит особо волноваться на этот счет до тех пор, пока вы не начнете изучать внутренние особенности языка. Но зная об этой особенности, становится проще понять почему метод не имеет доступ к переменной. Эта переменная не является локальной (local) для метода. Локальная — это любая переменная, объявленная внутри метода. К локальным переменным можно обратиться обычным способом: «` def calc_something 85

x = 2 + 2 puts x end «` Но для доступа к переменным экземпляра, они должны быть объявлены специальным образом: с помощью префикса `@`. Другими словами, мы можем переписать наш код с учетом этой особенности: «` @x = 123 def print_x puts @x end print_x «` Теперь метод `print_x` может получить доступ к этой переменной. В JavaScript все немного иначе. Метод может “видеть” переменную, объявленную в своем “родительском” методе. Такая конструкция называется замыканием (closure): «` x = 123 function printX() < console.log(x); >printX(); «` Как вы уже могли заметить, в разных языках есть разные особенности. Эти особенности определены чаще всего природой языка программирования: для какой цели был создан тот или иной язык. JavaScript это событийный асинхронный язык и замыкания — простые функции, имеющие доступ к переменным, объявленным вне себя — очень удобны, когда возникают какие-либо события (например, пользователь щелкает на каком-либо элементе).

Однорукий бандит (слот-машина)

Для закрепления материала напишем на этот раз игру попроще: “Однорукий бандит”. Положим деньги в банк, дернем виртуальную ручку и посмотрим на результат. Прикинем наш план. За деньги в банке будет отвечать отдельная переменная “balance”. В игре будут три места под игровые символы. Традиционными символами для слот-машин являются изображения фруктов, вишни, колокола и цифры “7”. В нашем случае это будут просто цифры от 0 до 5. Пусть переменные “x”, “y” и “z” будут представлять игровые символы. Значение этих переменных будет задаваться через генератор случайных чисел. Определимся с понятием выигрыша и проигрыша. Пусть совпадение всех трех переменных что-то означает. Например: ● ● ● ●

Если все переменные равны нулю, баланс обнуляется Если все переменные равны 1, на счет добавляется 10 долларов Если все переменные равны 2, на счет добавляется 20 долларов Иначе со счета списывается 50 центов

Программа должна работать до тех пор, пока на балансе есть деньги. Начнем с элементарной проверки возраста игрока: «` print ‘Ваш возраст: ‘ age = gets.to_i if age arr.size => 4 «` Или отсортировать массив в алфавитном порядке: «` $ irb . > arr.sort => [«Лондон», «Москва», «Нью-Йорк», «Сан-Франциско»] «` Можем сделать итерацию (проход) по каждому элементу массива: «` arr = [‘Сан-Франциско’, ‘Москва’, ‘Лондон’, ‘Нью-Йорк’] arr.each do |word| puts «В слове # # букв» end «` Результат работы программы: «` В слове В слове В слове В слове «`

Сан-Франциско 13 букв Москва 6 букв Лондон 6 букв Нью-Йорк 8 букв

Конечно, ничто не мешает нам объявить пустой массив: «` arr = [] «` Но зачем он нужен? Затем же, зачем нужна пустая корзина — что-нибудь туда положить. Положить объект (все в руби — объект) в массив можно несколькими способами, обычно используется два основных: ● ●

`arr.push(123)` — метод push также реализован в языке JavaScript, поэтому многие веб-программисты предпочитают использовать его `arr [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil] «` По-умолчанию он заполняются пустым значением (nil). Но мы также можем инициализировать массив каким-либо значением. Представим, что в компьютерной игре ноль представляет пустое место, а единица — одного солдата. Мы хотим создать взвод солдат, мы можем сделать это с помощью следующей конструкции (попробуйте самостоятельно в REPL): «` Array.new(10, 1) «` Конструкция создаст массив размером 10, где каждый элемент будет равен единице: «` [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] «`

Обращение к массиву До сих пор мы рассматривали итерацию по массиву. Но давайте посмотрим, как мы можем обратиться к определенному элементу массива. Обратиться, т.е. получить доступ — для чтения или записи — можно с помощью индекса. Индекс довольно хитрая штука, но в то же время очень простая. Это порядковый номер минус один, элементы массива считаются, начиная с нулевого, а не с первого. Т.е. если мы хотим обратиться к пятому элементу, нам нужен четвертый индекс. Попробуем создать массив строк на пять элементов в REPL: «` > arr = %w(one two three four five) => [«one», «two», «three», «four», «five»] «` Попробуем получить размер:

«` > arr.size => 5 «` Размер массива — 5. Т.е. в массиве пять элементов. Попробуем получить пятый элемент. Для того, чтобы его получить, нужно использовать четвертый индекс: «` > arr[4] => «five» «` Другими словами: ● `arr[0]` вернет “one” ● `arr[1]` вернет “two” ● `arr[2]` вернет “three” ● `arr[3]` вернет “four” ● `arr[4]` вернет “five” Разумеется, когда мы умеем вычислять это выражение, мы можем его использовать совместно с другими функциями: «` puts arr[4] «` Передавать в наш собственный метод: «` my_own_method(arr[4]) «` И так далее. Т.е. делать все то же самое, что мы уже умеем делать с переменной. Например, можно присвоить какому-нибудь элементу массива другое значение: «` arr[1] = ‘двундель’ «` Например, программа: «` arr = %w(one two three four five)

arr[1] = ‘двундель’ arr.each do |word| puts word end «` Выведет на экран: «` one двундель three four five «` Мы могли бы записать эту программу иначе, это не было бы ошибкой (для небольшого числа элементов): «` arr = %w(one two three four five) arr[1] = ‘двундель’ puts arr[0] puts arr[1] puts arr[2] puts arr[3] puts arr[4] «`

Битва роботов Для закрепления материала давайте напишем простейшую игру “битва роботов”. Возьмем 20 роботов и разделим их на 2 команды, в каждой команде по 10. Каждую команду будет представлять отдельный массив размером 10. Ячейка массива может принимать два значения: ● ●

Ноль — когда робот уничтожен Единица — когда робот еще жив

Объявим два массива. Единица говорит о том, что мы объявляем массивы с живыми роботами: «` 99

arr1 = Array.new(10, 1) arr2 = Array.new(10, 1) «` Каждые команды будут стрелять по-очереди. Определимся с термином “стрелять”, что это значит? Если ноль в массиве это уничтоженный робот, а единица — живой, то стрелять значит “изменить значение с единицы на ноль для определенной ячейки массива”. Но как мы будем определять какую ячейку менять? Тут есть два варианта: ●

Менять ячейку подряд. Т.е. сначала уничтожаем первого робота во второй команде (первая команда делает ход), потом первого робота в первой и т.д. Побеждает всегда тот, кто первый начал. Это не интересно. ● Намного интереснее выбирать индекс от 0 до 9 каждый раз случайно. Случайность не гарантирует того, что индекс не повторится. Поэтому одна команда может “стрельнуть” по одному и тому же месту. Например, через пять ходов вторая команда бьет в пятую ячейку, а выстрел по ней уже был до этого. Следовательно, выстрел не попал в цель, ячейка уже равна нулю, и количество убитых роботов не изменилось. Т.е. результат сражения заранее не гарантирован и зависит от везения. Выберем второй вариант. Определять случайный индекс от 0 до 9 мы уже умеем: «` i = rand(0..9) «` Далее осталось только обратиться к ячейке массива и, если она равна единице, то присвоить ей значение ноль. А если ячейка уже равна нулю, значит выстрел по этому месту уже был: «` if arr[i] == 1 arr[i] = 0 puts «Робот по индексу # убит» else puts ‘Промазали!’ end «` Выигрывает та команда, в которой остался хотя бы один робот. Также было бы полезно узнать, сколько именно роботов осталось. Как же нам это сделать? Представим, что у нас есть массив: «` arr = [1, 0, 1, 0, 1, 1]

«` Как определить количество элементов, равных единице? Мы можем использовать уже знакомый нам метод “each”, делать сравнение и записывать результат в переменную: «` arr = [1, 0, 1, 0, 1, 1] x=0 arr.each do |element| if element == 1 x += 1 end end puts «В массиве # единиц» «` Программа работает, но есть способ проще. Метод “count” класса Array (обязательно посмотрите документацию) делает то же самое, но выглядит намного проще: «` arr = [1, 0, 1, 0, 1, 1] x = arr.count do |x| x == 1 end puts «В массиве # единиц» «` Или более короткий способ записи: «` arr = [1, 0, 1, 0, 1, 1] x = arr.count < |x| x == 1 >puts «В массиве # единиц» «` На данном этапе у нас есть все, что нужно. Две команды роботов, стрелять будут по очереди, индекс выбирается случайно, проигрывает тот, у кого не осталось ни одного робота. Можно писать эту игру. Т.к. игра не требует ввода пользователя, мы будем на нее только смотреть. Задание: напишите эту игру самостоятельно. Сравните свой результат с программой ниже. Внимание: есть вероятность, что у вас что-то не получится, в данном случае неважно, сможете ли вы написать эту программу или нет, важен процесс и работа над ошибками.

Код программы: «` ############################### # ОБЪЯВЛЯЕМ МАССИВЫ ############################### # массив для первой команды @arr1 = Array.new(10, 1) # массив для второй команды @arr2 = Array.new(10, 1) ############################### # АТАКА ############################### # Метод принимает массив для атаки def attack(arr) sleep 1 # добавим sleep для красоты i = rand(0..9) if arr[i] == 1 arr[i] = 0 puts «Робот по индексу # уничтожен» else puts «Промазали по индексу #» end sleep 1 # еще один sleep для красоты вывода end ############################### # ПРОВЕРКА ПОБЕДЫ ############################### def victory? robots_left1 = @arr1.count < |x| x == 1 >robots_left2 = @arr2.count < |x| x == 1 >if robots_left1 == 0 puts «Команда 2 победила, в команде осталось # роботов» return true end if robots_left2 == 0

puts «Команда 1 победила, в команде осталось # роботов» return false end false end ############################### # СТАТИСТИКА ############################### def stats # количество живых роботов для первой и второй команды cnt1 = @arr1.count < |x| x == 1 >cnt2 = @arr2.count < |x| x == 1 >puts «1-ая команда: # роботов в строю» puts «2-ая команда: # роботов в строю» end ############################### # ГЛАВНЫЙ ЦИКЛ ############################### loop do puts ‘Первая команда наносит удар. ‘ attack(@arr2) exit if victory? stats sleep 3 puts # пустая строка puts ‘Вторая команда наносит удар. ‘ attack(@arr1) exit if victory? stats sleep 3 puts # пустая строка end «` Результат работы программы: «` Первая команда наносит удар.

Робот по индексу 2 уничтожен 1-ая команда: 10 роботов в строю 2-ая команда: 9 роботов в строю Вторая команда наносит удар. Робот по индексу 8 уничтожен 1-ая команда: 9 роботов в строю 2-ая команда: 9 роботов в строю … Первая команда наносит удар. Робот по индексу 7 уничтожен 1-ая команда: 1 роботов в строю 2-ая команда: 2 роботов в строю Вторая команда наносит удар. Робот по индексу 2 уничтожен Команда 2 победила, в команде осталось 2 роботов «` Задание: чтобы статистика была более наглядной, добавьте в программу выше вывод двух массивов. Задание: вместо бинарного значения ноль или единица пусть каждый робот имеет уровень жизни, который выражается целым числом от 1 до 100 (в самом начале это значение должно быть установлено в 100). Пусть каждая атака отнимает случайную величину жизни у робота от 30 до 100. Если уровень жизни ниже или равен нулю, робот считается уничтоженным.

Массивы массивов (двумерные массивы) При объявлении массива мы можем указать любой тип. Например, String: «` $ irb > Array.new(10, ‘hello’) => [«hello», «hello», «hello», «hello», «hello», «hello», «hello», «hello», «hello», «hello»] «` Или Boolean (несуществующий тип, созданный нами намеренно. Этот тип представлен двумя типами TrueClass и FalseClass):

«` $ irb > Array.new(10, true) => [true, true, true, true, true, true, true, true, true, true] «` Или Integer: «` $ irb > Array.new(10, 123) => [123, 123, 123, 123, 123, 123, 123, 123, 123, 123] «` Другими словами, элемент массива это любой объект. Если элемент массива любой объект и сам по себе массив это объект, значит мы можем объявить массив массивов: «` $ irb > Array.new(10, []) => [[], [], [], [], [], [], [], [], [], []] «` Если мы обратимся по какому-либо индексу, то мы получим массив внутри массива. Например, индекс 4 выбирает пятый по счету элемент. Давайте попробуем обратиться к элементу по индексу 4: «` $ irb > arr = Array.new(10, []) => [[], [], [], [], [], [], [], [], [], []] > element = arr[4] => [] > element.class => Array «` Мы видим, что этот элемент — массив, тип Array. Массив этот пустой. Когда мы ввели `element = arr[4]`, REPL посчитал нам это выражение и ответил `[]` (к слову, если бы это была последняя строка метода, то метод вернул бы `[]`). Что мы можем сделать с пустым массивом? Добавить туда что-нибудь. Давайте это сделаем: «`

element.push(‘something’) «` Вот такой результат мы ожидаем в переменной `arr` — массив массивов, где четвертый по индексу (и пятый по порядковому номеру) элемент что-то содержит наше значение: «` [[], [], [], [], [‘something’], [], [], [], [], []] «` Проверим в REPL: «` > arr => [[«something»], [«something»], [«something»], [«something»], [«something»], [«something»], [«something»], [«something»], [«something»], [«something»]] «` Ой-ой-ой! Что-то пошло не так! Посмотрим на текст программы целиком, что же в ней неправильно: «` arr = Array.new(10, []) element = arr[4] element.push(‘something’) puts arr.inspect # способ вывести информацию на экран так же, как ее выводит REPL «` Где ошибка? Слово знатокам, время пошло! Это, кстати, может быть хитрым вопросом на интервью. Вопрос не самый простой и подразумевает знакомство и понимание принципов работы языка руби, что такое объект, что такое ссылка. Помните, мы с вами немного говорили про ссылки? Когда есть подъезд и каждый звонок ведет в собственную квартиру? Мы можем повторить такой же фокус с классом String: «` arr = Array.new(10, ‘something’) element = arr[4] element.upcase! puts arr.inspect # способ вывести информацию на экран также, как ее выводит REPL «` Ожидаемый результат: 106

«` [«something», «something», «something», «something», «SOMETHING», «something», «something», «something», «something», «something»] «` Реальный результат: «` [«SOMETHING», «SOMETHING», «SOMETHING», «SOMETHING», «SOMETHING», «SOMETHING», «SOMETHING», «SOMETHING», «SOMETHING», «SOMETHING»] «` Что же тут происходит? А дело в том, что при инициализации массива мы передаем ссылку (reference) на один объект. Т.к. мы передаем параметр один раз, то и объект в массиве всегда “размножается по ссылке”. Т.е. на самом деле при такой инициализации массива, ячейки содержат не сам объект, а ссылку на объект. Чтобы этого не происходило, нужно чтобы ссылки на объекты были разные. При этом, конечно, и сами объекты будут разные — они будут располагаться в разных областях памяти, и если мы что-то изменим, то это не изменит состояние (state) других объектов. Если проводить аналогию с подъездом и жильцами дальше, то можно представить следующее. Мы принесли большую распределительную коробку (массив) и хотим поместить туда 10 звонков. Звонки поместили, но все провода идут к одной квартире. Поэтому, когда мы звоним в любой звонок, нам отвечают одни и те же жильцы. Нам просто нужны ссылки на разные квартиры и тогда все будет работать. Поэтому конструкция с массивом массивов неправильная, никогда так не делайте: «` arr = Array.new(10, []) «` Просто потому, что массив внутри массива предназначен для того, чтобы меняться. Зачем нам пустой массив? Ведь мы захотим когда-нибудь туда что-то записать. В случае со строками все немного проще, такая конструкция допустима: «` arr = Array.new(10, ‘something’) «` Но при одной оговорке — что мы не будем использовать опасные (danger) методы. Т.е. методы класса String с восклицательным знаком на конце, которые меняют состояние объекта. Теперь вы понимаете почему они опасные? В случае с числами все еще проще:

«` arr = Array.new(10, 123) «` В классе Integer нет опасных методов, поэтому, даже если у вас есть доступ к объекту, вы не сможете его изменить (но сможете заменить). Если вы напишите `arr[4] = 124`, то вы замените ссылку в массиве на новый объект, который будет представлять число “124”. Ссылки на один и тот же объект “123” в других частях массива сохранятся. Т.е. мы и получим то, что ожидаем: «` $ irb > arr = Array.new(10, 123) => [123, 123, 123, 123, 123, 123, 123, 123, 123, 123] > arr[4] = 124 => 124 > arr => [123, 123, 123, 123, 124, 123, 123, 123, 123, 123] «` Ничего страшного, если эти детали вам покажутся сложными. На практике редко приходится работать с очень сложными вещами, обычно требуется понимание этих принципов, способность разобраться или найти решение в Интернет. Некоторым опытным программистам это высказывание может не понравиться, но авторы книги рекомендуют не обращать внимание на чье-либо мнение, скорее находить удаленную работу и учиться уже на практике. Опыт учеников “руби школы” показывает, что этот путь верный. Но как же нам все-таки объявить двумерный массив? Представим, что нам нужно сделать игру “Морской бой”, где каждую строку на поле битвы представляет отдельный массив (ну а столбец — это индекс в этом отдельном массиве). Если бы у нас была одна строка на 10 клеток, то можно было бы обойтись одним массивом, но нам нужно 10 строк по 10 клеток. Как объявить такой массив, чтобы каждый элемент массива представлял собой ссылку на другой, совершенно отдельный элемент? Для объявления двумерного массива в языке C# используется довольно простая конструкция: «` var arr = new int[10, 10]; «` Для типа “string”:

«` var arr = new string[10, 10]; arr[9, 9] = «something»; «` Но в Ruby и JavaScript это, на удивление, делается немного сложнее. Правильный синтаксис для объявления двумерного массива 10 на 10 в руби (массив будет заполнен nil — объектом, представляющим пустое значение): «` arr = Array.new(10) < Array.new(10) >«` Вау! Но почему так? Давайте разберемся. Метод “new” (на самом деле это метод “initialize”, но это пока не важно) принимает один параметр и один блок. Первый параметр — фиксированный, это количество элементов массива. А второй параметр — блок, который надо исполнить для каждого элемента. Результат выполнения этого блока и будет новым элементом. Блок будет запускаться 10 раз (в нашем случае). Ничто не мешает написать нам блок таким образом: «` arr = Array.new(10) < 'something' >«` Результат будет аналогичен уже известному нам коду: «` arr = Array.new(10, ‘something’) «` В REPL и тот, и другой вариант выглядят одинаково: «` $ irb > arr1 = Array.new(10) < 'something' >=> [«something», «something», «something», «something», «something», «something», «something», «something», «something», «something»] > arr2 = Array.new(10, ‘something’) => [«something», «something», «something», «something», «something», «something», «something», «something», «something», «something»] «`

Но есть одна существенная разница. Первая конструкция при инициализации вызывает блок. В результате вызова блока каждый раз создается новое значение “something” в новой области памяти. А во втором случае (когда мы создаем `arr2`) берется “something”, который мы передали через параметр. Он создается в области памяти перед тем, как параметр будет передан в метод new, и используется для всех ячеек массива, всегда один и тот же. Это очень просто доказать. Для людей, недостаточно знакомых с языком руби, это кажется волшебным трюком. Модифицируем элемент по индексу 0 в первом массиве, где каждый элемент это всегда ссылка на отдельную строку, для каждого элемента массива ссылка разная. «` arr1[0].upcase! «` Выведем результат вычисления `arr1` на экран: «` > arr1 => [«SOMETHING», «something», «something», «something», «something», «something», «something», «something», «something», «something»] «` Изменилось только первое значение, что доказывает, что ссылка везде разная. Если же проделать точно такой же трюк со вторым массивом, то поменяется массив целиком, потому что ссылка на элемент во всех ячейках массива одинаковая: «` > arr2[0].upcase! => «SOMETHING» > arr2 => [«SOMETHING», «SOMETHING», «SOMETHING», «SOMETHING», «SOMETHING», «SOMETHING», «SOMETHING», «SOMETHING», «SOMETHING», «SOMETHING»] «` Если бы мы перед “upcase!” переинициализировали какой-либо элемент, то этот элемент не был бы затронут: «` > arr2[4] = ‘something’ => «something» > arr2[0].upcase! => «SOMETHING» > arr2 110

=> [«SOMETHING», «SOMETHING», «SOMETHING», «SOMETHING», «something», «SOMETHING», «SOMETHING», «SOMETHING», «SOMETHING», «SOMETHING»] «` Обратите внимание, что в примере выше элемент с индексом 4 не был затронут операцией “upcase!”, т.к. это совершенно другой объект, хотя при выводе на экран нам кажется что все одинаково. Поэтому правильная инициализация массива массивов выглядит так: «` arr = Array.new(10) < Array.new(10) >«` Если нужно заполнить массив значением, отличным от nil, передаем его во второй конструктор: «` arr = Array.new(10) < Array.new(10, 123) >«` Заполнить двумерный массив значением “0”: «` arr = Array.new(10) < Array.new(10, 0) >«` Создать массив в 4 строки и 10 столбцов и заполнить его значением “0”: «` arr = Array.new(4) < Array.new(10, 0) >«` Создать массив в 2 строки и 3 столбца и заполнить каждую строку одинаковым объектом “something”: «` arr = Array.new(2) < Array.new(3, 'something') >«` Создать массив в 3 строки и 2 столбца и заполнить каждую строку одинаковым объектом “something”: «` arr = Array.new(3)

«` Надеемся, что с созданием двумерных массивов проблем не будет. Когда у нас есть понимание того, что такое массив, что такое двумерный массив, есть смысл остановиться на способе записи двумерного массива с какими либо pre-defined (предопределенными) значениями. Одномерный массив записать просто, это массив каких-либо объектов: «` arr = [1, 2, 3] «` Или: «` arr = [‘one’, ‘two’, ‘three’] «` Т.е. массив содержит объекты. Двумерный массив это тот же самый массив, который содержит объекты, с той лишь разницей, что все эти объекты типа Array, а не Integer или String. Чтобы создать массив из трех строк, нам нужно написать:

«` arr = [. . . ] «` Но если необходимо создать массив пустых массивов, вместо троеточия нужно просто написать определение пустого массива: «` arr = [[], [], []] «` Давайте определим массив 3 на 3 для игры в крестики нолики, где ноль это нолик, единица — крестик, а пустая клетка это nil. Для такой матрицы: O

Массив будет выглядеть следующим образом:

«` arr = [[0, 0, 1], [nil, 0, nil], [1, nil, 1]] «` Запись в одну строку можно превратить в более читаемый вид с сохранением функциональности: «` arr = [ [0, 0, 1], [nil, 0, nil], [1, nil, 1] ] «` При желании можно добавить сколько угодно пробелов. Задание: если вы не попробовали в REPL все написанное выше, то перечитайте и попробуйте. Задание: Создайте массив в 5 строк и 4 столбца, заполните каждую строку случайным значением от 1 до 5 (только одно случайное значение для каждой строки). Пример для массива 2 на 3: «` [ [2, 2, 2], [5, 5, 5] ] «` Задание: создайте массив в 4 строки и 5 столбцов, заполните каждую строку случайным значением от 1 до 5 (только одно случайное значение для каждой строки). Задание: создайте массив 5 на 4 и заполните весь массив абсолютно случайными значениями от 0 до 9.

Установка gem’овов Все наши операции в REPL до текущего момента были не самыми сложными. Однако, в случае с двумерными массивами мы уже могли наблюдать потерю наглядности. Например, создание массива для игры в “Морской бой” выглядит следующим образом:

«` $ irb > Array.new(10) < Array.new(10) >=> [[nil, nil, nil, nil, nil, nil, nil, nil, nil, nil], [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil], [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil], [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil], [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil], [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil], [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil], [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil], [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil], [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil]] «` Синтаксис верный, но как понять где пятая строка и второй столбец? Приходится вглядываться в “простыню” этих значений. Разработчики языка руби знали, что нельзя написать инструмент, который понравится всем. И вместо того, чтобы завязывать разработчика на фиксированный набор инструментов, было решено добавить возможность расширять экосистему языка таким образом, чтобы каждый человек мог написать (или дописать) что-то свое. Разработчики со всего мира воспользовались этой возможностью и для языка руби было создано множество gem’овов (gem, читается как “джем” — драгоценный камень, жемчужина, что перекликается с названием “руби” — рубин). В других языках gem’овы называются библиотеками (library) или пакетами (packets). Например, альтернатива команде “gem” в Node.js команда “npm” — сокращение от Node Packet Manager (менеджер пакетов Node). Слово “gem” звучит поинтереснее, чем просто “пакет”. Но смысл один и тот же — просто какая-то программа, или программный код, который очень просто скачать или использовать, если знаешь имя gem’овa. Для установки gem’ова используется команда “gem”, которая является частью пакета языка руби (также как и “irb” и “ruby”). Давайте попробуем установить какой-нибудь gem: «` $ gem install cowsay «` “Cowsay” это “cow say” (“корова скажи”). Это не очень популярный gem, который был создан обычным энтузиастом. Этот gem добавляет в ваш shell команду “cowsay”, которая принимает аргумент и выводит на экран корову, которая что-то говорит: «` $ cowsay ‘Привет, Вася!’ _______________ | Привет, Вася! | —————\ ^__^ \ (oo)\_______

(__)\ )\/\ ||—-w | || || «` Существует огромное количество gem’овов на все случаи жизни. К слову, этим и славится язык руби (а также JavaScript). Для любой задачи, которая придет вам в голову наверняка существует какой-то gem (или пакет для JavaScript). Не обязательно gem должен добавлять какую-то команду. Часто бывает так, что gem предоставляет только определенный код, который вы можете использовать в своей программе, применив ключевое слово `require` (с параметром — обычно именем gem’ова). Для дальнейшего обучения нам потребуется установить наш первый (уже второй) gem, который является довольно популярным и практически стал стандартом в экосистеме руби (такое часто случается, независимые разработчики создают инструмент, который всем нравится и этот инструмент становится стандартом). Название gem’ова, который мы будем устанавливать — “pry” (читается как “прай”). Страница gem’овa на GitHub: https://github.com/pry/pry (зайдите, чтобы взглянуть на документацию). Что же такое pry? Вот что говорит нам GitHub: “An IRB alternative and runtime developer console”. Другими словами, альтернатива уже известному нам REPL — IRB. Если раньше мы вводили команду “irb”, то теперь будем вводить команду “pry”. Давайте же поскорее установим этот gem и посмотрим, чем он лучше: «` $ gem install pry . $ pry > «` Во-первых, если мы введем определение нашего массива в pry, то значения будут подкрашены (чего нет в “irb”): «` $ pry > arr = [[0, 0, 1], [nil, 0, nil], [1, nil, 1]] . «` Цифры подкрашиваются синим цветом, а nil — голубым. Казалось бы — ну и что? Это кажется незаметной деталью, но при работе с большим объемом данных визуальное облегчение информации — большое подспорье! Представьте количество дней, которое вы

в будущем проведете за компьютером и представьте, что теперь они будут немного лучше. Второй важный момент в pry — конфигурация. Причем, получается довольно любопытно. Gem это, грубо говоря, плагин для языка (или экосистемы — как будет угодно) руби. Но и для “плагина” pry существует свое множество плагинов, один из которых мы собираемся установить. Это плагин для “улучшенного” (более понятного) вывода информации на экран. Gem называется “awesome print”. Gem содержит в себе библиотеку кода, плагин для pry, плагин для irb (нам не потребуется, т.к. в будущем будем использовать только pry). Страница gem’овa на GitHub: https://github.com/awesome-print/awesome_print. Пройдите по ссылке, чтобы ознакомиться с документацией и понять, что делает awesome print. Если ничего непонятно, то ничего страшного, сейчас разберемся. Давайте установим awesome print: «` gem install awesome_print «` Сам по себе gem не создает никаких команд. Поэтому давайте подключим его к pry. Как это сделать описано в документации. Мы сделаем это вместе: «` $ cat > ~/.pryrc require ‘awesome_print’ AwesomePrint.pry! ^D «` Т.е. запускаем в терминале команду “cat”, которая считывает из стандартного ввода следующие две строки (мы должны их ввести с клавиатуры). В конце мы нажимаем Ctrl+D — комбинацию, которая говорит о том, что ввод закончен (в листинге выше это обозначается как “^D”). Возникает вопрос — а откуда взялись эти две строки и что они означают? Эти две строки взялись из документации, а именно из раздела «PRY Integration» readme репозитория гитхаба. Строки означают что-то, но на самом деле пока это неважно, читайте их как “подключение awesome print к pry”. Гем “awesome_print” подключается к “pry” только один раз на вашем компьютере. Теперь запустим pry и введем массив, который мы использовали для крестиков-ноликов: «` $ pry

> arr = [[0, 0, 1], [nil, 0, nil], [1, nil, 1]] [ [0] [ [0] 0, [1] 0, [2] 1 ], [1] [ [0] nil, [1] 0, [2] nil ], [2] [ [0] 1, [1] nil, [2] 1 ] ] «` Должно получиться вот так:

Вау! Вывод не только лучше, но и раскрашен в разные цвета! Наша связка “pry” с “awesome print” подкрашивает вывод, улучшает визуальную структуру и даже показывает нам индексы, чтобы мы легче могли добраться до нужного элемента! Сравните этот вывод со стандартным выводом IRB: «` $ irb > arr = [[0, 0, 1], [nil, 0, nil], [1, nil, 1]] => [[0, 0, 1], [nil, 0, nil], [1, nil, 1]] «` Примечание: одна из основных функциональностей “pry” это отладка программ. Мы займемся этим позже. Задание: попробуйте в “pry” вывести поле 10 на 10 для игры в “Морской бой”.

Обращение к массиву массивов Существует небольшая хитрость для обращения к массиву массивов (также говорят “к двумерному массиву”, к “2D array”). Хитрость заключается в том, что сначала нужно обратиться к строке (row), а потом к столбцу (column). Способ обращения к обычному массиву мы уже знаем. Для вывода значения используется следующая конструкция: «` puts arr[4] «` Для присваивания мы просто используем оператор `=`: «` arr[4] = 123 «` Где 4 — это индекс массива. В случае с двумерным массивом обычно используются двойные квадратные скобки. Например, следующий код обновит в 5-ой строке 9-ый столбец: «` arr[4][8] = 123 «`

Такой способ обращения может показаться непривычным для обычного человека, потому что человек привык сначала указывать столбец, потом строку; сначала X, потом Y. Но тем не менее, для доступа к массиву нам нужно сначала указывать индекс строки, а потом уже индекс столбца. Причем, ничто не мешает записать нам конструкцию присваивания иначе, она будет намного понятнее (правда, длиннее): «` row = arr[4] row[8] = 123 «` А вот так можно вывести значение девятого столбца в пятой строке (альтернативный способ): «` row = arr[4] # на этом этапе row уже будет одномерный (обычный) массив column = row[8] puts column «` Конечно, альтернативным способом редко кто пользуется, ведь общепринятый `arr[4][8]` проще и короче. В зависимости от типа задачи и от приложения с которым вы работаете, может использоваться разная терминология, обозначающая строку и столбец. Рассмотрим наиболее часто встречающиеся: ● ● ●

`row` — строка, `column` — столбец. Обращение к массиву: `arr[row][column]`. `y` — строка, `x` — столбец. Обращение к массиву: `arr[y][x]` `j` — строка, `i` — столбец. Обращение к массиву: `arr[j][i]`

Обратите внимание, что название переменной для индекса — `i` (от слова `index`). Если у нас есть более одной переменной для индекса, берется следующая буква в алфавите (`j`, а если массив трехмерный, то `k`). Впрочем, эти правила не являются каким-то стандартом, а всего-лишь наблюдением авторов. Попробуем создать двумерный массив и обойти (to traverse) его. Это элементарная задача, которая вам может встретиться на интервью: 2D array traversal: «` arr = [ %w(a b c), %w(d e f), %w(g h i)

] 0.upto(2) do |j| 0.upto(2) do |i| print arr[j][i] end end «` Вывод программы: «` abcdefghi «` Вверху мы видим двойной цикл (иногда его называют “вложенный цикл”, “double loop”, если имеют в виду цикл по `i` — то “inner loop”, “внутренний цикл”). Как же он работает? Мы уже знаем, что “цикл j” просто “проходит” по массиву. Он “не знает”, что у нас массив массивов, поэтому это обычная итерация по элементам массива: «` %w(a b c) %w(d e f) %w(g h i) «` Просто каждый элемент — еще один массив. Поэтому мы имеем право по нему пройти обычным образом, как мы это уже делали. Можно также записать нашу программу немного иначе, помощью “each”: «` arr = [ %w(a b c), %w(d e f), %w(g h i) ] arr.each do |row| row.each do |value| print value end end «`

Разумеется, что сам массив можно записать без помощи “%w” (согласитесь, что читаемость этого подхода немного ниже?): «` arr = [ [‘a’, ‘b’, ‘c’], [‘d’, ‘e’, ‘f’], [‘g’, ‘h’, ‘i’] ] «` Задание: обойдите массив выше “вручную”, без помощи циклов, крест-накрест, таким образом, чтобы вывести на экран строку “aeiceg” (подпрограмма займет 6 строк — по 1 строке для каждого элемента). Задание: создайте 2D массив размером 3 на 3. Каждый элемент будет иметь одинаковое значение (например, “something”). Сделайте так, чтобы каждый элемент массива был защищен от “upcase!”. Например, если мы вызовем `arr[2][2].upcase!`, этот вызов не изменит содержимое других ячеек массива. Проверьте свое задание в pry. Задание: к вам обратился предприниматель Джон Смит. Джон говорит, что его бизнес специализируется на создании телефонных номеров для рекламы. Они хотят подписать с вами контракт, но прежде хотелось бы убедиться, что вы хороший программист, можете работать с их требованиями, и доставлять качественное программное обеспечение. Они говорят: у нас есть номера телефонов с буквами. Например, для бизнеса по продаже матрасов существует номер “555-MATRESS”, который транслируется в “555-628-7377”. Когда наши клиенты набирают буквенный номер на клавиатуре телефона (см.картинку ниже), он транслируется в цифровой. Напишите программу, которая будет переводить (транслировать) слово без дефисов в телефонный номер. Сигнатура метода будет следующей: «` def phone_to_number(phone) # ваш код тут. end puts phone_to_number(‘555MATRESS’) # должно напечатать 5556287377 «` Иллюстрация телефонной клавиатуры:

Многомерные массивы Существуют также многомерные массивы. Если 2D массив это “массив массивов”, то 3D массив это “массив массивов массивов”. Иногда такие массивы называют “тензор”. Пример трехмерного массива: «` arr = [ [ %w(a b c), %w(d e f), %w(g h i) ], [ %w(aa bb cc), %w(dd ee ff), %w(gg hh ii) ] ] «`

Это массив 2 на 3 на 3: два блока, в каждом блоке 3 строки, в каждой строке 3 столбца. Размерность (dimension) массива это просто его свойство. Не обязательно знать размерность каждого массива, важно лишь знать как правильно к нему обратиться. Для обращения к элементу “f” нам нужно написать ` `. На практике многомерные массивы встречаются очень часто, но обычно в таких массивах также присутствует также другая структура данных — хеш (рассматривается далее). В случае с многомерными массивами нам нужно точно знать индексы определенных элементов. Если добавляется строка или столбец где-нибудь вначале, то индексы смещаются. Поэтому на практике доступ по индексу встречается лишь в простых случаях. Если массив “миксуется” с хешем, то такую структуру обычно называют JSON (JavaScript Object Notation), хотя в руби это название выглядит немного необычно — причем тут JavaScript, ведь это руби! Доступ к значениями хеша осуществляется по ключу (а не по индексу), где ключ обычно какая-нибудь строка. Задание: попробуйте создать массив, объявленный выше в pry, и обратиться к элементу “ee”. Задание: посмотрите официальную документацию к классу Array: http://ruby-doc.org/core2.5.1/Array.html

Наиболее часто встречающиеся методы класса Array Стоит подробнее остановиться на наиболее часто встречающихся методах класса Array, т.к. эти методы широко используются не только в руби, но и в rails. Даже не имея опыта с фреймворком Ruby on Rails, понимая принципы работы рассмотренных методов для массивов, легко догадаться о том, что делает программа.

Метод empty? Знак вопроса на конце метода означает, что метод будет возвращать значение типа Boolean (true или false). Метод “empty?” используется для того, чтобы убедиться в том, что массив не пустой (или пустой). Если массив пустой (empty), то “empty?” возвращает “true”: «` $ pry

> [].empty? => true «` Важный момент заключается в том, что объект “nil” не реализует метод “empty?”. Т.е. если вы не уверены, что какой-то метод возвращает массив, необходимо сделать проверку на nil: «` arr = some_method if !arr.nil? && !arr.empty? puts arr.inspect end «` Существует одна важная деталь. Т.к. любой руби-программист почти со 100% вероятностью будет работать с rails, нужно знать, что проверка коллекции (в т.ч.массива) в rails выполняется иначе. Т.е. если вы оставите этот синтаксис, то ошибки не будет, просто есть более эффективный способ: «` if !arr.blank? puts arr.inspect end «` Или используя прямо противоположный метод “present?”: «` if arr.present? puts arr.inspect end «` Другими словами, когда фреймворка rails нет, используем “empty?”, а когда работаем над rails-приложением, всегда используем “blank?” и “present?”. Эти методы реализованы для многих типов, и при наличии вопросов, в будущем рекомендуется обращаться к этой таблице:

Источник: https://stackoverflow.com/a/20663389/337085 TODO: сделать свою таблицу Таблица выше очень важная, стоит сделать особую заметку в книге. Как видно, методы “blank?” и “present?” совершенно противоположные (последний и предпоследний столбец). А из второго столбца следует, что только nil и false вычисляются в false. Другими словами, все конструкции ниже вычисляются в true и нет необходимости делать проверку (с помощью `==`, если мы хотим получить тип Boolean): «` if true # будет выполнено end if » # будет выполнено end if ‘ ‘ # будет выполнено end if [] # будет выполнено end # . «` И так далее. Также из таблицы видно, что метод “empty?” реализован для типов String, Array, Hash.

Методы length, size, count Методы length и size идентичны и реализованы для классов Array, String, Hash: «` [11, 22, 33].size # => 3 [11, 22, 33].length # => 3

str = ‘something’ str.size # => 9 str.length # => 9

hh = < a: 1, b: 2 >hh.size # => 2 hh.length # => 2 «` Метод count выполняет ту же функцию, что и length/size, но только для классов Array и Hash (не реализован в String). Однако, метод count может принимать блок, можно использовать его для каких-либо вычислений. Например, посчитать количество нулей в массиве: «` $ pry > [0, 0, 1, 1, 0, 0, 1, 0].count < |x| x == 0 >5 «` Удобно использовать метод count вместе с указателем на функцию. Если метод “zero?” реализован у всех элементов массива, можно записать конструкцию выше иначе: «` [0, 0, 1, 1, 0, 0, 1, 0].count(&:zero?) «` Важно заметить, что count с блоком обычно проходит по всему массиву. Если вы используете метод “count” в Rails, необходимо убедиться, чтобы запрос был эффективным (rails и SQL будут рассмотрены во второй части книги). Задание: с помощью указателя на функцию посчитайте количество четных элементов в массиве [11, 22, 33, 44, 55].

Метод include? Метод “include?” проверяет массив на наличие определенного элемента и возвращает значение типа Boolean. Например: «` $ pry > [1, 2, 3, 5, 8].include?(3) true «` Любопытная особенность в том, что “include” переводится на русский как “включать” (в смысле “содержать”), тогда как правильнее было бы написать “includes” — “включает” (с “s” в конце). В языке JavaScript версии ES6 и выше проверка на наличие элемента в массиве реализована как раз с помощью правильного слова “includes”: «` $ node > [1, 2, 3, 5, 8].includes(3); true «`

Добавление элементов Добавление элементов в массив реализовано с помощью уже знакомых нам методов “push” и “pop”. Эти методы производят операции с хвостом массива: добавить элемент в конец, извлечь последний. К слову, массив в руби реализует также структуру данных “стек”. Представьте себе “стек” тарелок, когда одна тарелка стоит на другой. Мы кладем одну наверх и берем также сверху. Но есть операции “unshift” и “shift”, которые делают то же самое, что и “push”, “pop”, но только с началом массива. Нередко у программистов возникает путаница при использовании unshift и shift, но важно помнить (или уметь посмотреть в документации) следующее: ● ●

unshift почти то же самое, что и push shift почти то же самое, что и pop

Полезная метафора тут может быть такая: shift сдвигает элементы и возвращает тот элемент, которому не досталось места.

Выбор элементов по критерию (select) Допустим, у нас есть список работников, у которых указан возраст. Нам нужно выбрать всех мужчин, которым в следующем году на пенсию. Для простоты предположим, что одного работника представляет какой-либо объект. Т.к. хеши мы еще не проходили, то пусть это будет массив. Первый элемент массива это будет возраст, второй — пол (1 для мужчины, 0 для женщины). Знакомьтесь, мужчина 30 лет: «` [30, 1] «` Женщина 25 лет: «` [25, 0] «` Таких объектов существует множество (массив объектов, в нашем случае двумерный массив): «` [ [30, 1], [25, 0], [64, 1], [64, 0], [33, 1] ] «` Выбираем (select) мужчин в возрасте 64 лет: «` $ pry > arr = [ [30, 1], [25, 0], [64, 1], [64, 0], [33, 1] ] . > arr.select < |element| element[0] == 64 && element[1] == 1 >(выбран 1 элемент) «` Выбираем всех мужчин: «` $ pry > arr = [ [30, 1], [25, 0], [64, 1], [64, 0], [33, 1] ] . > arr.select < |element| element[1] == 1 >(выбрано 3 элемента) «`

Отсечение элементов по критерию (reject) Метод “reject” класса Array работает аналогично “select”, но отсеивает элементы, удовлетворяющие критерию. Отсеять всех мужчин старше 30 лет (и выслать остальным повестку в военкомат): «` $ pry > arr = [ [30, 1], [25, 0], [64, 1], [64, 0], [33, 1] ] . > arr.reject < |element| element[0] >= 30 > (выбран 1 элемент двадцати пяти лет, который скоро пойдет в армию) «`

Метод take Метод “take” принимает параметр (число) и берет определенное количество элементов в начале массива: «` $ pry > [11, 22, 33, 44, 55].take(2) => [11, 22] «`

Есть ли хотя бы одно совпадение (any?) Допустим, у нас есть массив результатов лотереи. Нам нужно проверить, есть ли хотя бы один выигрыш. Из определения метода (знак вопроса в конце) понятно, что метод возвращает значение типа Boolean. В блоке должна быть конструкция сравнения, т.к. внутри метод “any?” будет использовать то, что мы укажем в блоке: «` $ pry > [false, false, false, true, false].any? < |element| element == true >true

Код выше показывает что среди 5 билетов есть 1 выигрыш. Этот метод только сообщает о том, что выигрыш имеется, он не говорит, какой именно билет выиграл. Т.е. метод не возвращает индекс. Чтобы найти индекс (какой билет выиграл), принцип наименьшего сюрприза подсказывает, что должен быть метод “find_index”. Проверим: «` $ pry > [false, false, false, true, false].find_index < |element| element == true >3 «` Работает!

Все элементы должны удовлетворять критерию (all?) Допустим, у нас массив возрастов пользователей, нам нужно убедиться, что все пользователи взрослые (18 лет или более). Как это сделать? Очень просто с помощью метода “all?”: «` $ pry > [20, 34, 65, 23, 18, 44, 32].all? < |element| element >= 18 > true «`

Несколько слов о популярных методах класса Array Мы рассмотрели некоторые методы класса “Array” (массив): ● ● ● ● ● ● ● ●

push, pop — добавить элемент, извлечь элемент arr[i] — обратиться по индексу empty? — проверка на пустоту length, size, count — один и тот же метод с разными названиями для получения размера массива include? — проверка на наличие элемента select, reject — выбрать по какому-либо условию или отклонить take — взять определенное количество элементов any?, all? — проверка на соответствие условию одного или всех элементов

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

своем проекте, эта информация вам обязательно пригодится. Более того, все эти методы также реализованы в веб-фреймворке Rails и вы сможете использовать их в разных ситуациях. Например: ● ● ● ●

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

Символы Символы (symbol) в руби — почти то же самое, что и строки. Символы являются экземпляром (instance) класса Symbol (а все строки являются экземплярами класса String). Другими словами, символы представляет класс Symbol, а строки класс String. Записать символ очень просто: «` x = :something «` Символы часто встречаются, когда одной и той же переменной в разных частях программы присваивается одинаковое по смыслу значение. Например: «` order.status = :confirmed order.status = :cancelled «` Символ “:confirmed” может встречаться в других частях программы. Но почему же используют символы — спросит читатель, ведь вместо символа всегда можно записать строку: «` order.status = ‘confirmed’ order.status = ‘cancelled’ «` Так и есть, можно было бы вообще обойтись без символов (и некоторые языки обходятся, например JavaScript). Но есть две причины по которым использование символов целесообразно.

Во-первых, символы являются неизменяемыми (immutable). Т.е. с ними нельзя выполнить “опасную” операцию, как например со строкой (типа upcase!). Другими словами, используя символ, вы показываете свое намерение: вот это значение всегда одинаково во всем приложении, и скорее всего существует ограниченный набор похожих значений. Это примерно также, как и билет в театр. Можно каждой бумажке от руки написать “Сектор А”, а можно сделать печать “Сектор А” и на определенных билетах ее ставить. Ведь поставить печать — занятие значительно менее ресурсоемкое, чем писать что-то от руки. Тем более каждую надпись нужно еще суметь разобрать, а вот печать универсальна, точно знаешь что это такое. Во-вторых, т.к. символы immutable, то целесообразно их использовать повторно (reuse), вместо того, чтобы выделять каждый раз на них память. Скажем, если у вас есть строка “something” (9 байт) и вы определяете ее в 1000 разных частях приложения, то это уже как минимум 9000 байт (на самом деле больше). Если это символ, то из-за того, что символы в памяти не повторяются, будет использовано только 9 байт памяти. Если, конечно, вы объявите новый символ “something_else”, то он тоже займет память, но только однажды. Выражаясь более техническим языком, ссылки на одинаковые символы всегда одинаковы. Ссылки на строки не всегда одинаковы — могут быть одинаковы, но не всегда. Например, создадим массив строк “хитрым способом” — когда для каждой операции создания вызывается блок, и из блока возвращается новая строка: «` arr = Array.new(100) < 'something' >«` Будет создано 100 строк “something”, эти строки будут находиться в разных участках памяти, это будут разные объекты. В этом легко убедиться, идентификатор объектов будет разный: «` > arr[0].__id__ 70100682145140 > arr[1].__id__ 70100682144840 «` Но если создать массив символов точно таким же способом, то идентификатор объектов будет всегда одинаковым: «` $ pry

> arr = Array.new(100) < :something >… > arr[0].__id__ 2893788 > arr[1].__id__ 2893788 «` Другими словами, массив символов содержит ссылки на один и тот же объект. Еще один положительный момент при использовании символов: символы сравниваются по ссылке. А ссылка это всего лишь значение вида 0xDEADBEEF, которое помещается в регистр компьютера (4-8 байт, в зависимости от архитектуры процессора и др.настроек). Поэтому сравнить два символа — это операция сравнения двух указателей (ссылок). А операция сравнения двух строк реализована через побайтное сравнение, т.к. два разных объекта, находящихся в разных участках памяти (и следовательно с разными указателями на эти участки) могут быть равны, а могут и нет. Поэтому нужно сравнивать их до последнего байта. Другими словами, сравнение двух символов занимает константное время (constant time, в компьютерной науке — computer science — обозначается как O(1)), а операция сравнения двух строк занимает линейное время (linear time, обозначается как O(N)). Не будет большой ошибки, если вы всегда будете применять строки, программа будет работать. Но ради экономии памяти, ради небольшого выигрыша в быстродействии, и ради демонстрации другим программистам своих намерений стоит применять символы. Задание: напишите игру “камень, ножницы, бумага” (`[:rock, :scissors, :paper]`). Пользователь делает свой выбор и играет с компьютером. Начало игры может быть таким: «` print «(R)ock, (S)cissors, (P)aper?» s = gets.strip.capitalize if s == . «`

Структура данных “Хеш” (Hash)

Хеш (также говорят хеш-таблица, hashtable, hash, map, dictionary, в языке JavaScript часто называют “объект”) и массив — две основные структуры данных, которые часто используются вместе. Хеш и массивы — разные структуры данных, но они преследуют одну цель — хранение и извлечение данных. Различаются лишь способы сохранения и извлечения. Что такое массив, как он хранит данные и как мы извлекаем данные из массива? Представьте, что у маленького ребенка много разных игрушек. Мама положила на полку все игрушки и каждому месту на полке присвоила порядковый номер. Чтобы найти игрушку в массиве, нам нужно просмотреть всю полку. Если полка с игрушками очень длинная, то это займет какое-то время. Но зато если мы точно знаем номер игрушки, мы можем найти ее моментально. Хеш напоминает волшебную корзину. В ней нет никакого порядка и мы не знаем как она устроена (знаем конечно, но многие программисты даже об этом не задумываются). В эту корзину можно положить какой угодно объект и сказать ей название: “волшебная корзина, это мяч”. Потом можно извлечь из этой корзины любой объект по имени: “волшебная корзина, дай мне эту вещь, про которую я говорил, что она называется мяч”. Важно что мы складываем объекты, указывая имя и извлекаем по имени (имя объекта называется ключом). Причем, извлечение происходит моментально — таким образом работает волшебная корзина. Как же работает волшебная корзина, почему в случае поиска элемента в массиве нужно просматривать весь массив, а в случае поиска какого-либо объекта в хеше поиск происходит моментально? Секрет в организации. На самом деле в большой корзине много маленьких корзин (они так и называются buckets). Если упростить, то все маленькие корзины внутри тоже пронумерованы, а объекты складываются туда по какому-либо признаку (скажем, по цвету). Если объектов много, то и маленьких корзин должно быть больше. Если хеши так хороши, то почему бы их не использовать всегда? Во-первых, эта структура данных не гарантирует порядок. Если мы добавляем данные в массив с помощью “push”, то мы точно знаем, какой элемент был добавлен сначала, какой после. В хеше нет никакого порядка, как только мы записали туда значение, нет возможности сказать когда именно оно туда попало: раньше или позже остальных. Примечание: несмотря на то, что структура данных “хеш” не гарантирует порядок, в руби порядок гарантируется (однако, авторы бы не рекомендовали на него надеяться). Вот что говорит официальная документация https://ruby-doc.org/core-2.5.1/Hash.html: > Hashes enumerate their values in the order that the corresponding keys were inserted.

Но т.к. любой веб-разработчик должен хотя бы на минимальном уровне знать JavaScript, то посмотрим что говорит по этому поводу документация по JS: > An object is a member of the type Object. It is an unordered collection of properties each of which contains a primitive value, object, or function Однако, в новой версии языка JavaScript (ES6 и выше) класс Map (альтернативная реализация хеша `<>`) будет возвращать значения из хеша в порядке добавления. Правило хорошего тона: при использовании хешей не надейтесь на порядок. А во-вторых, для каждой структуры данных существует такое понятие как “худшее время исполнения операции”: при неблагоприятных обстоятельствах (скажем, все игрушки оказались одного цвета и попали в одну и ту же внутреннюю маленькую корзину) операции доступа, вставки и извлечения для хеша работают за линейное время (linear time, O(N)). Другими словами, в худшем случае код для извлечения какого-либо элемента из хеша будет перебирать все элементы. А код для извлечения элемента из массива по индексу в худшем случае всегда занимает константное время (constant time, O(1)) — т.е. грубо говоря — всегда одно обращение, без прохода по массиву. Конечно, на практике худшие случаи встречаются не часто, и основная причина по которой программисты используют хеши — удобство для человека. Гораздо проще сказать “извлечь мяч”, чем “извлечь объект по индексу 148”. Объявить хеш в вашей программе очень просто, достаточно использовать фигурные скобки (квадратные скобки используются для массива): «` $ pry > obj = <> . > obj.class Hash 410, :tennis_ball => 58, :golf_ball => 45 > «` Эта запись полностью валидна с точки зрения языка Руби, и мы могли бы инициализировать наш хеш без записи значений (без использования операции присвоения): «` obj = < :soccer_ball =>410, :tennis_ball => 58, :golf_ball => 45 > «`

Оператор “=>” в руби называется “hash rocket” (в JavaScript “fat arrow”, но имеет другое значение). Однако, запись с помощью “hash rocket” считается устаревшей. Правильнее было бы записать так: «` obj = < soccer_ball: 410, tennis_ball: 58, golf_ball: 45 >«` Обратите внимание, что несмотря на то, что запись выглядит иначе, если мы напишем в REPL “obj”, то мы получим тот же вывод, что и выше. Другими словами, ключи (:soccer_ball, :tennis_ball, :golf_ball) в этом случае являются типами Symbol. Для извлечения значения (value) из хеша можно воспользоваться следующей конструкцией: «` puts ‘Вес мяча для гольфа:’ puts obj[:golf_ball] «` Обращение к хешу очень похоже на обращение к массиву. В случае с массивом — мы обращаемся по индексу, в случае с хешем — по ключу. Заметьте, что символы это не то же самое, что и строки. Если мы определяем хеш следующим образом: «` obj = <> obj[:golf_ball] = 45 obj[‘golf_ball’] = 45 «` То в хеш будет добавлено две пары ключ-значение (первый ключ типа Symbol, второй типа String: «` < :golf_ball =>45, «golf_ball» => 45 > «` Задание: используя инициализированный хеш вида:

«` obj = < soccer_ball: 410, tennis_ball: 58, golf_ball: 45 >«` Напишите код, который адаптирует этот хеш для условий на Луне. Известно, что вес на луне в 6 раз меньше, чем вес на Земле. Задание: “лунный магазин”. Используя хеш с новым весом из предыдущего задания напишите программу, которая для каждого типа спрашивает пользователя какое количество мячей пользователь хотел бы купить в магазине (ввод числа из консоли). В конце программа выдает общий вес всех товаров в корзине. Для сравнения программа должна также выдавать общий вес всех товаров, если бы они находились на Земле.

Другие объекты в качестве значений Мы уже разобрались с тем, что хеш это набор key-value pairs (пара ключ-значение), где key это обычно Symbol или String, а value это объект. В нашем примере в качестве объекта всегда было число. Но мы также можем использовать объекты любого другого типа в качестве значений, включая строки, массивы и даже сами хеши. То же самое и с массивами. В качестве элементов массива могут быть числа, строки, сами массивы (в этом случае получаются двумерные, многомерные массивы), а также и хеши. И эти хеши могут содержать в себе другие хеши или массивы массивов. Другими словами, при комбинации массивов и хешей получается уникальная структура данных, которую называют JSON (JavaScript Object Notation — мы уже говорили о том, что хеш в JavaScript часто называют “object”). Несмотря на то, что это название изначально появилось в JavaScript, в руби оно тоже широко используется. Вот как может выглядеть простая комбинация массива и хеша: «` obj = < soccer_ball: < weight: 410, colors: [:red, :blue] >, tennis_ball: < weight: 58, colors: [:yellow, :white] >, golf_ball: < weight: 45, colors: [:white] >> «`

Для каждого ключа выше мы определяем свой хеш, который в свою очередь представляет такие параметры как weight (вес, число, тип Integer) и доступные для этого товара цвета (colors, массив символов). Несмотря на то, что последнюю строку можно было записать как «` golf_ball: < weight: 45, color: :white >«` (т.к. мяч для гольфа доступен в одном цвете — в белом), мы намеренно записываем этот хеш универсальным образом, где «:white» это один элемент в массиве, который доступен по ключу «:colors». В этом случае говорят «сохранить схему [данных]». Схема данных — это просто определенная структура, которой мы решили придерживаться. Мы делаем это по двум причинам: ● ● ●

Чтобы не было путаницы. Каждая строка будет похожа на предыдущую. Чтобы оставался массив «colors», в который в будущем можно будет добавить мяч для гольфа другого цвета. Чтобы код, который работает с этой структурой данных оставался одним и тем же. Если добавить для какой-то строки отдельное свойство (color), то придется делать проверку с помощью конструкции «if» и иметь две ветки кода.

Другими словами, обычно JSON-объекты придерживаются какой-то определенной структуры. Но как же получить доступ к такому сложному объекту? Таким же образом, каким мы получаем доступ к массиву, с помощью нескольких операций доступа. Выведем все цвета мяча для тенниса: «` arr = obj[:tennis_ball][:colors] puts arr «` Выведем вес мяча для гольфа: «` weight = obj[:golf_ball][:weight] puts weight «` Добавим новый цвет «:green» в массив цветов мяча для тенниса: «` obj[:tennis_ball][:colors].push(:green)

«` Структура, которую мы определили выше начинается с открывающейся фигурной скобки. Это означает, что JSON имеет тип Hash. Но структура JSON может также быть массивом. Все зависит от нужд нашего приложения. Если наша задача — вывод списка, а не обращение к хешу, как к источнику данных, то JSON может быть записан другим образом: «` obj < < < ] ```

= [ type: :soccer_ball, weight: 410, colors: [:red, :blue] >, type: :tennis_ball, weight: 58, colors: [:yellow, :white] >, type: :golf_ball, weight: 45, colors: [:white] >

По сути эта структура ничто иное, как массив объектов с каким-то свойствами: «` obj = [ <>, <>, <> ] «` Другими словами, в зависимости от нашей задачи мы можем использовать ту или иную структуру данных, состоящую из массивов и хешей. Задание: корзина пользователя в Интернет-магазине определена следующим массивом (qty — количество единиц товара, которое пользователь хочет купить, type — тип): «` cart = [ < type: :soccer_ball, qty: 2 >, < type: :tennis_ball, qty: 3 >] «` А наличие на складе следующим хешем: «` inventory = < soccer_ball: < available: 2, price_per_item: 100 >, tennis_ball: < available: 1, price_per_item: 30 >, golf_ball: < available: 5, price_per_item: 5 >> «`

Написать программу, которая выводит на экран цену всех товаров в корзине (total), а также сообщает — возможно ли удовлетворить запрос пользователя — набрать все единицы товара, которые есть на складе.

Пример JSON-структуры, описывающей приложение Структура JSON является довольно универсальным способом записи практически любых данных. Например, следующая структура определяет состояние (state) интерфейса простейшего приложения “Задачи на сегодня” (также известным как “Список дел”, “TODOs”, “Купи батон” и т.д): «` < todos: [< text: 'Покушать', completed: true >, < text: 'Сходить в спортзал', completed: false >], visibility_fiter: :show_completed > «` На экране пользователя это приложение может выглядеть следующим образом:

Если разобрать эту структуру данных, то получается следующая простая конструкция: «`

< todos: [ < . >, < . >, . ], visibility_fiter: :show_completed > «` По ключу “todos” в хеше имеется значение — это массив. В массиве каждый элемент это отдельный хеш (объект), который имеет два свойства: текст и флаг завершенности какого-либо дела (тип Boolean — либо true — завершено, либо false — не завершено). Также в главном хеше есть свойство “visibility_filter” (фильтр видимости), который принимает значение “show_completed” (показать завершенные). Мы сами придумали название этого символа. В какой-то части нашей программы участок кода должен отвечать за отображение только завершенных данных. Несмотря на то, что в массиве “todos” у нас два элемента, на экране отображается только один. Если мы нажмем на переключатель, то экран будет иметь следующий вид:

И состояние программы в этом случае будет представлено немного измененным хешем. Например, таким: «` < todos: [< text: 'Покушать', completed: true >, < text: 'Сходить в спортзал', completed: false >],

visibility_fiter: :show_all > «` Когда добавляется какой-то элемент данных, то значение просто добавляется в массив: «` < todos: [< text: 'Покушать', completed: true >, < text: 'Сходить в спортзал', completed: false >, < text: 'Позвонить Геннадию', completed: false >], visibility_fiter: :show_all > «` А экран программы при этом будет выглядеть следующим образом:

Задание: напишите хеш, который бы отображал состояние следующего приложения:

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

Англо-русский словарь Для закрепления материала напишем простейшее приложение “англо-русский словарь”. Из самого названия приложения можно догадаться какую структуру данных мы будем использовать — хеш (который также называется dictionary — словарь). Самое основное в словаре — база данных. Речь идет не о специализированной Системе Управления Базами Данных (СУБД) типа MySQL, Postgres, и т.д., а о базе данных в виде обычной структуры в памяти. Она может выглядеть следующим образом: «` dict = < 'cat' =>‘кошка’, ‘dog’ => ‘собака’, ‘girl’ => ‘девушка’ > «` Авторы говорят “может выглядеть” из-за того, что они не настаивают на определенной точке зрения. Возможно, у вас будет какая-нибудь другая идея. Но в нашем случае

подойдет простейшая структура данных ключ-значение (key-value), где ключом будет слово (тип String), а значением — перевод (тип String). Эта структура данных позволяет легко искать вводимое пользователем слово в нашем словаре. Под словом “легко” подразумевается поиск с т.н. константным временем (constant time, O(1)). Другими словами, сколько бы слов мы не добавили в наш хеш, поиск всегда будет занимать одно и то же время. Если бы мы воспользовались структурой данных “массив”, то задача тоже была бы решаема. Например, можно было бы определить нашу структуру данных следующим образом: «` arr < < < ] ```

= [ word: ‘cat’, translation: ‘кошка’ >, word: ‘dog’, translation: ‘собака’ >, word: ‘girl’, translation: ‘девушка’ >

Но для поиска элемента нам необходимо перебрать весь массив (с помощью конструкции each). Если элементов будет много, то поиск будет занимать больше времени. Другими словами, с возрастанием размера массива возрастает и количество элементов, которое требуется просмотреть чтобы найти слово. В этом случае говорят, что поиск будет занимать линейное время (linear time, O(N)). Для небольшого количества элементов нет разницы как именно мы будем реализовывать поиск. Более того, в новых версиях языка руби хеш, который содержит не более 7 элементов реализован через массив. Снаружи мы это никак не определим, т.к. программист всегда использует API языка и не лезет во внутренности. Но если посмотреть исходный код языка и реализацию на языке “Си”, то эти подробности видны. В любом случае, хеш нам больше подходит, даже если количество элементов небольшое. Когда мы используем хеш (или другую структуру данных), мы также показываем свое намерение другим программистам: “эта структура данных вот такая, а следовательно я намереваюсь использовать ее правильным образом”. Конечно, если бы для каждого слова мы точно знали индекс, то поиск в массиве занимал бы константное время. Но пользователь не вводит индекс, он вводит слово. Поэтому и нужна структура данных “хеш”. Поиск в хеше выполняется простой конструкцией: «` dict[input] «`

Вся программа выглядит довольно просто: «` dict = < 'cat' =>‘кошка’, ‘dog’ => ‘собака’, ‘girl’ => ‘девушка’ > print ‘Введите слово: ‘ input = gets.chomp puts «Перевод слова: #» «` Результат работы программы: «` Введите слово: dog Перевод слова: собака «` Заметьте, что у нас получился англо-русский словарь. Этот словарь невозможно использовать как русско-английский, потому что доступ к хешу всегда осуществляется по ключу. Нет способа с помощью которого мы могли бы по значению (переводу) получить ключ (слово на английском языке). Единственный способ — создать еще один хеш, в этом случае ключом было бы русское слово, а значением английское — и получился бы русскоанглийский словарь. Константное O(1) и линейное O(N) время это понятия о т.н. Big-O (большое O), понятие из Computer Science. Начинающему программисту нет необходимости знать абсолютно все структуры данных и сложные алгоритмы. Однако, полезно задавать себе вопросы о теоретической скорости работы той или иной операции. Все популярные структуры данных сведены в единую таблицу, которую можно найти по адресу https://github.com/ro31337/bigoposter/blob/master/bigoposter.pdf Например, из таблицы видно, что в среднем (average) операция поиска в ма065ссиве занимает линейное O(N) время, а операция поиска в хеше константное O(1):

Задание: напишите “сложный” англо-русский словарь, где каждому английскому слову может соответствовать несколько переводов (например: cat это “кот”, “кошка”). Задание: задайте базу данных (хеш) своих контактов. Для каждого контакта (фамилия) может быть задано три параметра: email, cell_phone (номер моб.телефона), work_phone (номер рабочего телефона). Напишите программу, которая будет спрашивать фамилию и выводить на экран контактную информацию. Сравнительная таблица массивов и хешей Массив

arr.each do |element| . end

hh.each do |key, value| . end

или hh.each_key do |key| . end

Представление данных 0 … N

Для представления последовательности данных, списков, элементов идущих по-порядку

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

Наиболее часто используемые методы класса Hash В общем и целом, структура данных “хеш” довольно простая. В языке руби существуют некоторые методы, которые вам могут встретиться чаще, чем остальные. В остальных языках эти методы похожи. Скажем, обращение к хешу (объекту) в JavaScript выглядит следующим образом: «` $ node > hh = <>; <> > hh[‘something’] = ‘blabla’; ‘blabla’ > hh < something: 'blabla' >«` Различие лишь в том, что в JavaScript не существует типа Symbol, и в качестве ключей в большинстве случаев используются строки. Хеши также реализованы в некоторых других инструментах, например в базах данных. Довольно известная база данных Redis ничто иное как key-value storage (хранилище “ключ-значение”). В предыдущих примерах мы делали записную книжку. Но представим, что нам нужно сохранять все эти данные в случае перезапуска программы. Первый 148

вариант — сохранить все в файл. Этот способ прекрасно работает, но возможно он немного медленный, когда у вас есть несколько тысяч пользователей. Второй вариант воспользоваться NoSQL базой данных через особый API (интерфейс взаимодействия). В любом случае, используете ли вы библиотеку (gem), базу данных, язык руби или какойто другой, для хеша всегда существует два основных метода: ● ●

get(key) — получить значение (value) set(key, value) — установить значение для определенного ключа

Документация к NoSQL базе данных Redis https://github.com/redis/redis-rb говорит нам то же самое: «` redis.set(«mykey», «hello world») # => «OK» redis.get(«mykey») # => «hello world» «` Если посмотреть в Википедии, то Redis это ничто иное как хранилище ключ-значение: > Redis is. key-value store… Тут у читателя возникает вопрос — а зачем мне Redis-хеш, когда у меня есть хеш в языке руби? Во-первых, хеш в языке руби не сохраняет данные на диск. А во-вторых, Redis предназначен для эффективного хранения многих миллионов пар “ключ-значение”, а хеш в языке руби обычно не хранит много пар. Ниже мы рассмотрим наиболее часто встречающиеся методы класса Hash. Все эти методы также описаны в документации https://ruby-doc.org/core-2.5.1/Hash.html

Установка значения по-умолчанию Иногда полезно устанавливать значения в хеше по-умолчанию. Следует сделать заметку в книге, т.к. эта возможность часто забывается, но на практике потребность в значении по-умолчанию часто возникает на интервью. Одна из подобных задач — есть какое-то предложение, необходимо сосчитать частотность слов и вывести список. Например, слово “the” встречается 2 раза, слово “dog” 1 раз и так далее.

Как мы будем решать эту задачу? Представим, что у нас есть строка “the quick brown fox jumps over the lazy dog”. Разобьем ее на части: «` str = ‘the quick brown fox jumps over the lazy dog’ arr = str.split(‘ ‘) «` У нас получился массив слов, давайте обойдем этот массив и занесем каждое значение в хеш, где ключом будет слово, а значением — количество повторов этого слова. Попробуем для начала количество повторов установить в единицу. Как это сделать? Очень просто: «` hh = <> arr.each do |word| hh[word] = 1 end «` Далее нам каким-то образом нужно проверить: встречается ли слово в хеше. Если встречается, то увеличить счетчик на 1. Если не встречается, то добавить новое слово. «` arr.each do |word| if hh[word].nil? hh[word] = 1 else hh[word] += 1 end end «` Код программы целиком выглядел бы следующим образом: «` str = ‘the quick brown fox jumps over the lazy dog’ arr = str.split(‘ ‘) hh = <> arr.each do |word| if hh[word].nil? hh[word] = 1 else hh[word] += 1 end 150

end puts hh.inspect «` Программа работает, и результат работы выглядит следующим образом: «` 2, «quick»=>1, «brown»=>1, «fox»=>1, «jumps»=>1, «over»=>1, «lazy»=>1, «dog»=>1> «` В самом деле, у нас два слова “the”, а остальных по одному. Но эту программу можно было бы значительно облегчить, если знать, что в хеше можно установить значение поумолчанию: «` str = ‘the quick brown fox jumps over the lazy dog’ arr = str.split(‘ ‘) hh = Hash.new(0) arr.each do |word| hh[word] += 1 end puts hh.inspect «` Девять строк кода вместо тринадцати! Строка `Hash.new(0)` говорит языку руби о том, что если слово не найдено, то будет возвращено автоматическое значение — ноль. Если бы мы объявили хеш без значения поумолчанию, то мы получили бы ошибку “NoMethodError: undefined method `+’ for nil:NilClass”, ведь руби попытался бы сложить “nil” и единицу, а этого делать нельзя: «` $ pry [1] pry(main)> nil + 1 NoMethodError: undefined method `+’ for nil:NilClass «` В этом случае говорят, что метод “+” не реализован в классе nil.

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

Передача опций в методы Допустим, что нам нужно вызвать какой-то метод и передать ему несколько параметров. Например, пользователь выбрал определенное количество футбольных мячей, мячей для тенниса, и мячей для гольфа. Мы хотим написать метод, который считает общий вес. Это может быть сделано обычным способом: «` def total_weight(soccer_ball_count, tennis_ball_count, golf_ball_count) # . end «` В этом случае вызов выглядел бы следующим образом: «` x = total_weight(3, 2, 1) «` Три футбольных мяча, два мяча для тенниса, один для гольфа. Согласитесь, что когда мы смотрим на запись “total_weight(3, 2, 1)” не очень понятно, что именно означают эти параметры. Это мы знаем, что сначала идут футбольные мячи, потом должны идти мячи для тенниса, потом для гольфа. Но чтобы это понять другому программисту, нужно посмотреть на сам метод. Это не очень удобно, поэтому некоторые IDE (Integrated Development Environment, редакторы кода, среды разработки) автоматически подсказывают что именно это за параметр. Например, такая функциональность есть в RubyMine. Однако, в силу динамической природы языка RubyMine не всегда может определить правильное название параметров. Да и многие руби программисты используют текстовые редакторы попроще. Поэтому многие программисты предпочитали передавать в методы хеш с параметрами: «` def total_weight(options) a = options[:soccer_ball_count] b = options[:tennis_ball_count]

c = options[:golf_ball_count] puts a puts b puts c # . end params = < soccer_ball_count: 3, tennis_ball_count: 2, golf_ball_count: 1 >x = total_weight(params) «` Согласитесь, что код «` total_weight(< soccer_ball_count: 3, tennis_ball_count: 2, golf_ball_count: 1 >) «` выглядит более понятным, чем просто “total_weight(3, 2, 1)”. Несмотря на то, что запись выше выглядит длиннее, у нее есть два преимущества. Во-первых, точно видно какие параметры мы передаем, т.к. мы явно указываем названия этих параметров А во-вторых, порядок параметров в хеше не имеет значения. В случае с “total_weight(3, 2, 1)” нам нужно соблюдать порядок и всегда помнить: первый элемент это количество футбольных мячей и т.д. В случае с хешем можно указать обратный порядок и это не будет ошибкой: «` total_weight(< golf_ball_count: 1, tennis_ball_count: 2, soccer_ball_count: 3 >) «` Программа получается более наглядная, и нам не нужно помнить про порядок! Позднее синтаксис был упрощен, и теперь для вызова функции, которая принимает хеш с параметрами не нужно указывать фигурные скобки, метод все равно будет принимать хеш: «` total_weight(golf_ball_count: 1, tennis_ball_count: 2, soccer_ball_count: 3)

«` Теперь метод для подсчета веса можно переписать иначе: «` def total_weight(options) a = options[:soccer_ball_count] b = options[:tennis_ball_count] c = options[:golf_ball_count] puts a puts b puts c # . end x = total_weight(soccer_ball_count: 3, tennis_ball_count: 2, golf_ball_count: 1) «` Но что будет, если мы вызовем этот метод вообще без каких-либо аргументов? По идее, метод должен вернуть ноль. Но мы получаем сообщение об ошибке: «` ArgumentError: wrong number of arguments (given 0, expected 1) «` Руби нам говорит о том, что метод ожидает 1 параметр, а мы ничего не предоставили. С точки зрения бизнес-логики может показаться, что это правильно — “не нужно вызывать неправильно то, что что-то считает. Если хотите посчитать общий вес, то укажите сколько мячей или укажите явно — ноль мячей для футбола, ноль для тенниса, ноль для гольфа”. Это кажется разумным, но давайте представим, что total_weight может вызываться и без параметров. В этом случае, например, метод должен возвращать вес пустой коробки (29 грамм). Что же нам делать? Решение очень простое: сделать так, чтобы параметр options принимал какое-либо значение по-умолчанию. Например, пустой хеш. Если хеш будет пустой, то переменные “a, b, c” будут инициализированы значением nil и метод можно будет вызывать без параметров. Указать значение по-умолчанию можно в определении метода с помощью знака “равно”: «` def total_weight(options=<>) . «`

Важное примечание: несмотря на то, что “равно с пробелами” выглядит нагляднее, в руби-сообществе существует два мнения по этому поводу. Раньше было принято использовать равно без пробелов (но только при определении параметров метода поумолчанию). Сейчас чаще всего встречается “равно с пробелами”. В зависимости от предпочтений, которые существуют в вашей команде, инструмент статического анализа кода Rubocop может выдать предупреждение: «` # не рекомендуется указывать пробелы def total_weight(options = <>) . «` Код нашей программы полностью теперь выглядит так: «` def total_weight(options=<>) a = options[:soccer_ball_count] b = options[:tennis_ball_count] c = options[:golf_ball_count] puts a puts b puts c # . end x = total_weight(soccer_ball_count: 3, tennis_ball_count: 2, golf_ball_count: 1) «` Можно вызвать “total_weight” без параметров и не будет ошибки (попробуйте самостоятельно в pry). Давайте теперь перепишем эту программу, чтобы она на самом деле считала вес посылки вместе с коробкой: «` def total_weight(options=<>) a = options[:soccer_ball_count] b = options[:tennis_ball_count] c = options[:golf_ball_count] (a * 410) + (b * 58) + (c * 45) + 29 end

x = total_weight(soccer_ball_count: 3, tennis_ball_count: 2, golf_ball_count: 1) «` Программа работает и правильно считает. 3 футбольных мяча, 2 теннисных и 1 мяч для гольфа все вместе весят 1420 грамм. Попробуем вызвать метод “total_weight” без параметров: «` . > total_weight NoMethodError: undefined method `*’ for nil:NilClass «` О нет, ошибка! В чем же дело? Конечно, ведь если мы не указываем параметр, то его нет и в хеше. И когда мы пытаемся прочитать переменные “a, b, c”, то все они принимают значения nil. А nil нельзя умножать: «` $ pry > nil * 410 NoMethodError: undefined method `*’ for nil:NilClass «` Тут мы можем прибегнуть к хитрости и логическому оператору “или”. Попробуйте догадаться, что выведет на экран программа: «` if nil || true puts ‘Yay!’ end «` Программа выведет “Yay!”, потому что руби увидит nil, это выражение его не удовлетворит, потом встретит логический оператор “или” и решит вычислить то, что находится после этого логического оператора. А после находится “true”, и результат выражения “nil || true” равняется в итоге “true” (истина), которое передается оператору “if” (если). Получается конструкция “если истина, то вывести на экран Yay!”. Теперь попробуйте догадаться, чему будет равно значение переменной “x”: «` x = nil || 123 «`

Правильный ответ: 123. Эту же хитрость мы можем применить и к переменным “a, b, c” следующим образом: «` a = options[:soccer_ball_count] || 0 «` Другими словами, если значение в хеше “options” не указано (равно nil), то переменной “a” будет присвоено значение “0”. Код программы целиком: «` def total_weight(options=<>) a = options[:soccer_ball_count] || 0 b = options[:tennis_ball_count] || 0 c = options[:golf_ball_count] || 0 (a * 410) + (b * 58) + (c * 45) + 29 end x = total_weight(soccer_ball_count: 3, tennis_ball_count: 2, golf_ball_count: 1) «` Теперь метод “total_weight” работает без параметров и возвращает 29. Мы можем также передать один или несколько параметров: «` > total_weight(tennis_ball_count: 2, golf_ball_count: 1) 190 «` Получился работоспособный метод, который принимает хеш с параметрами, который выглядит понятно, и в который можно передать любое количество параметров. Представим теперь, что в технический отдел пришел директор продаж с новым требованием к нашей программе: “если пользователь не заказывает мячи для гольфа, мы даем ему один в подарок!” Это требование легко реализовать в нашей функции. Код метода получился бы таким: «` def total_weight(options=<>) a = options[:soccer_ball_count] || 0

b = options[:tennis_ball_count] || 0 c = options[:golf_ball_count] || 1 (a * 410) + (b * 58) + (c * 45) + 29 end «` Мы рассмотрели передачу опций в метод с помощью хешей. Этот способ широко используется, особенно когда количество параметров метода больше пяти. Похожие решения также существуют в других языках. Задание: Центр Управления Полетами поручил вам задание написать метод “launch” (от англ. “запуск”), который будет принимать набор опций в виде хеша и отправлять в космос астронавтов “Белку” и/или “Стрелку”. Метод должен принимать следующие параметры: ● ● ●

`angle` — угол запуска ракеты. Если не задан, то значение по-умолчанию равно 90 (градусов) `astronauts` — массив символов (:belka, :strelka), если не задан, то в космос нужно отправлять и Белку, и Стрелку. `delay` — количество секунд через которые запустить ракету, если не задано, то равно пяти

Метод должен вести подсчет оставшихся до запуска секунд (например: “Осталось секунд: 5 4 3 2 1”). После истечения задержки, метод должен выводить сообщение о том, какой астронавт (астронавты) запущены, а также под каким углом была запущена ракета. Метод может принимать любое количество параметров (ноль, один, два, три). Возможные варианты вызова метода: ● ● ● ● ● ●

`launch` `launch(angle: 91)` `launch(delay: 3)` `launch(delay: 3, angle: 91)` `launch(astronauts: [:belka])` и т.д.

Набор ключей (HashSet) В языке руби существует возможность вывести список ключей в каком-либо хеше. Работает этот метод довольно предсказуемо: «` $ pry > hh = <>

=> <> > hh[:red] = ‘ff0000′ => «ff0000» > hh[:green] = ’00ff00’ => «00ff00» > hh[:blue] = ‘0000ff’ => «0000ff» > hh.keys => [:red, :green, :blue] «` Выше мы определили хеш с ключом типа Symbol и значением типа String. К слову, строковые значения это общепринятое трехбайтное (в виде строки) обозначение цветов RGB, где первый байт отвечает за R(ed) — красный, второй за G(reen) — зеленый, третий за (B)lue — синий. Получение списка ключей — не самая часто встречающаяся операция. Однако, иногда возникает необходимость использовать только ключи в структуре данных “хеш”. Это можно сделать через хеш, задавая любые значения (например, true), но есть специальная структура данных, которая содержит только ключи (без значений). Она называется HashSet (в руби просто Set): (англ.) > Set implements a collection of unordered values with no duplicates (по-русски) > Set представляет (реализует) собой коллекцию неупорядоченных неповторяющихся значений (то есть без дубликатов) “Set” в переводе с английского языка это набор, множество. Т.е. это просто набор каких-то данных, объединенным каким-то признаком. Напишем небольшую программу для демонстрации структуры данных HashSet: есть предложение в нижнем регистре, нужно определить: все ли буквы английского языка используются в этом предложении? Известно, что предложение “quick brown fox jumps over the lazy dog” использует все буквы английского языка, поэтому его используют для визуального тестирования шрифтов. А вот в “brown fox jumps over the lazy dog” (без слова quick) нет буквы “q”. Нам нужно написать метод, который будет возвращать true, если в предложении содержатся все буквы и false если каких-то букв не хватает. Как мы могли бы написать эту программу? Подход простой: делаем итерацию по каждому символу, если это не пробел, то добавляем в структуру данных “хеш”. Т.к. в хеше не может быть дублированных значений,

то максимальное количество ключей в хеше — 26 (количество букв английского алфавита). Если количество букв 26, то все буквы были использованы. Что не так с обычным хешем в этой задаче? То, что добавляя в хеш мы должны указывать какое-то значение: «` hh[letter] = true «` Мы можем указать true, false, любую строку — это совершенно не важно, этот объект не несет никакой смысловой нагрузки. Поэтому хорошо бы иметь хеш без значений, чтобы можно было сэкономить память и, самое главное, показать намерение — “значение нам не важно”. В этом случае идеально подходит структура данных HashSet. Код программы может выглядеть следующим образом: «` # импортируем пространство имен, т.к. set # не определен в пространстве имен по-умолчанию require ‘set’ # наш метод, который принимает строку def f(str) # инициализируем set set = Set.new # итерация по каждому символу в строке str.each_char do |c| # только если символ между a и z (игнорируем пробелы и все остальное) if c >= ‘a’ && c «quick brown fox jumps over the lazy dog».split(») => [«q», «u», «i», «c», «k», » «, «b», «r», «o», «w», «n», » «, «f», «o», «x», » «, «j», «u», «m», «p», «s», » «, «o», «v», «e», «r», » «, «t», «h», «e», » «, «l», «a», «z», «y», » «, «d», «o», «g»] «` В этом случае произошло бы выделение дополнительной памяти. Представьте, что строка имеет размер в несколько гигабайт. Зачем формировать массив и расходовать память, когда можно просто воспользоваться итерацией по символам средствами класса String? Другая возможная ошибка в этом упражнении — итерация строки до конца. Если строка довольно большая, а распределение символов равномерно, то вероятность того, что все символы встретятся где-то вначале очень высока. Поэтому проверка на размер HashSet довольно полезна и в теории должна сэкономить вычислительные ресурсы. Задание: в программе выше допущена ошибка, которая приведет к большим расходам вычислительных ресурсов на больших строках. Сможете ли вы ее увидеть? Задание: после того, как вы прочитали эту главу, попробуйте потренироваться и написать эту программу самостоятельно, не подсматривая в книгу.

Итерация по хешу Итерация по хешу используется не часто: основное назначение хеша все-таки в добавлении и извлечении конкретного элемента. Но иногда она встречается. Мы уже знаем, что итерация по массиву имеет следующий вид: «` arr.each do |element| # do something with element end «` Итерация по всем парам ключ-значение имеет похожий вид: «` hh = < soccer_ball: 410,

tennis_ball: 58, golf_ball: 45 > hh.each do |k, v| puts «Вес # равняется #» end «` Результат работы программы: «` Вес soccer_ball равняется 410 Вес tennis_ball равняется 58 Вес golf_ball равняется 45 «` Переменные “k” и “v” означают “key” (ключ) и “value” (значение) соответственно. Если значение не нужно, то переменную “v” можно опустить, написать с подчеркиванием вначале или вообще заменить на подчеркивание. Это не синтаксис языка, а общепринятые соглашения о наименовании (naming conventions), с помощью которых другим программистам будет известно о ваших намерениях: «` hh = < soccer_ball: 410, tennis_ball: 58, golf_ball: 45 >hh.each do |k, _| puts «На складе есть #» end «` Код выше можно записать немного иначе, если воспользоваться методом “each_key” класса Hash. Задание: имеются следующие данные: «` data = < soccer_ball: < name: 'Футбольный мяч', weight: 410, qty: 5 >, tennis_ball: < name: 'Мяч для тенниса', weight: 58, qty: 10 >, golf_ball: < name: 'Мяч для гольфа', weight: 45, qty: 15 >162

> «` Написать программу, которая будет выводить на экран: «` На складе есть: Футбольный мяч, вес 410 грамм, количество: 5 шт. Мяч для тенниса, вес 58 грамм, количество: 10 шт. Мяч для гольфа, вес 45 грамм, количество: 15 шт. «`

Метод dig Допустим, у нас есть структура данных с несколькими уровнями вложенности: «` users < < < ]

= [ first: ‘John’, last: ‘Smith’, address: < city: 'San Francisco', country: 'US' >>, first: ‘Pat’, last: ‘Roberts’, address: < country: 'US' >>, first: ‘Sam’, last: ‘Schwartzman’ >

«` Структура имеет определенную схему. Т.е. для каждой записи (пользователя) формат данных одинаковый. Но иногда данных по какому-то параметру нет. Скажем, во второй записи отсутствует город. В третьей записи вообще нет адреса. Мы хотим вывести на экран все города из этого массива. Первое, что приходит на ум — итерация по массиву и “обычное” обращение к хешу: «` users.each do |user| puts user[:address][:city] end «` Попробуем запустить эту программу: «` San Francisco -:8:in `block in ‘: undefined method `[]’ for nil:NilClass (NoMethodError).

«` Программа выдает ошибку. В чем же дело? Давайте попробуем обратиться к каждому пользователю отдельно: «` $ pry > users[0][:address][:city] => «San Francisco» > users[1][:address][:city] => nil > users[2][:address][:city] NoMethodError: undefined method `[]’ for nil:NilClass «` Для первого пользователя конструкция сработала. Для второго пользователя тоже результат равен nil. Для третьего пользователя `users[2][:address]` уже равно nil. А когда мы делаем `nil[:city]`, то мы получаем ошибку, потому что обращение к каким-либо элементам в классе nil не реализовано. Так как же нам написать программу? Воспользуемся конструкцией if: «` users.each do |user| if user[:address] puts user[:address][:city] end end «` Ура! Программа работает и ошибку не выдает. Мы написали хороший код. Но давайте немного усложним структуру данных, добавив в хеш “address” еще один объект: «` street: < line1: '. ', line2: '. ' >«` Другими словами, будет street-адрес, который всегда состоит из двух строк. Структура данных полностью будет выглядеть следующим образом (по сравнению с вариантом выше эта структура также визуально оптимизирована): «` users = [ < first: 'John', 164

last: ‘Smith’, address: < city: 'San Francisco', country: 'US', street: < line1: '555 Market Street', line2: 'apt 123' >> >, < first: 'Pat', last: 'Roberts', address: < country: 'US' >>, < first: 'Sam', last: 'Schwartzman' >] «` Теперь наша задача — вывести line1 из street-адреса. Как мы напишем эту программу? Первое, что приходит на ум: «` users.each do |user| if user[:address] puts user[:address][:street][:line1] end end «` Но код выше споткнется уже не на третьей, а на второй записи. `user[:address][:street]` будет nil. Запишем этот код иначе: «` users.each do |user| if user[:address] && user[:address][:street] puts user[:address][:street][:line1] end end «` Работает, но пришлось добавить второе условие. Другими словами, чем сложнее конструкция и больше уровней вложенности, тем больше проверок на nil необходимо сделать. Это не очень удобно, и в версии 2.3.0 языка руби (проверить свою версию можно с помощью `ruby -v`) был представлен новый метод dig (англ. “копать”): «` users.each do |user| puts user.dig(:address, :street, :line1) 165

end «` Этот метод принимает любое количество параметров и обращается к сложной структуре данных без ошибок. Если какой-то из ключей в цепочке не найден, то возвращается значение nil. Примечание: когда вы будете работать с Rails, вы столкнетесь с похожим методом “try” и т.н. safe navigation operator (тоже был представлен впервые в версии 2.3.0): `&.`, в других языках программирования обозначается как `?.` (иногда ошибочно говорят “Elvis operator” — это понятие относится к немного другой конструкции). Safe navigation operator похож по своей сути на метод dig. Мы рекомендуем взглянуть на страницу в Википедии для того, чтобы иметь представление зачем это нужно: https://en.wikipedia.org/wiki/Safe_navigation_operator

Проверка наличия ключа В некоторых случаях необходимо просто проверить наличие ключа в хеше. Это можно сделать без извлечения значения с помощью метода “has_key?”: «` $ pry > hh = < login: 'root', password: '123456' >. > hh.has_key?(:password) true > «` “has_key?” проверяет только наличие ключа, но не выполняет никаких действий со значением. Задание: объясните чем отличается JSON вида «` < "books": [ < "id": 1, "name": "Tom Sawyer and Huckleberry Finn",

>, < "id": 2, "name": "Vingt mille lieues sous les mers", >] > «` От «` < "books": < "1": < "name": "Tom Sawyer and Huckleberry Finn" >, «2»: < "name": "Vingt mille lieues sous les mers" >> > «` В какой из структур данных выше поиск книги константный O(1), а в какой линейный O(N)? Каким образом предпочтительнее объявить структуру? Какое количество хешей и массивов используется в каждом из примеров? Как добавить книгу в каждом из случаев?

Введение в ООП Существует мнение, что Объектно-Ориентированное Программирование (ООП) является чем-то сложным, загадочным и недостижимым. Но на самом деле это довольно просто, если мы говорим о тех вещах, с которыми вам придется сталкиваться ежедневно. Правильное ООП может сильно облегчить жизнь программиста и проекта, но требует намного больше brain power, чем обычное ООП, которое повсеместно используется. В этой книге мы рассмотрим обычное ООП для начинающих. Если вам интересна тема правильного ООП, мы рекомендуем прочитать книгу “Elegant Objects” Егора Бугаенко.

Классы и объекты Само название “Объектно-ориентированное программирование” подразумевает, что гдето должен быть объект. Что же такое объект? Из обычной жизни мы знаем, что все вокруг объекты. Например, книга на столе. Человек, идущий по улице. Автомобиль BMW E34, который едет по дороге. Но если присмотреться, то автомобиль BMW E34 — это определенный класс объектов. Среди всего множества автомобилей, автомобили этой модели точно такие же, абсолютно одинаковые. Но все-таки это разные экземпляры. Самый простейший пример класса это чертеж, который все чертили в школе:

Рис. Пример чертежа. Источник: https://ru.wikipedia.org/wiki/Чертёж На чертеже изображается какая-либо деталь, ее размеры, различные параметры: ширина, высота и т.д. Класс это примерно то же самое, что чертеж, рисунок или шаблон какой-то детали. Сам по себе этот шаблон в принципе бесполезен. Зачем нужны шаблоны? Шаблоны нужны для того, чтобы по ним что-то можно было сделать. Т.е. мы посмотрели на чертеж и уже на основе чертежа мы можем создать какую-то деталь. Объект — как раз и есть эта деталь, которая создается на основе шаблона, или класса. У объекта есть также второе имя — “экземпляр” (instance) или “экземпляр класса” (class instance).

Классы и объекты в программировании это почти то же самое, что классы и объекты в жизни. Шаблон один, объектов много. По одному чертежу можно создать сколько угодно деталей: TODO: сделать 3D модель детали вверху и нарисовать несколько деталей Также и класс — один, объектов много. Мы можем объявить один класс и создать на его основе множество объектов: «` class Car end car1 = Car.new car2 = Car.new car3 = Car.new «` Все эти объекты будут храниться в памяти компьютера. Вот в общем-то и все объяснение, теперь вы знаете что такое классы и что такое объекты.

Состояние Состояние (state) — важное понятие в объектно-ориентированном языке. Руби это объектно-ориентированный язык. Другие примеры объектно-ориентированных языков: Java, C#, JavaScript. Существуют также другие, частично объектно-ориентированные языки (Golang — https://golang.org/doc/faq#Is_Go_an_object-oriented_language), т.н. функциональные языки программирования (Erlang/Elixir, Haskell) и пр. Основное отличие объектно-ориентированного языка от не-объектно-ориентированного в том, что в объектно-ориентированном языке есть такое понятие как состояние объекта. Что же такое состояние? Обратимся к нашему примеру с автомобилем BMW модели E34. Итак, где-то на заводе в Германии существует чертеж этого автомобиля, именно этой модели. По этому чертежу фабрикой было выпущено множество экземпляров автомобиля. Но автомобиль собран из отдельных деталей: ● ● ● ●

Двигатель Лобовое стекло Кузов Двери

Все эти объекты бездушные, не живые, и не представляют никакой ценности. Кто в своем уме купит колесо от автомобиля просто ради того, чтобы принести его домой? В этом нет никакого смысла. Но будучи собранным, автомобиль превращается в живой организм, в объект, у него появляется состояние. Несмотря на то, что все выпущенные машины на заводе за много лет были одинаковыми, у всех у них на данный момент сейчас совершенно разное состояние. Состояние отличает один конкретный автомобиль от множества точно таких же. В чем же выражается это состояние? Во-первых, пробег. Автомобиль довольно сложный механизм, и вряд ли у двух автомобилей существует одинаковый пробег с точностью до метра. Во-вторых, это может быть любой другой параметр: например, бензин в баке. Количество бензина в баке отражает состояние конкретного объекта “автомобиль BMW марки E34”. Мы знаем, что количество бензина меняется: мы можем приехать на заправку и изменить состояние этого объекта, долив бензина. В-третьих, включен автомобиль или выключен — это тоже состояние. Другими словами, в объектно-ориентированном языке объект — это живой механизм, у которого есть состояние. Это состояние каким-то образом можно менять. Это можно делать извне, а можно делать и изнутри. Если мы подходим к автомобилю и открываем дверь, то мы меняем объект извне. А если заводим его, находясь в автомобиле — то меняем состояние изнутри. Автомобиль сам может менять свое состояние. Например, когда двигатель нагревается до определенной температуры, включается принудительное охлаждение. Попробуем написать программу, которая продемонстрирует вышесказанное: «` class Car def initialize @state = :closed end def open @state = :open end def how_are_you puts «My state is #» end end

car1 = Car.new car1.how_are_you car2 = Car.new car2.open car2.how_are_you «` Результат работы программы: «` My state is closed My state is open «` Мы создали класс “Car” — начертили “чертеж” автомобиля с помощью языка руби. Далее мы создали объект (экземпляр) с помощью конструкции “Car.new” и присвоили переменной “car1” ссылку на этот объект. Важно отметить, что переменная car1 не “содержит” сам объект, это просто ссылка на область памяти где на самом деле этот объект хранится. Можно вспомнить аналогию с подъездом. Звонок — это ссылка на квартиру. Также и тут: переменная — это ссылка на объект. Мы можем иметь любое количество переменных, указывающих на один и тот же объект. Захотим и присвоим переменной car777 значение car1: «` car777 = car1 «` Далее в нашей программе мы спрашиваем у объекта — “how are you”, на что объект сообщает о своем состоянии. Первый объект сообщил, что “My state is closed” (мое состояние — закрыто), но почему это произошло? Дело в том, что мы объявили метод initialize: «` def initialize @state = :closed end «` Этот метод всегда вызывается при создании нового объекта. Другими словами, когда вы пишите `Car.new`, будет вызван метод `initialize`. Непонятно почему в языке руби выбрали такое длинное слово в котором легко сделать ошибку. Согласитесь, что гораздо проще выглядел бы такой код: «` 171

class Car def new # . end end Car.new «` Но к сожалению приходится использовать длинное слово “initialize”. Кстати, этот метод называется “конструктор”, и в языке JavaScript версии ES6 и выше он именуется именно так: «` class Car < constructor() < console.log('hello from constructor!'); >> let car1 = new Car(); «` Если запустить программу выше (например, `$ node` и вставить текст), то мы увидим сообщение “hello from constructor!”. Т.е. метод был вызван при создании объекта. Тот же самый код в руби выглядит следующим образом: «` class Car def initialize puts ‘hello from constructor!’ end end car1 = Car.new «` Это один из не самых очевидных моментов в языке Ruby — пишем “new”, а вызывается “initialize”. Для чего существует конструктор? Для того, чтобы определить начальное состояние объекта. Скажем, при выпуске автомобиля мы хотим чтобы: двери автомобиля были закрыты, окна были закрыты, капот и багажник были закрыты, все выключатели были переведены в положение “Выключено” и т.д.

Вы, наверное, обратили внимание, что мы использовали знак “@” (читается как “at”) перед переменной “state” в конструкторе. Этот знак говорит о том, что это будет “instance variable” — переменная экземпляра. Мы как-то говорили об этом в предыдущих главах. Но вообще, существует три типа переменных: Локальные переменные. Это переменные, объявленные в каком-то методе. Эти переменные недоступны из других методов. Если вы напишите вот такой код, то программа выдаст ошибку, потому что переменная “aaa” не определена в методе “m2”: «` class Foo def m1 aaa = 123 puts aaa end def m2 puts aaa end end foo = Foo.new foo.m1 # сработает, будет выведено 123 foo.m2 # будет ошибка, переменная не определена «` Instance variables — переменные экземпляра класса. К ним можно обращаться только через “@”: «` class Foo def initialize @aaa = 123 end def m1 puts @aaa end def m2 puts @aaa end end foo = Foo.new

foo.m1 foo.m2 «` Эти переменные определяют состояние объекта. Желательно объявлять instance variables в конструкторе, чтобы показать намерение: вот эта переменная будет отвечать за состояние, мы будем ее использовать. Однако, не будет синтаксической ошибки если вы объявите instance variable в каком-то методе. Просто этот метод должен быть вызван прежде, чем какой-либо другой метод обратится к этой переменной (а конструктор вызывается всегда при создании объекта). Объявляем переменную в методе “m1” и используем ее в методе “m2”: «` class Foo def m1 @aaa = 123 puts @aaa end def m2 puts @aaa end end foo = Foo.new foo.m1 foo.m2 «` Результат работы программы: «` 123 123 «` Если в программе выше поменять две последние строки местами, то фактического сообщения об ошибке не будет, программа сработает, но на экран будет выведена только одна строка: «` 123 «`

Руби попытается вызвать метод “m2”, т.к. переменная экземпляра класса не объявлена, то ее значение будет равно по-умолчанию nil, а `puts nil` не выводит на экран строку. В этом заключается первая любопытная особенность instance variable — если эта переменная не объявлена, то ее значение по-умолчанию равно nil. Если локальная переменная не объявлена, то будет ошибка исполнения программы. Class variables — переменные класса, переменные шаблона, иногда называются статическими переменными. Совершенно бесполезный тип переменных с префиксом “@@”. Смысл в том, что какое-то значение можно будет менять между всеми экземплярами класса. На практике это встречается довольно редко. Можно выделить еще два типа переменных: ●

Глобальные переменные (с префиксом `$`) — обратиться к этим переменным можно из любого места программы. Однако, из-за этой особенности возникает большой соблазн их использовать, что только приводит к запутанности программы. Специальные переменные. Например, переменная `ARGV` содержит аргументы, переданные в программу. А переменная `ENV` содержит параметры окружения (environment) — т.е. параметры, которые заданы в вашей оболочке (shell).

Другими словами, для создания программ в общем случае необходимо усвоить разницу между локальными переменными и instance variables (переменными экземпляра класса, которые определяют состояние объекта). А теперь вопрос. Что делает следующая программа? «` puts aaa «` Кто-то скажет “выводит переменную `aaa` на экран”. И будет прав, ведь можно записать программу полностью следующим образом: «` aaa = 123 puts aaa «` Но что если мы запишем программу иначе: «` def aaa rand(1..9) end

puts aaa «` Программа будет выводить случайное значение (в пределах от 1 до 9). Другими словами, мы не можем точно сказать что именно означает `puts aaa`, мы только знаем, что `aaa` это или переменная, или метод, или что-то еще. Про “что-то еще” мы поговорим подробнее в следующих главах, когда будем говорить о специальном методе “method_missing” (“метод отсутствует”). А пока наш класс выглядит следующим образом:

Из картинки видно, что полезных методов только два (два красных круга внизу и один конструктор). Т.е. снаружи мы можем только открыть дверь и попросить рассказать о своем состоянии (или создать объект с помощью конструктора). Состояние хранится в instance variable, и по-умолчанию мы никак не можем обратиться непосредственно к этой переменной снаружи. Состояние может быть каким угодно, мы можем завести 10 переменных, и внутри объекта реализовать любую сложную логику, но интерфейс взаимодействия (API, или сигнатуры методов) остается прежним. Если продолжить аналогию с реальным автомобилем, то внутри класса мы, может быть, захотим играть музыку. Но до тех пор, пока мы не реализовали это в API нашего объекта, о внутреннем состоянии никто не узнает — играет музыка внутри автомобиля или нет. Это называется инкапсуляция. Но, допустим, вы ехали по улице и решили подвезти прекрасную девушку. Вы остановились, но девушка такая скромная, что не будет сама открывать дверь. Она бы и рада зайти к вам в машину, но хочет видеть, что дверь открыта. Она хочет прочитать состояние нашего объекта и не хочет говорить “how are you” первому встречному. Другими словами, мы хотим всем разрешить читать состояние объекта. Что делать в этом случае? Самый простой способ — добавить метод с любым названием, который будет возвращать состояние. Мы могли бы добавить метод “aaa”, но давайте назовем его “state”. Код класса полностью:

«` class Car def initialize @state = :closed end # новый метод def state @state end def open @state = :open end def how_are_you puts «My state is #» end end «` Получился следующий класс:

Т.е. само состояние “@state” недоступно, но есть вариант его прочитать с помощью метода “state”. Название состояния и методы похожи и состоят из одного слова, но это для руби это две разные вещи. Мы могли бы назвать метод “aaa”, в этом бы не было ошибки. Отлично, теперь девушка видит, что машина открыта, она может прочитать состояние с помощью метода “state”. Но вот незадача — снаружи увидеть состояние можно (метод “state”), снаружи можно открыть дверь (“open”), но изменить состояние можно только изнутри. Что в общем-то и нормально — может быть не потребуется интерфейса для закрытия двери снаружи. Но

что если потребуется? Задача программиста — подумать о бизнес-логике, о том, как будет использоваться тот или иной компонент. Если мы точно знаем, что понравимся девушке, то интерфейс закрытия двери снаружи можно не реализовывать. А что если мы захотим закрывать дверь снаружи? Согласитесь, для автомобиля это полезная функциональность. Мы бы могли написать метод close: «` def close @state = :closed end «` И проблема была бы решена. Вот один из конечных вариантов класса:

Но что если мы, например, захотим завести автомобиль? Во-первых, наше состояние могло бы быть совокупностью описаний: open, closed, engine_on, engine_off (можно было бы представить его в виде массива). А во-вторых, пришлось бы добавлять еще два метода: on, off. В этом случае к четырем публичным методам прибавилось бы еще два. Получается довольно сложный класс. Иногда полезно просто оставить возможность управления состоянием извне: делай что хочешь, открывай двери, заводи двигатель, включай музыку. Как вы понимаете, это не всегда приводит к хорошим последствиям, но вполне практикуется. Для того, чтобы разрешить полное управление переменной экземпляра класса (в нашем случае “@state”), можно написать следующий код: «` attr_reader :state attr_writer :state

«` Этот код просто создает в классе два метода, для чтения переменной и для ее записи: «` def state @state end def state=(value) @state = value end «` Первый метод нам уже знаком — мы его создали для возврата состояния. Второй метод по сути уже содержит в себе знак равно и используется для присваивания. Но attr_reader и attr_writer можно заменить на всего-лишь одну строку: «` attr_accessor :state «` (Не путайте “attr_accessor” и “attr_accessible”, которое используется во фреймворке Rails, это разные понятия, но слова выглядят одинаково). Весь наш класс можно свести к такому простому коду: «` class Car attr_accessor :state def initialize @state = :closed end def how_are_you puts «My state is #» end end «` Пример использования: «` car1 = Car.new 179

car1.state = :open car2 = Car.new car2.state = :broken car1.how_are_you car2.how_are_you «` Результат работы программы: «` My state is open My state is broken «` Визуальное представление класса:

Задание: напишите класс Monkey (“обезьянка”). В классе должно быть 1) реализовано два метода: run, stop; 2) каждый из методов должен менять состояние объекта; 3) you must expose the state of an object так, чтобы можно было узнать о состоянии класса снаружи, но нельзя было его модифицировать (к сожалению, точно перевести на русский язык выражение “expose the state” не получилось. Посмотрите перевод слова “expose” в словаре). Создайте экземпляр класса Monkey, вызовите методы объекта и проверьте работоспособность программы. Задание: сделайте так, чтобы при инициализации класса Monkey экземпляру присваивалось случайное состояние. Создайте массив из десяти обезьянок. Выведите состояние всех элементов массива на экран. Читайте также: https://vk.com/@physics_math-skryvaite-sekrety-inkapsuliruite-detali-realizacii

Состояние, пример программы

Вроде бы более или менее что такое состояние понятно. Но как оно используется на практике? В чем его преимущество? Зачем держать состояние внутри объекта и зачем нужна инкапсуляция? Как уже было замечено выше, объект это живой организм. На практике оказалось полезным не заводить несколько переменных с разными именами, а инкапсулировать их под одной крышей. Представим, что у нас есть робот, который движется по земле, а мы на него смотрим сверху вниз. Робот начинает движение в какой-то точке и может ходить вверх, вниз, влево и вправо произвольное количество шагов. Кажется, что мы могли бы обойтись и без класса. Завели бы две переменных: x, y. Если робот ходит вправо, к переменной “x” прибавляется единица. Если вверх, то к переменной “y” прибавляется единица. Не нужны никакие объекты и классы. Все это так, но сложность возникает когда нужно создать двух роботов. Что получается? Нужно завести 4 переменные, по 2 на каждого робота. Первую пару мы назовем “x1” и “y1”, вторую “x2” и “y2”. Уже неудобно, но можно и обойтись. Но что если роботов будет больше? “Можно обойтись массивом”, — скажет читатель и будет прав. Можно создать массив переменных. Это просто будет какая-то структура данных, и какието методы будут знать как с ней работать. Но постойте, работать со структурой данных сложнее, чем просто с переменными! Намного проще написать `x = x + 1`, чем например `x[5] = x[5] + 1`. Другими словами, объекты и классы облегчают создание программы. Давайте создадим описанный класс робота: «` class Robot attr_accessor :x, :y def initialize @x = 0 @y = 0 end def right self.x += 1 end def left self.x -= 1 end def up self.y += 1 181

end def down self.y -= 1 end end robot1 = Robot.new robot1.up robot1.up robot1.up robot1.right puts «x = #, y = #» «` Во-первых обратите внимание на альтернативный синтаксис обращения к переменной экземпляра (instance variable) — через `self.` вместо `@`. Если не указать `self.` или `@`, то руби подумает, что мы хотим объявить локальную переменную в методе (даже если похожая переменная или accessor-метод уже существует). А во-вторых, попробуйте догадаться что выведет на экран программа? Правильный ответ: «` x = 1, y = 3 «` Робот сделал 4 шага и его координаты равны 1 по “x” и 3 по “y”. Для того, чтобы создать 10 таких роботов, мы просто создаем массив: «` arr = Array.new(10) < Robot.new >«` А теперь применим трюк и для каждого робота из массива вызовем случайный метод: «` arr.each do |robot| m = [:right, :left, :up, :down].sample robot.send(m) end «`

Трюк заключается в двух строках внутри блока. Первая строка выбирает случайный символ из массива и присваивает его переменной m. Вторая строка “отправляет сообщение” объекту — это просто такой способ вызвать метод (в руби могли бы назвать этот метод более понятным словом: “call” вместо “send”). Другими словами, выше мы не только создали объекты определенного рода, но и смогли относительно легко произвести взаимодействие с целой группой объектов. Согласитесь, это намного проще, чем взаимодействовать с объектами по-одиночке. Ради наглядного эксперимента “вообразим” на экране компьютера плоскость размером 60 на 25 и поставим каждого робота в середину. Каждую секунду будем проходить по массиву роботов, менять их положение случайным образом и перерисовывать нашу плоскость, отображая роботов звездочкой. Посмотрим, как роботы будут расползаться по экрану в случайном порядке. Ниже приведен код такой программы с комментариями. «` # Класс робота class Robot # Акцессоры — чтобы можно было узнать координаты снаружи attr_accessor :x, :y # Конструктор, принимает хеш. Если не задан — будет пустой хеш. # В хеше мы ожидаем два параметра — начальные координаты робота, # если не заданы, будут по-умолчанию равны нулю. def initialize(options=<>) @x = options[:x] || 0 @y = options[:y] || 0 end def right self.x += 1 end def left self.x -= 1 end def up self.y += 1 end def down self.y -= 1

end end # Класс «Командир», который будет командовать и двигать роботов class Commander # Дать команду на движение робота. Метод принимает объект # и посылает (send) ему случайную команду. def move(who) m = [:right, :left, :up, :down].sample who.send(m) end end # Создать объект командира, # командир в этом варианте программы будет один commander = Commander.new # Массив из 10 роботов arr = Array.new(10) < Robot.new ># В бесконечном цикле (для остановки программы нажмите ^C) loop do # Хитрый способ очистить экран puts «\e[H\e[2J» # Рисуем воображаемую сетку. Сетка начинается от -30 до 30 по X, # и от 12 до -12 по Y (12).downto(-12) do |y| (-30).upto(30) do |x| # Проверяем, есть ли у нас в массиве робот с координатами x и y found = arr.any? < |robot| robot.x == x && robot.y == y ># Если найден, рисуем звездочку, иначе точку if found print ‘*’ else print ‘.’ end end # Просто переводим строку: puts end # Каждого робота двигаем в случайном направлении

arr.each do |robot| commander.move(robot) end # Задержка в полсекунды sleep 0.5 end «` Результат работы программы после нескольких итераций: «` . . . . *. . . *. *. . . *. . *. . *.*. . *. . . . . *. *. . . . . «` Демо: https://asciinema.org/a/jMB47AhjBnxgMofSgIVzHObIH Задание: пусть метод initialize принимает опцию — номер робота. Сделайте так, чтобы номер робота был еще одним параметром, который будет определять его состояние (также как и координаты). Измените методы up и down — если номер робота четный, эти методы не должны производить операции над координатами. Измените методы left и right — если номер робота нечетный, эти методы также не должны производить никаких операций над координатами. Попробуйте догадаться, что будет на экране при запуске программы.

Полиморфизм и duck typing В объектно-ориентированном программировании много замысловатых понятий и определений. Однако, не все зарабатывающие программисты могут точно сказать что же такое полиморфизм и что такое duck typing. Происходит это потому, что некоторые принципы с легкостью усваиваются на практике, и часто откровение приходит потом: “ах вот что такое полиморфизм!”. Давайте заглянем в словарь, чтобы разобраться с этимологией самого загадочного слова — “полиморфизм”. Что это означает? Сайт wiktionary подсказывает: “возможность существования чего-либо в различных формах”, биологическое: “наличие в пределах одного вида резко отличных по облику особей, не имеющих переходных форм”. Другими словами, что-то похожее, но “резко отличное”. Ничего себе! Если рассматривать полиморфизм в программировании, то его можно проиллюстрировать известной шуткой. Брутальный байкер в кожаной куртке, весь в цепях с огромной злой собакой вызывает лифт, открываются двери — в лифте дедушка и бабушка божий одуванчик. Байкер заходит в лифт и командует громким голосом: “сидеть!”. Садятся трое: собака, бабушка и дедушка. См. также отрывок видео из “Полицейской академии”: https://www.youtube.com/watch? v=GLc9n3MV9ZE Что же произошло? Программист бы сказал, что у всех объектов одинаковый интерфейс. Объекты разные, но все объекты восприняли команду, которую отправил байкер: `obj.send(:sit)` и не выдали ошибки. Для того, чтобы сделать подобное в статически-типизированных языках, необходимо на самом деле объявить интерфейс. Пример программы на C#: «` interface IListener < void Sit(); >class Dog : IListener < public void Sit() < // . >> class Human : IListener < public void Sit() < // . 186

> > «` Мы объявили интерфейс “слушатель”. И собака с человеком реализуют этот интерфейс каким-то образом. Другими словами, мы можем приказать собаке сидеть: `dog.Sit()` и приказать сидеть человеку: `human.Sit()`. Только в случае наличия интерфейса программа на C# будет работать. Точнее, байкер сможет обратиться к произвольному объекту зная только его интерфейс и не зная точно к кому именно он обращается (который называется “слушатель”, “listener”). Но в языке руби интерфейсов нет. Это язык с динамической типизацией, и вместо интерфейсов в руби есть duck typing (что переводится как “утиная типизация” — но так редко кто говорит, говорят в основном по-английски). Duck typing сводится к следующему простому принципу: If it walks like a duck, and it quacks like a duck, then it has to be a duck. (Перевод: если что-то ходит как утка и квакает как утка, то это должно быть утка) Но какой же в этом смысл? А смысл в следующем. Если есть какие-либо классы, у которых есть одинаковые методы, то с точки зрения потребителя это одинаковые классы. Другими словами, с точки зрения байкера в шутке выше человек и собака это одно и тоже, потому что объекты реализуют одинаковый интерфейс с одним методом sit. Сама утка может быть реализована следующим образом: «` class Duck def walk end def quack end end «` Если мы реализуем два этих метода в собаке, то с точки зрения командира уток это будет утка. Командир будет приказывать собаке квакать и она будет квакать. Так работают динамически типизированные языки (руби, JavaScript, python и т.д.). Пример программы: «` # Утка class Duck def walk end

def quack end end # Собака class Dog def walk end def quack end end # Утиный командир, который дает команды class DuckCommander def command(who) who.walk who.quack end end # Создадим утку и собаку duck = Duck.new dog = Dog.new # Покажем, что утиный командир может командовать собакой # и уткой, и при этом не возникнет никакой ошибки dc = DuckCommander.new dc.command(duck) dc.command(dog) «` — Но зачем это все? — спросит читатель — Это все сложно, какое этому может быть применение в реальной жизни? На самом деле это облегчает программы. Попробуем добавить в нашу программу с десятью роботами еще один класс — собаку. И представим, что собаке надо пройти из левого верхнего угла в нижний правый и не столкнуться с роботами. Если робот поймал собаку — игра окончена. С чего начать? Во-первых, собака должна быть как-то иначе отображена на экране. Робот это звездочка. Пусть у собаки будет символ “@”. Вспомним “интерфейс” робота (а точнее duck typing), какие в нем реализованы методы? Up, down, left, right, x, y. Это подходит и для собаки. Чтобы различать робота и собаку, добавим еще один метод, `label`:

«` class Robot # . def label ‘*’ end end class Dog # . def label ‘@’ end end «` В итоге у нас получилось два “совершенно одинаковых” класса и в то же время разных. Помните что такое полиморфизм? “Возможность существования чего-либо в различных формах”. Одинаковы классы тем, что они реализуют единый интерфейс, они одинаковы с точки зрения потребителя этих классов. Разные они в том плане, что называются они поразному и содержат разную реализацию. Робот может ходить во все стороны и выглядит как звездочка. Собака может ходить только слева направо и сверху вниз (см.код ниже), и выглядит как ‘@’ов. Давайте немного изменим программу, которую мы уже писали выше и посмотрим что такое полиморфизм на практике. «` # Класс робота class Robot # Акцессоры — чтобы можно было узнать координаты снаружи attr_accessor :x, :y # Конструктор, принимает хеш. Если не задан — будет пустой хеш. # В хеше мы ожидаем два параметра — начальные координаты робота, # если не заданы, будут по-умолчанию равны нулю. def initialize(options=<>) @x = options[:x] || 0 @y = options[:y] || 0 end def right self.x += 1 end

def left self.x -= 1 end def up self.y += 1 end def down self.y -= 1 end # Новый метод — как отображать робота на экране def label ‘*’ end end # Класс собаки, тот же самый интерфейс, но некоторые методы пустые. class Dog # Акцессоры — чтобы можно было узнать координаты снаружи attr_accessor :x, :y # Конструктор, принимает хеш. Если не задан — будет пустой хеш. # В хеше мы ожидаем два параметра — начальные координаты собаки, # если не заданы, будут по-умолчанию равны нулю. def initialize(options=<>) @x = options[:x] || 0 @y = options[:y] || 0 end def right self.x += 1 end # Пустой метод, но он существует. Когда вызывается, # ничего не делает. def left end # Тоже пустой метод. def up end def down self.y -= 1 end

# Как отображаем собаку. def label ‘@’ end end

# Класс «Командир», который будет командовать, и двигать роботов # и собаку. ЭТОТ КЛАСС ТОЧНО ТАКОЙ ЖЕ, КАК В ПРЕДЫДУЩЕМ ПРИМЕРЕ. class Commander # Дать команду на движение объекта. Метод принимает объект # и посылает (send) ему случайную команду. def move(who) m = [:right, :left, :up, :down].sample # Вот он, полиморфизм! Посылаем команду, но не знаем кому! who.send(m) end end # Создать объект командира, # командир в этом варианте программы будет один. commander = Commander.new # Массив из 10 роботов и. arr = Array.new(10) < Robot.new ># . и одной собаки. Т.к. собака реализует точно такой же интерфейс, # все объекты в массиве «как будто» одного типа. arr.push(Dog.new(x: -12, y: 12)) # В бесконечном цикле (для остановки программы нажмите ^C) loop do # Хитрый способ очистить экран puts «\e[H\e[2J» # Рисуем воображаемую сетку. Сетка начинается от -12 до 12 по X, # и от 12 до -12 по Y (12).downto(-12) do |y| (-12).upto(12) do |x| # Проверяем, есть ли у нас в массиве кто-то с координатами x и y. # Заменили «any?» на «find» и записали результат в переменную somebody = arr.find < |somebody| somebody.x == x && somebody.y == y ># Если кто-то найден, рисуем label. Иначе точку. if somebody # ВОТ ОН, ПОЛИМОРФИЗМ! # Рисуем что-то, «*» или «@», но что это — мы не знаем! print somebody.label else

print ‘.’ end end # Просто переводим строку: puts end # Проверка столкновения. Если есть два объекта с одинаковыми # координатами и их «label» не равны, то значит робот поймал собаку. game_over = arr.combination(2).any? do |a, b| a.x == b.x && \ a.y == b.y && \ a.label != b.label end if game_over puts ‘Game over’ exit end # Каждого объекта двигаем в случайном направлении arr.each do |somebody| # Вызываем метод move, все то же самое, что и в предыдущем # варианте. Командир не знает кому он отдает приказ. commander.move(somebody) end # Задержка в полсекунды sleep 0.5 end

«` Несколько оговорок по поводу программы выше. Во-первых, чтобы собака примерно ходила по диагонали, размер поля был уменьшен до 12х12. Во-вторых, класс Commander остался точно таким же. Он не изменился, потому что этот класс изначально подразумевал duck typing — “если это ходит вверх, вниз, влево, вправо, то мне не важно кто это, робот или собака”. В-третьих, мы использовали хитрый способ определения столкновения. Он был честно найден в Интернете по запросу “ruby any two elements of array site:stackoverflow.com” — часто программисту нужно только уметь найти правильный ответ! Результат работы программы: «` . . *.

. . *. . @. . *.*. . *. *. . . *. . *. «` Демо: https://asciinema.org/a/KsenHLiaRbTilZa081EhZSFXF Задание: удалите все комментарии в программе выше. Способны ли вы разобраться в том, что происходит? Задание: добавьте на поле еще 3 собаки. Задание: исправьте программу: если все собаки дошли до правого или нижнего края поля, выводить на экран “Win!”

Наследование — Что такое наследование? — Быстрый способ разбогатеть! Наследование это третий кит, на котором стоит объектно-ориентированное программирование после инкапсуляции и полиморфизма. Но в то же время, наследование — весьма противоречивый концепт. Существует множество мнений по этому поводу. Тем не менее, сначала мы рассмотрим возможность, которую нам предлагает язык руби, а потом поговорим о том, почему это плохо. Давайте представим, что на поле с роботами и собаками мы захотели добавить еще одного игрока, человека (класс “Human”). Всего в игре получилось бы три типа: `Robot`, `Dog`, `Human`. Что сделал бы начинающий ООП-программист, знакомый с наследованием? Он бы сделал следующий трюк. Очевидно, что есть методы `up`, `down`, `left`, `right` — которые выполняют какие-то действия. Очевидно, что есть методы `x`, `y` (переменные экземпляра `@x` и `@y`, но `attr_accessor` добавляет методы, которые называются getter и setter). Есть метод label который для каждого типа разный. Методы `up`, `down`, `left`, `right` реализуют какую-то функциональность, которая почти всегда одинакова.

Другими словами, есть что-то повторяющееся, а есть что-то совершенно уникальное для каждого объекта (label). Пока наши методы `up`, `down`, `left`, `right` относительно простые — всего 1 строка и мы по сути копируем эти методы из объекта в объект: «` class Robot def right self.x += 1 end def left self.x -= 1 end def up self.y += 1 end def down self.y -= 1 end end class Dog # . def right self.x += 1 end def down self.y -= 1 end end class Human def right self.x += 1 end def left self.x -= 1 end def up

self.y += 1 end def down self.y -= 1 end end «`

Но что если каждый из этих методов будет по 10 строк или мы вдруг захотим что-нибудь улучшить (например, добавить координату “z”, чтобы получить трехмерное поле)? Придется копировать этот код между всеми классами. И если возникнет какая-либо ошибка, придется исправлять сразу в трех местах. Поэтому начинающий ООП-программист видит повторяющуюся функциональность и говорит: “ага! Вот это повторяется! Почему бы нам не воспользоваться наследованием? Есть робот, у которого есть все нужные методы, так почему бы не “переиспользовать” (reuse, share) уже встречающуюся функциональность?”: «` class Robot attr_accessor :x, :y def initialize(options=<>) @x = options[:x] || 0 @y = options[:y] || 0 end def right self.x += 1 end def left self.x -= 1 end def up self.y += 1 end def down self.y -= 1 end def label

‘*’ end end class Dog .ruby-version # записываем версию 2.3.1 в наш файл $ cd .. # выходим на уровень вверх $ cd rvm-test # и снова переходим в эту директорию Required ruby-2.3.1 is not installed. To install do: ‘rvm install «ruby-2.3.1″‘ «` Вот это да! Получилось! RVM выдал нам сообщение о том, что руби версии 2.3.1 не установлен и сразу же команду для установки (напоминаем, что список всех команд доступен в справке: `rvm —help`). Запустим эту команду, RVM попробует найти уже откомпилированный (бинарный) файл этой версии ruby именно для вашей операционной системы где-то у себя на серверах: «` Searching for binary rubies, this might take some time. No binary rubies available for: osx/10.13/x86_64/ruby-2.3.1. «` Если файл не будет найден, то будет этой версии будет скачан с официального сайта и будет откомпилирован на вашем компьютере! Согласитесь, что это немного проще, чем компиляция с помощью `./configure`, `make` и т.д., которую мы делали ранее? После того как файл будет откомпилирован и установлен, мы сможем проверить версию воспользоваться установленной версией языка руби: «` $ ruby -v ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-darwin17] $ which ruby /Users/ro/.rvm/rubies/ruby-2.3.1/bin/ruby «` Другими словами, rvm подменил нам руби на тот, который был указан в `.ruby-version`: Было:

При этом старый файл `/usr/local/bin/ruby` остался на диске. Просто была подмена PATH, переменной, которая отвечает за пути к файлам. И теперь наша оболочка просто обращается к другой директории. И все это в автоматическом (окей, полуавтоматическом) режиме. Иметь `.ruby-version` в директории проекта очень важно, т.к. другие программисты будут точно знать какой версией руби вы пользовались, когда создавали работали над проектом. Этот файл позволит избежать вопросов в команде “а какую версию руби мне устанавливать для проекта Х?”. Это, как говорят, single source of truth (единый источник истины). Если версия обновилась, то вся команда будет знать, где смотреть. Более того если этой версии на компьютере разработчика нет, то RVM подскажет как ее установить. Выше мы установили нужную версию руби, зная про секрет RVM. Но можно ли как-нибудь установить версию руби без этого секрета, без создания `.ruby-version`? Можно. Воспользуемся двумя командами: ● ●

`rvm list known` — выдает список доступных версий руби. Нас интересует версии MRI. `rvm install . ` — установить руби определенной версии, вместо троеточия необходимо указать версию языка.

«` $ rvm install 2.5.1 Searching for binary rubies, this might take some time. No binary rubies available for: osx/10.12/x86_64/ruby-2.5.1. Continuing with compilation. Please read ‘rvm help mount’ to get more information on binary rubies. . «` Выше мы ввели команду для установки руби версии “2.5.1”. Появилась отладочная информация, которая сказала о том, что скомпилированной (готовой) версии руби 2.5.1 для нашей операционной системы (macOS 10.12) пока не существует, поэтому сейчас будет скачаен и откомпилирован исходный код языка руби на нашем компьютере. Как вы могли заметить, RVM пытается найти где-то на своих серверах версии руби по следующим признакам: ● ● ● ●

`osx` — тип ОС, может быть Linux, Windows или что-то еще (теоретически) `10.12` — версия ОС, существует множество разных версий как macOS, так и остальных ОС `x86_64` — архитектура процессора `ruby-2.5.1` — версия языка

Если перемножить количество всевозможных типов ОС на количество различных версий этих ОС, на количество возможных архитектур процессора (не так много), и на количество версий языка, то получится довольно большое число. Другими словами, RVM держит на своих серверах тысячи откомпилированных версий руби. Некоторые из этих версий были откомпилированы на точно таких же компьютерах, как и у вас. Возникает вопрос: а зачем нужны откомпилированные версии? Дело в том, что скачивание откомпилированной версии занимает секунды, а компиляция — во много раз больше. Также можно было бы откомпилировать одну версию руби сразу для определенного семейства ОС (например, для macOS от 9 до 10 версий), но каждая ОС может содержать свои настройки производительности или тонкие моменты, о которых компилятору хорошо бы знать. Но с точки зрения “потребителя” особенности работы RVM не очень важны, нас интересует вопрос как установить и использовать RVM без дот-файла (файла, начинающегося с точки: `.ruby-version`). Мы разобрались с тем, как установить: например, `rvm install 2.5.1`. Но что же с использованием? Представьте, что установлено несколько версий: 1, 2, 3. Если дот-файлов в каталогах нет, то нам надо как-то выбирать какую именно версию мы хотим использовать. Для этого существует команда оболочки `use`, с очень простым синтаксисом: «` $ rvm use 2.5.1 Using /Users/ro/.rvm/gems/ruby-2.5.1 $ ruby -v ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin16] $ rvm use 2.3.1 Using /Users/ro/.rvm/gems/ruby-2.3.1 $ ruby -v ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-darwin16] «` Чтобы вывести список всех установленных руби существует команда `list`: «` $ rvm list ruby-2.3.1 ruby-2.4.2 * ruby-2.5.0 => ruby-2.5.1

x86_64 x86_64 x86_64 x86_64

# => — current # =* — current && default 252

# * — default «` Также в RVM существует такое понятие как “default” версия (версия по-умолчанию). Другими словами та версия, которая будет автоматически использоваться при открытии терминала. Ее можно установить с помощью команды `alias`: «` $ rvm alias create default 2.5.1 Creating alias default for ruby-2.5.1. $ rvm use default Using /Users/ro/.rvm/gems/ruby-2.5.1 «` Отныне каждый раз когда мы будем говорить `rvm use default` будет использоваться версия 2.5.1. На этом наше знакомство с инструментом RVM закончено. Возможно, что где-то вы услышите выражение “gemset”, которое означает ничто иное как “набор gem’овов”. Но сейчас оно все реже используется, т.к. при использовании инструмента Bundler последних версий отпадает необходимость в таком понятии как “gemset”. По сути понятие о gemset’овах это возможность иметь разные наборы gem’овов для одной и той же версии руби. Но Bundler уже позволяет иметь разные наборы gem’овов, и каждый раз когда вы вносите исправления в Gemfile и вводите “bundle”, все происходит автоматически и без каких-либо проблем. Запоминать все настройки RVM не нужно, но иметь представление о том, что это такое полезно, ведь помимо RVM существуют очень похожие инструменты для других языков: ● ●

NVM — Node.js Version Manager для JavaScript VirtualEnv для Python

Тестирование Перед тем, как мы познакомимся с тестированием, необходимо определиться а что же такое тестирование. Это понятие достаточно широкое, а следовательно могут существовать разные типы тестов. Например, перед тем как настоящий инженер захочет послушать только что собранный из радиодеталей приёмник, он быстро включит и выключит питание чтобы посмотреть — не

пойдет ли дым, не допущена ли где-нибудь фундаментальная ошибка. Такое тестирование называют smoke-тестами (smoke — дым). Затем инженер может приступить к «happy path» (дословно: «счастливый путь») тесту: включить приёмник и посмотреть — идет ли звук и настраивается ли он на частоту хотя бы какой-нибудь радиостанции. Перед запуском в продажу могут быть другие тесты. Например, нагрузочные: посмотреть как приёмник расходует батарею. А также тесты на качество сборки, на стабильность работы и так далее. Количество и глубина этих тестов зависят от требований. Делаем ли мы радиоприёмник для розничной продажи или это военный образец? Ответы на эти фундаментальные вопросы определяют какое именно тестирование нам нужно. Похожая картина наблюдается и при разработке программ. Существует большое количество тестов для программ: ручные тесты, автоматизированные, юнит-тесты (модульные тесты), интеграционные тесты, нагрузочные. Чтобы познакомиться со всеми типами тестов потребуется не один день. Мы рассмотрим тесты, с которыми чаще всего встречается программист: это юнит-тесты (unit tests, от англ. «unit» — модуль или часть). Что же такое юнит-тест и зачем он нужен? Не так давно про тесты никто не думал. Программы создавали в текстовом редакторе, проверяли их работу и сразу же запускали (или отправляли своим клиентам на дискетах, CD-ROM’ах, а позднее и через Интернет). Если возникала какая-то ошибка, то ее исправляли. Таким образом, в новой версии (новом релизе) программы могло быть исправлено несколько ошибок. Но сложность программ возрастала. Возрастало и количество разработчиков в командах. Нередко получалось так, что небольшое, казалось бы, улучшение вызывает ошибку. Эти ошибки, конечно, отлавливались командой ручных тестировщиков. Но от времени появления ошибки до момента ее выявления могло пройти несколько дней. Поэтому возник вопрос о выявлении ошибок на более ранних этапах. Если существует какая-то часть программы, можно ли каким-то образом хотя бы сделать «защиту от дурака»? По аналогии с реальной жизнью: вы выходите из дома и знаете, что утюг и газовая плита выключены, но на всякий случай вы делаете то, что называется double check (двойная проверка). В 99% случаев все будет так, как вы ожидаете, но в 1% случаев эта двойная проверка даст положительный результат. Тем более, цена двойной проверки очень мала. В программировании есть что-то подобное, но: ●

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

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

Согласитесь, что это удобно: разработчик изменил программу, запустил тесты и проверил что ничего фундаментального не сломалось. Если сломалось, то тут же исправил. В итоге, от появления ошибки до момента ее выявления прошли минуты, но никак не дни (запуск 500 тестов занимает около двух минут). Получается, что на относительно небольшом проекте на каждые 100 изменений будет запущено по 500 тестов на каждое изменение, что в общей сложности дает 50 тысяч запусков разных тестов. Этот подход позволил значительно улучшить качество написанных программ. Однако, у юниттестирования есть и недостатки. Во-первых, вместе с написанием кода программы программисты также должны писать тесты. Несмотря на то, что тесты писать легче, все равно необходимо уделять этому какое-то время. Для создания хороших тестов необходимо иметь знания фреймворков для юнит-тестирования, знания общепринятых подходов, и какой-то минимальный опыт. Во-вторых, юнит-тестирование никогда не покрывает абсолютно все участки кода. Десять конструкций «if. else» уже дают 1024 (двойка в десятой степени) возможных вариантов выполнения вашей программы. В некоторых проектах используют такое выражение как «покрытие кода» (code coverage), которое выражается в процентах. Например, говорят: «code coverage для нашего проекта составляет 80%» (при этом это является предметом гордости). На самом деле вопрос в том, как считаются эти проценты. Да, отдельные модули могут быть покрыты тестами. Но даже 100% покрытие не является панацеей от абсолютно всех ошибок: возможных вариантов выполнения программы всегда во много раз больше, чем тестов, которые может написать человек. В-третьих, существует особенность о которой редко говорят. Юнит-тесты обычно пишут сразу после написания какого-либо кода. Но на начальном этапе программный дизайн функциональности обычно еще не зафиксирован. Как художник перед написанием полотна рисует этюды, так и программист чаще всего (порой даже неосознанно) сначала делает работоспособный набросок. Этот набросок потом может меняться, ведь со временем мысль в голове имеет свойство оформляться в более изящные формы. Так почему какая-то часть программы не может быть улучшена сразу после того, как она была написана в текстовом редакторе? Юнит-тесты фиксируют дизайн программы на этапе когда дизайн еще достаточно свеж и может поменяться. Если поспешить с написанием юнит-тестов, то есть вероятность того, что тесты нужно будет переписывать. Но несмотря на все недостатки, юнит-тестирование оказало неоценимый вклад в развитие индустрии программных продуктов. Юнит-тестирование является стандартом в индустрии, и абсолютно для каждого языка программирования существует фреймворк для создания тестов (очень часто таких фреймворков существует сразу несколько). В

нашей книге мы рассмотрим более наиболее популярный фреймворк для языка руби, который называется Rspec.

RSpec Совершенно нет необходимости рассматривать стандартную библиотеку для тестирования, т.к. очень высока вероятность, что вы будете использовать не стандартную библиотеку, а очень популярный фреймворк, который называется RSpec. В списке инструментов для тестирования https://github.com/markets/awesome-ruby#testing он занимает первое место. Нужно отметить, что существует множество мнений по поводу лучшего фреймворка для создания тестов. Например, один из создателей фреймворка Rails DHH не любит RSpec, о чем не стесняется говорить: > RSpec раздражает меня эстетически: без ощутимой выгоды в обмен на сложность, которую он привносит в юнит тесты Источник: https://twitter.com/dhh/status/52807321499340800 (и др.) Но руби-сообщество придерживается другого мнения. Хотя и с мнением DHH можно согласиться: когда тесты разрастаются и начинаются умные (“smart”) трюки RSpec’ова, то тесты на самом деле становятся менее читаемы. Это чем-то похоже на спортивную машину, которая едет по загруженному шоссе, постоянно перестраивается, но в итоге все равно едет в общем потоке. Поэтому иногда лучше быть не smart, а simple (проще), и писать более понятные тесты. Плюс, изначальная конфигурация RSpec может занять какое-то время у начинающего программиста. Но хорошая новость в том, что этот инструмент уже прошел фазу взросления, и на подавляющее большинство проблем, с которыми вы можете столкнуться, уже будет готовый ответ в Интернете. На практике качество тестов в проекте очень зависит от команды. Неважно каким именно инструментом вы пользуетесь: если бы существовал инструмент, который решает все проблемы, то ему не было бы цены. И вопрос читаемых тестов это не вопрос инструмента, а вопрос баланса smart vs simple. Основой RSpec является т.н. DSL — Domain Specific Language (“предметноориентированный язык”). Само название говорит о том, что это какой-то язык, созданный специально для описания каких-то предметов,

Это, можно сказать, особый синтаксис, который появляется в языке после установки gem’овa “rspec”. Помимо стандартных ключевых слов, появляются новые: describe, it, let, before, after. В этой книге мы не рассматривали как именно работает механизм DSL. Для наших целей пока достаточно знать, что этот механизм позволяет создавать свой синтаксис внутри языка руби. Попробуем установить и настроить rspec с нуля и написать первый тест. Для начала установим последнюю версию стабильную версию руби. Для этого введем команду `rvm list known`, она покажет список доступных версий языка для установки. Нас интересует версия “MRI” (Matz’s Ruby Interpreter, версия языка от создателя языка руби Юкихиро Мацумото, эта версия является основной). Для установки достаточно ввести: «` $ rvm install 2.5.1 «` Или любую другую версию без суффикса “-preview”. После этого создадим каталог приложения и “закрепим” версию руби за этим приложением: «` $ mkdir rspec_demo $ cd rspec_demo $ echo «2.5.1» > .ruby-version «` Проверим, что версия установленная версия руби соответствует ожидаемой (ваш вывод может немного отличаться): «` $ ruby -v ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin17] «` Если изменения не были “подхвачены”, необходимо выйти и снова войти в каталог: «` $ cd .. $ cd rspec_demo «` Раньше мы устанавливали gem’овы (дополнительные библиотеки) с помощью команды “gem install …”, но полезно где-то держать список всех необходимых gem’овов для вашего приложения. Для этих целей есть специальный файл, который называется Gemfile. Лучше создать его с помощью команды:

«` $ bundle init «` Gemfile будет выглядеть следующим образом: «` # frozen_string_literal: true source «https://rubygems.org» git_source(:github) <|repo_name| "https://github.com/#» > # gem «rails» «` Теперь необходимо установить rspec. Это можно сделать при помощи команды `gem install rspec`, но раз уж мы договорились держать все одном месте, изменим Gemfile на следующий: «` source «https://rubygems.org» gem «rspec» «` И введем команду bundle (“связка”, “связать”): «` $ bundle … Fetching rspec 3.8.0 Installing rspec 3.8.0 Bundle complete! 1 Gemfile dependency, 7 gems now installed. Use `bundle info [gemname]` to see where a bundled gem is installed. «` Если вы посмотрите на текущую директорию, то увидите файл `Gemfile.lock`. Этот файл был создан автоматически, не рекомендуется менять его вручную. Он указывает какие именно версии gem’овов были использованы для вашего приложения. Как и все в этом мире, gem’овы обновляются и могут менять версии. Не факт, что следующая версия будет совместима с предыдущей. Но вы же хотите, чтобы ваша программа работала через 5-10 лет? Поэтому мы “локаем” (от слова “lock” — “замок”) ее на определенные версии gem’овов, и

в будущем автоматического обновления gem’овов не будет. Не переживайте, мы всегда сможем обновить gem’овы вручную, когда мы этого захотим. В директории вашего приложения должно быть три файла: `.ruby-version`, `Gemfile`, `Gemfile.lock` (воспользуйтесь командой `ls -a`, т.к. все файлы, начинающиеся с точки не выводятся с помощью команды `ls`). Но возникает вопрос — если rspec был установлен, то куда? Все верно, когда мы ввели команду “bundle”, пакет “rspec” был скачан из Интернета и размещен где-то в вашей файловой системе. Команда `gem which rspec` поможет вам увидеть точный путь, но знать точный путь обычно никогда не требуется. Все остальные программисты вашей команды будут также вводить “bundle” и на основе трех файлов смогут “воссоздать” точно такую же среду исполнения, какая сейчас существует на вашем компьютере, с точно такими же gem’овами. Правда, может отличаться номер патча. Например, версия руби “ruby 2.5.1p57” согласно SEMVER (www.semver.org) говорит о том, что патч в руби версии “2.5.1” это единица. Но метка “p57” по сути тоже означает номер патча: пятьдесят седьмой патч. Это, скажем так, тоже патч, но еще менее значимый. Какие-то очень незначительные изменения, исправление багов, улучшение безопасности. Звучит сложно? Но за это нам и платят деньги! Команда `rspec —help` поможет определить что делать дальше: нас интересует команда `rspec —init`: «` $ rspec —init create .rspec create spec/spec_helper.rb «` Было создано два файла (`.rspec` и `spec_helper.rb`) и одна директория `spec`. Теперь можно поговорить о том, что такое “spec”. Это то же самое, что и тест. Это слово образовано от другого слова: “specification” (спецификация). По-русски иногда говорят “спек” или “спеки”. Файл `spec_helper.rb` достаточно объемный (порядка сотни строк), но по большей части это комментарии. Этот файл является вспомогательным и служит для настройки инструмента “rspec”. Настраивать на данном этапе мы ничего не будем, поэтому оставим все настройки по-умолчанию. Посмотрим на структуру нашего приложения:

Рис. Структура приложения без какого-либо “полезного” кода. Что видно из этого рисунка? Программа еще не написана, но уже существует 5 файлов! Два файла являются т.н. dot-файлами (dotfiles, начинаются с точки). Есть файл с подчеркиванием (snake_case), есть файл с дефисом (kebab-case). Есть файлы, начинающиеся с большой буквы, есть файлы начинающиеся с маленькой буквы. Есть файлы с расширением, есть без. Остается только сказать, что мы живем не в идеальном мире, а перфекционисты в программировании могут почувствовать себя не очень уютно. Давайте напишем какой-нибудь “полезный” код, а потом покроем его тестами. И тут сразу же нужно сделать отступление. В сообществе разработчиков не утихают дебаты по поводу правильного подхода: что нужно делать сначала: писать полезный код, а потом тесты? Или же сначала создавать тесты, а потом код? (т.н. Test Driven Development, TDD). На сайте Youtube есть видео дебатов DHH (одного из создателей фреймворка Rails), Кента Бека (основателя методологии TDD) и Мартина Фаулера (известного автора трудов по основам объектно-ориентированного программирования и проектирования). Авторы этой книги солидарны с DHH и придерживаются мнения, что сначала нужно написать код, а потом покрывать существующий код тестами. Наш “полезный” код уже нам знаком: «` def total_weight(options=<>) a = options[:soccer_ball_count] || 0 b = options[:tennis_ball_count] || 0 c = options[:golf_ball_count] || 0 (a * 410) + (b * 58) + (c * 45) + 29 end x = total_weight(soccer_ball_count: 3, tennis_ball_count: 2, golf_ball_count: 1) «`

Мы рассматривали этот метод, когда считали общий вес заказа. Выше мы умножаем количество футбольных мячей “a” на вес каждого футбольного мяча (410 грамм), количество мячей для тенниса “b” на вес каждого мяча для тенниса (58 грамм), количество мячей для гольфа “c” на вес каждого мяча для гольфа (45 грамм) и прибавляем вес коробки (29 грамм). Сейчас с этим методом все в порядке. Но почему именно этот метод стоит покрыть тестами? Чтобы ответить на этот вопрос, подумаем что может пойти не так. Во-первых, речь идет о деньгах — о стоимости посылки. Там где деньги, там нужен точный расчет и нужна надежность. Какой-нибудь программист через год или два может заглянуть в этот метод и добавить новую функциональность. Например, новый тип мячей. Чтобы убедиться в том, что ничего не сломано, нужно хотя бы запустить этот метод и сравнить результат с ожидаемым. Но лучше делать это автоматически. Во-вторых, кто-то может посчитать, что конструкция `|| 0` лишняя. Это мнение имеет право на существование, т.к. следующий код вполне работоспособен (попробуйте запустить в pry): «` def total_weight(options=<>) a = options[:soccer_ball_count] b = options[:tennis_ball_count] c = options[:golf_ball_count] (a * 410) + (b * 58) + (c * 45) + 29 end x = total_weight(soccer_ball_count: 3, tennis_ball_count: 2, golf_ball_count: 1) «` Но только до той поры, пока код вызывается со всеми параметрами. Когда один из них отсутствует, будет выдана ошибка: «` $ pry . x = total_weight(soccer_ball_count: 3, tennis_ball_count: 2) NoMethodError: undefined method `*’ for nil:NilClass from (pry):12:in `total_weight’ «` Хороший тест предотвратит эту ошибку.

В-третьих, представьте какой-нибудь более сложный сценарий. Например, если общий вес мячей больше определенного значения, то требуются две коробки. Или мы знаем, что при покупке хотя бы одного мяча для тенниса в коробку кладется рекламный буклет весом 25 грамм. Конечно, можно было бы обойтись и без тестов. Достаточно написать правильный метод, проверить его вручную (например, в pry) и использовать в приложении. Но согласитесь, что неплохо бы было дать другим программистам возможность проверить написанный вами код. А еще лучше сделать так, чтобы этот код был представлен в общем наборе тестов, среди всех других проверок, чтобы одной с помощью одной консольной команды можно было запустить все тесты сразу и убедиться, что все впорядке. Добавим в наше приложение одну директорию `lib` и два файла: `shipment.rb` и `app.rb`:

`app.rb` будет выглядеть следующим образом: «` require ‘./lib/shipment’ x = Shipment.total_weight(soccer_ball_count: 3, tennis_ball_count: 2, golf_ball_count: 1) puts x «` `lib/shipment.rb` будет содержать упомянутую выше функцию, но код будет представлен в виде модуля: «` module Shipment module_function

def total_weight(options=<>) a = options[:soccer_ball_count] || 0 b = options[:tennis_ball_count] || 0 c = options[:golf_ball_count] || 0 (a * 410) + (b * 58) + (c * 45) + 29 end end «` Можно было бы создать класс и объявить метод в виде метода класса `self.total_weight`, но не рекомендуется создавать классы, когда мы не собираемся создавать их экземпляры (см. https://github.com/rubocop-hq/ruby-style-guide#modules-vs-classes). Поэтому мы ограничимся модулем и специальным синтаксисом `module_function`. При запуске `app.rb` на экран выводится вес отправления: «` $ ruby app.rb 1420 «` Выше мы разбили программу на две части (на два юнита): часть, которая содержит логику `shipment.rb`. И часть, которая вызывает логику `app.rb`. Мы создадим тест для первого юнита, `shipment.rb`, который содержит основную логику. Второй юнит пока не является чем-то сложным, поэтому покрывать тестом мы его не будем. Добавьте в директорию `spec` файл `shipment_spec.rb`:

Cо следующим содержимым: «` require ‘./lib/shipment’ describe Shipment do it ‘should work without options’ do expect(Shipment.total_weight).to eq(29) end end «` И запустите тесты (параметры устанавливают форматирование в значение “d” documentation, в этом случае rspec выводит имена тестов): «` $ rspec -f d Shipment should work without options Finished in 0.00154 seconds (files took 0.09464 seconds to load) 1 example, 0 failures «` Тест отлично отработал, но что же произошло в программе? Давайте разберемся. Вот код программы с комментариями: «` # подключаем юнит require ‘./lib/shipment’ # специальный синтаксис, который дословно говорит: # «описываем Shipment (отправление)» describe Shipment do # специальный синтаксис, который дословно говорит: # «это должно работать без опций» # (то, что в кавычках — это строка, мы сами её пишем, слово «it» служебное) it ‘should work without options’ do # ожидаем, что общий вес отправления будет равен 29 (eq от англ.»equal») expect(Shipment.total_weight).to eq(29) end end

«` Согласитесь, что код выглядит не вполне обычно. То, что вы видите выше это т.н. rspec DSL (Domain Specific Language — язык предметной области). Он работает только в rspec. Давайте добавим еще один тест и посмотрим на результат: «` require ‘./lib/shipment’ describe Shipment do it ‘should work without options’ do expect(Shipment.total_weight).to eq(29) end it ‘should calculate shipment with only one item’ do expect(Shipment.total_weight(soccer_ball_count: 1)).to eq(439) expect(Shipment.total_weight(tennis_ball_count: 1)).to eq(87) expect(Shipment.total_weight(golf_ball_count: 1)).to eq(74) end end «` Результат: «` $ rspec -f d Shipment should work without options should calculate shipment with only one item Finished in 0.00156 seconds (files took 0.09641 seconds to load) 2 examples, 0 failures «` Что произошло выше? “It should calculate shipment with only one item” дословно переводится как “это должно рассчитывать отправление только с одной вещью”. Другими словами, как раз то, что мы желаем проверить: код должен работать в тех случаях, когда программист передает только 1 аргумент в функцию `total_weight`. Кстати, вместо непонятных цифр 439, 87, 74 лучше написать ожидаемый результат в виде сложения. В будущем возможно потребуется заменить 29 на какое-то другое значение, да и вообще, полезно иметь возможность понять откуда взялись эти цифры: «` expect(Shipment.total_weight(soccer_ball_count: 1)).to eq(410 + 29) 265

expect(Shipment.total_weight(tennis_ball_count: 1)).to eq(58 + 29) expect(Shipment.total_weight(golf_ball_count: 1)).to eq(45 + 29) «` Давайте подробнее разберем строку: «` expect(something).to eq(some_value) «` которая также может быть представлена как: «` expect(something).to be(some_value) «` О разнице между «eq» и «be» немного ниже. Эта строка похожа на предложение в английском языке. Например, мама говорит мальчику: «Son, when you go to school, I expect you to be a good boy» («Сынок, когда ты идешь в школу, я ожидаю, что ты будешь хорошим мальчиком»). На языке RSpec DSL это может быть записано следующим образом: «` expect(son).to be(a_good_boy) «` Или немного иначе: «` expect(son).not_to be(a_bad_boy) «` Если бы мы записывали программу на чистом руби, то мы скорее всего написали бы чтото вроде: «` if son != a_good_boy panic end «` Но RSpec дает нам возможность записать все в виде одной строки, и в более естественном (с точки зрения RSpec) виде. Под капотом там, конечно, используется обычная конструкция «if». Другими словами, в тестах мы не пишем «if», а сообщаем о наших ожиданиях. Мы не используем императивный стиль, а используем декларативный.

Мама не говорит мальчику что конкретно делать («не обижай девочек», «учись хорошо»), она говорит что она от него ожидает («будь хорошим»). Другими словами, это spec, спецификация, которая где-то задана и которой надо соответствовать. Выражения типа `expect(son).to` и `expect(son).not_to` являются ожиданием (expectation). А выражения `eq(. )` (от слова «equal»), `be(. )` называют матчерами (matchers). Матчеры и ожидания бывают разных типов. Обычно ожидания могут принимать или вид выражения, или вид блока: Выражение (expression) в ожидании используется, когда мы проверяем какое-то существительное, или результат действия. Например: мальчик, вес мяча, вес посылки: «` expect(son).to be(a_good_boy) expect(soccer_ball_weight).to eq(410) expect(Shipment.total_weight(soccer_ball_count: 1)).to eq(439) «` Блоки в ожидании используются когда требуется или проверить что-то во время операции, или сделать какое-то измерение. Например: проверить, что метод выдает исключение, если запущен с определенными параметрами; проверить, что метод меняет состояние экземпляра класса — например, добавляем товар в корзину, а общее количество элементов в корзине увеличилось на один. Если в случае выражений мы просто помещали их в скобки, то в случае с блоками мы передаем их в фигурных скобках: «` expect < Shipment.total_weight(ford_trucks: 100) >.to raise_error expect < some_order.add(item) >.to change < order.item_count >.by(1) «` Синтаксис является немного необычным и требует привыкания. Ожидания и стандартные матчеры доступны на официальном сайте по адресу https://relishapp.com/rspec/rspecexpectations/docs/built-in-matchers. Хочется заметить, что из практики программирования этого набора обычно достаточно. У нас нет задачи дать полную справку по rspec, но следует упомянуть о различии матчеров «eq» и «be». «Be» означает «быть», т.е. быть в смысле «точно вот этим». А «eq» означает «равен» («equals»). Т.е. не обязательно быть точно таким же, но нужно равняться. Например, надписи на заборах из трех букв обычно равны («eq»), но каждая из них уникальная по-своему, поэтому нельзя применить к ним матчер «be». Ведь надпись может быть нарисована разной краской, разным размером и т.д.

Т.к. все в руби объект, то это важно. Например, переменные «a» и «b» ниже равны, но их идентификаторы разные, т.к. это разные объекты и они расположены в разных областях памяти: «` $ pry > a = «XXX» > b = «XXX» > a == b => true > a.__id__ == b.__id__ => false «` Давайте напишем еще один тест для нашей программы: «` it ‘should calculate shipment with multiple items’ do expect( Shipment.total_weight(soccer_ball_count: 3, tennis_ball_count: 2, golf_ball_count: 1)

«` Ради удобства чтения выражение, которое мы хотим проверить было перенесено на отдельную строку. Результат выполнения всех тестов: «` $ rspec -f d Shipment should work without options should calculate shipment with only one item should calculate shipment with multiple items Finished in 0.00291 seconds (files took 0.19016 seconds to load) 3 examples, 0 failures «` Все это хорошо, но выше был дан пример тестирования «статического» метода, или метода класса (точнее модуля, что почти одно и то же), но не экземпляра. Заметьте, что мы нигде не создавали никакого объекта, а вызывали класс напрямую. В случае наличия объекта для тестирования все становится намного интереснее.

Можно долго рассказывать про rspec и существуют отдельные книги на эту тему (например, https://leanpub.com/everydayrailsrspec), но самый лучший совет, который могут дать авторы: при написании программ старайтесь думать о том, как вы будете тестировать написанный вами код. Существует множество приёмов, но наша задача познакомить вас с синтаксисом и дать основы. Задание: попробуйте заменить 1420 выше на 1421 и посмотрите что произойдет (тест не должен сработать). Задание: код файла `shipment.rb` был изменен: если в метод “total_weight” не переданы аргументы, генерируется ошибка (также говорят “выбрасывается исключение”): «` module Shipment module_function def total_weight(options=<>) raise «Can’t calculate weight with empty options» if options.empty? a = options[:soccer_ball_count] || 0 b = options[:tennis_ball_count] || 0 c = options[:golf_ball_count] || 0 (a * 410) + (b * 58) + (c * 45) + 29 end end

«` Измените тест таким образом, чтобы тест проверял, что ошибка на самом деле генерируется.

Заключение Мы рассмотрели лишь некоторые возможности языка Руби, выполнили задания, заложили фундамент, который позволит уверенно двигаться дальше. Для любого программиста очень важно понимание основ и возможностей инструментов. Знания, изложенные в этой книге, в течение нескольких лет собирались из разных источников. Они дают неоспоримое преимущество перед другими учащимися. Возможно, вам придется обращаться к этой книге снова — это нормальный процесс, т.к. полученная информация усваивается постепенно, оседает слоями, каждый раз дополняя общее понимание. Если какие-то моменты оказались сложны для вашего понимания, мы рекомендуем вернуться к прочитанному материалу через 2-3 месяца. Авторы желают успехов, не

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

Программирование на языке С#

Технология программирования. Функции и структуры

1. Программирование на языке С# В 2000 году компания Microsoft объявила о создании нового языка программирования — языка C#.

Главным инструментом создания приложений для
платформы .Net является Microsoft Visual Studio, которая
имеет множество редакций.

2.

Платформа .NET по сути представляла собой новую модель создания
приложений, которая включает в себя следующие возможности:
1) использование библиотеки базовых классов, предлагающих
целостную объектно-ориентированную модель программирования
для всех языков программирования, поддерживающих .NET;
2) полное и абсолютное межъязыковое взаимодействие, позволяющее
разрабатывать фрагменты одного и того же проекта на различных
языках программирования;
3) общая среда выполнения приложений .NET, независимо от того, на
каких языках программирования для данной платформы они были
созданы; при этом среда берет на себя контроль за безопасностью
выполнения приложений и управление ресурсами;
4) упрощенный процесс развертывания приложения, в результате чего
установка приложения может свестись к простому копированию
файлов приложения в определенный каталог.
Одним из основных элементов .NET Framework является библиотека
классов под общим именем FCL (Framework Class Library), к которой
можно обращаться из различных языков программирования, в частности,
из С#.

3. Common Language Runtime — CLR

Кроме FCL в состав платформы .NET входит Common Language
Runtime (CLR — единая среда выполнения программ), название
которой говорит само за себя — это среда ответственна за
поддержку выполнения всех типов приложений, разработанных
на различных языках программирования с использованием
библиотек .NET.
Среда CLR берет на себя всю низкоуровневую работу, например,
автоматическое управление памятью.
Среда CLR обеспечивает интеграцию языков и позволяет
объектам, созданным на одном языке, использовать объекты,
написанные на другом. Такая интеграция возможна благодаря
стандартному набору типов и информации, описывающей тип
(метаданным).

4. Первый проект в среде Visual Studio

Текст программы:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace pr1
< class Program
static void Main(string[] args)
< double x,y,f;
string s;
Console.Write(«x=»);
s=Console.ReadLine();
x = Convert.ToDouble(s);
y = Math.Sin(0.1 * x) + Math.Cos(x);
f = Math.Tan(1 / x) + 3;
Console.Write(«y= f = «,y, f);
Console.ReadKey();
> > >

5. Первая программа

Запускаем Visual Studio
Выбираем New Project

6. Первая программа

В строке Name вводим имя pr1, выбираем папку для
размещения проекта. Щелкаем по кнопке ОК

7. Первая программа

Вводим текст программы

8. Первая программа

Вводим текст программы

9. Первая программа

Рассмотрим текст программы:
using System – это директива, которая разрешает использовать имена
стандартных классов из пространства имен System непосредственно, без
указания имени пространства, в котором они были определены. Так,
например, если бы этой директивы не было, то пришлось писать бы
System.Console.WriteLine. Писать полное пространство имен каждый раз
очень неудобно. При указании директивы using можно писать просто
имя, например, Console.WriteLine.
Для консольных программ ключевое слово namespace создает свое
собственное пространство имен, которое по умолчанию называется
именем проекта. В нашем случае пространство имен называется pr1.
Каждое имя, которое встречается в программе, должно быть
уникальным. В больших и сложных приложениях используются
библиотеки разных производителей. В этом случае трудно избежать
конфликта между используемыми в них именами. Пространства имен
предоставляют простой механизм предотвращения конфликтов имен.
Они создают разделы в глобальном пространстве имен.

10. Первая программа

С# – объектно-ориентированный язык, поэтому написанная на нем
программа будет представлять собой совокупность взаимодействующих
между собой классов.
Автоматически создан класс с именем Program. Данный класс содержит
только один метод – метод Main(), который является точкой входа в
программу. Это означает, что именно с данного метода начнется
выполнение приложения. Каждая консольная программа на языке С#
должна иметь метод Main ().
Для запуска программы
щелкнем по кнопке
,
вместо кнопки можно
нажать
клавишу F5 или
выполнить команду

11. Первая программа

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

12. Состав языка

Алфавит – совокупность допустимых в языке символов.
Алфавит языка С# включает:
прописные и строчные латинские буквы и буквы
национальных алфавитов (включая кириллицу);
арабские цифры от 0 до 9, шестнадцатеричные цифры от A до
F;
специальные знаки:
» < >, |; [ ] ( ) + — / % * . \ ‘ : ? < = >! & ~ ^@_
пробельные символы: пробел, символ табуляции, символ
перехода на новую строку.
Из символов алфавита формируются лексемы (элементы) языка:
идентификаторы, ключевые (зарезервированные) слова, знаки
операций, константы, разделители (скобки, точка, запятая,
пробельные символы).

13. Состав языка

Идентификатор – это имя программного элемента: константы,
переменной, метки, типа, класса, объекта, метода и т.д.
Идентификатор может включать латинские буквы и буквы
национальных алфавитов, цифры и символ подчеркивания.
Прописные и строчные буквы различаются, например, myname,
myName и MyName — три различных имени.
Первым символом идентификатора может быть буква или
знак подчеркивания, но не цифра. Пробелы и другие разделители
внутри имен не допускаются. Язык С# не налагает никаких
ограничений на длину имен, однако для удобства чтения и записи
кода не стоит делать их слишком длинными.
Ключевые слова – это зарезервированные идентификаторы,
которые имеют специальное значение для компилятора, например,
static, int и т.д. Ключевые слова можно использовать только по
прямому назначению. Однако если перед ключевым словом
поставить символ @, например, @int, @static, то полученное имя
можно использовать в качестве идентификатора.

14. Система типов

В С# типы делятся на три группы:
базовые типы – предлагаемые языком;
типы, определяемые пользователем;
анонимные типы — типы, которые автоматически создаются на
основе инициализаторов объектов.
Кроме того, типы С# разбиваются на две другие категории:
размерные типы (value type)
ссылочные типы (reference type).
Принципиальное различие между размерными и ссылочными
типами состоит в способе хранения их значений в памяти. В
первом случае фактическое значение хранится в стеке (или как
часть большого объекта ссылочного типа). Адрес переменной
ссылочного типа тоже хранится в стеке, но сам объект хранится в
куче.

15. Система типов

Стек – это структура, используемая для хранения элементов по
принципу LIFO (Last input – first output или первым пришел последним ушел). Под стеком понимается область памяти,
обслуживаемая процессором, в которой хранятся значения
локальных переменных.
Куча – область памяти, используемая для хранения данных,
работа с которыми реализуется через указатели и ссылки. Память
для размещения таких данных выделяется программистом
динамически, а освобождается сборщиком мусора.
Сборщик мусора уничтожает программные элементы в стеке
через некоторое время после того, как закончит существование
раздел стека, в котором они объявлены. То есть, если в пределах
блока (фрагмента кода, помещенного в фигурные скобки <>)
объявлена локальная переменная, соответствующий программный
элемент будет удален по окончании работы данного блока. Объект
в куче подвергается сборке мусора через некоторое время после
того, как уничтожена последняя ссылка на него.

16. Система типов

Стандарт языка включает следующий набор фундаментальных типов.
Логический тип ( bool ).
Символьный тип ( char ).
Целые типы. Целые типы могут быть одного из трех размеров — short,
int, long, сопровождаемые описателем signed или unsigned, который
указывает, как интерпретируется значение, — со знаком или без оного.
Типы с плавающей точкой. Эти типы также могут быть одного из
трех размеров — float, double, long double.
Tип void, используемый для указания на отсутствие информации.
Указатели (например, int* — типизированный указатель на
переменную типа int ).
Ссылки (например, double& — типизированная ссылка на переменную
типа double ).
Массивы (например, char[] — массив элементов типа char ).
Язык позволяет конструировать пользовательские типы
Перечислимые типы ( enum ) для представления значений из
конкретного множества.
Структуры ( struct ).
Классы.

17. Система типов

Первые три вида типов называются интегральными или счетными.
Значения их перечислимы и упорядочены.
Согласно классификации все типы можно разделить на четыре
категории:
Типы-значения ( value ), или значимые типы.
Ссылочные ( reference ).
Указатели ( pointer ).
Тип void.
Особый статус имеет и тип void, указывающий на отсутствие какоголибо значения.
В языке C# жестко определено, какие типы относятся к ссылочным, а
какие — к значимым. К значимым типам относятся: логический,
арифметический, структуры, перечисление.
Массивы, строки и классы относятся к ссылочным типам.
Структуры C# представляют частный случай класса. Определив свой
класс как структуру, программист получает возможность отнести класс к
значимым типам.

18. Система типов

Все встроенные типы C# совпадают с системными типами каркаса Net
Framework, размещенными в пространстве имен System. Поэтому
всюду, где можно использовать имя типа, например, — int, с тем же
успехом можно использовать и имя System.Int32.
Логический тип
Имя типа
Системный тип
Значения
Размер
bool
System.Boolean
true, false
8 бит
Арифметические целочисленные типы
Имя типа
Системный тип
Диапазон
Размер
sbyte
System.SByte
-128 — 127
Знаковое, 8 Бит
byte
System.Byte
0 — 255
Беззнаковое, 8 Бит
short
System.Short
-32768 —32767
Знаковое, 16 Бит
ushort
System.UShort
0 — 65535
Беззнаковое, 16 Бит
int
System.Int32
(-2*10^9 — 2*10^9)
Знаковое, 32 Бит
uint
System.UInt32
(0 — 4*10^9)
Беззнаковое, 32 Бит
long
System.Int64
(-9*10^18 — 9*10^18) Знаковое, 64 Бит
ulong
System.UInt64
(0— 18*10^18)
Беззнаковое, 64 Бит

19. Система типов

Арифметический тип с плавающей точкой
Имя типа
Системный тип
Диапазон
Точность
float
System.Single
+1.5*10^-45 -/+3.4*10^38
7 цифр
double
System.Double
+5.0*10^-324 -/+1.7*10^308
15-16 цифр
Арифметический тип с фиксированной точкой
Имя типа
Системный тип
Диапазон
Точность
decimal
System.Decimal
+1.0*10^-28 — +7.9*10^28
28-29 значащих цифр
Символьные типы
Имя типа
Системный тип
Диапазон
Точность
char
System.Char
U+0000 — U+ffff
string
System.String
Строка из символов
Unicode
16 бит Unicode
символ
Имя типа
Системный тип
Примечание
object
System.Object
Прародитель всех встроенных и
пользовательских типов
Объектный тип

20. Переменные и константы

Переменная представляет собой типизированную область
памяти. Программист создает переменную, объявляя ее
тип и указывая имя.
При объявлении переменной ее можно инициализировать
(присвоить ей начальное значение), а затем в любой
момент ей можно присвоить новое значение, которое
заменит собой предыдущее.
В языке С# требуется, чтобы переменные были явно
проинициализированы до их использования.

21. Переменные и константы

Константа, в отличие от переменной, не может менять свое
значение. Константы бывают трех видов:
литералы
типизированные константы
перечисления.
Число 32 является литеральной константой. Его значение
всегда равно 32 и его нельзя изменить.
Типизированные константы именуют постоянные значения.
Объявление типизированной константы происходит
следующим образом:
const = ;

22. Переменные и константы

Перечисление — это особый размерный тип, состоящий из
набора именованных констант (называемых списком
перечисления). Синтаксис объявления перечисления
следующий:
[атрибуты] [модификаторы] enum [ : базовый
тип] ;
Базовый тип — это тип самого перечисления.
Если не указать базовый тип, то по умолчанию будет
использован тип int.
В качестве базового типа можно выбрать любой целый
тип, кроме char.

23. Организация ввода-вывода данных. Форматирование.

Программа при вводе данных и выводе результатов
взаимодействует с внешними устройствами. Совокупность
стандартных устройств ввода (клавиатура) и вывода (экран)
называется консолью. В языке С# нет операторов ввода и
вывода. Вместо них для обмена данными с внешними
устройствами используются специальные классы. В
частности, для работы с консолью используется
стандартный класс Console, определенный в пространстве
имен System.

24. Вывод данных

Вывод данных на экран может выполняться с помощью
метода WriteLine, реализованного в классе Console.
Существует несколько способов применения данного
метода:
на экран выводится значение идентификатора х
Console.WriteLine(x);
на экран выводится строка, образованная
последовательным слиянием строки «x=», значения x,
строки «у=» и значения у
Console.WriteLine(«x=» + x +»y=» + y);
на экран выводится строка, формат которой задан
первым аргументом метода, при этом вместо параметра
выводится значение x, а вместо – значение y
Console.WriteLine(«x= y=», x, y);

25. Вывод данных

Пусть нам дан следующий фрагмент программы:
int i=3, j=4;
Console.WriteLine(» «, i, j);
При обращении к методу WriteLine через запятую
перечисляются три аргумента: » «, i, j. Первый
аргумент » » определяет формат выходной строки.
Следующие аргументы нумеруются с нуля, так
переменная i имеет номер 0, j – номер 1. Значение
переменной i будет помещено в выходную строку на
место параметра , а значение переменной j — на место
параметра .
В результате на экран будет выведена строка: 3 4.

26. Использование управляющих последовательностей

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

27. Управление размером поля вывода

Первым аргументом WriteLine указывается строка вида
– где n определяет номер идентификатора из списка
аргументов метода WriteLine, а m – количество позиций
(размер поля вывода), отводимых под значение данного
идентификатора. При этом значение идентификатора
выравнивается по правому краю. Если выделенных позиций
для размещения значения идентификатора окажется
недостаточно, то автоматически добавится необходимое
количество позиций.
Пример:
static void Main() < double x= Math.E;
Console.WriteLine(«E=», x);
Console.WriteLine(«E=», x); >

28. Управление размещением вещественных данных

Первым аргументом WriteLine указывается строка вида
– где n определяет номер идентификатора из
списка аргументов метода WriteLine, а ##.### определяет
формат вывода вещественного числа. В данном случае, под
целую часть числа отводится две позиции, под дробную –
три. Если выделенных позиций для размещения целой части
значения идентификатора окажется недостаточно, то
автоматически добавится необходимое количество позиций.
Пример:
static void Main()
< double x= Math.E;
Console.WriteLine(«E=», x);
Console.WriteLine(«E=», x);
>

29. Управление форматом числовых данных

Первым аргументом WriteLine указывается строка
вида :
m>
где n определяет номер идентификатора из списка
аргументов метода WriteLine,
— определяет формат данных,
m – количество позиций для дробной части
значения идентификатора.

30. Спецификаторы

31. Пример

static void Main()
Console.WriteLine(«C Format: \t», 12345.678);
Console.WriteLine(«D Format: \t», 123);
Console.WriteLine(«E Format: \t», 12345.6789);
Console.WriteLine(«G Format: \t»,
12345.6789);
Console.WriteLine(«N Format: \t», 12345.6789);
Console.WriteLine(«X Format: «, 1234);
Console.WriteLine(«P Format: «, 0.9);
>

32. Ввод данных

Для ввода данных обычно используется метод ReadLine,
реализованный в классе Console.
Данный метод в качестве результата возвращает строку, тип
которой string.
static void Main()
< string s = Console.ReadLine(); Console.WriteLine(s); >
Для того чтобы получить числовое значение, необходимо
воспользоваться преобразованием данных.
static void Main()
string s = Console.ReadLine();
int x = int.Parse(s); //преобразование строки в число
Console.WriteLine(x);
>

33. Ввод данных

Для преобразования строкового представления целого числа
в тип int используем метод Parse(), который реализован для
всех числовых типов данных. Таким образом, если
потребуется преобразовать строковое представление в
вещественное, можно воспользоваться методом float.Parse()
или double.Parse().
static void Main() double x = double.Parse(Console.ReadLine());
Console.WriteLine(x);
>

34. Выражения

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

35. Выражения

Инкремент (++) и декремент(—). Эти операции имеют две
формы записи — префиксную, когда операция записывается
перед операндом, и постфиксную — операция записывается
после операнда. Префиксная операция инкремента
(декремента) увеличивает (уменьшает) свой операнд и
возвращает измененное значение как результат.
Постфиксные версии инкремента и декремента возвращают
первоначальное значение операнда, а затем изменяют его.
Рассмотрим эти операции на примере.
static void Main() < int i = 3, j = 4;
Console.WriteLine(» «, i, j);
Console.WriteLine(» «, ++i, —j);
Console.WriteLine(» «, i++, j—);
Console.WriteLine(» «, i, j);
Console.ReadKey(); >

36. Выражения

Операция new.
Используется для создания нового объекта. С помощью ее
можно создавать как объекты ссылочного типа, так и
размерные, например: object z=new object(); int i=new int();
Отрицание.
1. Арифметическое отрицание (-) – меняет знак операнда на
противоположный.
2. Логическое отрицание (!) – определяет операцию
инверсии для логического типа.
static void Main() < int i = 3, j=-4;
bool a = true, b=false;
Console.WriteLine(» «, -i, -j);
Console.WriteLine(» «, !a, !b);
Console.ReadKey();
>

37. Выражения

Явное преобразование типа.
Используется для явного преобразования из одного типа в
другой.
Формат операции:
() ;
static void Main()
int i = -4;
byte j = 4;
int a = (int)j; //преобразование без потери точности
byte b = (byte)i; //преобразование с потерей точности
Console.WriteLine (» «, a, b);
Console.ReadKey();
>

38. Выражения

Умножение (*), деление (/) и деление с остатком (%).
Операции умножения и деления применимы для
целочисленных и вещественных типов данных. Для других
типов эти операции применимы, если для них возможно
неявное преобразование к целым или вещественным типам.
При этом тип результата равен «наибольшему» из типов
операндов, но не менее int. Если оба операнда при делении
целочисленные, то и результат тоже целочисленный.
static void Main()
int i = 100, j = 15;
double a = 14.2, b = 3.5;
Console.WriteLine(» «, i*j, i/j, i%j);
Console.WriteLine(» «, a * b, a / b, a % b);
Console.ReadKey();
>

39. Выражения

Сложение (+) и вычитание (-).
Операции сложения и вычитания применимы для целочисленных
и вещественных типов данных. Для других типов эти операции
применимы, если для них возможно неявное преобразование к
целым или вещественным типам.
Операции отношения ( , >=, ==, !=). Операции отношения
сравнивают значения левого и правого операндов. Результат
операции логического типа: true – если значения совпадают, false
– в противном случае.
static void Main()
< int i = 15, j = 15; Console.WriteLine(iConsole.WriteLine(i <=j); //меньше или равно
Console.WriteLine(i>j); //больше
Console.WriteLine(i>=j); //больше или равно
Console.WriteLine(i==j); //равно
Console.WriteLine(i!=j); //не равно
Console.ReadKey(); >

40. Выражения

41. Выражения

Операции присваивания.
Формат операции простого присваивания (=):
операнд_2 = операнд_1;
В результате выполнения этой операции вычисляется значение
операнда_1, и результат записывается в операнд_2. Можно связать
воедино сразу несколько операторов присваивания, записывая такие
цепочки: a=b=c=100. Выражение такого вида выполняется справа налево:
результатом выполнения c=100 является число 100, которое затем
присваивается переменной b, результатом чего опять является 100,
которое присваивается переменной a. Кроме простой операции
присваивания существуют сложные операции присваивания:
*= умножение с присваиванием,
/= деление с присваиванием,
%= остаток от деления с присваиванием,
+= сложение с присваиванием,
-= вычитание с присваиванием. В сложных операциях присваивания,
например, при сложении с присваиванием, к операнду_2 прибавляется
операнд_1, и результат записывается в операнд_2. То есть, выражение
с += а является более компактной записью выражения с = с + а.

42. Выражения

Вычисление значения выражения происходит с учетом
приоритета операций , которые в нем участвуют.
Если в выражении соседствуют операции одного
приоритета, то унарные операции, условная операция и
операции присваивания выполняются справа налево,
остальные — слева направо.
а = b = с означает a=(b=c),
a+b+c означает (а + b) + с.
Если необходимо изменить порядок выполнения операций,
то в выражении необходимо поставить круглые скобки.

43. Приоритеты операций Операции языка С# приведены в порядке убывания приоритетов. Операции с разными приоритетами разделены

44. Продолжение таблицы приоритетов операций

45. Преобразование типов в выражениях. Иерархия типов

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

46. МАТЕМАТИЧЕСКИЕ ФУНКЦИИ ЯЗЫКА С#

47. ОПЕРАТОРЫ ЯЗЫКА C#

Все операторы можно разделить на четыре группы:
операторы следования,
операторы ветвления,
операторы цикла ,
операторы передачи управления.
Операторы следования
Операторы следования выполняются в естественном порядке:
начиная с первого до последнего. К операторам следования
относятся: выражение и составной оператор. Любое
выражение, завершающееся точкой с запятой, рассматривается
как оператор, выполнение которого заключается вычислением
значения выражения или выполнением законченного действия,
например, вызовом метода
++i; //оператор инкремента
x+=y; //оператор сложения с присваиванием
Console.WriteLine(x); //вызов метода
x=Math.Pow(a,b)+a*b; //вычисление сложного выражения

48. ОПЕРАТОРЫ ЯЗЫКА C#

Составной оператор
Составной оператор или блок представляет собой
последовательность операторов, заключенных в фигурные
скобки <>.
Блок обладает собственной областью видимости:
объявленные внутри блока имена доступны только внутри
данного блока или блоков, вложенных в него.
Составные операторы применяются в случае, когда правила
языка предусматривают наличие только одного оператора, а
логика программы требует нескольких операторов. Если
заключить несколько операторов в фигурные скобки, то
получится блок, который будет рассматриваться
компилятором как единый оператор.

49. ОПЕРАТОРЫ ЯЗЫКА C#. Операторы ветвления

Условный оператор if
используется для разветвления процесса обработки данных
на два направления.
Форма сокращенного оператора if:
if (B) S;
где В – логическое выражение, истинность которого
проверяется;
S – оператор: простой или составной.
При выполнении сокращенной формы оператора if сначала
вычисляется выражение B, затем проводится анализ его
результата: если B истинно, то выполняется оператор S; если
B ложно, то оператор S пропускается. Таким образом, с
помощью сокращенной формы оператора if можно либо
выполнить оператор S, либо пропустить его.

50. ОПЕРАТОРЫ ЯЗЫКА C#. Операторы ветвления

Условный оператор if
Форма полного оператора if:
if (B) S1; else S2;
где B – логическое выражение, истинность которого
проверяется;
S1, S2- оператор: простой или составной.
При выполнении полной формы оператора if сначала
вычисляется значение выражения B, затем анализируется его
результат: если B истинно, то выполняется оператор S1, а
оператор S2 пропускается; если B ложно, то выполняется
оператор S2, а S1 – пропускается. Таким образом, с помощью
полной формы оператора if можно выбрать одно из двух
альтернативных действий процесса обработки данных.

51. ОПЕРАТОРЫ ЯЗЫКА C#. Операторы ветвления

Примеры.
Сокращенная форма c простым оператором if:
if (a > 0) x=y;
Сокращенная форма c составным оператором
if (++i>0)
Полная форма с простым оператором
if (a > 0 || b <0) x=y; else x=z;
Полная форма с составными операторами
if (i!=j-1) < x= 0; y= 1;>else

52. ОПЕРАТОРЫ ЯЗЫКА C#. Операторы ветвления

ОПЕРАТОРЫ ЯЗЫКА C#.
Уровни вложенности
Операторы ветвления

53. ОПЕРАТОРЫ ЯЗЫКА C#. Операторы ветвления

54. ОПЕРАТОРЫ ЯЗЫКА C#. Операторы ветвления

Выражение, стоящее за ключевым словом switch, должно иметь
арифметический, символьный, строковый тип или тип указатель.
Все константные выражения должны иметь разные значения, но
их тип должен совпадать с типом выражения, стоящего внутри
скобок switch или приводиться к нему. Ключевое слово case и
расположенное после него константное выражение называют
также меткой case. Выполнение оператора начинается с
вычисления выражения, расположенного за ключевым словом
switch. Полученный результат сравнивается с меткой case. Если
результат выражения соответствует метке case, то выполняется
оператор, стоящий после этой метки, за которым обязательно
должен следовать оператор перехода: break, goto и т.д. В случае
отсутствия оператора перехода компилятор выдаст сообщении об
ошибке. При использовании оператора break происходит выход из
switch и управление передается оператору, следующему за switch.
Если же используется оператор goto, то управление передается
оператору, помеченному меткой, стоящей после goto.

55. ОПЕРАТОРЫ ЯЗЫКА C#. Операторы ветвления

Пример.
По заданному виду арифметической операции (сложение,
вычитание, умножение и деление) и двум операндам, вывести на
экран результат применения данной операции к операндам.
static void Main() < Console.Write("OPER= ");
char oper=char.Parse(Console.ReadLine()); bool ok=true;
Console.Write(«A= «); double a=double.Parse(Console.ReadLine());
Console.Write(«B= «); double b=double.Parse(Console.ReadLine());
double res=0;
switch (oper) case ‘+’ :
res = a + b; break;
case ‘-‘ :
res = a — b; break;
case ‘*’ : res = a * b; break;
case ‘:’ : if (b != 0) < res = a / b; break; >else < goto default; >
default: ok = false; break; >
if (ok) < Console.WriteLine(" = ", a, oper, b, res); >
else < Console.WriteLine("error"); >>

56. ОПЕРАТОРЫ ЯЗЫКА C#. Операторы ветвления

Если необходимо, чтобы для разных меток выполнялось одно и
то же действие, то метки перечисляются через двоеточие.
case ‘:’ : case ‘/’ : //перечисление меток
if (b != 0) < res = a / b; break; >

57. ОПЕРАТОРЫ ЯЗЫКА C#. Операторы цикла

Операторы цикла используются для организации многократно
повторяющихся вычислений. К операторам цикла относятся:
цикл с предусловием while, цикл с постусловием do while,
цикл с параметром for и цикл перебора foreach.
Цикл с предусловием while . Оператор цикла while организует
выполнение одного оператора (простого или составного)
неизвестное заранее число раз. Формат цикла while:
while (B) S; где B – выражение, истинность которого
проверяется (условие завершения цикла); S – тело цикла (простой
или составной оператор). Перед каждым выполнением тела цикла
анализируется значение выражения В: если оно истинно, то
выполняется тело цикла, и управление передается на повторную
проверку условия В; если значение В ложно – цикл завершается и
управление передается на оператор, следующий за оператором S.
Если результат выражения B окажется ложным при первой
проверке, то тело цикла не выполнится ни разу.

58. ОПЕРАТОРЫ ЯЗЫКА C#. Операторы цикла

Цикл с постусловием do while
Оператор цикла do while также организует выполнение одного
оператора) неизвестное заранее число раз. Однако в отличие от
цикла while условие завершения цикла проверяется после
выполнения тела цикла.
Формат цикла do while: do S while (B);
где В – выражение, истинность которого проверяется;
S – тело цикла (простой или составной оператор). Сначала
выполняется оператор S, а затем анализируется значение
выражения В: если оно истинно, то управление передается
оператору S, если ложно — цикл завершается, и управление
передается на оператор, следующий за условием B. Так как
условие В проверяется после выполнения тела цикла, то в любом
случае тело цикла выполнится хотя бы один раз. В операторе do
while, так же как и в операторе while, возможна ситуация
зацикливания в случае, если условие В всегда будет оставаться
истинным.

59. ОПЕРАТОРЫ ЯЗЫКА C#. Операторы цикла

Пример.
Вывод на экран целых чисел из интервала от 1 до n.
static void Main()
Console.Write(«N= «);
int n=int.Parse(Console.ReadLine());
int i = 1;
do
Console.Write(» «, i++);
//
Console.Write(» «, ++i);
while (i >

60. ОПЕРАТОРЫ ЯЗЫКА C#. Оператор foreach

Оператор foreach применяется для перебора элементов в
специальном образом организованной группе данных.
Удобство этого вида цикла заключается в том, что не
требуется определять количество элементов в группе и
выполнять перебор по индексу.
Синтаксис оператора:
foreach (in) ;
где имя определяет локальную по отношению к циклу
переменную, которая будет по очереди перебирать все
значения из указанной группы; ее тип соответствует
базовому типу элементов группы. Ограничением оператора
foreach является то, что с его помощью можно только
просматривать значения элементов. Никаких изменений ни
с самой группой, ни с находящимися в ней данными
проводить нельзя.

61. ОПЕРАТОРЫ ЯЗЫКА C#. Вложенные циклы

Циклы могут быть простые или вложенные (кратные, циклы
в цикле).
Вложенными могут быть циклы любых типов:
while,
do while,
for.
Каждый внутренний цикл должен быть полностью вложен
во все внешние циклы. «Пересечения» циклов не
допускаются.

62. ОПЕРАТОРЫ ЯЗЫКА C#. Операторы безусловного перехода

В С# есть несколько операторов, изменяющих естественный
порядок выполнения команд:
оператор безусловного перехода goto,
оператор выхода break,
оператор перехода к следующей итерации цикла continue,
оператор возврата из метода return,
оператор генерации исключения throw.
Оператор безусловного перехода goto
Формат: goto ;
В теле того же метода должна присутствовать ровно одна
конструкция вида: : ;
Оператор goto передает управление оператору с меткой.

63. ОПЕРАТОРЫ ЯЗЫКА C#. Операторы break и continue

Оператор break используется внутри операторов цикла и
оператора выбора для обеспечения перехода в точку
программы, находящуюся непосредственно за оператором,
внутри которого находится break.
Оператор перехода к следующей итерации цикла continue
пропускает все операторы, оставшиеся до конца тела цикла, и
передает управление на начало следующей итерации
(повторение тела цикла).
static void Main()
Console.WriteLine(«n=»);
int n = int.Parse(Console.ReadLine());
for (int i = 1; i
Console.Write(» «, i); >
>

64. ОПЕРАТОРЫ ЯЗЫКА C#.

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

65. Пример

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace pr3
< class Program static void Main()
Console.Write(«x= «);
double x=double.Parse(Console.ReadLine());
double y; if (x<0) < y=Math.Pow(Math.Pow(x,3)+1,2); >
else if (x <1) y=0; >
else < y=Math.Abs(x*x-5*x+1); >>
Console.WriteLine («y()=»,x,y);
Console.ReadLine();
> >>

66. Пример

Написать программу, которая выводит на экран
квадраты всех четных чисел из диапазона от А до В
(А и В целые числа, при этом А≤В).
Указания по решению задачи.
Из диапазона целых чисел от А до В необходимо
выбрать только четные числа. Напомним, что
четными называются числа, которые делятся на два
без остатка. Кроме того, четные числа представляют
собой упорядоченную последовательность, в которой
каждое число отличается от предыдущего на 2.
Решить эту задачу можно с помощью каждого
оператора цикла.

67. Пример

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace pr3
< class Program
< static void Main()
< Console.Write("a= ");
int a = int.Parse(Console.ReadLine());
Console.Write(«b= «); int b = int.Parse(Console.ReadLine());
int i; Console.Write(«FOR: «); a = (a % 2 == 0) ? a : a + 1;
for (i = a; i «, i * i); >
Console.Write(«\nWHILE: «);
i = a; while (i «, i * i); i += 2; >
Console.Write(«\nDO: «);
i = a; do < Console.Write(" ", i * i); i += 2; > while (i Console.ReadLine(); > > >

68. Пример

Построить таблицу значений
функции:
для х [a,b] с шагом h.

69. Пример

namespace pr3
< class Program
static void Main()
Console.Write(«a= «);
double a=double.Parse(Console.ReadLine());
Console.Write(«b= «);
double b=double.Parse(Console.ReadLine());
Console.Write(«h= «);
double h=double.Parse(Console.ReadLine());
double y; int i=1;
Console.WriteLine(» «, «#», «x», «у(x)»);
for (double x=a; x <=b; x+=h, ++i) if (x <0)
< y=Math.Pow(Math.Pow(x,3)+1,2); >
else < if (x<1)
y=0;
> else
y=Math.Abs(x*x-5*x+1);
> >
Console.WriteLine(» «,i,x,y); >
Console.ReadLine(); > > >

70. МЕТОДЫ

Метод – это функциональный элемент класса, который реализует
вычисления или другие действия, выполняемые классом или его
экземпляром (объектом).
Метод представляет собой законченный фрагмент кода, к
которому можно обратиться по имени. Он описывается один раз,
а вызываться может многократно.
Совокупность методов класса определяет, что конкретно может
делать класс. Например, стандартный класс Math содержит
методы, которые позволяют вычислять значения математических
функций.
Синтаксис объявления метода:
[атрибуты] [спецификторы] тип_результата имя_метода
([список_формальных_параметров]) < тело_метода;
return значение; >

71. МЕТОДЫ

1) атрибуты и спецификторы являются необязательными
элементами в описании метода.
2) тип_результата определяет тип значения, возвращаемого
методом. Это может быть любой тип, включая типы классов,
создаваемые программистом, а также тип void, который говорит
о том, что метод ничего не возвращает.
3) имя_метода используется для обращения к нему из других
мест программы, является идентификатором.
4) список_формальных_параметров представляет собой
последовательность пар, состоящих из типа данных и
идентификатора, разделенных запятыми. Формальные параметры
— это переменные, которые получают значения, передаваемые
методу при вызове. Если метод не имеет параметров, то
список_параметров остается пустым.
5) return – это оператор безусловного перехода, который
завершает работу метода и возвращает значение, стоящие после
оператора return, в точку его вызова. Тип значения должен
соответствовать типу__результата, или приводиться к нему. Если
метод не должен возвращать никакого значения, то указывается
тип void, и в этом случае оператор return либо отсутствует, либо
указывается без возвращаемого значения.

72. МЕТОДЫ

class Program <
static double Func( double x)
< return x*x; >
static void Main() < Console.Write("a=");
double a=double.Parse(Console.ReadLine());
Console.Write(«b=»); double b=double.Parse(Console.ReadLine());
Console.Write(«h=»); double h=double.Parse(Console.ReadLine());
for (double x = a; x <
double y = Func(x);
Console.WriteLine(«y()=», x, y); >
Console.ReadLine(); > >
В данном примере метод Func содержит параметр х типа double.
Для того, чтобы метод Func возвращал в вызывающий его метод
Main значение выражения x*x (типа double), перед именем метода
указывается тип возвращаемого значения – double, а в теле метода
используется оператор передачи управления – return. Оператор
return завершает выполнение метода и передает управление в точку
его вызова.

73. МЕТОДЫ

В С# предусмотрено четыре типа параметров:
параметры-значения,
параметры-ссылки,
выходные параметры и параметры, позволяющие создавать
методы с переменным количеством аргументов.
При передаче параметра по значению метод получает копии
параметров, и операторы метода работают с этими копиями.
Доступа к исходным значениям параметров у метода нет, а,
следовательно, нет и возможности их изменить.
namespace pr3
< static void Func(int x)
< x += 10; Console.WriteLine("In Func: " + x); >
static void Main()
< int a = 10; Console.WriteLine("In Main: ", a);
Func(a); Console.WriteLine(«In Main: «, a);
Console.ReadLine();
> >
>

74. МЕТОДЫ

При передаче параметров по ссылке метод получает копии адресов
параметров, что позволяет осуществлять доступ к ячейкам памяти
по этим адресам и изменять исходные значения параметров. Для
того чтобы параметр передавался по ссылке, необходимо при
описании метода перед формальным параметром и при вызове
метода перед соответствующим фактическим параметром поставить
спецификатор ref.
static void Func(int x, ref int y)
< x += 10; y += 10;
Console.WriteLine(«In Func: , «, x, y);
>
static void Main() int a=10, b=10;
Console.WriteLine(«In Main: , «, a, b);
Func(a, ref b);
Console.WriteLine(«In Main: , «, a, b);
>

75. МЕТОДЫ

Передача параметра по ссылке требует, чтобы аргумент был
инициализирован до вызова метода .
Однако не всегда имеет смысл инициализировать параметр до
вызова метода, например, если метод считывает значение этого
параметра с клавиатуры, или из файла. В этом случае параметр
следует передавать как выходной, используя спецификатор out.
class Program static void Func(int x, out int y)
< x += 10; y = x;
Console.WriteLine(«In Func: , «, x, y);
>
static void Main()
< int a=10, b;
Console.WriteLine(«In Main: «, a);
Func(a, out b);
Console.WriteLine(«In Main: , «, a, b);
Console.ReadLine(); > >

76. Перегрузка методов

Методы, реализующие один и тот же алгоритм для различного
количества параметров или различных типов данных, могут иметь
одно и то же имя. Использование таких методов называется
перегрузкой методов. Компилятор определяет, какой именно метод
требуется вызвать, по типу и количеству фактических параметров.
Max – возвращает значение наибольшей цифры
static int Max(int a) < int b = 0;
while (a > 0) b) b = a % 10; a /= 10; >
return b;
>
Max – возвращает значение наибольшего из двух чисел
static int Max(int a, int b)
< if (a >b) return a;
else return b;
Max – возвращает значение наибольшего из трех чисел
static int Max(int a, int b, int c)
< if (a >b && a > c) return a;
else if (b > c) return b; else return c; >
static void Main() int a = 1283, b = 45, c = 35740;
Console.WriteLine(Max(a));
Console.WriteLine(Max(a, b));
Console.WriteLine(Max(a, b, c)); > >
>

77. Перегрузка методов

При вызове метода Max выбирается вариант, соответствующий типу
и количеству передаваемых в метод аргументов. Если точного
соответствия не найдено, выполняются неявные преобразования
типов в соответствии с общими правилами. Если преобразование
невозможно, выдается сообщение об ошибке. Если выбор
перегруженного метода возможен более чем одним способом, то
выбирается «лучший» из вариантов (вариант, содержащий меньшее
количество и длину преобразований в соответствии с правилами
преобразования типов). Если существует несколько вариантов, из
которых невозможно выбрать подходящий, выдается сообщение об
ошибке.
Перегрузка методов является проявлением полиморфизма, одного
из основных принципов объектно-ориентированного
программирования. Программисту гораздо удобнее помнить одно
имя метода и использовать его для работы с различными типами
данных, а решение о том, какой вариант метода вызвать, возложить
на компилятор.

78. МАССИВЫ

Массив — набор элементов одного и того же типа, объединенных
общим именем.
С#-массивы относятся к ссылочным типам данных. При этом имя
массива является ссылкой на область кучи (динамической памяти), в
которой последовательно размещается набор элементов
определенного типа.
Выделение памяти под элементы происходит на этапе объявления
или инициализации массива, а за освобождением памяти следит
сборщик мусора.
Массивы реализуются в С# как объекты, для которых разработан
большой набор методов, реализующих различные алгоритмы
обработки элементов массива.

79. МАССИВЫ

Одномерный массив – это фиксированное количество элементов
одного и того же типа, объединенных общим именем, где каждый
элемент имеет свой номер. Нумерация элементов массива в С#
начинается с нуля, то есть, если массив состоит из 10 элементов, то
они будут иметь следующие номера: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9.
Одномерный массив в С# реализуется как объект, поэтому его
создание представляет собой двухступенчатый процесс.
Сначала объявляется ссылочная переменная типа массив, затем
выделяется память под требуемое количество элементов базового
типа, и ссылочной переменной присваивается адрес нулевого
элемента в массиве. Базовый тип определяет тип данных каждого
элемента массива. Количество элементов, которые будут храниться
в массиве, определяется размером массива.

80. МАССИВЫ

Для объявления одномерного массива может использоваться одна
из следующих форм записи:
1)
базовый_тип [] имя_массива;
Например: char [] a;
Объявлена ссылка на одномерный массив символов (имя ссылки а),
которая в дальнейшем может быть использована для: адресации на
уже существующий массив; передачи массива в метод в качестве
параметра; отсроченного выделения памяти под элементы массива.
2)
базовый_тип [] имя_массива = new базовый_тип [размер];
Например: int [] b=new int [10]; Объявлена ссылка b на одномерный
массив целых чисел. Выделена память для 10 элементов целого
типа, адрес этой области памяти записан в ссылочную переменную
b.

81. МАССИВЫ

В C# элементам массива присваиваются начальные значения по
умолчанию в зависимости от базового типа. Для арифметических
типов – нули, для ссылочных типов – null, для символов — символ с
кодом ноль.
3)
базовый_тип [] имя_массива=;
Например: double [] c=;
Объявлена ссылка c на одномерный массив вещественных чисел.
Выделена память под одномерный массив, размерность которого
соответствует количеству элементов в списке инициализации четырем. Адрес этой области памяти записан в ссылочную
переменную с. Значение элементов массива соответствует списку
инициализации.

82. МАССИВЫ

Обращение к элементу массива происходит с помощью индекса:
указывается имя массива и, в квадратных скобках, номер данного
элемента.
Например, a[0], b[8], c[i].
Так как массив представляет собой набор элементов, объединенных
общим именем, то обработка массива обычно производится в цикле.
Вывод массива на экран :
Использование оператора for
static void Main() int[] myArray = < 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 >;
for (int i = 0; i < 10; i++)
Console.WriteLine(myArray[i]);
>>
Использование оператора foreach
static void Main() int[] myArray = < 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 >;
foreach (int elem in myArray)
< Console.WriteLine(elem); >>

83. МАССИВЫ

Ввод элементов массива
Static void Main() int[] myArray; //создаем ссылку на массив
Console.Write(«n= «);
int n=int.Parse(Console.ReadLine());
myArray=new int [n]; //выделяем память под массив
for (int i=0; i < Console.Write("A[]= ",i);
myArray[i]=int.Parse(Console.ReadLine());
>
foreach (int elem in myArray) //выводим массив на экран
< Console.Write("",elem); > >

84. МАССИВЫ

Заполнение массива случайными элементами
Заполнить массив данными можно с помощью генератора
случайных чисел
Для этого используется класс Random:
Static void Main() Random rnd = new Random(); //инициализируем генератор случайных чисел
int[] myArray;
int n = rnd.Next(5, 10); //генерируем случайное число из диапазона [5..10)
myArray = new int[n];
for (int i = 0; i < n; i++)
myArray[i] = rnd.Next(10); // заполняем массив случайными числами
>
foreach (int elem in myArray)
Console.Write(» «, elem);
>>

85. МАССИВЫ

Контроль границ массива
Выход за границы массива в C# расценивается как критическая
ошибка, которая ведет к завершению работы программы и
генерированию стандартного исключения IndexOutOfRangeException.
Рассмотрим следующий фрагмент программы:
int[] myArray = < 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 >;
Console.WriteLine(myArray[10]);
В данном случае описан массив из 10 элементов. Так как нумерация
элементов массива ведется с нуля, то последний элемент имеет
номер 9, и, следовательно, элемента с номером 10 не существует.
В этом случае выполнение программы завершится, о чем на консоль
будет выдано соответствующее сообщение.

86. МАССИВЫ

Массив как параметр
Так как имя массива фактически является ссылкой, то он передается
в метод по ссылке и, следовательно, все изменения элементов
массива, являющегося формальным параметром, отразятся на
элементах соответствующего массива, являющегося фактическим
параметром. При этом указывать спецификатор ref не нужно.
class Program static void Print(int[] a) foreach (int elem in a) < Console.Write("", elem); >
Console.WriteLine(); >
static void Change(int[] a, int n) for (int i = 0; i < n; i++) < if (a[i] < 0) < a[i] = 0; >> >
static void Main() < int[] myArray = < 0, -1, -2, 3, 4, 5, -6, -7, 8, -9 >;
Console.Write(«Исходный массив: «); Print(myArray);
Change(myArray, 10); Console.Write(«Измененный массив: «);
Print(myArray); > >

87. МАССИВЫ

То, что имя массива является ссылкой, следует учитывать при
попытке присвоить один массив другому.
class Program < static void Print(int[] a) foreach (int elem in a) < Console.Write("", elem); >
Console.WriteLine(); >
static void Main()
< int[] one = < 1, 2, 3, 4, 5>; Console.Write(«Первый массив: «);
Print(one);
int[] two = < 6, 7, 8, 9>; Console.Write(«Второй массив: «);
Print(two); one=two; two[0]=-100;
Console.WriteLine(«После присвоения «);
Console.Write(«Первый массив: «); Print(one);
Console.Write(«Второй массив: «); Print(two); > >
Cсылки one и two ссылаются на один массив.
А исходный массив, связанный сo ссылкой one,
оказался потерянным, и будет удален
сборщиком мусора.

88. МАССИВЫ

Массив как объект
В С# массивы реализованы как объекты. Они реализованы на основе
базового класса Array, определенного в пространстве имен System.
Данный класс содержит различные свойства и методы.
Например, свойство Length позволяет определять количество
элементов в массиве.
Пример.
static void Change(int[] a)
for (int i = 0; i < if (a[i] >0) < a[i] = 0; >
>>
Информация о длине массива передается в метод Change неявным
образом вместе с массивом, и нет необходимости вводить
дополнительную переменную для хранения размерности массива.

89. МАССИВЫ

Элемент
Вид
Описание
BinarySearch
статический метод
Осуществляет двоичный поиск в
отсортированном массиве.
Clear
статический метод
Присваивает элементам массива
значения, определенные по умолчанию, т.е
для арифметических типов нули, для
ссылочных типов null.
Copy
статический метод
Копирует элементы одного массива в
другой массив.
CopyTo
экземплярный
метод
Копирует все элементы текущего
одномерного массива в другой массив.
IndexOf
статический метод
Осуществляет поиск первого вхождения
элемента в одномерный массив. Если
элемент найден, то возвращает его
индекс, иначе возвращает значение -1.
LastIndexOf
статический метод
Возвращает число размерностей массива.
Для одномерного массива Rank
возвращает 1, для двумерного – 2 и т.д.

90. МАССИВЫ

Элемент
Вид
Описание
Length
свойство
Возвращает количество элементов в
массиве
Rank
свойство
Возвращает число размерностей массива.
Для одномерного массива Rank
возвращает 1, для двумерного – 2 и т.д.
Reverse
статический метод
Изменяет порядок следования элементов
в массиве на обратный.
Sort
статический метод
Упорядочивает элементы одномерного
массива
Вызов статических методов происходит через обращение к имени
класса, например, Array.Sort(myArray). В данном случае обращаемся
к статическому методу Sort класса Array и передаем данному методу
в качестве параметра объект myArray — экземпляр класса Array.
Обращение к свойству или вызов экземплярного метода производится
через обращение к экземпляру класса, например, myArray.Length или
myArray.GetValue(i).

91. МАССИВЫ

Использование спецификатора params
Чтобы иметь возможность передавать различное количество
аргументов, необходимо ввести параметр метода, помеченный
спецификатором params. Он размещается в списке параметров
последним и является массивом требуемого типа неопределенной
длины. В методе может быть только один параметр помеченный
спецификатором params.
class Program static int F(params int []a) < int s=0; foreach (int x in a) < s+=x; >
return s; >
static void Main() < int a = 1, b = 2, c = 3, d=4;
Console.WriteLine(F()); Console.WriteLine(F(a));
Console.WriteLine(F(a, b)); Console.WriteLine(F(a, b, c));
Console.WriteLine(F(a, b, c, d)); > >
Результат работы программы: Недостаточно аргументов 0 1 3 6 10
Как видим, в метод F может быть передано различное количество
аргументов, в том числе, и нулевое.

92. МАССИВЫ. Двумерные массивы

Многомерные массивы имеют более одного измерения. Чаще всего
используются двумерные массивы, которые представляют собой
таблицы Каждый элемент массива имеет два индекса, первый
определяет номер строки, второй – номер столбца, на пересечении
которых находится элемент. Нумерация строк и столбцов
начинается с нуля.
Объявить двумерный массив можно одним из предложенных
способов:
1)
базовый_тип [,] имя_массива;
Например: int [,] a;
Объявлена ссылка на двумерный массив целых чисел (имя ссылки
а), которая в дальнейшем может быть использована: для адресации
на уже существующий массив; передачи массива в метод в качестве
параметра; отсроченного выделения памяти под элементы массива.

93. МАССИВЫ. Двумерные массивы

2)
базовый тип [,] имя_массива = new базовый_тип [размер1,
размер2];
Например float [,] a= new float [3, 4];
Объявлена ссылка b на двумерный массив вещественных чисел.
Выделена память для 12 элементов вещественного типа, адрес
данной области памяти записан в ссылочную переменную b.
Элементы массива инициализируются по умолчанию нулями.
3)
базовый_тип [,] имя_массива==, … ,
>;
Например: int [,] a= new int [,], >;

94. МАССИВЫ. Двумерные массивы

Обращение к элементу массива происходит с помощью индексов:
указывается имя массива и, в квадратных скобках, номер строки и
через запятую номер столбца, на пересечении которых находится
данный элемент. Например, a[0, 0], b[2, 3], c[i, j]. Так как массив
представляет собой набор элементов, объединенных общим именем,
то обработка массива обычно производится с помощью вложенных
циклов.
При обращении к свойству Length для двумерного массива
получаем общее количество элементов в массиве.
Чтобы получить количество строк, нужно обратиться к методу
GetLength с параметром 0. Чтобы получить количество столбцов –
к методу GetLength с параметром 1. В качестве примера
рассмотрим программу, в которой сразу будем учитывать два факта:
1) двумерные массивы относятся к ссылочным типам данных; 2)
двумерные массивы реализованы как объекты.

95. МАССИВЫ. Двумерные массивы

class Program <
static void Print(int[,] a) <
for (int i = 0; i for (int j = 0; j Console.Write(» «, a[i, j]); > Console.WriteLine(); > >
static void Input(out int[,] a) <
Console.Write(«n= «); int n=int.Parse(Console.ReadLine());
Console.Write(«m= «); int m=int.Parse(Console.ReadLine());
a=new int[n,m];
for (int i = 0; i for (int j = 0; j Console.Write(«a[,]= «, i, j);
a[i,j]=int.Parse(Console.ReadLine()); > > >
static void Change(int[,] a) <
for (int i = 0; i for (int j = 0; j if (a[i, j] % 2 == 0) < a[i, j] = 0;
>
>
static void Main() < int [,]a; Input(out a);
Console.WriteLine(«Исходный массив:»); Print(a); Change(a);
Console.WriteLine(«Измененный массив:»); Print(a); > >

96. МАССИВЫ. Ступенчатые массивы

Структура двумерного ступенчатого массива:
Объявление ступенчатого массива:
тип [][] имя_массива;
Например: int[][]a;
Объявлен одномерный массив ссылок на целочисленные
одномерные массивы. При таком описании потребуется не только
выделять память под одномерный массив ссылок, но и под каждый
из целочисленных одномерных массивов.

97. МАССИВЫ. Ступенчатые массивы

Пример.
Первый способ выделения памяти:
int [][] a= new int [3][];
// Создаем три строки
a[0]=new int [2];
// 0-ая строка ссылается на 2-х элементный одномерный массив
a[1]=new int [3];
// 1-ая строка ссылается на 3-х элементный одномерный массив
a[2]=new int [10];
// 2-ая строка ссылается на 10-х элементный одномерный массив
Другой способ выделения памяти:
int [][] a= ;
Так как каждая строка ступенчатого массива фактически является
одномерным массивом, то с каждой строкой можно работать как с
экземпляром класса Array. Это является преимуществом
ступенчатых массивов перед двумерными массивами.

98. МАССИВЫ. Ступенчатые массивы

class Program < static void Print(int [][] a) <
for (int i = 0; i for (int j = 0; j < a[i].Length; j++) <
Console.Write(» «, a[i][j]); > Console.WriteLine(); > >
static void Input( out int [][] a) <
Console.Write(«n= «); int n = int.Parse(Console.ReadLine());
a = new int [n][]; for (int i = 0; i Console.Write(«введите количество элементов в строке: «, i);
int j = int.Parse(Console.ReadLine()); a[i] = new int[j];
for (j = 0; j < a[i].Length; j++) <
Console.Write(«a[][]= «, i, j);
a[i][j] = int.Parse(Console.ReadLine()); > > >
static void Change (int [][]a) < for (int i = 0; i < Array.Sort(a[i]); >>
static void Main() < int [][]a; Input(out a);
Console.WriteLine(«Исходный массив:»); Print(a); Change(a);
Console.WriteLine(«Измененный массив:»); Print(a); > >

99. Примеры

Дан массив из n целых чисел. Написать программу для подсчета
суммы этих чисел.
class Program < static int[] Input() < Console.Write("n= ");
int n=int.Parse(Console.ReadLine()); int []a=new int[n];
for (int i=0;i
Console.WriteLine(); >
static int Min(int[] a) int min=a[0]; for (int i=0; i > > return min; >
static void Change(int[] a, int x, int y) for (int i = 0; i > >
static void Main() < int[] a = Input(); Console.WriteLine("Исходный
массив:»); Print(a); int min=Min(a); const int n=2; Change(a, min,n);
Console.WriteLine(«Измененный массив:»); Print(a); > >

102. Примеры

Дан массив из n целых чисел. Написать программу, которая
подсчитывает количество пар соседних элементов массива, для
которых предыдущий элемент равен последующему.
class Program static int[] Input() Console.Write(«n= «);
int n = int.Parse(Console.ReadLine());
int[] a = new int[n];
for (int i = 0; i < a.Length; i++)
Console.Write(«a[]= «, i); a[i] = int.Parse(Console.ReadLine());
return a; >
static int F(int[] a) int k=0; for (int i = 0; i < a.Length-1; i++)
if (a[i] == a[i+1]) < ++k; >
>
return k; >
static void Main() int[] a = Input();
Console.WriteLine(«k=», F(a)); > >
>

103. Примеры

Дана квадратная матрица, элементами которой являются вещественные
числа. Подсчитать сумму элементов главной диагонали.
class Program static void Print(int[,] a) for (int i = 0; i < a.GetLength(0); i++)
for (int j = 0; j < a.GetLength(1); j++)
< Console.Write("", a[i, j]);
Console.WriteLine();
> >
static void Input(out int[,] a) Console.Write(«n= «);
int n = int.Parse(Console.ReadLine());
a = new int[n, n];
for (int i = 0; i < a.GetLength(0); i++)
for (int j = 0; j < a.GetLength(1); j++)
Console.Write(«a[,]= «, i, j);
a[i, j] =int.Parse(Console.ReadLine());
>
> >
static int F (int[,] a) int k = 0;
for (int i = 0; i < a.GetLength(0); i++)
k += a[i, i];
>
return k; >
static void Main() int[,] a;
Input(out a);
Console.WriteLine(«Исходный массив:»);
Print(a); Console.WriteLine(«Сум. элем. на глав. диаг. «, F(a)); > >
>

104. Примеры

Дана прямоугольная матрица n×m, элементами которой являются
целые числа. Поменять местами ее строки следующим образом:
первую строку с последней, вторую с предпоследней и т.д.
class Program static void Print(int[][] a) for (int i = 0; i < a.Length; i++)
< for (int j = 0; j < a[i].Length; j++)
< Console.Write("", a[i][j]); > Console.WriteLine();
> >
static void Input(out int[][] a) Console.Write(«n= «);
int n = int.Parse(Console.ReadLine());
Console.Write(«m= «);
int m = int.Parse(Console.ReadLine());
a = new int[n][]; for (int i = 0; i < a.Length; i++) < a[i] = new int[m];
for (int j = 0; j < a[i].Length; j++) < Console.Write("a[][]= ", i, j);
a[i][j] = int.Parse(Console.ReadLine()); >
> >
static void Change(int[][] a) < int[] z; int n = a.Length;
for (int i = 0; i < (n / 2); i++) < z = a[i]; a[i] = a[n-i-1]; a[n-i -1] = z; >>
static void Main() int[][] a; Input(out a); Console.WriteLine(«Исх. массив:»); Print(a);
Change(a); Console.WriteLine(«Измененный массив:»); Print(a); > >

105. Примеры

Дана прямоугольная матрица, элементами которой являются целые числа.
Для каждого столбца подсчитать среднее арифметическое его нечетных
элементов и записать полученные данные в новый массив. class Program static void Print(double[] a) foreach (double elem in a)
Console.WriteLine(); >
static void Print(int[,] a) for (int i = 0; i < a.GetLength(0); i++)
for (int j = 0; j < a.GetLength(1); j++) Console.Write(" ", a[i, j]); >
Console.WriteLine();
> >
static void Input(out int[,] a) Console.Write(«n= «);
int n = int.Parse(Console.ReadLine());
a = new int[n, n];
for (int i = 0; i < a.GetLength(0); i++)
for (int j = 0; j < a.GetLength(1); j++) < Console.Write("a[,]= ", i, j);
a[i, j] = int.Parse(Console.ReadLine()); > > >
static double[] F(int[,] a) double[] b = new double[a.GetLength(1)]; for (int j = 0; j < a.GetLength(1); j++)
> if (k != 0) > return b; >
static void Main() < int[,] a; Input(out a); Console.WriteLine("Исх.массив:");
Print(a); double[] b=F(a); Console.WriteLine(«Иск. массив:»); Print(b); > >

106. Символы char

Тип char предназначен для хранения символа в кодировке
Unicode.
Кодировка Unicode является двухбайтной, т.е. каждый
символ представлен двумя байтами, а не одним, как это
сделано кодировке ASCII, используемой в ОС Windows.
Из-за этого могут возникать некоторые проблемы, если
вы решите, например, работать посимвольно с файлами,
созданными в стандартном текстовом редакторе Блокнот.
Символьный тип относится к встроенным типам данных
С# и соответствует стандартному классу Сhar библиотеки
.Net из пространства имен System.

107. Символы char . Основные методы

108. Символы char

class Program <
static void Main() <
Console.WriteLine(» «,»код», «символ»,
«назначение»);
for (ushort i=0; i <255;i++)
< char a=(char)i; Console.Write("\n", i, a);
if (char.IsLetter(a)) Console.Write(«»,»Буква»);
if (char.IsUpper(a)) Console.Write(«»,»Верхний регистр»);
if (char.IsLower(a)) Console.Write(«»,»Нижний регистр»);
if (char.IsControl(a)) Console.Write(«»,»Управл.символ»);
if (char.IsNumber(a)) Console.Write(«»,»Число»);
if (char.IsPunctuation(a)) Console.Write(«»,»Знак препинан»);
if (char.IsDigit (a)) Console.Write(«»,»Цифра»);
if (char.IsSeparator (a)) Console.Write(«»,»Разделитель»);
if (char.IsWhiteSpace (a)) Console.Write(«»,»Пробел.сим.»);
> >>

109. Символы char

Используя символьный тип можно организовать массив символов
и работать с ним на основе базового класса Array:
class Program static void Print(char[] a) foreach (char elem in a) Console.Write(elem); > Console.WriteLine(); >
static void Main() char[] a =< 'm', 'a', 'Х', 'i', 'M', 'u', 'S' , '!', '!', '!' >;
Console.WriteLine(«Исходный массива:»); Print(a);
for (int x=0;x >
Console.WriteLine(«Измененный массив а:»); Print(a);
Console.WriteLine(); //преобразование строки в массив символов
char [] b=»кол около колокола».ToCharArray();
Console.WriteLine(«Исходный массив b:»); Print(b);
Array.Reverse(b); Console.WriteLine(«Измененный массив b:»);
Print(b); > >

110. Строковый тип string

Тип string, предназначенный для работы со строками символов в
кодировке Unicode, является встроенным типом С#. Ему
соответствует базовый тип класса System.String библиотеки .Net.
Тип string относится к ссылочным типам.
Существенной особенностью данного класса является то, что
каждый его объект – это неизменяемая (immutable)
последовательность символов Unicode. Любое действие со
строкой ведет к тому, что создается копия строки, в которой и
выполняются все изменения. Исходная же строка не меняется.
Такой подход к работе со строками может показаться странным,
но он обусловлен необходимостью сделать работу со строками
максимально быстрой и безопасной. Например, при наличии
нескольких одинаковых строк CLR может хранить их по одному
и тому же адресу (данный механизм называется stringinterning),
экономя таким образом память.

111. Строковый тип string

Создать объект типа string можно несколькими способами:
1) string s; // инициализация отложена
2) string s=»кол около колокола»;
инициализация строковым литералом
3) string s=@»Привет!
4) int x = 12344556; //инициализировали целочисленную переменную
string s=x.ToString(); //преобразовали ее к типу string
5) string s=new string (‘ ‘, 20); //конструктор создает строку из 20 пробелов
6) char [] a=; //создали массив символов
string v=new string (a); //создание строки из массива символов
7) char [] a=;
string v=new string (a, 0, 2)
— создание строки из части массива символов, при этом: 0
показывает с какого символа, 2 – сколько символов использовать для
инициализации.

112. Строковый тип string

С объектом типа string можно работать посимвольно, т.е
поэлементно:
class Program < static void Main() string a ="кол около колокола";
Console.WriteLine(«Дана строка: «, a);
char b=’о’; int k=0;
for (int x=0;x >
Console.WriteLine(«Символ содержится в ней раз», b, k );
>>

113. Строковый тип string. Основные методы

114. Строковый тип string. Основные методы

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

115. Строковый тип string. Основные методы

Очень важными методами обработки строк, являются методы
разделения строки на элементы — Split и слияние массива строк в
единую строку — Join.
class Program < static void Main() string poems = "тучки небесные вечные странники";
char[] div = < ' '>; //создаем массив разделителей
// Разбиваем строку на части,
string[] parts = poems.Split(div);
Console.WriteLine(«Результат разбиения строки на части: «);
for (int i = 0; i // собираем эти части в одну строку, в качестве разделителя
используем символ |
string whole = String.Join(» | «, parts);
Console.WriteLine(«Результат сборки: «);
Console.WriteLine(whole); > >

116. Строковый тип StringBuilder

Строковый тип StringBuilder определен в пространстве имен
System.Text и предназначен для создания строк, значение которых
можно изменять. Объекты данного класса всегда создаются с помощью
явного вызова конструктора класса, т.е. через операцию new. Создать
объект класса StringBuilder возможно одним из следующих способов:
1) //создание пустой строки, размер которой по умолчанию 16
символов
StringBuilder a = new StringBuilder();
2) //инициализация строки и выделение памяти под 4 символа
StringBuilder b = new StringBuilder(«abcd»);
3) //создание пустой строки и выделение памяти под 100 символов
StringBuilder с = new StringBuilder(100);
4) //инициализация строки и выделение памяти под 100 символов
StringBuilder d = new StringBuilder(«abcd», 100);
5) //инициализация подстрокой «bcd», и выделение памяти под 100
символов
StringBuilder d = new StringBuilder(«abcdefg», 1, 3,100);

117. Строковый тип StringBuilder

С объектами класса StringBuilder можно работать
посимвольно:
using System; //подключили пространство имен для работы склассом
StringBuilder using System.Text;
namespace Example class Program static void Main() StringBuilder a = new StringBuilder(«кол около колокола»);
Console.WriteLine(«Дана строка: «, a); char b=’о’;
int k=0; for (int x=0;x >
Console.WriteLine(«Символ содержится в ней раз», b,
k ); > > >

118.

119. Строковый тип StringBuilder

class Program <
static void Main() <
StringBuilder str=new StringBuilder(«Площадь»);
Console.WriteLine(«Максимальный объем буфера: \n «,
str.MaxCapacity); Print(str);
str.Append(» треугольника равна»); Print(str);
str.AppendFormat(» см «, 123.456); Print(str);
str.Insert(8, «данного «); Print(str);
str.Remove(7, 21); Print(str);
str.Replace(«а», «…»); Print(str);
str.Length=0; Print(str); >
static void Print(StringBuilder a) < Console.WriteLine("Строка:
«, a); Console.WriteLine(«Текущая длина строки: «,
a.Length); Console.WriteLine(«Объем буфера: «,
a.Capacity); Console.WriteLine(); > >

120. Строковый тип StringBuilder

Все выполняемые действия относились только к одному объекту str.
Никаких дополнительных объектов не создавалось. Таким образом, класс
StringBuilder применяется тогда, когда необходимо модифицировать
исходную строку. Следует обратить внимание на то, что при увеличении
текущей длины строки возможно изменение объема буфера, отводимого для
хранения значения строки. А именно, если длина строки превышает объем
буфера, то он увеличивается в два раза. Обратное не верно, т.е. при
уменьшении длины строки буфер остается неизменным.

121. Сравнение классов String и StringBuilder

Основное отличие классов String и StringBuilder заключается в
том, что при создании строки типа String выделяется ровно
столько памяти, сколько необходимо для хранения
инициализирующего значения. Если создается строка как объект
класса StringBuilder, то выделение памяти происходит с
некоторым запасом. По умолчанию, под каждую строку
выделяется объем памяти, равный минимальной степени двойки,
необходимой для хранения инициализирующего значения, хотя
возможно задать эту величину по своему усмотрению. Например,
для инициализирующего значения ”это текст” под строку типа
String будет выделена память под 9 символов, а под строку типа
StringBuilder – под 16 символов, из которых 9 будут
использованы непосредственно для хранения данных, а еще 7 –
составят запас, который можно будет использовать в случае
необходимости.

122. Сравнение классов String и StringBuilder

Следующий пример демонстрирует различие между
результатами, которые возвращают эти свойства.
class Program < static void Main(string[] args) string s = "Это текст";
StringBuilder sb = new StringBuilder(s);
Console.WriteLine(«Длина строки \»\» = «, s, s.Length);
Console.WriteLine(«Длина этой строки в StringBuilder = «,
sb.Length);
Console.WriteLine(«Реальная длина StringBuilder = «,
sb.Capacity);
Console.WriteLine(«Максимальная длина StringBuilder = «,
sb.MaxCapacity); Console.Read(); > >

123. Сравнение классов String и StringBuilder

Использование StringBuilder позволяет сократить затраты памяти
и времени центрального процессора при операциях, связанных с
изменением строк. В то же время, на создание объектов класса
StringBuilder также тратится некоторое время и память. Как
следствие, в некоторых случаях операции по изменению строк
оказывается «дешевле» производить непосредственно с самими
строками типа String, а в некоторых – выгоднее использовать
StringBuilder. Пусть нам необходимо получить строку,
состоящую из нескольких слов «текст» идущих подряд. Сделать
это можно двумя способами.
Первый способ (прямое сложение строк):
string str = «»; for (int j = 0; j < count; j++) str += "текст"; >
Второй способ (использование StringBuilder):
StringBuilder sb = new StringBuilder();
for (int j = 0; j < count; j++) < sb.Append("текст"); >

124. Строковый тип StringBuilder

Все выполняемые действия относились только к одному
объекту str. Никаких дополнительных объектов не
создавалось. Таким образом, класс StringBuilder
применяется тогда, когда необходимо модифицировать
исходную строку. Следует обратить внимание на то, что
при увеличении текущей длины строки возможно
изменение объема буфера, отводимого для хранения
значения строки. А именно, если длина строки
превышает объем буфера, то он увеличивается в два раза.
Обратное не верно, т.е. при уменьшении длины строки
буфер остается неизменным.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *