Dichiarazione del metodo astratto in TypeScript


195

Sto cercando di capire come definire correttamente i metodi astratti in TypeScript:

Utilizzando l'esempio di eredità originale:

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

Vorrei sapere come definire correttamente il metodo makeSound, in modo che sia digitato e sia possibile usarlo eccessivamente.

Inoltre, non sono sicuro di come definire correttamente i protectedmetodi: sembra essere una parola chiave, ma non ha alcun effetto e il codice non verrà compilato.


4
Classi e metodi astratti sono ora una nuova funzionalità dell'imminente TypeScript 1.6.
falconepl,

Risposte:


285

La nameproprietà è contrassegnata come protected. Questo è stato aggiunto in TypeScript 1.3 e ora è consolidato.

Il makeSoundmetodo è contrassegnato come abstract, così come la classe. Non puoi istanziare direttamente un Animalora, perché è astratto. Questo fa parte di TypeScript 1.6 , che è ora ufficialmente attivo.

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

Il vecchio modo di imitare un metodo astratto era gettare un errore se qualcuno lo usava. Non dovresti più doverlo fare una volta che TypeScript 1.6 arriva nel tuo progetto:

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

È normale che il compilatore non si lamenti se manco un parametro, cambio un tipo di parametro o cambio il tipo di ritorno durante l'override di un metodo astratto?
Vetterjack,

1
È valido per omettere un parametro (se non lo si utilizza, è possibile ignorare qualsiasi valore passato) e si possono avere parametri di tipi compatibili. Si otterrebbe un errore se si provasse a implementare un metodo astratto makeSound(input : number) : string {basato sull'esempio sopra dove inputdovrebbe essere una stringa. Type 'string' is not assignable to type 'number'..
Fenton,

19

Se prendi la risposta di Erics un po 'oltre, puoi effettivamente creare un'implementazione abbastanza decente di classi astratte, con il pieno supporto per il polimorfismo e la capacità di chiamare metodi implementati dalla classe base. Cominciamo con il codice:

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

Poiché la Animalclasse richiede un'implementazione di IAnimalè impossibile costruire un oggetto di tipo Animalsenza avere una valida implementazione dei metodi astratti. Si noti che per far funzionare il polimorfismo è necessario passare attorno a casi di IAnimalno Animal. Per esempio:

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

La differenza principale qui con la risposta Erics è che la classe base "astratta" richiede un'implementazione dell'interfaccia, e quindi non può essere istanziata da sola.


1
Almeno per me con Typescript v1 - non posso fare riferimento a 'this' dall'interno di un costruttore per passare a super. Pensieri?
Kieran Benton,

Quale versione esatta del compilatore usi e quale errore ricevi? tsc 1.0.1 compila perfettamente i frammenti di cui sopra.
Tiddo,

La parola chiave "this" non è consentita in super (). Im usando
tsc

È piuttosto strano. Usi il compilatore CLI o Visual Studio?
Tiddo,

Anch'io non posso usare "this" nella chiamata super (). Posso usarlo subito dopo, per impostare il membro del genitore per l'implementazione figlio, ma questo non impone l'estensione della classe astratta. Sto usando il plug-in Eclispe Typsscript di Palantir, v1.0.1. Ho notato che super (questo) funziona benissimo in typescriptlang.org/Playground .
Eric

2

Credo che l'utilizzo di una combinazione di interfacce e classi base possa funzionare per te. Applicherà i requisiti comportamentali al momento della compilazione (rq_ post "sotto" si riferisce a un post sopra, che non è questo).

L'interfaccia imposta l'API comportamentale che non viene soddisfatta dalla classe base. Non sarai in grado di impostare i metodi della classe base per fare appello ai metodi definiti nell'interfaccia (perché non sarai in grado di implementare quell'interfaccia nella classe base senza dover definire tali comportamenti). Forse qualcuno può inventare una cassaforte trucco per consentire la chiamata dei metodi di interfaccia nel genitore.

