Объявление абстрактного метода в TypeScript


195

Я пытаюсь выяснить, как правильно определить абстрактные методы в TypeScript:

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

class Animal {
    constructor(public name) { }
    makeSound(input : string) : string;
    move(meters) {
        alert(this.name + " moved " + meters + "m.");
    }
}

class Snake extends Animal {
    constructor(name) { super(name); }
    makeSound(input : string) : string {
        return "sssss"+input;
    }
    move() {
        alert("Slithering...");
        super.move(5);
    }
}

Я хотел бы знать, как правильно определить метод makeSound, чтобы он был набран и возможно переопределено.

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


4
Абстрактные классы и методы теперь являются новой функцией в предстоящем TypeScript 1.6.
Falconepl

Ответы:


285

nameСвойство помечается как protected. Это было добавлено в TypeScript 1.3 и теперь твердо установлено.

makeSoundМетод помечен как abstract, как класс. Вы не можете напрямую создать экземпляр Animalсейчас, потому что оно абстрактно. Это часть TypeScript 1.6 , которая сейчас официально запущена.

abstract class Animal {
    constructor(protected name: string) { }

    abstract makeSound(input : string) : string;

    move(meters) {
        alert(this.name + " moved " + meters + "m.");
    }
}

class Snake extends Animal {
    constructor(name: string) { super(name); }

    makeSound(input : string) : string {
        return "sssss"+input;
    }

    move() {
        alert("Slithering...");
        super.move(5);
    }
}

Старый способ имитации абстрактного метода состоял в том, чтобы выдавать ошибку, если кто-то ее использовал. Вам больше не нужно делать это, как только TypeScript 1.6 появится в вашем проекте:

class Animal {
    constructor(public name) { }
    makeSound(input : string) : string {
        throw new Error('This method is abstract');
    }
    move(meters) {
        alert(this.name + " moved " + meters + "m.");
    }
}

class Snake extends Animal {
    constructor(name) { super(name); }
    makeSound(input : string) : string {
        return "sssss"+input;
    }
    move() {
        alert("Slithering...");
        super.move(5);
    }
}

Это нормальное поведение, когда компилятор не жалуется, если я пропускаю параметр, меняю тип параметра или тип возвращаемого значения при переопределении абстрактного метода?
Vetterjack

