Deklarowanie metody abstrakcyjnej w TypeScript


195

Próbuję dowiedzieć się, jak poprawnie zdefiniować metody abstrakcyjne w TypeScript:

Korzystając z oryginalnego przykładu dziedziczenia:

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

Chciałbym wiedzieć, jak poprawnie zdefiniować metodę makeSound, więc jest ona wpisana i możliwa do przekroczenia.

Nie jestem też pewien, jak poprawnie zdefiniować protectedmetody - wydaje się, że jest słowem kluczowym, ale nie ma żadnego efektu i kod się nie skompiluje.


4
Klasy i metody abstrakcyjne są teraz nową funkcją nadchodzącego TypeScript 1.6.
falconepl,

Odpowiedzi:


284

nameNieruchomość jest oznaczona jako protected. Zostało to dodane w TypeScript 1.3 i jest teraz mocno ustalone.

makeSoundMetoda jest oznaczona jako abstract, jak to klasa. Nie można teraz bezpośrednio utworzyć instancji Animal, ponieważ jest ona abstrakcyjna. Jest to część TypeScript 1.6 , który jest teraz oficjalnie dostępny.

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

Stary sposób naśladowania metody abstrakcyjnej polegał na zwróceniu błędu, jeśli ktoś go użył. Nie powinieneś tego więcej robić, gdy TypeScript 1.6 wyląduje w twoim projekcie:

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

Czy to normalne zachowanie, że kompilator nie narzeka, jeśli pominie parametr, zmieni typ parametru lub zmieni typ zwracany podczas nadpisywania metody abstrakcyjnej?
Vetterjack,

1
Można pominąć parametr (jeśli go nie użyjesz, możesz zignorować przekazaną wartość) i możesz mieć parametry zgodnych typów. Wystąpiłby błąd, gdybyś spróbował zaimplementować metodę abstrakcyjną na makeSound(input : number) : string {podstawie powyższego przykładu, gdzie inputpowinien być ciąg. Type 'string' is not assignable to type 'number'..
Fenton,

19

Jeśli pójdziesz dalej od odpowiedzi Ericssona, możesz stworzyć całkiem przyzwoitą implementację klas abstrakcyjnych, z pełnym wsparciem dla polimorfizmu i możliwością wywoływania zaimplementowanych metod z klasy podstawowej. Zacznijmy od kodu:

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

Ponieważ Animalklasa wymaga implementacji IAnimal, niemożliwe jest zbudowanie obiektu typu Animalbez prawidłowej implementacji metod abstrakcyjnych. Pamiętaj, że aby polimorfizm zadziałał, musisz przekazywać instancje IAnimal, a nie Animal. Na przykład:

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

Główna różnica tutaj w przypadku odpowiedzi Erics polega na tym, że „abstrakcyjna” klasa podstawowa wymaga implementacji interfejsu, a zatem nie może być tworzona samodzielnie.


1
Dla mnie przynajmniej z Typescript v1 - nie mogę odwoływać się do „tego” z konstruktora, aby przejść do super. Myśli?
Kieran Benton

Z jakiej dokładnie wersji kompilatora korzystasz i jaki błąd się pojawia? tsc 1.0.1 doskonale kompiluje powyższe fragmenty.
Tiddo

Słowo kluczowe „this” jest niedozwolone w super (). Używam tsc 1.0.3
Zasz

To dość dziwne. Czy korzystasz z kompilatora CLI lub Visual Studio?
Tiddo

Ja również nie mogę używać „this” w wywołaniu super (). Mogę go użyć zaraz po tym, aby ustawić element nadrzędny dla implementacji potomnej, ale nie wymusza to rozszerzenia klasy abstrakcyjnej. Korzystam z wtyczki Palantir Eclispe Typsscript, wersja 1.0.1. Zauważam, że super (this) działa dobrze w typecriptlang.org/Playground .
Eric

2

Uważam, że użycie kombinacji interfejsów i klas podstawowych może być dla Ciebie przydatne. Będzie wymuszać wymagania behawioralne w czasie kompilacji (rq_ post „below” odnosi się do powyższego postu, który nie jest tym).

Interfejs ustawia behawioralny interfejs API, który nie jest spełniony przez klasę podstawową. Nie będziesz mógł ustawić metod klasy bazowej do wywoływania metod zdefiniowanych w interfejsie (ponieważ nie będziesz w stanie zaimplementować tego interfejsu w klasie bazowej bez konieczności definiowania tych zachowań). Może ktoś może wymyślić sejf wymyśli sztuczkę, która pozwoli na wywołanie metod interfejsu w rodzicu.

Musisz pamiętać o rozszerzeniu i wdrożeniu w klasie, którą utworzysz. Zaspokaja obawy związane ze zdefiniowaniem kodu błędu wykonania. Nie będziesz nawet w stanie wywoływać metod, które mogłyby rzygać, jeśli nie zaimplementujesz interfejsu (na przykład, jeśli spróbujesz utworzyć instancję klasy Animal). Próbowałem, aby interfejs rozszerzył BaseAnimal poniżej, ale ukrył konstruktor i pole „name” BaseAnimal przed Snake. Gdybym był w stanie to zrobić, użycie modułu i eksportu mogłoby zapobiec przypadkowemu bezpośredniemu utworzeniu klasy BaseAnimal.

Wklej tutaj, aby zobaczyć, czy to działa dla Ciebie: 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);

