Abstrakte Methode in TypeScript deklarieren


195

Ich versuche herauszufinden, wie abstrakte Methoden in TypeScript richtig definiert werden:

Verwenden des ursprünglichen Vererbungsbeispiels:

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);
    }
}

Ich würde gerne wissen, wie man die Methode makeSound richtig definiert, damit sie getippt und möglicherweise übertönt wird.

Ich bin mir auch nicht sicher, wie ich protectedMethoden richtig definieren soll - es scheint ein Schlüsselwort zu sein, hat aber keine Auswirkung und der Code wird nicht kompiliert.


4
Abstrakte Klassen und Methoden sind jetzt eine neue Funktion des kommenden TypeScript 1.6.
Falconepl

Antworten:


284

Die nameEigenschaft ist gekennzeichnet als protected. Dies wurde in TypeScript 1.3 hinzugefügt und ist nun fest etabliert.

Die makeSoundMethode ist abstractwie die Klasse markiert . Sie können ein AnimalJetzt nicht direkt instanziieren , da es abstrakt ist. Dies ist Teil von TypeScript 1.6 , das jetzt offiziell live ist.

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);
    }
}

Die alte Art, eine abstrakte Methode nachzuahmen, bestand darin, einen Fehler auszulösen, wenn jemand sie verwendete. Sie sollten dies nicht mehr tun müssen, sobald TypeScript 1.6 in Ihrem Projekt landet:

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);
    }
}

Ist es normal, dass sich der Compiler nicht beschwert, wenn ich einen Parameter verpasse, einen Parametertyp ändere oder den Rückgabetyp ändere, wenn eine abstrakte Methode überschrieben wird?
Vetterjack

