在TypeScript中声明抽象方法

251
我正在尝试找出如何在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方法 - 它似乎是一个关键字,但没有效果,代码无法编译。


7
抽象类和方法将成为即将推出的TypeScript 1.6的新功能。 - falconepl
5个回答

371

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'. - Fenton

27

如果您进一步了解Eric的答案,实际上您可以创建一个相当不错的抽象类实现,具有完全的多态支持和从基类调用已实现方法的能力。让我们从以下代码开始:

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

这里与Eric的回答主要区别在于“抽象”基类需要实现接口,因此不能单独实例化。

1
至少对于我来说,在Typescript v1中,我无法从构造函数内部引用'this'并将其传递给super。你有什么想法吗? - Kieran Benton
你使用的编译器是哪个确切的版本,出现了什么错误?tsc 1.0.1 可以完美地编译上述代码片段。 - Tiddo
“this” 关键字不能在 super() 中使用。我正在使用 tsc 1.0.3。 - Zasz
这很奇怪。你使用CLI编译器还是Visual Studio? - Tiddo
我也无法在super()调用中使用"this"。我可以在之后立即使用它,为子实现设置父级成员,但这并不强制扩展抽象类。我正在使用Palantir的Eclispe Typsscript插件v1.0.1。我注意到在http://www.typescriptlang.org/Playground/中,super(this)可以正常工作。 - Eric
我在 Playground 中尝试了一下,发现只有在没有内联初始化任何类成员的情况下 super(this) 才有效。例如,如果我将 hello = 'world'; 声明并初始化为字段,则使用 super(this) 会出错,但如果我使用 get hello() { return 'world'; } 或在构造函数中初始化它,则不会出错。 - Joe Skeen

4
我认为使用接口和基类的组合可能对您有用。它将在编译时强制执行行为要求(rq_post "below"是指上面的帖子,而不是本帖)。
接口设置未被基类满足的行为API。您将无法设置基类方法以调用在接口中定义的方法(因为您将无法在基类中实现该接口而不必定义这些行为)。也许有人可以想出一个安全的技巧来允许在父级中调用接口方法。
您必须记住在您将要实例化的类中扩展和实现。它满足了定义运行时失败代码的关注点。如果您尝试实例化动物类,则甚至无法调用会引起错误的方法(例如,如果您尝试实例化Animal类)。我尝试让接口扩展下面的BaseAnimal,但它隐藏了构造函数和BaseAnimal的“name”字段,使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的回答,如下链接。他认为抽象类是一种反模式,并建议使用实现类的注入实例来实例化基类'abstract'类。这是公平的,但也有相反的观点。请自行阅读: https://typescript.codeplex.com/discussions/449920

第二部分: 我遇到了另一个案例,我想要一个抽象类,但我不能使用上述的解决方案,因为“抽象类”中定义的方法需要引用匹配接口中定义的方法。所以,我采用了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;

}

2

我以前在基类中抛出异常。

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

接下来你需要在子类中实现这个方法。 缺点是没有编译错误,但是会在运行时出现问题。优点是你可以从父类中调用这个方法,假设它能正常工作 :)

希望对你有所帮助!

Milton


-23

不,不,不! 当语言不支持该功能时,请不要尝试制作自己的“抽象”类和方法;对于您希望某个语言支持的任何语言功能都是如此。在TypeScript中没有正确实现抽象方法的方法。只需使用命名约定构造代码,使某些类永远不会直接实例化,但不明确强制执行此禁令。

此外,上面的示例仅在运行时提供此执行,而不是在编译时提供,这是您在Java/C#中期望的。


8
我理解你的观点,但我不同意。如果一种语言实现了某些功能,重新再实现一遍是不好的。但如果你没有这个功能,那么你别无选择,只能自己去实现。当然,你在运行时才会发现其中的问题,但第一次测试时抛出异常会让你很快知道你错了。当然,这并不是理想的做法,这也是我认为Typescript需要抽象类支持的原因。但在Typescript实现之前... - Maverick
我希望JavaScript能够有类、类型推断、静态类型和接口,而你知道吗,TypeScript实现了这些。对于抽象方法也是一样的,编译器只需检查继承抽象类的任何类是否实现了抽象方法,就像它已经为接口做的那样(接口本质上就是只有抽象方法的类)。 - Tony BenBrahim
1
我倾向于同意@rq_的观点。抽象方法的目的是为了获得编译时验证,以确保程序不会进入无效状态。提出的解决方案只是给你运行时检查,这意味着当你的程序运行时,你不能确定它处于有效状态。这意味着你应该在假设该方法未实现的情况下进行操作,并相应地进行保护。欺骗自己拥有抽象方法只会导致意外的运行时行为。 - Micah Zoltu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接