Подскажите, пожалуйста, планируете ли вы возобновление программ высшего образования? Если да, есть ли какие-то примерные сроки? Спасибо! |
Основные конструкции языков Java и C# (продолжение)
В этой лекции мы продолжаем рассмотрение элементов Java 5 и C# 2.0 на основе описывающих их стандартов [1] и [2,3].
Наследование
Отношение вложенности между типами определяется наследованием. Обычно говорят, что класс наследует другому классу или является его потомком, наследником, если он определяет более узкий тип, т.е. все объекты этого класса являются также и объектами наследуемого им. Второй класс в этом случае называют предком первого. Взаимоотношения между интерфейсами описываются в тех же терминах, но вместо "класс наследует интерфейсу" обычно говорят, что класс реализует интерфейс.
В обоих языках класс может наследовать только одному классу и реализовывать несколько интерфейсов. Интерфейс может наследовать многим интерфейсам.
Классы, которые не должны иметь наследников, помечаются в Java как final, а в C# — как sealed.
При наследовании, т.е. сужении типа, возможно определение дополнительных полей и дополнительных операций. Возможно также определение в классе-потомке поля, имеющего то же имя, что и некоторое поле в классе-предке. В этом случае происходит перекрытие имен — определяется новое поле, и в коде потомка по этому имени становится доступно только оно.
Если же необходимо получить доступ к соответствующему полю предка, нужно использовать разные подходы в зависимости от того, статическое это поле или нет, т.е. относится ли оно к самому классу или к его объектам. К статическому полю можно обратиться, указав его полное имя, т.е. ClassName.fieldName, к нестатическому полю из кода класса-потомка можно обратиться с помощью конструкций super.fieldName в Java и base.fieldName в C# (естественно, если оно не перекрыто в каком-то классе, промежуточном между данными предком и потомком). Конструкции super в Java и base в C# можно использовать и для обращения к операциям, декларированным в предке данного класса. Для обращения к полям и операциям самого объекта в обоих языках можно использовать префикс this, являющийся ссылкой на объект, в котором вызывается данная операция.
Основная выгода от использования наследования — возможность перегружать (override) реализации операций в типах-наследниках. Это значит, что при вызове операции с данной сигнатурой в объекте наследника может быть выполнена не та реализация этой операции, которая определена в предке, а совсем другая, определенная в точном типе объекта. Такие операции называют виртуальными (virtual). Чтобы определить новую реализацию некоторой виртуальной операции предка в потомке, нужно определить в потомке операцию с той же сигнатурой. При этом необходимо следовать общему принципу, обеспечивающему корректность системы типов в целом — принципу подстановки (Liskov substitution principle) [4,5]. Поскольку тип-наследник является более узким, чем тип -предок, его объект может использоваться всюду, где может использоваться объект типа-предка. Принцип подстановки, обеспечивающий это свойство, требует соблюдения двух правил:
- Во всякой ситуации, в которой можно вызвать данную операцию в предке, ее вызов должен быть возможен и в наследнике. Говоря по-другому, предусловие операции при перегрузке не должно усиливаться.
- Множество ситуаций, в которых система в целом может оказаться после вызова операции в наследнике, должно быть подмножеством набора ситуаций, в которых она может оказаться в результате вызова этой операции в предке. То есть постусловие операции при перегрузке не должно ослабляться.
Статические операции, относящиеся к классу в целом, а не к его объектам, не виртуальны. Они не могут быть перегружены, но могут быть перекрыты, если в потомке определяются статические операции с такими же сигнатурами.
Приводимые ниже примеры на обоих языках иллюстрируют разницу в работе виртуальных и невиртуальных операций.