Наследование

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

Наследование также позволяет использовать в коде преимущества полиморфизма . Полиморфизм — это возможность использовать одно имя для метода, который ведет себя по-разному при применении к разным типам данных. Самый простой пример — это базовый класс Shape, который имеет два подкласса с именами Circle и Square. Класс Shape определяет метод area() , который возвращает площадь фигуры. При применении полиморфизма метод area() можно вызывать для объектов типа Circle и Square, получая при этом правильные расчеты. Наследование делает возможным полиморфизм, так как позволяет подклассам наследовать и переопределять ( override ) методы из базового класса. В следующем примере метод area() переопределяется классами Circle и Square.

class Shape 
{ 
    public function area():Number 
    { 
        return NaN; 
    } 
} 
 
class Circle extends Shape 
{ 
    private var radius:Number = 1; 
    override public function area():Number 
    { 
        return (Math.PI * (radius * radius)); 
    } 
} 
 
class Square extends Shape 
{ 
    private var side:Number = 1; 
    override public function area():Number 
    { 
        return (side * side); 
    } 
} 
 
var cir:Circle = new Circle(); 
trace(cir.area()); // output: 3.141592653589793 
var sq:Square = new Square(); 
trace(sq.area()); // output: 1

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

function draw(shapeToDraw:Shape) {} 
 
var myCircle:Circle = new Circle(); 
draw(myCircle);

Свойства экземпляра и наследование

Свойство экземпляра, определенное с использованием ключевых слов function , var или const , наследуется всеми подклассами, если оно не объявлено с атрибутом private в базовом классе. Например, класс Event в ActionScript 3.0 имеет ряд подклассов, наследующих свойства, общие для всех объектов событий.

Для некоторый типов событий класс Event содержит все свойства, необходимые для определения события. Эти типы событий не требуют других свойств экземпляра, кроме определенных в классе Event. К таким событиям относятся событие complete , которое происходит после успешной загрузки данных, и событие connect , которое происходит после успешной установки соединения.

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

public class Event 
{ 
    public function get type():String; 
    public function get bubbles():Boolean; 
    ... 
 
    public function stopPropagation():void {} 
    public function stopImmediatePropagation():void {} 
    public function preventDefault():void {} 
    public function isDefaultPrevented():Boolean {} 
    ... 
}

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

public class MouseEvent extends Event 
{ 
    public static const CLICK:String= "click"; 
    public static const MOUSE_MOVE:String = "mouseMove"; 
    ... 
 
    public function get stageX():Number {} 
    public function get stageY():Number {} 
    ... 
}

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

Если свойство объявлено с использованием ключевого слова public , оно будет доступно для всего кода. Это означает, что ключевое слово public , в отличие от слов private , protected и internal , не накладывает ограничений на наследование свойств.

Если свойство объявлено с использованием ключевого слова private , оно доступно только в определяющем его классе, то есть не наследуется никакими подклассами. Такое поведение отличается от предыдущих версий ActionScript, где поведение ключевого слова private больше напоминало поведение protected в ActionScript 3.0.

Ключевое слово protected указывает, что свойство доступно не только для определяющего его класса, но и для всех подклассов. В отличие от ключевого слова protected в языке программирования Java, в ActionScript 3.0 protected не делает свойство доступным для всех других классов в одном пакете. В ActionScript 3.0 свойство, объявленное с использованием ключевого слова protected доступно только для подклассов. Более того, защищенное свойство доступно для подкласса независимо от того, находится он в том же пакете, что и базовый класс, или в другом.

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

В следующем примере демонстрируется влияние каждого из идентификаторов управления доступом на наследование в пределах пакета. Следующий код определяет основной класс приложения AccessControl и два других класса с именами Base и Extender. Класс Base находится в пакете foo, а его подкласс Extender находится в пакете bar. Класс AccessControl импортирует только класс Extender и создает экземпляр Extender, который пытается обратиться к переменной str , определенной в классе Base. Переменная str объявляется с идентификатором public , поэтому код компилируется и выполняется, как показано в следующем фрагменте.

// Base.as in a folder named foo 
package foo 
{ 
    public class Base 
    { 
        public var str:String = "hello"; // change public on this line 
    } 
} 
 
// Extender.as in a folder named bar 
package bar 
{ 
    import foo.Base; 
    public class Extender extends Base 
    { 
        public function getString():String { 
            return str; 
        } 
    } 
} 
 
// main application class in file named AccessControl.as 
package 
{ 
    import flash.display.MovieClip; 
    import bar.Extender; 
    public class AccessControl extends MovieClip 
    { 
        public function AccessControl() 
        { 
            var myExt:Extender = new Extender(); 
            trace(myExt.str);// error if str is not public 
            trace(myExt.getString()); // error if str is private or internal 
        } 
    } 
}

Чтобы оценить влияние других идентификаторов управления доступом на компиляцию и выполнения предыдущего примера, измените атрибут переменной str на private , protected или internal , удалив или скрыв с помощью комментария следующую строку из класса AccessControl :

trace(myExt.str);// error if str is not public

Запрет переопределения переменных