1
Допустимо либо пропустить параметр (если вы его не используете, вы можете игнорировать любое переданное значение), и вы можете иметь параметры совместимых типов. Вы получите ошибку, если попытаетесь реализовать абстрактный метод, makeSound(input : number) : string {основанный на приведенном выше примере, где inputдолжна быть строка. Type 'string' is not assignable to type 'number'.,
Фентон

19

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

/**
 * The interface defines all abstract methods and extends the concrete base class
 */
interface IAnimal extends Animal {
    speak() : void;
}

/**
 * The abstract base class only defines concrete methods & properties.
 */
class Animal {

    private _impl : IAnimal;

    public name : string;

    /**
     * Here comes the clever part: by letting the constructor take an 
     * implementation of IAnimal as argument Animal cannot be instantiated
     * without a valid implementation of the abstract methods.
     */
    constructor(impl : IAnimal, name : string) {
        this.name = name;
        this._impl = impl;

        // The `impl` object can be used to delegate functionality to the
        // implementation class.
        console.log(this.name + " is born!");
        this._impl.speak();
    }
}

class Dog extends Animal implements IAnimal {
    constructor(name : string) {
        // The child class simply passes itself to Animal
        super(this, name);
    }

    public speak() {
        console.log("bark");
    }
}

var dog = new Dog("Bob");
dog.speak(); //logs "bark"
console.log(dog instanceof Dog); //true
console.log(dog instanceof Animal); //true
console.log(dog.name); //"Bob"

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

//This works
function letTheIAnimalSpeak(animal: IAnimal) {
    console.log(animal.name + " says:");
    animal.speak();
}
//This doesn't ("The property 'speak' does not exist on value of type 'Animal')
function letTheAnimalSpeak(animal: Animal) {
    console.log(animal.name + " says:");
    animal.speak();
}

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


1
Для меня по крайней мере с Typescript v1 - я не могу ссылаться на «это» из конструктора, чтобы перейти к супер. Мысли?
Киран Бентон

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

Ключевое слово this запрещено в super (). Я использую TSC 1.0.3
Zasz

Это довольно странно. Вы используете компилятор CLI или Visual Studio?
Tiddo

Я тоже не могу использовать «this» в вызове super (). Я могу использовать его сразу после, чтобы установить родительский элемент для дочерней реализации, но это не требует расширения абстрактного класса. Я использую плагин Eclispe Typsscript от Palantir, v1.0.1. Я заметил, что super (this) отлично работает в typescriptlang.org/Playground .
Eric

2

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

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

Вы должны помнить, чтобы расширять и реализовывать в классе, который вы создадите. Это удовлетворяет опасениям по поводу определения кода ошибки времени выполнения. Вы также не сможете даже вызывать методы, которые вылились бы, если бы вы не реализовали интерфейс (например, если вы пытаетесь создать экземпляр класса Animal). Я попытался сделать так, чтобы интерфейс расширял BaseAnimal ниже, но он скрыл конструктор и поле 'name' BaseAnimal от Snake. Если бы мне удалось это сделать, использование модуля и экспортов могло бы предотвратить случайное прямое создание экземпляра класса BaseAnimal.

Вставьте это здесь, чтобы увидеть, работает ли оно для вас: http://www.typescriptlang.org/Playground/

// The behavioral interface also needs to extend base for substitutability
interface AbstractAnimal extends BaseAnimal {
    // encapsulates animal behaviors that must be implemented
    makeSound(input : string): string;
}

class BaseAnimal {
    constructor(public name) { }

    move(meters) {
        alert(this.name + " moved " + meters + "m.");
    }
}

// If concrete class doesn't extend both, it cannot use super methods.
class Snake extends BaseAnimal implements AbstractAnimal {
    constructor(name) { super(name); }
    makeSound(input : string): string {
        var utterance = "sssss"+input;
        alert(utterance);
        return utterance;
    }
    move() {
        alert("Slithering...");
        super.move(5);
    }
}

var longMover = new Snake("windy man");

longMover.makeSound("...am I nothing?");
longMover.move();

var fulture = new BaseAnimal("bob fossil");
// compile error on makeSound() because it is not defined.
// fulture.makeSound("you know, like a...")
fulture.move(1);

Я наткнулся на ответ FristvanCampen, как указано ниже. Он говорит, что абстрактные классы являются антишаблоном, и предлагает создать экземпляр базовых «абстрактных» классов, используя внедренный экземпляр реализующего класса. Это справедливо, но есть контраргументы. Прочитайте для себя: https://typescript.codeplex.com/discussions/449920

Часть 2: У меня был другой случай, когда я хотел абстрактный класс, но я не мог использовать свое решение выше, потому что определенные методы в «абстрактном классе» должны были ссылаться на методы, определенные в соответствующем интерфейсе. Итак, я использую совет FristvanCampen. У меня есть неполный "абстрактный" класс, с реализациями методов. У меня есть интерфейс с нереализованными методами; этот интерфейс расширяет «абстрактный» класс. Затем у меня есть класс, который расширяет первый и реализует второй (он должен расширять оба, потому что в противном случае супер-конструктор недоступен). Смотрите (неработающий) пример ниже:

export class OntologyConceptFilter extends FilterWidget.FilterWidget<ConceptGraph.Node, ConceptGraph.Link> implements FilterWidget.IFilterWidget<ConceptGraph.Node, ConceptGraph.Link> {

    subMenuTitle = "Ontologies Rendered"; // overload or overshadow?

    constructor(
        public conceptGraph: ConceptGraph.ConceptGraph,
        graphView: PathToRoot.ConceptPathsToRoot,
        implementation: FilterWidget.IFilterWidget<ConceptGraph.Node, ConceptGraph.Link>
        ){
        super(graphView);
        this.implementation = this;
    }
}

и

export class FilterWidget<N extends GraphView.BaseNode, L extends GraphView.BaseLink<GraphView.BaseNode>> {

    public implementation: IFilterWidget<N, L>

    filterContainer: JQuery;

    public subMenuTitle : string; // Given value in children

    constructor(
        public graphView: GraphView.GraphView<N, L>
        ){

    }

    doStuff(node: N){
        this.implementation.generateStuff(thing);
    }

}

export interface IFilterWidget<N extends GraphView.BaseNode, L extends GraphView.BaseLink<GraphView.BaseNode>> extends FilterWidget<N, L> {

    generateStuff(node: N): string;

}

1

Я использую, чтобы бросить исключение в базовом классе.

protected abstractMethod() {
    throw new Error("abstractMethod not implemented");
}

Затем вы должны реализовать в подклассе. Минусы в том, что нет ошибки сборки, но во время выполнения. Плюсы в том, что вы можете вызывать этот метод из суперкласса, предполагая, что он будет работать :)

НТН!

Милтон


-20

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

Кроме того, приведенный выше пример будет обеспечивать это применение только во время выполнения, а НЕ во время компиляции, как вы ожидаете в Java / C #.


4
Я могу видеть, откуда ты, но я с уважением не согласен. Если язык что-то реализует, плохо самому его заново реализовать. Но если у вас нет чего-то, то у вас нет выбора, кроме как реализовать это самостоятельно. Конечно, вы не увидите проблем до времени выполнения, но, если вы впервые протестируете что-то, вы получите исключение, и вы узнаете, что вы довольно глупы. Конечно, это не идеал - поэтому, IMO, Typescript нуждается в поддержке абстрактных классов. Однако, пока это не произойдет ...
Maverick

Мне бы хотелось, чтобы в JavaScript были классы, вывод типов, статическая типизация и интерфейсы, и угадайте, что в Typescript есть. Это было бы то же самое для абстрактного метода, компилятор просто должен проверить, что любой класс, расширяющий абстрактный класс, реализует абстрактный метод, как это уже делает для интерфейсов (интерфейс по сути является просто классом только с абстрактным методом)
Тони БенБрахим

1
Я склонен согласиться с @rq_ здесь. Смысл абстрактных методов состоит в том, чтобы получить подтверждение времени компиляции, что программа не может войти в недопустимое состояние. Предложенные решения просто дают вам проверки во время выполнения, что означает, что когда ваша программа работает, вы не можете быть уверены, что она находится в допустимом состоянии. Это означает, что вы должны работать в предположении, что метод не реализован, и соблюдать меры предосторожности. Лгать себе, что у вас есть абстрактные методы, просто просить, чтобы вас укусило неожиданное поведение во время выполнения.
Мика Золту
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.