Для чего необходимы методы по умолчанию в интерфейсах

Для чего необходимы методы по умолчанию в интерфейсах

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

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

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

Данный интерфейс называется Printable. Интерфейс может определять константы и методы, которые могут иметь, а могут и не иметь реализации. Методы без реализации похожи на абстрактные методы абстрактных классов. Так, в данном случае объявлен один метод, который не имеет реализации.

Чтобы класс применил интерфейс, надо использовать ключевое слово implements :

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

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

Класс Book и класс Journal связаны тем, что они реализуют интерфейс Printable. Поэтому мы динамически в программе можем создавать объекты Printable как экземпляры обоих классов:

Интерфейсы в преобразованиях типов

Все сказанное в отношении преобразования типов характерно и для интерфейсов. Например, так как класс Journal реализует интерфейс Printable, то переменная типа Printable может хранить ссылку на объект типа Journal:

И если мы хотим обратиться к методам класса Journal, которые определены не в интерфейсе Printable, а в самом классе Journal, то нам надо явным образом выполнить преобразование типов: ((Journal)p).getName();

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

Статические методы

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

Приватные методы

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

Константы в интерфейсах

Кроме методов в интерфейсах могут быть определены статические константы:

Множественная реализация интерфейсов

Если нам надо применить в классе несколько интерфейсов, то они все перечисляются через запятую после слова implements:

Наследование интерфейсов

Интерфейсы, как и классы, могут наследоваться:

При применении этого интерфейса класс Book должен будет реализовать как методы интерфейса BookPrintable, так и методы базового интерфейса Printable.

Вложенные интерфейсы

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

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

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

Интерфейсы как параметры и результаты методов

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

Метод read() в качестве параметра принимает объект интерфейса Printable, поэтому в этот метод мы можем передать как объект Book, так и объект Journal.

Метод createPrintable() возвращает объект Printable, поэтому также мы можем возвратить как объект Book, так и Journal.

Источник

Статические и стандартные методы в интерфейсах Java

Узнайте, как писать и использовать статические методы и методы по умолчанию в интерфейсах Java.

1. Обзор

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

2. Зачем нужны Методы по умолчанию в Интерфейсах

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

Давайте рассмотрим простой пример:

Причина, по которой методы default были включены в выпуск Java 8, довольно очевидна.

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

Таким образом, обратная совместимость аккуратно сохраняется без необходимости рефакторинга разработчиков.

3. Методы интерфейса по умолчанию в действии

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

И давайте напишем реализующий класс:

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

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

4. Правила Наследования Нескольких Интерфейсов

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

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

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

Давайте рассмотрим пример, в котором используются методы default из интерфейса Vehicle :

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

5. Методы статического интерфейса

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

Теперь предположим, что мы хотим рассчитать мощность двигателя данного транспортного средства. Мы просто вызываем метод getHorsePower() :

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

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

6. Заключение

В этой статье мы подробно изучили использование методов static и default interface в Java 8. На первый взгляд эта функция может показаться немного неаккуратной, особенно с точки зрения объектно-ориентированного пуриста. В идеале интерфейсы не должны инкапсулировать поведение и должны использоваться только для определения общедоступного API определенного типа.

Однако, когда дело доходит до поддержания обратной совместимости с существующим кодом, методы static и default являются хорошим компромиссом.

Источник

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

Почему эти переменные я называю константами? Дело в том, что в Java к этим определениям автоматически добавляются ключевые слова:

public static final

и любые переменные превращаются в общедоступные статические константы. То есть, в интерфейсах попросту нельзя объявлять переменные – только константы. Далее, мы можем использовать MIN_COORD и MAX_COORD в классах, где применен интерфейс GeomInterface. Например, в классе Line:

Смотрите, мы здесь объявили сеттер setCoord и в нем проверяем соответствие переданных координат диапазону [MIN_COORD; MAX_COORD] с помощью вспомогательного приватного метода isCheck. Наличие констант как раз и объясняется их объявлением в интерфейсе GeomInterface.

Статические методы в интерфейсах

Но если в интерфейсе можно объявлять статические константы, то можно ли задавать и статические методы? Да, это стало возможно, начиная с версии JDK 8, и делается очевидным образом:

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