Свойства, объявленные с ключевыми словами var и const наследуются, но не могут быть переопределены. Переопределение свойства выполняется в подклассе. Единственными типами свойств, допускающими переопределение, являются средства доступа get и set (свойства, объявленные с ключевым словом function ). Хотя нельзя переопределить переменную экземпляра, можно получить подобный результат путем создания методов get и set для переменной экземпляра и переопределения этих методов.

Переопределение методов

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

  • Метод экземпляра объявлен в базовом классе без использования ключевого слова final . Атрибут final рядом с методом экземпляра указывает на то, что программист явно запретил переопределение метода подклассами.

  • Метод экземпляра объявлен в базовом классе без использования идентификатора управления доступом private . Если метод отмечен как private  в базовом классе, то при определении одноименного метода в подклассе не требуется использовать ключевое слово override , так как метод базового класса не будет доступен для подкласса.

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

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

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

  • Параметры переопределенного метода должны иметь те же аннотации типа данных, что и параметры в метода базового класса.

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

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

Инструкция super

Переопределяя метод, программисты нередко хотят расширить поведение метода суперкласса, а не заменить его полностью. Для этого требуется механизм, позволяющий методу подкласса вызывать версию этого же метода в суперклассе. Инструкция super обеспечивает такой механизм тем, что содержит ссылку на непосредственный суперкласс. В следующем примере определяется класс Base, содержащий метод thanks() , и его подкласс Extender, переопределяющий метод thanks() . Метод Extender.thanks() использует инструкцию super для вызова Base.thanks() .

package { 
    import flash.display.MovieClip; 
    public class SuperExample extends MovieClip 
    { 
        public function SuperExample() 
        { 
            var myExt:Extender = new Extender() 
            trace(myExt.thanks()); // output: Mahalo nui loa 
        } 
    } 
} 
 
class Base { 
    public function thanks():String 
    { 
        return "Mahalo"; 
    } 
} 
 
class Extender extends Base 
{ 
    override public function thanks():String 
    { 
        return super.thanks() + " nui loa"; 
    } 
}

Переопределение методов get и set

Несмотря на то, что нельзя переопределять переменные суперкласса, можно переопределять методы get и set. Например, следующий код переопределяет метод get currentLabel , определенный в классе MovieClip в ActionScript 3.0.

package 
{ 
    import flash.display.MovieClip; 
    public class OverrideExample extends MovieClip 
    { 
        public function OverrideExample() 
        { 
            trace(currentLabel) 
        } 
        override public function get currentLabel():String 
        { 
            var str:String = "Override: "; 
            str += super.currentLabel; 
            return str; 
        } 
    } 
}

Инструкция trace() в конструкторе класса OverrideExample выдает значение Override: null , которое указывает на то, что код смог переопределить унаследованное свойство currentLabel .

Статические свойства не наследуются

Статические свойства не наследуются подклассами Это означает, что к статическим свойствам невозможно обратиться через экземпляр подкласса. Статическое свойство можно получить только через объект определяющего его класса. Например, следующий код определяет базовый класс Base и его подкласс Extender. Статическая переменная test определена в классе Base. Код, представленный в следующем фрагменте, не компилируется в строгом режиме и выдает ошибку выполнения в стандартном режиме.

package { 
    import flash.display.MovieClip; 
    public class StaticExample extends MovieClip 
    { 
        public function StaticExample() 
        { 
            var myExt:Extender = new Extender(); 
            trace(myExt.test);// error 
        } 
    } 
} 
 
class Base { 
    public static var test:String = "static"; 
} 
 
class Extender extends Base { }

Обратиться к статической переменной test можно только через объект класса, как показано в следующем коде.

Base.test;

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

package 
{ 
    import flash.display.MovieClip; 
    public class StaticExample extends MovieClip 
    { 
        public function StaticExample() 
        { 
            var myExt:Extender = new Extender(); 
            trace(myExt.test);// output: instance 
        } 
    } 
} 
 
class Base 
{ 
    public static var test:String = "static"; 
    public var test:String = "instance"; 
} 
 
class Extender extends Base {}

Статические свойства и цепочка области действия

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

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

package 
{ 
    import flash.display.MovieClip; 
    public class StaticExample extends MovieClip 
    { 
        public function StaticExample() 
        { 
            var myExt:Extender = new Extender(); 
        } 
    } 
} 
 
class Base { 
    public static var test:String = "static"; 
} 
 
class Extender extends Base 
{ 
    public function Extender() 
    { 
        trace(test); // output: static 
    } 
     
}

Если в одном классе или суперклассе определено свойство экземпляра с тем же именем, что и статическое свойство, то свойство экземпляра имеет более высокий приоритет в цепочке области действия. Свойство экземпляра, так сказать, затеняет статическое свойство, то есть значение свойства экземпляра используется вместо значения статического свойства. Например, следующий код демонстрирует, что если класс Extender определяет переменную экземпляра с именем test , инструкция trace() использует ее значение вместо значения статической переменной.

package 
{ 
    import flash.display.MovieClip; 
    public class StaticExample extends MovieClip 
    { 
        public function StaticExample() 
        { 
            var myExt:Extender = new Extender(); 
        } 
    } 
} 
 
class Base 
{ 
    public static var test:String = "static"; 
} 
 
class Extender extends Base 
{ 
    public var test:String = "instance"; 
    public function Extender() 
    { 
        trace(test); // output: instance 
    } 
     
}