1. 程式人生 > >TypeScript 旅途5:泛型,設計一個雙向連結串列

TypeScript 旅途5:泛型,設計一個雙向連結串列

當我們設計元件的時候,我們總是希望元件能支援足夠多的資料型別,甚至是將來可能會新增的資料型別或者是自定義的資料型別,這樣在構建系統的時候就提供了非常靈活的功能。像C++、Java、C#一樣,TypeScript的泛型就是用來提供這種靈活性的。
先來看一個不使用泛型又支援多種資料型別的函式。

function identity(arg: any): any {
    return arg;
}

雖然使用 any 關鍵字讓我們能夠接收任意型別的引數,並返回任意型別。但是我們卻丟失了具體的返回型別。

再來看一個用泛型定義的函式。

function identity<T
>(a: T): T { return a; }

這裡的 <T>表示這是一個泛型函式, T 是一個 型別引數 ,它表示的是一種在執行時確定的型別。現在我們可以確定這個函式接收的引數型別和返回值的引數型別是同一種了。

如何使用這個泛型方法呢?

let output = identity<string>("myString"); 
let output = identity("myString");

我們會經常用第二種呼叫方法,因為編譯器的型別推導會幫我們推匯出來 T 的型別。

那麼一個泛型函式的函式型別是什麼呢?

function
identity<T>(a: T): T { return a; } let fun: <T>(a: T)=> T = identity;

泛型函式的型別與非泛型函式的型別沒什麼不同,只是有一個 型別引數 在最前面,像函式宣告一樣。

泛型介面與泛型類

interface IList<T> {
    add(a: T);//新增元素
}

class Item<T> {
    private _name: string;
    private _value: T;
}

泛型類與泛型介面跟非泛型類和非泛型介面定義一樣,只是名字末尾跟了一個 型別引數

T 被當作了一種確定的型別。

接下來看一個泛型的具體例子。實現一個雙向的連結串列。雙向連結串列就像是一個環一樣,每個元素既有一個指向前一個元素的引用(prev),又有一個指向下一個元素的引用(next)。預設新增一個頭元素一個尾元素,當沒有給列表新增元素時,這個列表就只有這一個頭元素,一個尾元素。
初始化列表頭尾元素
當我們新增一個元素後,要修改頭、尾元素的引用。
新增一個新元素,修改頭尾引用
我們可以發現,不管我們新增多少元素,或是移除全部元素,頭的prev引用和尾的next引用都是相互引用的,並且自始至終都不會修改。
現在該修改新新增的元素的prev和next引用了(初始化的時候我們給 prevnext 預設賦值為null)。
修改新元素的prev和next引用
再新增一個元素後是什麼樣呢?
新增第二個元素後
這裡為了圖片清晰些,沒有把頭的next引用和尾的prev引用用箭頭標出來。
明白了原理後,我們就來使用泛型實現這個雙向連結串列。
首先定義泛型介面,明確這個列表有哪些功能。

interface IList<T> {
    add(a: T);//新增元素
    insert(item: T, a: T);//插入元素
    remove(a: T);//移除元素
    header(): T;//返回頭元素
    tail(): T;//返回尾元素
    find(a: T): T;//查詢元素
    reverse_find(a: T): T;//反向查詢元素
    size(): number;//返回列表元素個數
    empty(): boolean;//是否空列表
    clear(): void;//清空列表
}

現在定義一個包裝列表中元素的泛型型別。

class Item<T> {
    private _name: string;
    private _value: T;//值
    private _prev: Item<T>;//指向的上一個元素
    private _next: Item<T>;//指向的下一個元素
    constructor(value: T) {
        this._name = value + '';
        this._value = value;
        this._prev = null;
        this._next = null;
    }
    set name(name: string) {
        this._name = name;
    }
    get name(): string {
        return this._name;
    }
    set value(value: T) {
        this._value = value;
    }
    get value(): T {
        return this._value;
    }
    set prev(item: Item<T>) {
        this._prev = item;
    }
    get prev(): Item<T> {
        return this._prev;
    }
    set next(item: Item<T>) {
        this._next = item;
    }
    get next(): Item<T> {
        return this._next;
    }
}

這個包裝型別的_name屬性只是為了列印的時候比較清晰,實際使用的時候沒有任何用處。

用泛型類實現連結串列。

class DobuleList<T> implements IList<T> {
    private _count: number = 0;//記錄元素個數
    private _header: Item<T>;//頭元素
    private _tail: Item<T>;//尾元素
    constructor() {
        this._header = new Item<T>(null);
        this._header.name = 'header';
        this._tail = new Item<T>(null);
        this._tail.name = 'tail';
        this._header.prev = this._header.next = this._tail;
        this._tail.next = this._tail.prev = this._header;
    }
    add(a: T) {
        let item = new Item<T>(a);
        item.prev = this._tail.prev;
        item.next = this._tail;
        this._tail.prev = item;
        item.prev.next = item;
        this._count++;
    }

    insert(item: T, a: T) {
        if (this.empty()) {
            return;
        }
        let indexItem = this._header.next;
        while(indexItem !== this._tail) {
            if (indexItem.value == item) {
                let valueItem = new Item<T>(a);
                valueItem.prev = indexItem;
                valueItem.next = indexItem.next;
                indexItem.next.prev = valueItem;
                indexItem.next = valueItem;
                this._count++;
                break;
            }
            indexItem = indexItem.next;
        }
    }

    remove(a: T): T {
        if (this.empty()) {
            return;
        }
        let indexItem = this._header.next;
        while(indexItem !== this._tail) {
            if (indexItem.value == a) {
                indexItem.prev.next = indexItem.next;
                indexItem.next.prev = indexItem.prev;
                indexItem.next = null;
                indexItem.prev = null;
                let value = indexItem.value;
                indexItem.value = null;
                indexItem = null;
                this._count--;
                return value;
            }
            indexItem = indexItem.next;
        }
    }

    header(): T {
        return this._header.next.value;
    }

    tail(): T {
        return this._tail.prev.value;
    }

    find(a: T): T {
        if (this.empty()) {
            return;
        }
        let indexItem = this._header.next;
        while(indexItem !== this._tail) {
            if (indexItem.value == a) {
                return indexItem.value;
            }
            indexItem = indexItem.next;
        }
        return null;
    }

    reverse_find(a: T): T {
        if (this.empty()) {
            return;
        }
        let indexItem = this._tail.prev;
        while(indexItem !== this._header) {
            if (indexItem.value == a) {
                return indexItem.value;
            }
            indexItem = indexItem.prev;
        }
        return null;
    }

    size(): number {
        return this._count;
    }

    empty(): boolean {
        return this._count === 0;
    }

    clear(): void {
        let item = this._header.next;
        while(item !== this._tail) {
            item.prev = null;
            item.value = null;
            item = item.next;
            item.prev.next = null;
        }
        this._header.next = this._tail;
        this._tail.prev = this._header;
        this._count = 0;
    }

    print() {
        let item = this._header.next;
        while(item !== this._tail) {
            console.log(item);
            item = item.next;
        }
    }
}

print()方法只是為了列印除錯。

測試一下我們的雙向連結串列。

let list1 = new DobuleList<number>();
list1.add(3);
list1.add(5);
list1.insert(3, 4);
list1.insert(4, 8);
list1.print();
/*輸出:
Item {_name: "3", _value: 3, _prev: Item, _next: Item}
Item {_name: "4", _value: 4, _prev: Item, _next: Item}
Item {_name: "8", _value: 8, _prev: Item, _next: Item}
Item {_name: "5", _value: 5, _prev: Item, _next: Item}
*/
list1.remove(4);
list1.print();
/*輸出:
Item {_name: "3", _value: 3, _prev: Item, _next: Item}
Item {_name: "8", _value: 8, _prev: Item, _next: Item}
Item {_name: "5", _value: 5, _prev: Item, _next: Item}
*/
console.log(list1.find(4));//null
console.log(list1.reverse_find(3));//3
list1.clear();

let list2 = new DobuleList<string>();
list2.add('3');
list2.add('5');
list2.insert('3', '4');
list2.insert('4', '8');
list2.print();
/*輸出:
Item {_name: "3", _value: "3", _prev: Item, _next: Item}
Item {_name: "4", _value: "4", _prev: Item, _next: Item}
Item {_name: "8", _value: "8", _prev: Item, _next: Item}
Item {_name: "5", _value: "5", _prev: Item, _next: Item}
*/
list2.remove('4');
list2.print();
/*輸出:
Item {_name: "3", _value: "3", _prev: Item, _next: Item}
Item {_name: "8", _value: "8", _prev: Item, _next: Item}
Item {_name: "5", _value: "5", _prev: Item, _next: Item}
*/
list2.clear();

//用自定義型別來測試下
class Demo {
    constructor(public name: string, 
        public value: number) {
        this.name = name;
        this.value = value;
    }
    toString(): string {
        return `{name: ${this.name} value: ${this.value}}`;
    }
}
let list3 = new DobuleList<Demo>();
let demos = [new Demo('3', 3), new Demo('5', 5),
    new Demo('4', 4), new Demo('8', 8)];
list3.add(demos[0]);
list3.add(demos[1]);
list3.insert(demos[0], demos[2]);
list3.insert(demos[2], demos[3]);
list3.print();
/*輸出:
Item {_name: "{name: 3 value: 3}", _value: Demo, _prev: Item, _next: Item}
Item {_name: "{name: 4 value: 4}", _value: Demo, _prev: Item, _next: Item}
Item {_name: "{name: 8 value: 8}", _value: Demo, _prev: Item, _next: Item}
Item {_name: "{name: 5 value: 5}", _value: Demo, _prev: Item, _next: Item}
*/
list3.remove(demos[2]);
list3.print();
/*輸出:
Item {_name: "{name: 3 value: 3}", _value: Demo, _prev: Item, _next: Item}
Item {_name: "{name: 8 value: 8}", _value: Demo, _prev: Item, _next: Item}
Item {_name: "{name: 5 value: 5}", _value: Demo, _prev: Item, _next: Item}
*/
list3.clear();

實際上常用的資料結構堆、棧、優先佇列等等都可以使用泛型來實現。