Программирование скриптов в Vim. Часть 7. Словари и объектно-ориентированное программирование
Предыдущие части
Оглавление
- Введение
- Создание словарей
- Доступ к элементам словарей
- Удаление элементов из словарей
- Глубокие копии
- Объектно-ориентированное программирование в Vim
- Комментарии
Введение
Нам осталось рассмотреть последний тип данных, существующий в Vim. Это словари, которые представляют собой хранилище данных, доступ к которым осуществляется по строковому ключу. Подобные типы данных в других языках программирования называют или картами (map), или ассоциативными массивами. Важное свойство словарей заключается в том, что хранящиеся в нем данные не упорядочены, как в списке, поэтому доступ к элементам невозможен по порядковому номеру, а возможен исключительно по ключу.
Словари в Vim очень похожи на одноименный тип данных Python, основное отличие заключается в том, что в Vim в качестве ключа может выступать только строка, но в качестве хранимых данных могут выступать любые типы данных, в том числе и другие словари.
С помощью словарей в Vim можно организовать и объектно-ориентированный подход к программированию скриптов, о чем мы тоже поговорим.
Создание словарей
Для начала разберемся с тем как создаются словари, а создавать их можно двумя способами. Первый способ заключается в том, что при создании словаря сразу указывается некоторое количество пар ключ-значение (ключ-данные), которые сразу же окажутся в словаре. Синтаксис такого подхода очень напоминает Python. Давайте создадим несколько словарей, каждый из которых будет содержать различную информацию о работниках: имя (поле 'name'), должность (поле 'occupation') и зарплата (поле 'salary'):
let worker2 = {'name': 'Иван Иваныч', 'occupation': 'Начальник', 'salary': 'Не говорит'}
echo worker1
echo worker2
Как Vim выводит словари вы можете увидеть на следующем скриншоте.
Такое применение словарей уже напоминает объектно-ориентированный подход, поэтому в дальнейших примерах мы будем придерживаться такой же структуры словарей. Как видите, в одном словаре в качестве данных могут выступать разные типы данных (в данном случае - строки и целые числа).
Второй подход к созданию словарей состоит в том, чтобы сначала создать пустой словарь или словарь с начальными элементами, а потом уже его дополнить нужными данными.
let worker['name'] = 'Петя'
let worker['occupation'] = 'Админ'
let worker['salary'] = 1500
echo worker
Такая запись встречается во многих языках программирования, и думаю, что какие-то пояснения здесь не нужны. Но хочется обратить ваше внимание на порядок отображения этого словаря в Vim командой 'echo':
{'occupation': 'Админ', 'name': 'Петя', 'salary': 1500}
Здесь уже порядок вывода полей не совпадает с тем в какой последовательности мы добавляли их, что еще раз доказывает то, что на этот порядок нельзя рассчитывать - он может быть любым.
Выше говорилось, что в качестве ключа может выступать только строка, это верно, но при написании скриптов мы можем использовать в качестве ключа и целые числа, но они будут автоматически преобразованы в строки. Для того, чтобы в этом убедиться, немного изменим предыдущий пример:
let worker[10] = 'Петя'
let worker[20] = 'Админ'
let worker[30] = 1500
echo worker
В результате на экране мы увидим следующую строку:
{'10': 'Петя', 'occupation': '20', '30': 1500}
Таким образом словари можно использовать в качестве разреженных массивов, когда у элементов массива используются большие индексы, но самих элементов не так много, и большая часть массива остается пустой. В этом случае не надо заводить действительно большой массив (список), а достаточно использовать целые числа в качестве индексов, в результате будет казаться, что мы работаем с огромным массивом.
У второго способа создания (заполнения) словарей есть еще сокращенная форма, которая по записи еще ближе нас подводит к объектно-ориентированному программированию. Эта запись выглядит следующим образом:
let worker.name = 'Петя'
let worker.occupation = 'Админ'
let worker.salary = 1500
echo worker
Эти два примера равносильны.
А теперь вспомните, что у нас в арсенале есть такой замечательный тип данных, как указатели на функции, которые мы тоже можем хранить в словаре, таким образом мы получим и инкапсуляцию (включение) членов, и наличие методов. У таких функций будут дополнительные возможности для работы со словарями, куда они входят, но об этом речь пойдет чуть позже, в последующих разделах.
Доступ к элементам словарей
Итак, создавать словари мы уже умеем, теперь рассмотрим операции, которые мы можем с ними проделывать. Начнем с извлечения данных по ключу, это одна из самых часто используемых операций, ведь мы же не просто так сохраняем данные, надо научиться их и читать. Извлечение данных по синтаксису ничем не отличается от заполнения. Сразу рассмотрим пример:
let worker['name'] = 'Петя'
let worker['occupation'] = 'Админ'
let worker['salary'] = 1500
echo worker['name']
echo worker['occupation']
echo worker['salary']
Или то же самое в объектно-ориентированном стиле:
let worker.name = 'Петя'
let worker.occupation = 'Админ'
let worker.salary = 1500
echo worker.name
echo worker.occupation
echo worker.salary
При работе со словарями иногда необходимо перебирать все хранящиеся в нем элементы, чтобы их как-то обработать. Для подобных операций в Vim существует несколько функций.
- keys() - возвращает список всех ключей в словаре.
- values() - возвращает список всех хранящихся в словаре значений (данные).
- items() - возвращает список списков. Каждый элемент этого списка, содержит список из двух элементов: ключ и соответствующее значение из словаря.
Давайте для начала посмотрим что вернут эти функции для нашего последнего примера.
let worker.name = 'Петя'
let worker.occupation = 'Админ'
let worker.salary = 1500
echo keys (worker)
echo values (worker)
echo items (worker)
Результат работы этого скрипта показан на следующем скриншоте.
Рассмотрим наиболее типичные применения этих функций:
let worker.name = 'Петя'
let worker.occupation = 'Админ'
let worker.salary = 1500
for key in keys(worker)
echo key "-" worker[key]
endfor
Здесь мы получаем список ключей и, перебирая их в цикле for, получаем и выводим также соответствующее ключу значение.
Чтобы на каждой итерации цикла не надо было бы вручную получать соответствующие значения, можем воспользоваться функцией items() и получать поочередно пары ключ-значение одновременно:
let worker.name = 'Петя'
let worker.occupation = 'Админ'
let worker.salary = 1500
for [key, value] in items(worker)
echo key "-" value
endfor
Также есть полезная функция has_key(), которая принимает словарь и строковое значение и возвращает 1 в случае, если в словаре есть элемент под соответствующем ключом:
let worker.name = 'Петя'
let worker.occupation = 'Админ'
let worker.salary = 1500
echo has_key (worker, 'salary')
echo has_key (worker, 'age')
В результате мы увидим:
Удаление элементов из словарей
Теперь мы уже умеем создавать, заполнять словарь содержимым, а также извлекать из него данные. Теперь осталось научиться удалять из словаря элементы, ставшие ненужными. Для этого есть два подхода. Первый - использовать команду unlet, уже используемую нами при удалении переменных. Второй способ - это воспользоваться функцией remove().
Рассмотрим эти подходы по очереди.
let worker.name = 'Петя'
let worker.occupation = 'Админ'
let worker.salary = 1500
unlet worker['salary']
echo worker
В результате на экран будет выведена надпись:
Ту же операцию мы можем переписать в объектно-ориентированном стиле:
let worker.name = 'Петя'
let worker.occupation = 'Админ'
let worker.salary = 1500
unlet worker.salary
echo worker
Теперь воспользуемся функцией remove(), она принимает в качестве параметров словарь и строку, являющуюся ключом к элементу, который должен быть удален. Сама функция remove() возвращает значение, которое хранилось по указанному ключу. Это продемонстрировано в следующем скрипте.
let worker.name = 'Петя'
let worker.occupation = 'Админ'
let worker.salary = 1500
echo remove(worker, 'salary')
echo worker
Результат работы этого скрипта показан на следующем скриншоте.
Также может быть полезна функция empty(), которая принимает в качестве параметра словарь (или список) и возвращает 1, если словарь (или список) пуст, и 0 в противном случае. Как и для списков, чтобы узнать количество элементов в словаре, можно воспользоваться функцией len().
Глубокие копии
Мы уже обсуждали вопросы копирования в третьей части, когда разбирались со списками. Все сказанное там в полной мере относится и к словарям, поэтому лишь кратко вспомним в чем состоит проблема. Дело в том, что при присвоении одной переменной значения другой переменной, которая является словарем, мы, по сути, присваиваем только указатель на словарь. При этом сам словарь остается в единственном экземпляре, и его могут изменить обе переменные.
let worker.name = 'Петя'
let worker.occupation = 'Админ'
let worker.salary = 1500
let worker2 = worker
echo worker
echo worker2
let worker2.salary = 2000
echo worker
echo worker2
В результате мы увидим:
Чтобы создать обычную (не глубокую) копию словаря можно воспользоваться функцией copy():
let worker.name = 'Петя'
let worker.occupation = 'Админ'
let worker.salary = 1500
let worker2 = copy (worker)
echo worker
echo worker2
let worker2.salary = 2000
echo worker
echo worker2
В данном случае это помогло:
Однако, если в словаре будет храниться другой словарь или список, то в копию словаря попадет не заново скопированный словарь/список, а лишь указатель на него, в результате внутри обоих словарей будет храниться указатель на один и тот же внутренний словарь/список:
echo foo
let spam = copy (foo)
let spam.key2.bar = 'new_val'
echo foo
echo spam
Чтобы указать интерпретатору, что нужно копировать и внутренние списки/словари, нужно воспользоваться функцией deepcopy():
echo foo
let spam = deepcopy (foo)
let spam.key2.bar = 'new_val'
echo foo
echo spam
В результате копирования создается так называемая "глубокая копия", в которой все внутренние структуры скопированы рекурсивно (максимум до 100 уровня вложенности):
Объектно-ориентированное программирование в Vim
Итак, для доступа к элементам словаря в Vim можно использовать запись, напоминающую объектно-ориентированное программирование (ООП), что мы уже делали. Но ведь словари могут хранить и указатели на функции, что еще ближе нас подводит к ООП. Мы можем написать следующий код:
echo 'Привет!'
endfunction
let worker = {}
let worker.name = 'Петя'
let worker.occupation = 'Админ'
let worker.salary = 1500
let worker.hello = function ('s:hello')
call worker.hello()
В результате на экране мы увидим приветствие:
Привет!
Но такой вызов функции хоть и может полезен тем, что функция и объект-словарь становятся связанными, но от ООП мы обычно ожидаем большего. В данном примере основная проблема состоит в том, что связь функции и объекта односторонняя: объект имеет доступ к функции, а функция к своему владельцу - нет, поэтому функция не может обращаться к членам объекта. А нам, допустим, хотелось бы, чтобы эта функция не просто писала приветствие, но и обращалась к объекту по имени через член name.
Мы можем в явном виде передать в функцию наш словарь:
echo printf ('Привет, %s!', a:self.name)
endfunction
let worker = {}
let worker.name = 'Петя'
let worker.occupation = 'Админ'
let worker.salary = 1500
let worker.hello = function ('s:hello')
call worker.hello(worker)
Теперь скрипт выведет строку:
Привет, Петя!
Но согласитесь, что такое решение не особо красивое, мы вынуждены дважды указывать объект worker при вызове функции. Было бы здорово, если бы параметр self передавался неявно, как это сделано с одноименным параметром в Python или this в C++. И разработчики Vim дали такую возможность, для этого достаточно после объявления функции добавить ключевое слово dict, тогда в функцию неявно будет передаваться указатель на объект-словарь, из которого функция была вызвана. Имя параметра так и останется self, с той лишь разницей, что перед именем этой переменной не нужно ставить префикс a:, показывающий, что это аргумент функции.
Теперь предыдущий пример у нас будет выглядеть следующим образом:
echo printf ('Привет, %s!', self.name)
endfunction
let worker = {}
let worker.name = 'Петя'
let worker.occupation = 'Админ'
let worker.salary = 1500
let worker.hello = function ('s:hello')
call worker.hello()
Кроме того, существует возможность еще немного сократить запись, указав имя объекта при объявлении функции:
let worker.name = 'Петя'
let worker.occupation = 'Админ'
let worker.salary = 1500
function! worker.hello() dict
echo printf ('Привет, %s!', self.name)
endfunction
call worker.hello()
Здесь мы использовали запись function! worker.hello() dict, что позволило избавиться от строки let worker.hello = function ('s:hello').
Теперь мы можем создавать полноценные объекты с членами и методами.
На этот раз не будет какого-либо жизненного примера, поскольку мне не хватило фантазии придумать какой-нибудь короткий пример, где бы использование словарей не было бы притянуто за уши, а использование объектно-ориентированный подход чаще всего оправдывает себя в сравнительно крупных скриптах. Но из-за этого расстраиваться не стоит, теперь мы разобрали все типы данных, существующие в Vim, поэтому следующая часть статьи будет посвящена полностью написанию плагинов.
Часть 8. Более подробно о плагинах
Вы можете подписаться на новости сайта через RSS, Группу Вконтакте или Канал в Telegram.