Мы уже говорили с вами, что такое статические переменные и методы и как они себя ведут (https://www.youtube.com/watch?v=jEUXJRsHwmY). Я не стану здесь повторяться. Отмечу лишь, что это метод, располагающийся в строго определенной области памяти на всем протяжении работы программы. Следовательно, к нему можно обратиться и вызвать непосредственно из интерфейса, в котором он определен. Например, так:

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

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

Вложенные интерфейсы и их расширение

Далее, интерфейсы можно объявлять внутри классов. Делается это очевидным образом, и я здесь приведу лишь отвлеченный пример. Пусть имеется класс InterfaceGroup, в котором определены два интерфейса: Interface_1 и Interface_2:

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

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

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

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

Приватные методы интерфейса

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

Тогда при расширении второго интерфейса этот метод унаследован не будет.

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

Интерфейсы с абстрактными классами

Давайте теперь зададимся вопросом: а можно ли к абстрактному классу применять интерфейсы? Оказывается, да, можно и при этом реализация интерфейсного метода getSquare в нем может отсутствовать:

В этом случае метод getSquare обязательно должен быть определен в дочернем классе. Если же этот метод прописать непосредственно в классе Geom:

То дочерние классы могут его не переопределять. Тогда при обращении к getSquare() будет возвращаться значение 0. Благодаря такой гибкости, мы можем в программе реализовывать самую разную логику взаимодействия с интерфейсами.

Методы с реализацией по умолчанию

Фактически вот этот последний пример позволяет использовать абстрактный класс для определения метода getSquare с реализацией по умолчанию (то есть, его действие, когда он не переопределяется в дочерних классах). Так приходилось делать до версии JDK 8, чтобы не «заставлять» программистов определять методы интерфейса, если это не требовалось. Теперь (начиная с JDK 8 и выше) в интерфейсах можно определять методы с реализацией по умолчанию и такие методы можно не переопределять в классах. Для их объявления используется следующий синтаксис:

default ([аргументы]) <
[операторы]
>

Например, определим в интерфейсе MathGeom метод getSquare с реализацией по умолчанию:

И применим его ко всем классам графических примитивов:

Смотрите, в классе Line мы не переопределяли метод getSquare, а в классах Rectangle и Triangle он переопределен. Теперь, создавая экземпляры этих классов в функции main:

мы можем совершенно свободно вызывать у них метод getSquare:

Обратите внимание, нам здесь сначала нужно привести ссылку g[i] к типу MathGeom и только потом вызвать метод getSquare. В консоли увидим значения:

Здесь первый ноль был получен из реализации метода по умолчанию для класса Line. Остальные значения – из переопределенных методов. То есть, теперь, мы можем не прописывать реализацию метода getSquare в классах примитивов, если она нам не нужна. И это добавляет дополнительное удобство при программировании.

Но что будет, если в GeomInterface также определить метод getSquare с реализацией по умолчанию:

Тогда для класса Line, который применяет оба интерфейса, какая реализация будет использована? В действительности, никакая. Виртуальная машина Java в этом случае выдаст ошибку и потребуется явное определение этого метода. И это можно сделать так:

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

Заключение

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

Великий подвиг. Объявить класс DataGraph для хранения данных для графика в виде массива вещественных чисел размерностью N элементов (число N задать как константу, например, N=10). Записать отдельные классы (НЕ дочерние): LineGraph (точки в графике соединяются линиями), BarGraph (график в виде столбцов), ChartGraph (график в виде круговой диаграммы). При создании экземпляров этих классов они должны хранить ссылку на объект класса DataGraph. При рисовании графиков, данные следует брать через публичный метод getData() (класса DataGraph), т.е. получать ссылку на массив из N вещественных чисел. Взаимодействие между объектами классов должно выглядеть так:

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

Далее, объявить интерфейс Observer с методом update() и применить его к классам LineGraph, BarGraph и ChartGraph. По методу update() должно происходить обновление данных и перерисовка графика. В классе DataGraph хранить массив graphs для экземпляров классов LineGraph, BarGraph и ChartGraph. Как только происходит изменение данных в массиве data, вызывать метод update через ссылки graphs. (Изменение данных делать искусственно, например, в программе поменять данные, а затем, вызвать некий метод в DataGraph для запуска вызовов update).

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

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

Видео по теме

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

#11 Концепция объектно-ориентированного программирования (ООП)

Источник

Интерфейсы C# — Самый подробный разбор

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

Наследование классов и реализация интерфейсов. В чем разница?

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

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

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

За счет того, что класс непосредственно в себе содержит определения методов, CLR способна различать к какому интерфейсу относится какой метод и избегать конфликтов. Благодаря этому мы можем реализовывать несколько интерфейсов, в отличии от множественного наследования. Но вот чем похожи наследование и реализация интерфейсов, так это возможностью подставлять экземпляры производного типа на место базовых. То есть наш любимый полиморфизм. Мы можем объявить переменную интерфейсного типа и в нее поместить экземпляр любого класса, который реализует этот интерфейс. Данная схема применяется очень часто, например, в DI-контейнерах или в mock-тестах.

Определение интерфейсов

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

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

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

Для именования интерфейсов принято соглашение об использовании заглавной буквы I в начале имени. Как и всегда, это не является требованием системы, и программа сможет скомпилироваться как бы ты не назвал интерфейс. Но вот другие программисты могут и по голове настучать…

Реализация интерфейсов

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

Для примера возьмем стандартный и широко используемый интерфейс IComparable :

Данный интерфейс определяет механизм сравнения двух объектов. Теперь реализуем его в классе Point (весь исходный код доступен в GitHub):

InterfacesCSharp/ComparePoints

Для того, чтобы класс реализовывал интерфейс необходимо при объявлении класса через двоеточие указать название интерфейса (точно также как записывается наследование). А в теле класса реализовать все методы, которые были объявлены внутри интерфейса.

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

Для методов, реализующих интерфейс есть несколько требований:

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

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

InterfacesCSharp/VirtualAndSealedImplementation

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

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

Мы совершенно спокойно можем переопределить реализацию интерфейсного метода, так как он НЕ запечатан.

Но что нам делать, если нам нужно переопределить запечатанный у предка метод и при этом сохранить связь с интерфейсом? И для этого тоже есть решение:

Подробнее о вызовах интерфейсных методов

Для чего необходимы методы по умолчанию в интерфейсах. Смотреть фото Для чего необходимы методы по умолчанию в интерфейсах. Смотреть картинку Для чего необходимы методы по умолчанию в интерфейсах. Картинка про Для чего необходимы методы по умолчанию в интерфейсах. Фото Для чего необходимы методы по умолчанию в интерфейсахПомощник IntelliSence отображает весь список доступных методов, как определенных в самом классе (например, EndsWith()), в интерфейсах (CompareTo()) и в object (GetHashCode())

Но в CLR у нас также есть замечательная возможность определять переменные интерфейсного типа. В таком случае, эта переменная может содержать в себе, ну а если точнее, то ссылаться на экземпляр любого типа, который реализует этот интерфейс. В данном случае для использования будут доступны только те методы, которые определены в самом интерфейсе, плюс методы из object, так как любой тип унаследован от object и CLR может быть на 100% уверена, что эти методы там будут.

InterfacesCSharp/CallingInterfaceMethod

GetMethods() возвращает имена доступных методов типа. Но так как перегрузка метода считается за отдельный метод, то на выходе получается повторение некоторых методов несколько раз. И с помощью Select и Distinct я просто избавляюсь от повторов перегруженных методов.

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

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

Явная и неявная реализация интерфейсных методов

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

InterfacesCSharp/ExtinctInterfaceMethodImplementation

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

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

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

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

Обобщенные интерфейсы

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

Ну а теперь к преимуществам. Если коротко — их три:

Давай рассмотрим, данные преимущества обобщенных интерфейсов на примере:

InterfacesCSharp/GenericInterfaces

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

А теперь рассмотрим примеры улучшений, которые предоставляют нам обобщенные интерфейсы:

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

Обобщения и ограничения интерфейса

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

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

InterfacesCSharp/MethodConstraints

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

Реализация нескольких интерфейсов с одинаковыми сигнатурами и именами методов

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

InterfacesCSharp/NameConflict

Объявим два интерфейса, у которых совпадают сигратуры методов (имя и принимаемые аргументы), но которые при этом имеют совершенно разную логику:

И потом получается так, что один и тот же класс реализует оба этих интерфейса:

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

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

Реализация интерфейсов по умолчанию

Interfaces.Csharp.DefaultInterfaceImplementation

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

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

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

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

Совершенствование контроля типов за счет явной реализации интерфейсных методов

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

InterfacesCSharp/EimiForNoneGenericInterfaces

Для примера возьмем стандартный необобщенный интерфейс IComparable

И реализуем его в тривиальном значимом типе:

При работе с этим типом мы можем столкнуться с нежелательным поведением системы — падение производительности за счет упаковки, а также возможность совершить ошибку при написании кода, так как не соответствие типов не будет определяться синтаксическим анализатором Visual Studio.

А теперь попробуем решить обе этих проблемы одним махом с помощью явной реализации интерфейсных методов:

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

Опасность явной реализации интерфейсных методов

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

Путаница в документации или еще отсутствие

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

InterfacesCSharp/DangerousOfEimi

Для того, чтобы решить подобную проблему, нам необходимо выполнить приведение переменной к интерфейсному типу, так как тип int реализует интерфейс IConvertible явно. И разработчику, который не знаком с подобной особенностью приходится догадываться, как так получается, что тип реализует интерфейс, а вызвать методы, однако, не получается. И даже мой любимый IntelliSense — в данном случае не помогает.

Но даже подобный ход является далеко не оптимальным решением из-за второй проблемы

Упаковка

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

EIMI нельзя вызывать из производных типов

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

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

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

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

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

Базовый класс или интерфейс C#?

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

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

Как использовать базовый абстрактный класс C#

InterfacesCSharp/BaseClassVsInterface

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

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

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

Как использовать интерфейсы C#

Рассмотрим другой синтетический пример. У нас есть интерфейс для всех геометрических фигур, которые определяет метод для вычисления пощади.

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

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

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

Interface ⇽ BaseClass ⇽ ParticularClass

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

Советую прочитать предыдущую статью — 7 обязательных навыков дата-сайентиста
А также подписывайтесь на группу ВКонтакте, Telegram, Инстаграм и YouTube-канал. Там еще больше полезного и интересного для программистов.

Источник

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

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