Дженерик что это программирование

Зачем Go нужны дженерики

Авторизуйтесь

Зачем Go нужны дженерики

Это статья о том, как введение дженериков может изменить Go и почему это будет целесообразным шагом. Здесь также будут затронуты изменения, которые придётся внести в язык для выполнения задуманного.

По отзывам пользователей, именно отсутствие дженериков — одна из главных проблем Go.

Зачем нужны дженерики?

Что же такое «добавление дженериков» и зачем это вообще нужно?

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

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

Предположим, что это целочисленный массив.

Простейшая функция, но и в ней можно обнаружить ошибку.

При определении переменной, содержащей индекс последнего элемента, надо уменьшить размер среза на 1.

Теперь создадим функцию для осуществления такой же операции с массивом строк.

В языках с динамической типизацией, таких как Python или JavaScript, функцию можно написать, не обращая внимания на определение типа элементов. На Go такой способ не сработает, поскольку это язык со статической типизацией и требует точного указания типа среза и типа его элементов. Большая часть других языков со статической типизацией, таких как C++, Java, Rust или Swift, поддерживает дженерики, чтобы устранить это затруднение.

Как Go обходится без дженериков сейчас

Интерфейсы

Создать функцию, которая может обрабатывать различные типы данных в Go можно, используя интерфейсный тип. Для этого требуется определить методы для тех типов срезов, которые вы намереваетесь передавать функции. Именно так работает функция sort.Sort из стандартной библиотеки.

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

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

Методы по умолчанию

Ещё один способ, при котором нам не придётся самостоятельно писать методы — определение в самом языке методов по умолчанию для некоторых типов. В настоящее время такой подход не поддерживается в Go, но, к примеру, в языке можно было бы определить для каждого массива метод Index, возвращающий элемент. Однако, чтобы использовать этот способ на практике, метод должен возвращать пустой интерфейсный тип, а в таком случае мы потеряем все преимущества статического типирования. Более того, мы не смогли бы создать функцию, которая принимает два среза с элементами одного типа или map с элементами определённого типа и возвращает срез. Статическая типизация облегчает Go создание больших программ. И не хотелось бы терять это преимущество ради плюсов, которые дают дженерики.

Пакет reflect

Ещё один вариант — написать функцию Reverse с помощью пакета reflect, но это настолько неудобно и непрактично, что почти никто так не делает. Кроме того, этот подход требует чёткого определения типов и не допускает их статической проверки.

Генераторы кода

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

Не слишком ли много лишней работы ради функций, которые отличаются только типом элемента? Должен быть способ намного лучше!

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

Что могут привнести в Go дженерики

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

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

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

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

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

Кроме того, как упоминалось ранее, применение дженериков затронет не только функции, но и структуры данных.

В Go встроены две основных структуры данных общего назначения: срезы и map ‘ы. Эти структуры могут содержать значения любого типа данных, со статической проверкой типа для хранящихся и запрашиваемых значений. Значения данных в этих структурах хранятся без опосредования через интерфейсы. Таким образом в []int хранятся именно целые числа, а не целые числа, преобразованные через интерфейс.

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

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

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

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

Чем придётся пожертвовать

Справедливости ради стоит сказать, что за каждое изменение в языке нужно заплатить определённую цену. И добавление дженериков в Go определённо усложнит язык. Как и в случае с любыми другими изменениями, нужно подумать о том, как максимизировать выгоду и минимизировать потери.

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

Ниже приведён список правил, которых стоит придерживаться при модификации языка:

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

Наброски изменений

К счастью, есть способ сделать это. Во второй половине статьи мы перейдём к тому, как можно реализовать дженерики в Go.

На Gophercon 2019 Ян Лэнс Тэйлор и Роберт Гризмер обнародовали наброски предполагаемых изменений. В этом документе содержится полная информация, здесь же мы осветим основные аспекты.

Вот как будет выглядеть функция Reverse с применением дженериков:

Как видите, тело функции осталось то же, изменилась лишь сигнатура.

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

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

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

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

Контракты

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

Функция Reverse может работать со срезами любого типа. Единственная операция, которую эта функция осуществляет с типом Element — присвоение, а эту операцию на Go можно провести с любым типом. Для подобных распространённых функций с дженериками нам не нужно определять каких-то специфических условий для параметров.

Рассмотрим другую функцию.

Вот как для этого примера определяется контракт Sequence :

Если вы вспомните, о чём говорилось на Gophercon’е 2018, то увидите, что способ определения контрактов сильно упростился. Разработчики учли отзывы участников конвента, которым контракты образца 2018 года показались излишне сложными. Новые контракты куда проще писать, читать и понимать.

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

Контракты с методами

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

Контракты с множественными типами

Вот пример контракта со множеством параметров типа:

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

Упорядоченные типы

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

Контракт Ordered говорит, что тип T должен быть упорядоченным типом, что означает поддержку таких операторов, как «меньше чем», «больше чем» и т.д.