1
Es ist gültig, entweder einen Parameter wegzulassen (wenn Sie ihn nicht verwenden, können Sie jeden übergebenen Wert ignorieren) und Sie können Parameter kompatibler Typen haben. Sie würden eine Fehlermeldung erhalten, wenn Sie versuchen würden, eine abstrakte Methode zu implementieren, makeSound(input : number) : string {die auf dem obigen Beispiel basiert inputund eine Zeichenfolge sein sollte. Type 'string' is not assignable to type 'number'..
Fenton

19

Wenn Sie die Antwort von Erics etwas weiter nehmen, können Sie tatsächlich eine recht anständige Implementierung abstrakter Klassen erstellen, die den Polymorphismus voll unterstützt und implementierte Methoden aus der Basisklasse aufruft. Beginnen wir mit dem Code:

/**
 * 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"

Da die AnimalKlasse eine Implementierung erfordert, ist IAnimales unmöglich, ein Objekt vom Typ Animalzu erstellen, ohne eine gültige Implementierung der abstrakten Methoden zu haben. Beachten Sie, dass Sie, damit Polymorphismus funktioniert, Instanzen von IAnimalnicht weitergeben müssen Animal. Z.B:

//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();
}

Der Hauptunterschied zur Erics-Antwort besteht darin, dass die "abstrakte" Basisklasse eine Implementierung der Schnittstelle erfordert und daher nicht alleine instanziiert werden kann.


1
Zumindest für mich mit Typescript v1 - ich kann nicht innerhalb eines Konstruktors auf 'dies' verweisen, um es an super zu übergeben. Gedanken?
Kieran Benton

Welche genaue Version des Compilers verwenden Sie und welchen Fehler erhalten Sie? tsc 1.0.1 kompiliert die obigen Snippets einwandfrei.
Tiddo

Das Schlüsselwort 'this' ist in super () nicht zulässig. Ich benutze tsc 1.0.3
Zasz

Das ist ziemlich seltsam. Verwenden Sie den CLI-Compiler oder Visual Studio?
Tiddo

Auch ich kann "this" im super () -Aufruf nicht verwenden. Ich kann es direkt danach verwenden, um das übergeordnete Mitglied für die untergeordnete Implementierung festzulegen, aber dies erzwingt keine Erweiterung der abstrakten Klasse. Ich verwende das Eclispe Typsscript-Plugin von Palantir, v1.0.1. Ich stelle fest, dass super (this) in typescriptlang.org/Playground gut funktioniert .
Eric

2

Ich glaube, dass die Verwendung einer Kombination aus Schnittstellen und Basisklassen für Sie funktionieren könnte. Es werden Verhaltensanforderungen zur Kompilierungszeit durchgesetzt (rq_ post "unten" bezieht sich auf einen Beitrag oben, der nicht dieser ist).

Die Schnittstelle legt die Verhaltens-API fest, die von der Basisklasse nicht erfüllt wird. Sie können keine Basisklassenmethoden festlegen, um in der Schnittstelle definierte Methoden aufzurufen (da Sie diese Schnittstelle nicht in der Basisklasse implementieren können, ohne diese Verhaltensweisen definieren zu müssen). Vielleicht kann sich jemand einen sicheren Trick einfallen lassen, um das Aufrufen der Schnittstellenmethoden im übergeordneten Element zu ermöglichen.

Sie müssen daran denken, die zu instanziierende Klasse zu erweitern und zu implementieren. Es erfüllt Bedenken hinsichtlich der Definition von Laufzeitfehlercode. Sie können auch nicht einmal die Methoden aufrufen, die kotzen würden, wenn Sie die Schnittstelle nicht implementiert hätten (z. B. wenn Sie versuchen, die Animal-Klasse zu instanziieren). Ich habe versucht, die Schnittstelle das unten stehende BaseAnimal erweitern zu lassen, aber es hat den Konstruktor und das Feld 'name' von BaseAnimal vor Snake versteckt. Wenn ich dazu in der Lage gewesen wäre, hätte die Verwendung eines Moduls und von Exporten eine versehentliche direkte Instanziierung der BaseAnimal-Klasse verhindern können.

Fügen Sie dies hier ein, um zu sehen, ob es für Sie funktioniert: 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);

Ich bin auf die Antwort von FristvanCampen gestoßen, wie unten verlinkt. Er sagt, dass abstrakte Klassen ein Anti-Muster sind, und schlägt vor, dass man 'abstrakte' Basisklassen unter Verwendung einer injizierten Instanz einer implementierenden Klasse instanziiert. Das ist fair, aber es werden Gegenargumente vorgebracht. Lesen Sie selbst: https://typescript.codeplex.com/discussions/449920

Teil 2: Ich hatte einen anderen Fall, in dem ich eine abstrakte Klasse wollte, aber ich wurde daran gehindert, meine obige Lösung zu verwenden, da die definierten Methoden in der "abstrakten Klasse" auf die in der passenden Schnittstelle definierten Methoden verweisen mussten. Also, ich arbeite mit dem Rat von FristvanCampen. Ich habe die unvollständige "abstrakte" Klasse mit Methodenimplementierungen. Ich habe die Schnittstelle zu den nicht implementierten Methoden; Diese Schnittstelle erweitert die "abstrakte" Klasse. Ich habe dann eine Klasse, die die erste erweitert und die zweite implementiert (sie muss beide erweitern, da auf den Superkonstruktor sonst nicht zugegriffen werden kann). Siehe das (nicht ausführbare) Beispiel unten:

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;
    }
}

und

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

Ich verwende, um eine Ausnahme in der Basisklasse auszulösen.

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

Dann müssen Sie in der Unterklasse implementieren. Die Nachteile sind, dass es keinen Build-Fehler gibt, sondern Laufzeit. Der Vorteil ist, dass Sie diese Methode aus der Superklasse aufrufen können, vorausgesetzt, sie funktioniert :)

HTH!

Milton


-20

Nein nein Nein! Bitte versuchen Sie nicht, eigene 'abstrakte' Klassen und Methoden zu erstellen, wenn die Sprache diese Funktion nicht unterstützt. Gleiches gilt für alle Sprachfunktionen, für die eine bestimmte Sprache unterstützt werden soll. Es gibt keine korrekte Möglichkeit, abstrakte Methoden in TypeScript zu implementieren. Strukturieren Sie Ihren Code einfach mit Namenskonventionen, sodass bestimmte Klassen niemals direkt instanziiert werden, ohne dieses Verbot explizit durchzusetzen.

Das obige Beispiel bietet diese Durchsetzung nur zur Laufzeit, NICHT zur Kompilierungszeit, wie Sie es in Java / C # erwarten würden.


4
Ich kann sehen, woher du kommst, aber ich bin respektvoll anderer Meinung. Wenn eine Sprache etwas implementiert, ist es schlecht, es selbst erneut zu implementieren. Aber wenn Sie etwas nicht haben, haben Sie keine andere Wahl, als es selbst irgendwie umzusetzen. Sicher, Sie werden die Probleme erst zur Laufzeit sehen, aber wenn Sie beim ersten Testen eine Ausnahme auslösen, werden Sie wissen, dass Sie ziemlich schnell einen Fehler gemacht haben. Es ist natürlich nicht ideal - weshalb Typescript, IMO, Unterstützung für abstrakte Klassen benötigt. Bis es jedoch ...
Maverick

Ich wünschte, JavaScript hätte Klassen, Typinferenz, statische Typisierung und Schnittstellen, und raten Sie mal, Typescript hat es. Dies gilt auch für die abstrakte Methode. Der Compiler muss lediglich überprüfen, ob eine Klasse, die die abstrakte Klasse erweitert, die abstrakte Methode implementiert, wie dies bereits für Schnittstellen der Fall ist (eine Schnittstelle ist im Wesentlichen nur eine Klasse mit nur abstrakter Methode).
Tony BenBrahim

1
Ich stimme hier eher mit @rq_ überein. Bei abstrakten Methoden geht es darum, eine Validierung der Kompilierungszeit zu erhalten, damit das Programm nicht in einen ungültigen Zustand versetzt werden kann. Mit den vorgeschlagenen Lösungen erhalten Sie lediglich Laufzeitprüfungen. Wenn Ihr Programm ausgeführt wird, können Sie nicht sicher sein, ob es sich in einem gültigen Zustand befindet. Dies bedeutet, dass Sie unter der Annahme arbeiten sollten, dass die Methode nicht implementiert ist, und entsprechend schützen sollten. Sich selbst zu belügen, dass Sie abstrakte Methoden haben, bedeutet nur, von unerwartetem Laufzeitverhalten gebissen zu werden.
Micah Zoltu
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.