Наследование (программирование)

Материал из Википедии — свободной энциклопедии
Перейти к: навигация, поиск

Наследование (англ. inheritance) — концепция объектно-ориентированного программирования, согласно которой абстрактный тип данных может наследовать данные и функциональность некоторого существующего типа, способствуя повторному использованию компонентов программного обеспечения.

Терминология[править | править вики-текст]

  • В объектно-ориентированном программировании, начиная с «Simula 67», абстрактные типы данных называются классами;
  • Класс, опредёленный через наследование от другого класса, называется: производным классом, классом потомком (англ. derived class) или подклассом (англ. subclass);
  • Класс, от которого новый класс наследуется, называется: предком (англ. parent), базовым классом (англ. base class) или суперклассом (англ. parent class).

Применение[править | править вики-текст]

Наследование является механизмом повторного использования кода (англ. code reuse) и способствует независимому расширению программного обеспечения через открытые классы (англ. public classes) и интерфейсы (англ. interfaces). Установка отношения наследования между классами порождает иерархию классов (англ. class hierarchy).

Наследование и полиморфизм подтипов[править | править вики-текст]

Часто наследование отождествляют с полиморфизмом подтипов (англ. subtyping):

Несмотря на приведённое выше замечание, наследование является широко используемым механизмом установки отношения «является» (англ. is-a relationship). Некоторые языки программирования согласуют наследование и полиморфизм подтипов (в основном, это относится к языкам со статической типизацией: «C++»«C#»«Java» и «Scala») — в то время как другие разделяют вышеописанные концепции.

Наследование, — даже в языках программирования, которые поддерживают применение наследования как механизма, обеспечивающего полиморфизм подтипов, — не гарантирует поведенческий полиморфизм подтипов; смотри: «Принцип подстановки» Барбары Лисков.

Типы наследования[править | править вики-текст]

«Простое» наследование[править | править вики-текст]

Терминология[править | править вики-текст]

  • «Базовые», «родительские» (англ. base class) — классы, производящие наследование;
  • «Потомки», «наследники», «производные» (англ. derived class) — классы, наследуемые от базовых.

Абстрактные классы и создание объектов[править | править вики-текст]

Для некоторых языков программирования справедлива следующая концепция.

Существуют «абстрактные» классы (объявляемые таковыми произвольно или из-за приписанных им абстрактных методов); их можно описывать имеющими поля и методы. Создание же объектов (экземпляров) означает конкретизацию, применимую лишь к неабстрактным классам (в том числе, к неабстрактным наследникам абстрактных), — представителями которых, по итогу, будут созданные объекты.

Пример: Пусть базовый класс «Сотрудник ВУЗа», — от которого наследуются классы «Аспирант» и «Профессор», — абстрактный. Общие поля и функции классов (например, поле «Год рождения») могут быть описаны в базовом классе. И в программе будут созданы объекты только производных классов: «Аспирант» и «Профессор»; обычно нет смысла создавать объекты базовых классов.

Множественное наследование[править | править вики-текст]

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

Множественное наследование реализовано в «C++». Из других языков, предоставляющих эту возможность, можно отметить «Python» и «Eiffel». Множественное наследование поддерживается в языке «UML».

Множественное наследование — потенциальный источник ошибок, которые могут возникнуть из-за наличия одинаковых имён методов в предках. В языках, которые позиционируются как наследники «C++» («Java», «C#» и другие), от множественного наследования было решено отказаться в пользу интерфейсов. Практически всегда можно обойтись без использования данного механизма. Однако, если такая необходимость всё-таки возникла, то для разрешения конфликтов использования наследованных методов с одинаковыми именами возможно, например, применить операцию расширения видимости — «::» — для вызова конкретного метода конкретного родителя.

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

Большинство современных объектно-ориентированных языков программирования («C#», «Java», «Delphi» и другие) поддерживают возможность одновременно наследоваться от класса-предка и реализовать методы нескольких интерфейсов одним и тем же классом. Этот механизм позволяет во многом заменить множественное наследование — методы интерфейсов необходимо переопределять явно, что исключает ошибки при наследовании функциональности одинаковых методов различных классов-предков.

Единый базовый класс[править | править вики-текст]

В ряде языков программирования, все классы, — явно или неявно, — наследуются от некого базового класса. «Smalltalk» был одним из первых языков, в которых использовалась эта концепция. К таким языкам также относятся: «Objective-C» (класс NSObject), «Perl» (UNIVERSAL), «Eiffel» (ANY), «Java» (java.lang.Object), «C#» (System.Object), «Delphi» (TObject), «Scala» (Any).

Наследование в языках программирования[править | править вики-текст]

«C++»[править | править вики-текст]

Наследование в «C++»:

class A {}; // Базовый класс

class B : public A {}; // Public-наследование
class C : protected A {}; // Protected-наследование
class Z : private A {}; // Private-наследование

В «C++» существует три типа наследования: public, protected, private. Спецификаторы доступа членов базового класса меняются в потомках следующим образом:

  • Если класс объявлен как базовый для другого класса со спецификатором доступа …
    • … «public»:
      • «public»-члены базового класса — доступны как «public»-члены производного класса;
      • «protected»-члены базового класса — доступны как «protected»-члены производного класса;
    • … «protected»:
      • «public»- и «protected»- члены базового класса — доступны как «protected»-члены производного класса;
    • … «private»:
      • «public»- и «protected»- члены базового класса — доступны как «private»-члены производного класса.

Одним из основных преимуществ «public»-наследования является то, что указатель на классы-наследники может быть неявно преобразован в указатель на базовый класс, то есть для примера выше можно написать:

A* a = new B();

Эта интересная особенность открывает возможность динамической идентификации типа (RTTI).

«Delphi» («Object Pascal»)[править | править вики-текст]

Для использования механизма наследования в «Delphi» необходимо в объявлении класса справа от слова class указать класс предок:

Предок:

TAncestor = class
private
protected
public
  // Виртуальная процедура
  procedure VirtualProcedure; virtual; abstract; 
  procedure StaticProcedure;
end;

Наследник:

TDescendant = class(TAncestor)
private
protected
public
  // Перекрытие виртуальной процедуры
  procedure VirtualProcedure; override;
  procedure StaticProcedure;
end;

Абсолютно все классы в «Delphi» являются потомками класса TObject. Если класс-предок не указан, то подразумевается, что новый класс является прямым потомком класса TObject.

Множественное наследование в «Delphi» частично поддерживается за счёт использования классов-помощников (Сlass Helpers).

«Python»[править | править вики-текст]

«Python» поддерживает как одиночное, так и множественное наследование. При доступе к атрибуту, просмотр производных классов происходит в порядке разрешения метода (англ. method resolution order, MRO)[1].

class Ancestor1(object):   # Предок-1
    def m1(self): pass
class Ancestor2(object):   # Предок-2
    def m1(self): pass
class Descendant(Ancestor1, Ancestor2):   # Наследник
    def m2(self): pass

d = Descendant()           # Инстанциация
print d.__class__.__mro__  # Порядок разрешения метода:
(<class '__main__.Descendant'>, <class '__main__.Ancestor1'>, <class '__main__.Ancestor2'>, <type 'object'>)

С версии «Python» 2.2 в языке сосуществуют «классические» классы и «новые» классы. Последние являются наследниками object. «Классические» классы будут поддерживаться вплоть до версии 2.6, но удалены из языка в «Python» версии 3.0.

Множественное наследование применяется в «Python», в частности, для введения в основной класс классов-примесей (англ. mix-in).

«PHP»[править | править вики-текст]

Для использования механизма наследования в «PHP» необходимо в объявлении класса после имени объявляемого класса-наследника указать слово extends и имя класса-предка:

class Descendant extends Ancestor {
}

В случае перекрытия классом-наследником методов предка, доступ к методам предка можно получить с использованием ключевого слова parent:

class A {
  function example() {
    echo "Вызван метод A::example().<br />\n";
  }
}

class B extends A {
  function example() {
    echo "Вызван метод B::example().<br />\n";
    parent::example();
  }
}

Можно предотвратить перекрытие классом-наследником методов предка; для этого необходимо указать ключевое слово final:

class A {
  final function example() {
    echo "Вызван метод A::example().<br />\n";
  }
}

class B extends A {
  function example() { //вызовет ошибку
    parent::example(); //и никогда не выполнится
  }
}

Чтобы при наследовании обратиться к конструктору родительского класса, необходимо дочернему классу в конструкторе указать parent::__construct();[2]

«Objective-C»[править | править вики-текст]

@interface A : NSObject 
- (void) example;
@end

@implementation
- (void) example
{
    NSLog(@"Class A");
}
@end

@interface B : A
- (void) example;
@end

@implementation
- (void) example
{
    NSLog(@"Class B");
}
@end

В интерфейсе объявляют методы, которые будут видны снаружи класса (public).

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

Все методы в «Objective-C» виртуальные.

«Java»[править | править вики-текст]

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

        public class A { }
        public interface I1 { }
        public interface I2 { }
        public class B extends A implements I1, I2 { }

Директива final в объявлении класса делает наследование от него невозможным.

«C#»[править | править вики-текст]

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

        public class A { }
        public interface I1 { }
        public interface I2 { }
        public class B : A, I1, I2 { }

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

        public class A<T>
        { }
        public class B : A<int>
        { }
        public class B2<T> : A<T>
        { }

Допустимо также наследование вложенных классов от классов, их содержащих:

    class A // default class A is internal, not public class B can not be public

    {
        class B : A { }
    }

Директива sealed в объявлении класса делает наследование от него невозможным.[3]

«Ruby»[править | править вики-текст]

class Parent

  def public_method
    "Public method"
  end

  private

    def private_method
      "Private method"
    end

end

class Child < Parent

  def public_method
    "Redefined public method"
  end

  def call_private_method
    "Ancestor's private method: " + private_method
  end

end

Класс Parent является предком для класса Child, у которого переопределён метод public_method.

child = Child.new
child.public_method #=> "Redefined public method"
child.call_private_method #=> "Ancestor's private method: Private method"

Приватные методы предка можно вызывать из наследников.

«JavaScript»[править | править вики-текст]

var Parent = function( data ) {
    this.data = data || false;
    this.public_method = function() { return 'Public Method'; }
}

var Child = function() {
    this.public_method = function() { return 'Redefined public method'; }
    this.getData = function() { return 'Data: ' + this.data; }
}

Child.prototype = new Parent('test');
var Test = new Child();

Test.getData(); // => "Data: test"
Test.public_method(); // => "Redefined public method"
Test.data; // => "test"

Класс Parent является предком для класса Child, у которого переопределен метод public_method.

В «JavaScript» используется прототипное наследование.

Конструкторы и деструкторы[править | править вики-текст]

В «С++» конструкторы при наследовании вызываются последовательно от самого раннего предка до самого позднего потомка, а деструкторы наоборот — от самого позднего потомка до самого раннего предка.

class First
{
public:
    First()  { cout << ">>First constructor" << endl; }
    ~First() { cout << ">>First destructor" << endl; }
};

class Second: public First
{
public:
    Second()  { cout << ">Second constructor" << endl; }
    ~Second() { cout << ">Second destructor" << endl; }
};

class Third: public Second
{
public:
    Third()  { cout << "Third constructor" << endl; }
    ~Third() { cout << "Third destructor" << endl; }
};

// выполнение кода
Third *th = new Third();
delete th;

// результат вывода
/*
>>First constructor
>Second constructor
Third constructor

Third destructor
>Second destructor
>>First destructor
*/

Ссылки[править | править вики-текст]

Примечания[править | править вики-текст]

  1. о порядке разрешения метода в Python
  2. Что такое объектно-ориентированное программирование. wh-db.com (30.06.2015).
  3. C# Language Specification Version 4.0, Copyright © Microsoft Corporation 1999—2010