Контракт Ordered — просто список всех упорядоченных типов, вшитых в язык. Этот контракт принимает любой из перечисленных типов, а также любой пользовательский тип, основанный на одном из перечисленных. Фактически это любой тип, к которому можно применить оператор «меньше чем».

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

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

Дженерик структуры данных

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

Неэкспортированный метод возвращает указатель либо на слот, содержащий v, либо на то место в дереве, где она должна быть.

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

Вот код, предназначенный для проверки того, содержит ли дерево значение:

А этот код добавляет новое значение:

Использовать дерево довольно просто.

Так и должно быть. Разрабатывать структуры данных с дженериками чуть сложнее, поскольку вам зачастую нужно чётко определить аргументы с типами, однако использование этого кода обычно не сложнее, чем работа с традиционными структурами данных.

Дальнейшие шаги

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

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

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

Цель создателей Go — добавить в язык дженерики, не усложняя языка и сохраняя его идентичность.

Источник

Дженерики в Java для самых маленьких: синтаксис, границы и дикие карты

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

Дженерик что это программирование. Смотреть фото Дженерик что это программирование. Смотреть картинку Дженерик что это программирование. Картинка про Дженерик что это программирование. Фото Дженерик что это программирование

Дженерик что это программирование. Смотреть фото Дженерик что это программирование. Смотреть картинку Дженерик что это программирование. Картинка про Дженерик что это программирование. Фото Дженерик что это программирование

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

Теперь представьте, что содержимое коробки вы отвозите на переработку, а перед этим каждый раз приходится отделять бумагу от прочего мусора. Не хотели бы вы заполучить такую коробку, которая не даст положить в себя что-то, кроме бумаги? Если ваш ответ «да» — вам понравятся дженерики (generics).

Содержание

Дженерик что это программирование. Смотреть фото Дженерик что это программирование. Смотреть картинку Дженерик что это программирование. Картинка про Дженерик что это программирование. Фото Дженерик что это программирование

Фулстек-разработчик. Любимый стек: Java + Angular, но в хорошей компании готова писать хоть на языке Ада.

Знакомимся с дженериками

До появления дженериков программисты могли неявно предполагать, что какой-то класс, интерфейс или метод работает с элементами определённого типа.

Посмотрите на этот фрагмент кода:

Здесь предполагается, что метод printSomething работает со списком строк. Мы можем догадаться об этом, потому что в цикле все элементы приводятся (преобразуются) к классу String, а потом ещё и метод length этого класса вызывается.

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

Вот только тестировщик назначил баг не кому-то из них, а Паше, который написал метод printSomething, — потому что ошибка произошла именно во время его выполнения.

Дженерик что это программирование. Смотреть фото Дженерик что это программирование. Смотреть картинку Дженерик что это программирование. Картинка про Дженерик что это программирование. Фото Дженерик что это программирование

Паша быстро нашёл истинных виновников и попросил их исправить заполнение списка. Но на будущее решил подстраховаться от подобных ситуаций и переписал метод с использованием дженериков. Вот так:

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

Дженерик что это программирование. Смотреть фото Дженерик что это программирование. Смотреть картинку Дженерик что это программирование. Картинка про Дженерик что это программирование. Фото Дженерик что это программирование

Обратите внимание, что во второй версии Пашиного метода item не приводится насильно к типу String. Мы просто получаем в цикле очередной элемент списка, и компилятор соглашается, что это, очевидно, будет строка. Код стал менее громоздким, читать его стало проще.

Объявляем дженерик-классы и создаём их экземпляры

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

В классе два метода:

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

Теперь создадим коробку для бумаги. Пусть за бумагу отвечает класс Paper, а значит, экземпляр правильной коробки создаётся вот так:

Это полный вариант записи, но можно и короче:

Так как слева мы уже показали компилятору, что нужна коробка именно для бумаги, справа можно опустить повторное упоминание Paper — компилятор «догадается» о нём сам.

Это «угадывание» называется type inference — выведение типа, а оператор « <>» — это diamond operator. Его так назвали из-за внешнего сходства с бриллиантом.

E — element, для элементов параметризованных коллекций;

K — key, для ключей map-структур;

V — value, для значений map-структур;

N — number, для чисел;

T — type, для обозначения типа параметра в произвольных классах;

S, U, V и так далее — применяются, когда в дженерик-классе несколько параметров.

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

А можно пойти ещё дальше и создать дженерик-класс с двумя параметрами для коробки с двумя отсеками. Вот так:

Теперь легко запрограммировать коробку, в одном отсеке которой будет собираться пластик, а во втором — стекло:

Обратите внимание, что type inference и diamond operator позволяют нам опустить оба параметра в правой части.

Объявляем и реализуем дженерик-интерфейсы

Объявление дженерик- интерфейсов похоже на объявление дженерик-классов. Продолжим тему переработки и создадим интерфейс пункта переработки GarbageHandler сразу с двумя параметрами: тип мусора и способ переработки:

