Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Стандартным способом обучения молодых программистов объектно-ориентированному программированию является использование метафор реального мира. И я сам постоянно этим пользуюсь в этом блоге, обычно проводя аналогии с царством животных. «Класс» в реальной жизни систематизирует общность определенного набора объектов: у млекопитающих, например, есть много общего; у них есть позвоночник, у них растет шерсть, они выделяют тепло и т.п. Класс в языке программирования делает то же самое: определяет общность определенного набора объектов с помощью механизма наследования. Наследование обеспечивает общность поскольку, как мы уже обсуждали, по определению оно означает, что «все (*) экземпляры базового типа также являются экземплярами производного типа».
Отношение наследования классов (**) обычно моделирует отношение вида «является разновидностью» (is a special kind of). Жираф является разновидностью млекопитающего, поэтому класс Giraffe наследуется от класса Mammal, который, в свою очередь, наследуется от класса Animal, который наследуется от Object. И это здорово, поскольку это явно представляет отношение типа «является разновидностью». Однако у меня всегда были проблемы с фундаментальной метафорой «наследования». Почему «наследование»? Вы наследуете генетическую информацию и другие свойства, так что если вы законный лорд, то вы унаследуете это звание от своих родителей. И если вы нарисуете диаграмму классов, то она будет походить на «фамильное дерево», в котором класс-наследник является «потомком» базового «родительского» класса. И действительно, многие говорят о базовом классе, как о «родителе» дочернего класса, особенно при разговоре с начинающими программистами.
Но метафора «наследования от родителя к потомку» является ужасной. Жираф не является «потомком млекопитающего»; жираф является потомком Папы Жирафа и Мамы Жирафы. «Потомок» не является «особой разновидностью родителя». На самом деле, вы наследуете лишь половину своего генотипа от каждого из родителей, и вы можете унаследовать реальные свойства из любого другого родственного отношения или, если на то уж пошло, даже не из родственного отношения вовсе. В языках программирования классы «наследуются» только от родственных типов, при котором наследуются все их члены (*). В реальной жизни, у всех двое родителей (***), но в языках программирования это не так: некоторые языки позволяют иметь множество «родителей», а некоторые – только одного. В реальности, один конкретный человек наследует определенные свойства от одного конкретного родителя, а двое других детей могут унаследовать совершенно другие свойства; в языках программирования отношение «наследования» не применимо на уровне индивидуальных объектов, каждый ребенок наследует одинаковый набор свойств от своих родителей. И в реальности вы наследуете реальные ценности только после смерти предков!
Но, постойте, все еще хуже. В любом языке программирования, который поддерживает лексическую вложенность типов (lexical nesting) и именное выделение подтипов (nominal subtyping), метафора родитель-ребенок является неоднозначной:
class B<T>
{
class D<U> : B<U> { }
}
А ну ка, быстро ответьте на вопрос: какой родительский тип у B<string>.D<int>? Это B<T>, B<string> или B<int>? Лексически этот тип располагается внутри B<T>, логически – внутри B<string>, и наследуется от B<int>; какой из этих трех типов является родительским? Если вы нарисуете граф лексических или логических отношений, то в результате получите граф, выглядящий в точности как «семейное дерево», показывающее отношение наследования. Лексическое вложение дает доступ ко всем свойствам окружающего типа, включая ненаследуемые члены и такие члены, как закрытые конструкторы, к которым обычно нет доступа извне! И не ясно, почему один тип «родительских» отношений более «родительский» нежели другие.
Как мы уже видели, наличие множества разных «родительских» отношений для конкретного типа может приводить к невероятно коварному коду. Мы должны быть невероятно осторожными при написании спецификации и реализации компилятора, для точного и однозначного описания взаимоотношений между типами. Именно поэтому я стараюсь избегать метафоры «родитель-ребенок»; для более понятного описания некоторого примера значительно проще пользоваться метафорой «базовых и производных типов», а не «родительских и дочерних типов».
(*) За исключением конструкторов и деструкторов.
(**) Здесь я буду говорить только о наследовании классов; хотя моя критика одинаково применима и к наследованию интерфейсов, но я не хочу касаться тонкостей в различиях между наследованиями классов и интерфейсов. Кроме того, я вообще не люблю использование метафоры наследования применительно к интерфейсам; в данном случае «контрактные обязательства» кажутся более подходящей метафорой.
(***) Предполагаем, что речь идет о видах с половым размножением.
Comments
- Anonymous
September 14, 2012
Полезно, спасибо!