Khai báo phương thức trừu tượng trong TypeScript


195

Tôi đang cố gắng tìm ra cách xác định chính xác các phương thức trừu tượng trong TypeScript:

Sử dụng ví dụ thừa kế ban đầu:

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

Tôi muốn biết làm thế nào để xác định chính xác phương thức makeSound, vì vậy nó được gõ và có thể được ghi đè.

Ngoài ra, tôi không chắc chắn làm thế nào để xác định chính xác protectedcác phương thức - nó dường như là một từ khóa, nhưng không có tác dụng và mã sẽ không được biên dịch.


4
Các lớp và phương thức trừu tượng hiện là một tính năng mới của TypeScript 1.6 sắp tới.
falconepl

Câu trả lời:


284

Các nametài sản được đánh dấu là protected. Điều này đã được thêm vào TypeScript 1.3 và hiện đã được thiết lập vững chắc.

Các makeSoundphương pháp được đánh dấu là abstract, như là lớp. Bạn không thể trực tiếp khởi tạo một Animalbây giờ, bởi vì nó là trừu tượng. Đây là một phần của TypeScript 1.6 , hiện đã chính thức hoạt động.

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

Cách cũ để bắt chước một phương pháp trừu tượng là ném lỗi nếu có ai sử dụng nó. Bạn không cần phải làm điều này thêm một lần nữa khi TypeScript 1.6 hạ cánh trong dự án của bạn:

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

Có phải đó là hành vi bình thường, trình biên dịch không phàn nàn nếu tôi bỏ lỡ một tham số, thay đổi loại tham số hoặc thay đổi loại trả về khi ghi đè một phương thức trừu tượng?
Vetterjack

1
Nó là hợp lệ để bỏ qua một tham số (nếu bạn không sử dụng nó, bạn có thể bỏ qua bất kỳ giá trị nào được thông qua) và bạn có thể có các tham số của các loại tương thích. Bạn sẽ gặp lỗi nếu bạn cố gắng thực hiện một phương thức trừu tượng makeSound(input : number) : string {dựa trên ví dụ trên, nơi inputphải là một chuỗi. Type 'string' is not assignable to type 'number'..
Fenton

19

Nếu bạn trả lời Erics thêm một chút nữa, bạn thực sự có thể tạo ra một triển khai khá tốt các lớp trừu tượng, với sự hỗ trợ đầy đủ cho đa hình và khả năng gọi các phương thức được thực hiện từ lớp cơ sở. Hãy bắt đầu với mã:

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

Animallớp yêu cầu triển khai IAnimalnên không thể xây dựng một đối tượng kiểu Animalmà không có triển khai hợp lệ các phương thức trừu tượng. Lưu ý rằng để đa hình hoạt động, bạn cần phải vượt qua các trường hợp IAnimalchứ không phải Animal. Ví dụ:

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

Sự khác biệt chính ở đây với câu trả lời của Erics là lớp cơ sở "trừu tượng" đòi hỏi phải thực hiện giao diện và do đó không thể tự khởi tạo được.


1
Đối với tôi ít nhất là với Bản mô tả v1 - Tôi không thể tham khảo 'cái này' từ bên trong một hàm tạo để chuyển sang siêu. Suy nghĩ?
Kieran Benton

Phiên bản chính xác nào của trình biên dịch bạn sử dụng và bạn gặp phải lỗi gì? tsc 1.0.1 biên dịch các đoạn trên hoàn toàn tốt.
Tiddo

Từ khóa 'this' không được phép trong super (). Tôi đang sử dụng tsc 1.0.3
Zasz

Điều đó thật kỳ quặc. Bạn có sử dụng trình biên dịch CLI hoặc Visual Studio không?
Tiddo

Tôi cũng vậy, không thể sử dụng "this" trong lệnh gọi super (). Tôi có thể sử dụng nó ngay sau đó, để thiết lập thành viên của cha mẹ cho việc triển khai con, nhưng điều này không bắt buộc mở rộng lớp trừu tượng. Tôi đang sử dụng plugin Eclispe Typsscript của Palantir, v1.0.1. Tôi nhận thấy rằng siêu (cái này) hoạt động tốt trong typecriptang.org/Playground .
Eric

2

Tôi tin rằng việc sử dụng kết hợp các giao diện và các lớp cơ sở có thể phù hợp với bạn. Nó sẽ thực thi các yêu cầu hành vi tại thời gian biên dịch (rq_ post "bên dưới" đề cập đến một bài viết ở trên, không phải là bài này).

Giao diện đặt API hành vi không được đáp ứng bởi lớp cơ sở. Bạn sẽ không thể đặt các phương thức lớp cơ sở để gọi các phương thức được xác định trong giao diện (vì bạn sẽ không thể thực hiện giao diện đó trong lớp cơ sở mà không phải xác định các hành vi đó). Có lẽ ai đó có thể đưa ra một mẹo an toàn để cho phép gọi các phương thức giao diện trong cha mẹ.

Bạn phải nhớ mở rộng và thực hiện trong lớp bạn sẽ khởi tạo. Nó thỏa mãn mối quan tâm về việc xác định mã thời gian chạy thất bại. Bạn thậm chí sẽ không thể gọi các phương thức sẽ gây khó chịu nếu bạn chưa triển khai giao diện (chẳng hạn như nếu bạn cố gắng khởi tạo lớp Animal). Tôi đã thử để giao diện mở rộng BaseAnimal bên dưới, nhưng nó đã ẩn hàm tạo và trường 'tên' của BaseAnimal từ Snake. Nếu tôi đã có thể làm điều đó, việc sử dụng một mô-đun và xuất khẩu có thể ngăn chặn việc khởi tạo trực tiếp ngẫu nhiên của lớp BaseAnimal.

Dán cái này vào đây để xem nó có phù hợp với bạn không: 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);