Реализовать (имплементить) этот интерфейс можно в обычном, не дженерик- классе:

Но можно пойти другим путём и сначала объявить дженерик-класс с двумя параметрами:

Или скомбинировать эти два способа и написать дженерик-класс только с одним параметром:

Дженерик-классы и дженерик-интерфейсы вместе называются дженерик-типами.

Можно создавать экземпляры дженерик-типов «без расшифровки», то есть никто не запретит вам объявить переменную типа Box — просто Box:

Для такого случая даже есть термин — raw type, то есть «сырой тип». Эту возможность оставили в языке для совместимости со старым кодом, который был написан до появления дженериков.

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

Пишем дженерик-методы

В примерах выше мы уже видели параметризованные методы в дженерик-классах и интерфейсах. Типизированными могут быть как параметры метода, так и возвращаемый тип.

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

У метода transfer есть свой личный параметр для типа, который не обязан совпадать ни с типом T, ни с типом S. При первом упоминании новый параметр, как и в случае с заголовком класса или интерфейса, пишется в угловых скобках.

Дженерик-методы можно объявлять и в обычных (не дженерик) классах и интерфейсах. Наш класс для переработки мог быть выглядеть так:

Здесь дженерики используются только в методе.

Обратите внимание на синтаксис: параметры типов объявляются после модификатора доступа ( public), но перед возвращаемым типом ( void). Они перечисляются через запятую в общих угловых скобках.

Ограничиваем дженерики сверху и снизу

Давайте немного расширим наше представление о мусоре и введём для него дополнительное свойство — массу «типичного представителя», то есть массу одной пластиковой бутылки или листка бумаги, например.

Теперь попробуем использовать эту массу в методе уже знакомого класса Box:

И получим ошибку при компиляции: мы не рассказали компилятору, что T — это какой-то вид мусора. Исправим это с помощью так называемого upper bounding — ограничения сверху:

Теперь метод getItemWeight успешно скомпилируется.

Здесь T extends Garbage означает, что в качестве T можно подставить Garbage или любой класс-наследник Garbage. Из уже известных нам классов это могут быть, например, Paper или Plastic. Так как и у Garbage, и у всех его наследников есть метод getWeight, его можно вызывать в новой версии дженерик-класса Box.

Для одного класса или интерфейса можно добавить сразу несколько ограничений. Вспомним про интерфейс для пункта приёма мусора и введём класс для метода переработки — HandleMethod. Тогда GarbageHandler можно переписать так:

Wildcards

Термин wildcard пришёл в программирование из карточной игры. В покере, например, так называют карту, которая может сыграть вместо любой другой. Джокер — известный пример такой «дикой карты».

Wildcard нельзя подставлять везде, где до этого мы писали буквенные обозначения. Не получится, например, объявить класс Box или дженерик-метод, который принимает такой тип:

Wildcards удобно использовать для объявления переменных и параметров методов совместно с классами из Java Collection Framework — здесь собраны инструменты Java для работы с коллекциями. Если вы не очень хорошо знакомы с ними, освежите знания, прочитав эту статью.

В примере ниже мы можем подставить вместо «?» любой тип, в том числе Paper, поэтому строка успешно скомпилируется:

Wildcards можно применять для ограничений типов:

Это уже знакомое нам ограничение сверху, upper bounding, — вместо «?» допуст им Garbage или любой его класс-наследник, то есть Paper подходит.

Но можно ограничить тип и снизу. Это называется lower bounding и выглядит так:

Дженерик что это программирование. Смотреть фото Дженерик что это программирование. Смотреть картинку Дженерик что это программирование. Картинка про Дженерик что это программирование. Фото Дженерик что это программирование

Собираем понятия, связанные с дженериками

Мы не успели разобраться с более сложными вещами — например, с заменами аргументов типов в классах-наследниках, с переопределением дженерик-методов, не узнали об особенностях коллекций с wildcards.

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

ТерминРасшифровка
Дженерик-типы (generic types)Дженерик-класс или дженерик-интерфейс с одним или несколькими параметрами в заголовке
Параметризованный тип (parameterized types)Вызов дженерик-типа. Для дженерик-типа List параметризованным типом будет, например, List
Параметр типа (type parameter)Используются при объявлении дженерик-типов. Для Box T — это параметр типа
Аргумент типа (type argument)Тип объекта, который может использоваться вместо параметра типа. Например, для Box

Paper — это аргумент типа

WildcardОбозначается символом «?» — неизвестный тип
Ограниченный wildcard (bounded wildcard)Wildcard, который ограничен сверху — или снизу —
Сырой тип (raw type)Имя дженерик-типа без аргументов типа. Для List сырой тип — это List

Ещё больше о дженериках, коллекциях и других элементах языка Java узнайте на нашем курсе «Профессия Java-разработчик». Научим программировать на самом востребованном языке и поможем устроиться на работу.

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

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *