Моделирование объектов в Swift
Другие статьи из серии «Конспекты начинающего программиста»:
- Как я поборол рекурсию: учебный детектив;
- Опциональные типы данных в языке Swift;
- Кортежи (tuples) в Swift.
Моделирование объектов в Swift
В статье я рассмотрю тему «Моделирование объектов предметной действительности в Swift» на примере задачи «Место буквы в алфавите».
Метаданные: эта статья для начинающих программистов, особенно для тех, которые изучают Swift с нуля. Поэтому логика изложения может отличаться от обычных учебников, и ассоциации для понимания и запоминания тоже могут быть другими.
Учебная тема: «Строки и символы». Задача взята из учебного курса Алексея Скутаренко.
Условия задачи
- Создайте строку английский алфавит, все буквы малые от a до z;
- Задайте константу - один из символов этого алфавита;
- Используя цикл for, определите под каким индексом в строке находится этот символ.
Исходная идея (учебная)
По ходу решения этой задачи как-то непроизвольно захотелось проанализировать процесс перехода от объектов внешней действительности к объектам языка программирования. Т. е. у нас есть условия задачи, как теперь эту задачу решать? Где тут переменные, типы данных, какие использовать процедуры, алгоритмы и т.д.?
Два слова о моделировании
Моделирование - это создание моделей, а модель - это копия какой-то реальной вещи. Это значит, что есть нечто в реальном мире, а мы берём и делаем его копию. Эта копия часто бывает уменьшенной и упрощённой; мы не можем воспроизвести всё, что есть во внешнем мире, мы воспроизводим только некоторые элементы. В реальном мире есть свои объекты, сущности; в модели мы их заменяем объектами, сущностями, которые есть в нашем языке программирования. И есть ещё действия, т.е. мы должны в своей модели воспроизвести и какие-то действия. Внешний, реальный мир, нередко также называют предметной областью. В коммерческих программах - это та область, в которой разбираются соответствующие специалисты - бухгалтера, финансисты, менеджеры и пр.
В целом все программы так или иначе строят модели каких-то объектов, сущностей из внешнего мира, из предметной области. И наверное, не ошибусь, если скажу, что уметь подбирать объекты и сущности в языке программирования для отображения процессов во внешнем мире, есть одна из самых важных вещей для программиста. Этим занимаются архитекторы программ. В общем, у программиста есть конструкторский набор и надо из него построить что-то, что посложнее, чем самолётики и кораблики в детском конструкторе. Нет образца, нет инструкций, их ещё тоже надо создать.
Итак, переходим к задаче.
Шаг 1. Искомая буква
Первым делом создадим возможность для ввода буквы, которую потом будем искать в алфавите. Это будет константа и эта константа с типом значения Character.
let letter : Character = "a"
Важно: тип данных надо обозначить явно, поскольку если мы введём только одну букву, то Swift по умолчанию посчитает её строкой (String).
Шаг 2. Создаем объект «Алфавит»
Сначала находим сам алфавит. Например, есть такая, достаточно привычная запись с точки зрения обычного человека:
a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z
Но использовать алфавит в таком виде будет неправильно. Ведь здесь кроме самих букв есть ещё и другие символы: запятая и пробел. И если мы начнём искать индекс буквы, то получим совсем другие цифры, ведь в такой записи будет не 26 букв, а 76 символов. Поэтому убираем лишнее и получаем:
abcdefghijklmnopqrstuvwxyz
Затем оформляем алфавит как константу. Здесь уже тип значения можно не указывать, Swift сам определит, что это строка (String):
let alphabet = "abcdefghijklmnopqrstuvwxyz"
Шаг 3. Моделируем действие поиска
В условиях задачи сказано, что поиск нужно выполнить с помощью цикла for in. Его синтаксис в принципе понятен:
for … in … {}
Но алфавит у нас записан строкой как одно значение, отсюда и в цикле он будет выдавать только одно значение. И второй момент: искать мы будем отдельную букву, т.е. символ. Поэтому предварительно нужно строку разложить на отдельные символы. Это можно сделать с помощью сабскрипта characters.
Например, если записать такую команду,
alphabet.characters.count
то она выдаст количество символов в строке - 26. Здесь мы используем 2 сабскрипта - characters и count. Первый преобразовывает строку в символы (тип String в тип Character), а второй делает подсчёт символов. Теперь это можно вносить в цикл. Начало цикла будет выглядеть так:
for number in alphabet.characters {}
Сабскрипт count в данном случае не нужен, поскольку мы будем искать одну конкретную букву.
Здесь же у нас появляется слово number, на котором стоит задержать внимание на некоторое время. Во-первых, именно оно будет показывать число, цифру, которое обозначает место искомой буквы в алфавите. Во-вторых (и это важно), в свойствах цикла сказано, что на этом месте (т.е. после слова for) мы неявно создаём и инициализируем константу. Это очень важное свойство цикла. Т.е. неявно создаётся, объявляется константа без слова let. И здесь же она сразу и инициализируется. В качестве инициализирующих значений в нашем примере выступают символы, полученные из строки в константе alphabet.
Слово number в данном примере - это имя итератора (спасибо за подсказку юзеру paketik с форума swiftbook.ru). Это действие, которое внутри цикла по очереди перебирает значения. Это такое правильное имя. Мне же больше понравилось название «коллективное имя». Это общее имя константы для всех значений, которые в неё входят в цикле. Может оно не совсем правильное и точное, но помогло в своё время понять и запомнить смысл синтаксиса цикла.
И далее, если мы в тело цикла запишем нашу любимую команду print и вставим в скобки это наше коллективное, общее имя константы для всех значений в цикле - number, то в консоль выведутся все буквы алфавита, каждая в отдельной строке. А песочница покажет количество итераций, повторений действия в цикле - 26 Times.
for number in alphabet.characters
{
print(number)
}
Но для данной конкретной задачи это действие не нужно. Нужно найти одну конкретную букву и узнать номер её места в алфавите. Или как сказано в условиях задачи - «определите под каким индексом в строке находится этот символ». Другими словами, нужно определить индекс буквы среди символов строки. Кажется, мы должны внутри цикла использовать условие if и написать так:
for number in alphabet.characters
{
if number == letter
{
…
}
}
И далее, в теле условия if записать команду, которая выдаст индекс искомой буквы (символа).
Но вот здесь оказывается, что напрямую мы этого сделать не можем. И связано это с тем, что символы в Swift используются как значения Юникода. И там есть такие символы, которые могут использовать как одно значение, так и два. Например, русская буква "ё". В одном случае, это может быть одно значение Юникода, в другом - два: буква "е" и две точки сверху. Отсюда у них разная величина, разное место в памяти и соответственно задача подсчёта индексов для таких символов выходит за рамки простого алгоритма. Поэтому в Swift нет прямого вычисления индекса для определённого символа, а есть сабскрипты для определения первого индекса, последнего и др. Но вот прямо, чтобы найти индекс конкретного символа, такого пока нет.
-
let letter : Character = "a"
-
let alphabet = "abcdefghijklmnopqrstuvwxyz"
-
var index = 0
-
-
for number in alphabet.characters
-
{
-
index += 1
-
if number == letter
-
{
-
print("The letter number \(number) in the alphabet is \(index)")
-
}
-
}
Отдельный вопрос: какое стартовое значение установить в переменную index в 3-й строке? Мы помним, что программа начинает счёт индексов с 0, а в обычной жизни мы начинаем счёт с 1. По идее, поскольку это наш собственный счётчик (т.е. искусственный), мы можем установить и 1, чтобы было привычнее и меньше путаться. Тогда внутри цикла эту переменную нужно переместить на место после условия if. В противном случае наш счётчик сразу присвоит себе значение 2 и только потом начнёт проверять первый символ в алфавите. Т.е. вся последовательность цифр сместится на единицу.
Два варианта приемлемы, не зря ведь говорят, что в программировании можно решить задачу разными способами. Но мне в данном случае больше импонирует задать счётчику (переменной index) стартовое значение 0, как и в нативном счётчике самой программы. Чисто для однообразия, раз все индексы начинаются с нуля, то пусть и здесь он тоже начинается с нуля.
И последний штрих: в строке 10, там где мы печатаем результат, используется интерполяция. Конечно, это только для "украшательства"; можно было обойтись и без неё и напечатать просто print(index). Но поскольку задача учебная, то тренируемся ещё в одном методе.
- На первых порах это бывает путается в памяти, поэтому можно использовать такие запоминалки. Слово "интерполяция" более простое, но реализация у него более сложная. Здесь в одну строку могут объединяться разные типы значений (String, Int и др.) и используются обратные слэши, дополнительные скобки.
- И наоборот, слово "конкатенация" более сложное для восприятия, но реализация там более простая. Конкатенация используется, когда все элементы имеют одинаковый тип String и метод просто объединяет их в одну строку. Здесь достаточно использовать оператор плюс (+).
- И ещё, запомнить само слово "конкатенация" можно так: на джавараш предлагают разделить его на 3 части: кон, кате и нация. Отсюда, можно придумывать всякие запоминалочные фразы, типа - "на кону нация котов", "нация котов любит плюсоваться" и т.д.
Ошибки и решения по ходу
Поскольку это материал учебный, для большей пользы стоит обратить внимание и на ошибки, которые происходили в процессе решения задачи. Моей главной ошибкой сначала было желание использовать алфавит в том виде, как он обычно пишется, т.е. с запятыми и пробелами:
a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z
Например, сделать из него константу сразу:
let alphabet = a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z
Но Swift выдал ошибку, что используются не разрешенные идентификаторы. Последующие попытки использовать тюпл или массив дали тот же результат и ту же ошибку:
let alphabet = (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z)
let alphabet = [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z]
Ошибки конечно глупые, но на первых порах, когда хочешь сам, по памяти, не заглядывая в учебники, что-то сделать, то бывает и такое. Путём проб и ошибок, путём набивания шишек, понимаешь и запоминаешь многие вещи.
И второй момент: объекты внешней действительности порою надо преобразовывать, чтобы их использовать в качестве объектов, типов данных в программировании. Так и здесь, есть объект внешней действительности - алфавит. Обычно, как отмечалось выше, он записывается так:
a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z
Но чтобы сделать из него объект, сущность в программировании, нужно его преобразовать, подвергнуть обработке. В свою очередь, чтобы знать, как и во что его преобразовывать, надо заранее представлять себе, что у нас есть в нашем конструкторе, какие детали, какие объекты и сущности есть в языке программирования. Мы заранее сопоставляем объекты из внешней действительности с объектами в языке программирования и смотрим, какие детали конструктора мы можем использовать не только в начале процесса решения задачи, но и в его конце. Может быть когда-нибудь этот процесс удастся даже каким-то образом автоматизировать; если не целиком, то хотя бы частично. А для этого нужно сначала как-то его зафиксировать и попытаться систематизировать выполняющиеся операции.
Итак, в данном конкретном случае мы берём такую сущность как константа/переменная, определяем, что это будет константа. Затем смотрим, какого типа значения это будут. Ясно, что это не цифры, а буквы, значит это будет текст. Дальше - это будет строка String или символы Character. И поскольку Character может содержать в себе только один символ, то алфавит может быть смоделирован только в виде типа String. Затем нужно это записать с помощью синтаксиса. Строка записывается с помощью кавычек. Но дальше есть два варианта синтаксиса: делать кавычки на все буквы сразу или на каждую букву по отдельности.
Вариант первый:
let alphabet = "a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z"
Здесь кавычки общие и все значения внутри них. Это строка, содержащая в себе одно значение, т.е. все символы как одно значение. И как указывалось ранее, здесь кроме символов, обозначающих буквы, содержатся запятые и пробелы. Для данной задачи они лишние, поэтому их удаляем. В итоге получаем:
let alphabet = "abcdefghijklmnopqrstuvwxyz"
Далее решение задачи проходит по алгоритму, описанному выше.
Второй способ для создания строковой константы: каждую букву заключить в свои индивидуальные кавычки. Например:
let alphabet = "a", "b", "с" // и так до буквы z
Этот способ бракуется сразу по причине того, что надоест делать по две кавычки каждой букве. Лень - двигатель прогресса! Поэтому, конечно, более удобен предыдущий способ, когда все символы помещаются внутри общих кавычек.
Но даже если бы мы набрались терпения и сделали все символы в кавычках, то такая запись не пройдёт и по другой причине. А именно, появится сообщение об ошибке с предложением использовать pattern (шаблон). Т.е. действительно, здесь в одной переменной мы задаём сразу 3 значения, когда правильно будет для одной переменной одно значение.
Вариантов решения может быть три. Можно, использовать как раз ту же конкатенацию. Т.е. склеить значения в одну строку (три значения в одно):
let alphabet = "a" + "b" + "с"
Получаем значение "abc". А если будут все буквы, то получится строка аналогичная той, которая с общими кавычками.
Второй вариант - использовать тюпл:
let alphabet = ("a", "b", "с")
Наконец, третий - можно оформить алфавит как массив:
let alphabet = ["a", "b", "с"]
Таким образом, мы смоделировали объект внешней действительно "алфавит" с помощью нескольких разных объектов из языка Swift. Дополнительно, по ходу мы также выполнили некоторую трансформацию исходного объекта (убрали запятые, пробелы, добавили кавычки). Вполне возможно, что и в реальных проектах также происходит не только моделирование внешнего объекта имеющимися средствами языка программирования, но и некоторая трансформация исходного объекта.
Какой из этих готовых объектов выбрать, зависит от других объектов, которые будут использоваться при решении задачи. По идее каждый из них может использоваться, другое дело, если в сочетании с определёнными другими объектами, сущностями. Например, склеенная строка вполне подойдёт в том варианте решения задачи, который приведён ранее.
А вот массив может иметь своё, оригинальное решение.
Вариант решения задачи с помощью массива
Если мы не поленимся и оформим каждую букву алфавита в индивидуальные кавычки, то решить задачу можно иначе и где-то даже проще. Оформляем алфавит в виде массива, а затем используем специальную функцию enumerated(), которая сама определит индекс буквы в массиве (алфавите). Отметим, что эта функция используется только для массива! Для обычной строковой константы она не подойдёт. Но и в этом решении есть свои сюрпризы. Так, если мы возьмём предыдущее решение и просто заменим одну функцию на другую, то получим ошибку. См. код ниже:
-
let letter : Character = "a"
-
let alphabet = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m","n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]
-
-
-
for (number, value) in alphabet.enumerated()
-
{
-
-
if value == letter
-
{
-
print("Индекс буквы \(value) в английском алфавите равен \(number + 1)")
-
}
-
}
Что изменилось в коде? Из 3-й и 7-й строки убран индекс. Во 2-й строке вместо строковой константы инициализирован массив. В 5-й строке, в цикле "имя итератора" или "коллективное имя" для значений массива задано тюплом, где number будет подхватывать индекс, а value - букву. Здесь же после имени массива убран сабскрипт characters и вместо него вставлен enumerated(). Наконец, в 10-й строке изменён текст для печати, чтобы было более наглядно видно, что откуда берётся. Здесь же добавляем к индексу в number единицу, чтобы уйти от машинного индексирования и дать цифру более привычную для пользователей.
Теперь об ошибке: она возникает в 8-й строке, там где задаётся условие if. И состоит эта ошибка в том, что бинарный оператор == не применяется к операндам типов String и Character. И тут мы вспоминаем, что в самой первой строке искомая буква задана как Character, т.е. как одиночный символ. Отсюда и конфликт. И сразу же становится понятным и следующий момент: внутри массива мы имеем набор значений не с типом Character, т.е. не отдельных символов, а набор строковых значений String.
Ещё раз: внутри массива мы видим набор отдельных букв, но эти буквы не одиночные символы, как можно было бы подумать, не Character. Это набор строк, т.е. каждая буква и соответственно весь массив в целом имеют тип значения String. В этом легко убедиться, наведя курсор на название массива с нажатой клавишей Option (Alt). Появится всплывающая подсказка, что это тип String.
Почему это именно так? Вспоминаем, что тип Character может содержать только один элемент, а здесь их много. Значит это не Character, а String. Таким образом, чтобы условие if сработало, нам нужно, чтобы оба операнда в нём были одного типа. Привести массив к Charater мы не можем, значит изменим тип искомой буквы в первой строке. Конечный код получается следующий: просто убран тип Character в 1-й строке и задана обычная константа.
-
let letter = "a"
-
let alphabet = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m","n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]
-
-
-
for (number, value) in alphabet.enumerated()
-
{
-
-
if value == letter
-
{
-
print("Индекс буквы \(value) в английском алфавите равен \(number + 1)")
-
}
-
}
Таким образом, мы получили ещё одно решение, отличающееся от предыдущего тем, что здесь индекс определяет сама функция, нам не надо его присваивать.
И еще один интересный момент, связанный с массивом и с тем, что ранее мы показали, что алфавит можно оформить и как тюпл. Если задаться вопросом, для какого способа решения данной задачи может подойти такой тюпл, то на первый взгляд чисто внешне он вроде бы ближе к варианту с сабскриптом characters. Но на самом деле, как оказывается, он ближе к решению через массив. Причём связь не прямая, а обратная. Так в блоге Владимира Бабина нагуглил такую информацию, что функция enumerated() «из элементов массива делает кортежи (tuple)», которые включают в себя индексы элементов и их значения.
Несколько заключительных соображений
Итак, мы уже в принципе понимаем, что язык программирования - это своего рода конструктор, т.е. набор различных деталей (объектов), с помощью которых мы создаём модели, упрощённые копии объектов внешней действительности. Возможно ли их как-то перечислить, как в том же конструкторе, где обычно есть список всех деталей? Наверное сложно. Во всяком случае для начинающего программиста так точно нереально. Но тогда хотя бы систематизировать, сгруппировать по каким-то признакам, чтобы более наглядно представлять себе, что такое объекты и что такое процесс написания программы, начиная от постановки задачи и до выбора средств программирования.
В этом отношении очень удобно воспользоваться примером MS Access. В этой базе данных от Microsoft есть четыре основных объекта. Их даже лучше называть макро-объектами, т.е. большими, глобальными, базовыми:
- Таблица, в которой хранятся данные, - базовый, опорный объект;
- Форма, которая используется для ввода данных, а также может быть использована для просмотра данных в таблице и для других действий;
- Запрос, с помощью которого можно выполнять определённые действия с данными, например, делать выборки, вычисления, изменять данные и т.д.;
- Отчёт, который представляет данные в удобном для просмотра виде.
В сложных системах жёстких смысловых разделительных границ между этими макро-объектами нет. Например, форма может быть представлена и в виде таблицы, в ней же могут показываться результаты запроса, запрос может быть похож на отчёт и т.д. Но техническая реализация программными средствами у них явно разная.
Идем дальше: довольно похожий набор макро-объектов есть в отечественной программе для бизнеса - 1С. Здесь в качестве таблиц фигурируют справочники, которые нужны для хранения постоянной, не часто изменяемой информации. Например, справочник товаров, справочник клиентов, валют и т.д.
Следующий важный объект в 1С - документ. Это такой объект, который производит определённые действия. Например, пришёл товар на склад, есть документ оприходования, который фиксирует название и количество товара. Что очень важно, он добавляет информацию о товаре в соответствующий справочник. В итоге, если запросить остаток по складу, то он уже будет другой, чем до поступления товара. Сразу же видно, что макро-объект "Документ" в 1С похож по своим функциям на макро-объект "Запрос" в MS Access. Есть в 1С и другие макро-объекты, похожие на макро-объекты в Access. Они почти также и называются: формы для ввода данных и отчёты. Отсюда, невольно напрашивается мысль, что этот набор макро-объектов какой-то общий для разных языков программирования. Каждый из этих объектов обладает определённым набором свойств и методов, которые его отличают от других.
Если таким образом подойти к языку Swift, то найти что-то похожее будет не очень легко, особенно если учесть статус "начинающий программист". Тем не менее некоторые моменты можно всё-таки отметить. Например, в Swift явно выделяются 2 большие группы объектов. Можно их условно назвать статичные и динамичные. Статичные служат в основном для хранения информации, а динамичные выполняют какие-то действия с этой информацией, изменяют её. Например, константы и переменные, объекты, классы, структуры, множества, тюплы, массивы, коллекции, словари - это всё статичные объекты, которые определённым образом организуют хранение информации. Из того, что уже известно в ходе обучения, к динамичным можно отнести цикл for, условие if, функции. Они производят определённые действия. Или взять те же классы и объекты; каждый из них описывается двумя группами характеристик: методы как действия (динамика) и свойства как статика. К этим двум группам - статика и динамика - можно добавить ещё и третью - интерфейс, внешний вид. Его функции довольно специфичны и отличаются и от хранения данных, и от действий с данными. Интерфейс представляет удобную форму для восприятия данных. Т.е. мы оперируем какими-то визуальными объектами и с их помощью создаём "переходник", "мостик" между восприятием пользователя и кодом программы.
Нетрудно заметить, что эта третья группа макро-объектов в Swift аналогична формам и отчётам в той же Access. Точно также эти объекты в Swift служат для более удобного ввода информации и для более удобного восприятия результатов запросов, которые выполняются внутри программы с помощью динамичных объектов, действий, методов. Таким образом, в конструкторе Swift мы имеем три большие группы макро-объектов: статичные (хранение информации), динамичные (преобразование информации) и визуальные (визуальное представление информации).
Заключение
Вот такие ассоциации появились у начинающего программиста в ходе решения задачи. Получился не совсем конспект или шпаргалка по теме изучения. Но такой опыт позволяет глубже понять и закрепить пройденный материал, постепенно формировать своё видение вопросов, связанных с программированием.
Скажу честно, первое решение, которое с сабскриптом characters, найдено не самостоятельно, а через форум таких же программистов, изучающих Swift. C одной стороны это, конечно, не самый лучший вариант с точки зрения самостоятельности. Но программисты так и развиваются: не только сами находят решения, но и активно гуглят в поисках возможных вариантов.
Второе решение через массив нашёл самостоятельно в учебнике. Оно хоть и не самое жизнеспособное для данной конкретной задачи, но вполне может пригодиться в каких-то других случаях. Надо сказать, что такая, казалось бы, простая тема «Строки и символы» оказалась довольно интересной в плане её применения для решения задач.