Tôi đã xem qua câu trả lời của FristvanCampen như được liên kết dưới đây. Ông nói rằng các lớp trừu tượng là một mô hình chống và gợi ý rằng một lớp khởi tạo các lớp 'trừu tượng' bằng cách sử dụng một thể hiện của một lớp triển khai. Điều này là công bằng, nhưng có những lập luận chống lại được thực hiện. Đọc cho chính mình: https://typescript.codeplex.com/discussions/449920

Phần 2: Tôi đã có một trường hợp khác khi tôi muốn một lớp trừu tượng, nhưng tôi đã bị ngăn không sử dụng giải pháp của mình ở trên, bởi vì các phương thức được định nghĩa trong "lớp trừu tượng" cần để tham khảo các phương thức được định nghĩa trong giao diện phù hợp. Vì vậy, tôi xin lời khuyên của FristvanCampen. Tôi có lớp "trừu tượng" không hoàn chỉnh, với việc triển khai phương thức. Tôi có giao diện với các phương thức chưa thực hiện; giao diện này mở rộng lớp "trừu tượng". Sau đó tôi có một lớp mở rộng lớp thứ nhất và thực hiện lớp thứ hai (nó phải mở rộng cả hai vì siêu xây dựng không thể truy cập bằng cách khác). Xem mẫu (không chạy được) dưới đây:

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

Tôi sử dụng để ném một ngoại lệ trong lớp cơ sở.

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

Sau đó, bạn phải thực hiện trong lớp học phụ. Nhược điểm là không có lỗi xây dựng, nhưng thời gian chạy. Ưu điểm là bạn có thể gọi phương thức này từ siêu hạng, giả sử rằng nó sẽ hoạt động :)

HTH!

Milton


-20

Không không không! Vui lòng không thử tạo các lớp và phương thức 'trừu tượng' của riêng bạn khi ngôn ngữ không hỗ trợ tính năng đó; điều tương tự cũng xảy ra với bất kỳ tính năng ngôn ngữ nào bạn muốn một ngôn ngữ nhất định được hỗ trợ. Không có cách chính xác để thực hiện các phương thức trừu tượng trong TypeScript. Chỉ cần cấu trúc mã của bạn với các quy ước đặt tên sao cho các lớp nhất định không bao giờ được khởi tạo trực tiếp, nhưng không thực thi rõ ràng lệnh cấm này.

Ngoài ra, ví dụ trên sẽ chỉ cung cấp sự thực thi này trong thời gian chạy, KHÔNG phải vào thời gian biên dịch, như bạn mong đợi trong Java / C #.


4
Tôi có thể thấy bạn đến từ đâu, nhưng tôi không đồng ý. Nếu một ngôn ngữ thực hiện một cái gì đó, thật tệ khi tự mình thực hiện lại nó. Nhưng nếu bạn không có thứ gì đó, thì bạn không còn cách nào khác ngoài tự mình thực hiện nó. Chắc chắn, bạn sẽ không thấy các vấn đề cho đến thời gian chạy, nhưng ném một ngoại lệ vào lần đầu tiên bạn kiểm tra một cái gì đó sẽ cho bạn biết bạn đã bị lừa khá nhanh. Tất nhiên đó không phải là lý tưởng - đó là lý do tại sao, IMO, Typecript cần hỗ trợ lớp trừu tượng. Tuy nhiên, cho đến khi nó ...
Maverick

Tôi muốn JavaScript có các lớp, kiểu suy luận, kiểu gõ tĩnh và giao diện và đoán xem, typecript có gì. Phương thức trừu tượng cũng giống như vậy, trình biên dịch chỉ cần kiểm tra xem bất kỳ lớp nào mở rộng lớp trừu tượng đều thực hiện phương thức trừu tượng, giống như đối với các giao diện (một giao diện về cơ bản chỉ là một lớp chỉ có phương thức trừu tượng)
Tony BenBrahim

1
Tôi có xu hướng đồng ý với @rq_ ở đây. Điểm của các phương thức trừu tượng là để có được xác thực thời gian biên dịch mà chương trình có thể vào trạng thái không hợp lệ. Các giải pháp được đề xuất chỉ cung cấp cho bạn kiểm tra thời gian chạy, có nghĩa là khi chương trình của bạn chạy, bạn không thể chắc chắn nó đang ở trạng thái hợp lệ. Điều này có nghĩa là bạn nên hoạt động theo giả định rằng phương thức này không được thực hiện và bảo vệ tương ứng. Nói dối với bản thân rằng bạn có các phương pháp trừu tượng chỉ là yêu cầu bị cắn bởi hành vi thời gian chạy bất ngờ.
Micah Zoltu
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.