Devi ricordare di estendere e implementare nella classe che istanzerai. Soddisfa le preoccupazioni sulla definizione del codice runtime-fail. Inoltre, non sarai nemmeno in grado di chiamare i metodi che potrebbero vomitare se non avessi implementato l'interfaccia (come se provassi a creare un'istanza della classe Animal). Ho provato a far sì che l'interfaccia estendesse BaseAnimal in basso, ma nascondeva il costruttore e il campo "name" di BaseAnimal da Snake. Se fossi stato in grado di farlo, l'uso di un modulo e delle esportazioni avrebbero potuto impedire l'istanza diretta accidentale della classe BaseAnimal.

Incollalo qui per vedere se funziona per te: 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);

Ho trovato la risposta di FristvanCampen come linkato di seguito. Dice che le classi astratte sono un anti-modello, e suggerisce che una istanza di base "classi astratte" usando un'istanza iniettata di una classe di implementazione. Questo è giusto, ma ci sono argomenti contrari. Leggi per te: https://typescript.codeplex.com/discussions/449920

Parte 2: ho avuto un altro caso in cui volevo una classe astratta, ma mi è stato impedito di utilizzare la mia soluzione sopra, perché i metodi definiti nella "classe astratta" dovevano fare riferimento ai metodi definiti nell'interfaccia corrispondente. Quindi, attrezzo il consiglio di FristvanCampen, in un certo senso. Ho la classe "astratta" incompleta, con implementazioni di metodi. Ho l'interfaccia con i metodi non implementati; questa interfaccia estende la classe "astratta". Ho quindi una classe che estende il primo e implementa il secondo (deve estendersi entrambi perché altrimenti il ​​super costruttore non è accessibile). Vedi l'esempio (non eseguibile) di seguito:

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

e

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

Uso un'eccezione nella classe base.

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

Quindi devi implementare nella sottoclasse. Il contro è che non c'è errore di compilazione, ma runtime. Il vantaggio è che puoi chiamare questo metodo dalla super classe, supponendo che funzionerà :)

HTH!

Milton


-20

No, no, no!Non tentare di creare classi e metodi "astratti" quando la lingua non supporta tale funzione; lo stesso vale per qualsiasi funzione linguistica che desideri supportare una determinata lingua. Non esiste un modo corretto per implementare metodi astratti in TypeScript. Basta strutturare il codice con convenzioni di denominazione in modo tale che determinate classi non vengano mai istanziate direttamente, ma senza applicare esplicitamente questo divieto.

Inoltre, l'esempio sopra fornirà questa applicazione solo in fase di esecuzione, NON in fase di compilazione, come ci si aspetterebbe in Java / C #.


4
Posso vedere da dove vieni, ma non sono rispettosamente rispettoso. Se una lingua implementa qualcosa, è male implementarla di nuovo da soli. Ma se non hai qualcosa, non hai altra scelta che implementarla da solo in qualche modo. Certo, non vedrai i problemi fino al runtime, ma lanciare un'eccezione la prima volta che verifichi qualcosa ti farà sapere che hai fatto lo stupido abbastanza rapidamente. Naturalmente non è l'ideale - ecco perché, IMO, Typescript ha bisogno di un supporto di classe astratto. Fino a quando non lo fa ...
Maverick,

Avrei voluto che JavaScript avesse classi, inferenza di tipo, digitazione statica e interfacce, e indovina un po ', Typescript ce l'ha. Sarebbe lo stesso per il metodo astratto, il compilatore deve solo verificare che qualsiasi classe che estende la classe astratta implementa il metodo astratto, come già fa per le interfacce (un'interfaccia è essenzialmente solo una classe con solo un metodo astratto)
Tony BenBrahim

1
Tendo ad essere d'accordo con @rq_ qui. Il punto dei metodi astratti è ottenere la convalida del tempo di compilazione che il programma non può ottenere in uno stato non valido. Le soluzioni proposte ti danno solo controlli di runtime, il che significa che quando il tuo programma è in esecuzione non puoi essere sicuro che sia in uno stato valido. Ciò significa che dovresti operare supponendo che il metodo non sia implementato e proteggerti di conseguenza. Mentire a te stesso che hai metodi astratti è solo chiedere di essere morso da un comportamento di runtime inaspettato.
Micah Zoltu,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.