Natrafiłem na odpowiedź FristvanCampen, o której mowa poniżej. Mówi, że klasy abstrakcyjne są anty-wzorcem i sugeruje, że jedna podstawowa klasa „abstrakcyjna” korzysta z wstrzykniętej instancji klasy implementującej. To jest sprawiedliwe, ale wysunięto kontrargumenty. Przeczytaj sam: https://typescript.codeplex.com/discussions/449920

Część 2: Miałem inny przypadek, w którym chciałem klasy abstrakcyjnej, ale nie mogłem użyć powyższego rozwiązania, ponieważ zdefiniowane metody w „klasie abstrakcyjnej” musiały odnosić się do metod zdefiniowanych w dopasowanym interfejsie. Tak więc, w pewnym sensie korzystam z porad FristvanCampen. Mam niepełną klasę „abstrakcyjną” z implementacjami metod. Mam interfejs z niezaimplementowanymi metodami; interfejs ten rozszerza klasę „abstrakcyjną”. Następnie mam klasę, która rozszerza pierwszą i implementuje drugą (musi rozszerzyć obie, ponieważ w przeciwnym razie superkonstruktor jest niedostępny). Zobacz (niedziałającą) próbkę poniżej:

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

i

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

Używam do zgłaszania wyjątku w klasie podstawowej.

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

Następnie musisz zaimplementować w podklasie. Wadą jest to, że nie ma błędu kompilacji, ale czas działania. Zaletą jest to, że możesz wywołać tę metodę z superklasy, zakładając, że zadziała :)

HTH!

Milton


-20

Nie nie nie! Nie próbuj tworzyć własnych „abstrakcyjnych” klas i metod, jeśli język nie obsługuje tej funkcji; to samo dotyczy dowolnej funkcji językowej, dla której dany język ma być obsługiwany. Nie ma prawidłowego sposobu implementacji metod abstrakcyjnych w TypeScript. Po prostu ustrukturyzuj swój kod za pomocą konwencji nazewnictwa, tak aby pewne klasy nigdy nie były tworzone bezpośrednio, ale bez wyraźnego egzekwowania tego zakazu.

Również powyższy przykład zapewni to wymuszenie tylko w czasie wykonywania, a NIE w czasie kompilacji, jak można by oczekiwać w Javie / C #.


4
Widzę, skąd pochodzisz, ale z szacunkiem się nie zgadzam. Jeśli jakiś język coś implementuje, źle jest go ponownie wdrożyć. Ale jeśli czegoś nie masz, nie masz innego wyjścia, jak samemu to wdrożyć. Jasne, nie zobaczysz problemów aż do czasu wykonania, ale zgłoszenie wyjątku przy pierwszym testowaniu czegoś da ci znać, że nie wygłupiłeś się dość szybko. Oczywiście nie jest to idealne - dlatego IMO, Typescript potrzebuje abstrakcyjnego wsparcia klasy. Ale dopóki tak się nie stanie ...
Maverick,

Chciałem, żeby JavaScript miał klasy, wnioskowanie o typach, pisanie statyczne i interfejsy, i zgadnij co, Typescript ma. Podobnie byłoby w przypadku metody abstrakcyjnej, kompilator musi tylko sprawdzić, czy każda klasa rozszerzająca klasę abstrakcyjną implementuje metodę abstrakcyjną, tak jak ma to już miejsce w przypadku interfejsów (interfejs jest zasadniczo tylko klasą z jedyną metodą abstrakcyjną)
Tony BenBrahim

1
Zazwyczaj zgadzam się z @rq_ tutaj. Istotą metod abstrakcyjnych jest uzyskanie sprawdzania poprawności czasu kompilacji, której program nie może wprowadzić w niepoprawny stan. Proponowane rozwiązania dają ci tylko kontrolę środowiska wykonawczego, co oznacza, że ​​podczas działania programu nie możesz być pewien, że jest w poprawnym stanie. Oznacza to, że powinieneś działać przy założeniu, że metoda nie jest zaimplementowana, i odpowiednio się strzeż. Kłamanie sobie, że masz abstrakcyjne metody, to po prostu prośba o ukąszenie przez nieoczekiwane zachowanie w czasie wykonywania.
Micah Zoltu
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.