如何看待Typescript中的重載(Overload)?
從官網的例子出發。
而在我看來重載應該是這樣的。wiki上對overload的描述也和我想的類似。In some programming languages, function overloading or method overloading is the ability to create multiple methods of the same name with different implementations. Calls to an overloaded function will run a specific implementation of that function appropriate to the context of the call, allowing one function call to perform different tasks depending on context.
The overloaded function must differ either by the arity or data typesThe same function name is used for various instances of function call,而ts的重載更像只是重載函數聲明。告訴調用者我可以有這幾種調用方式。而不是寫的時候就可以重載。那我們應該如何看待這個重載功能呢?它到底是不是重載。
補充一下,TypeScript 在 github 上有很多 issue 和所謂的 "better and classic" compile time overload 有關,搜一下就可以看到。
官方對此類 proposal 的態度是覺得此類的提議違反了部分TypeScript design goals,包括
- breaking changes (11)
- changing behavior of a JS program (7)
- though a subjective argument, generates not-pretty-code (4)
具體可以看 https://github.com/Microsoft/TypeScript/issues/3442
幾點。
第一,常見的靜態類型語言中的overload是發生在編譯時的,編譯器可以清楚的將每一處同名函數調用對應到你寫的不同的函數實現。也就是,同名只是一個(讓程序員看到的)假象。如你寫了fun(x),有兩個實現fun(x: string)、fun(x: int),真正編譯後的程序里實際會有兩個函數,假設記做fun_string和fun_int,而每個fun(x)調用會被自動替換成fun_string(x)或fun_int(x)。有沒有可能編譯器無法確定替換成哪一個?當然有可能,這個時候編譯器就報錯了嘛,意思是你代碼寫錯啦!
第二,JavaScript是動態類型,所以是沒有上面這種意義上的overload的。但JS程序員可以在運行時判斷類型,也就是 function fun(x) { if (typeof x === "string") ... else/* assume x is int */ ... } 。TypeScript 的『overload』只是允許給這樣的函數標註多個類型。某輪說這是『繞過編譯器類型檢查』,是有問題的。這不是繞過,把函數參數標記為 (x: any) 才叫『繞過』。不過因為函數的具體實現只有一個,代碼本身會比上面那種overload要麻煩一些,比如說為了檢測類型偶爾你需要自己實現一些 type guard。至於說『下標函數也不能自己寫,這個很傻逼』,我估計某輪指的是 operator overload,然而很多語言都不允許(比如 java)。所以單單罵 TS/JS 有點扯。
第三,TS理論上當然是可以實現傳統的 overload 的,比如直接生成兩個函數,fun1、fun2。問題是從TS與JS的互操作性上來說,這事情就比較麻煩,比如一個js項目用了ts的庫,我不能直接寫fun,而得寫fun1、fun2。本來 overload 就是希望給程序員提供便利,但現在就並沒有什麼卵用。其實像java之類有『真』重載的語言編譯到js,或直接和js互操作,都有類似的問題。早在二十年前rhino里就有這問題——你在js里要指定到底調用的是哪一個java的重載方法是非常煩人的。特別是構造器,一般函數你說編譯成fun_string、fun_int也就算了,但構造器呢?相當棘手。
那TS能不能自動生成一個把多個實現合併起來的fun呢?
不好辦。因為runtime的類型檢查和編譯時類型檢查是很不一樣的(可以上網去查下override和overload的差異,前者通常就是runtime的),而且TS編譯後並沒有保留類型信息,所以複雜一點的類型根本沒法在runtime檢查。並且不帶有runtime類型檢查是TS的設計目標確定的,所謂by design是也。(其他答案也都提到了這一點。)
最後,這種只有『函數簽名重載』而函數實現卻還是只有一個,看上去只是因為TS要遷就JS而導致的限制。但從另外一方面說,『真』重載本身也並非只有優點沒有缺點,如維基詞條里寫的:
Caveats
If a method is designed with an excessive number of overloads, it may be difficult for developers to discern which overload is being called simply by reading the code. This is particularly true if some of the overloaded parameters are of types that are inherited types of other possible parameters (for example "object"). An IDE can perform the overload resolution and display (or navigate to) the correct overload.Type based overloading can also hamper code maintenance, where code updates can accidentally change which method overload is chosen by the compiler.
因此不少語言故意不支持重載。【至於運算符重載,爭議就更大了。本題不討論。】
TS的『假』重載卻沒有上述這些問題。派發規則是在函數里顯式寫的。也不存在代碼更新導致編譯器改變方法選擇(因為只有一個真正的方法)。
再如,分散式的、跨語言的場合,比如rpc框架,重載也可能是一個麻煩的問題。而像TS這種最終其實只有一個函數實現,就一定程度上規避了這些問題。
以上。
ts設計的原則之一就是不把類型檢查帶到runtime。
這個功能的實現要接近完全重寫整個 ts,因為 ts 的類型可不止基本類型(number 之類的能用 typeof 判斷的弱雞),更有 generics 這種複雜的沒那麼好判斷的東西,為了實現你這個 runtime 類型檢查的需要,ts 必須把整個類型系統暴露給編譯出來的 js 才能真正實現任何 runtime 檢查的需求,這樣和 ts 的設計原則走遠了。因為 JavaScript 根本就沒有重載這個特性,而且又鑒於是弱類型語言,所以如果你想在 JS 里實現所謂的重載,就需要自己判斷參數的個數和類型。而 TypeScript 作為一個 compile-to-js 的語言,自然也就無法實現重載特性了。
當然了,要實現也不是不可以,做法就是對不同版本的函數在編譯時起不一樣的名字,然後把調用處的函數名也改成相對應的,就可以初步實現重載了。為了與 JS 的兼容性,還需要生成一個函數,裡面就是根據不同的參數類型來派發給不同的函數重載。
至於為什麼 TypeScript 沒這麼做,就不得而知了。
題主應該是糾結TS的重載是不是重載。
如果按照維基百科的定義可以說不是,至少不是傳統Java/C#/C++的函數重載。傳統重載是同名函數有不同的實現和介面。
但是如果我們放寬定義的話,TS中的重載是create different function of the same name with different signature. 這點仍然和傳統重載同名不同介面是一脈相承的。所以把這個feature起名成重載無可厚非。
推薦不要過於糾結於術語名,尤其是在其內涵,外延和場景都比較清晰的時候。
說完是不是,再來看為什麼。原因很簡單,TS的設計中明確了類型信息不會在編譯結果內,所以必須讓程序員手寫重載實現,達到所寫即所得的效果。類型是純編譯期產物有很多好的特性,這裡不做展開。有興趣有時間再寫。
不過如果我們放開「Compile time type only」這條假設,其他保持TS的設計原則不變,會怎麼樣呢?
其實重載還是很難寫。首先,TypeScript is gradual typed. 一個 any 這個類型存在,使得編譯時派發是不可能的。any制止了編譯器檢查,所以相應的編譯器沒法在編譯時判斷一個重載該選哪個實現。如果碰到any默認選取第一個符合的函數簽名的話,又沒法處理只有函數申明沒有實現的type declaration的情況。
於是我們只有合併多個實現生成一個函數,用runtime type inspection決定派發。這也是不可靠的。
TypeScript的類型系統是結構定型(structural typing)的,只要兩個介面的欄位名和欄位類型一樣,這兩個介面就是一樣的。而且class也是strucutural typed的。運行時檢測不好做。
這會使得我們運行時結果和編譯期結果完全不同。比如下面的例子:
interface Runner { run(): void }
interface Reporter { report(): string }
var hkJournalist = {
run() {},
report: () =&> "big news!"
}
var runnable: Runner = hkJournalist
function test(reporter: Reporter): string { return "make big news" }
function test(runner: Runnable): number { return 888 }
test(runnable) // number or string?
在這裡, hkJournalist既能跑又能報,所以可以賦值給runnable。而編譯時,test的參數類型如果是Runner,那麼返回number。所以最後一個函數調用應該是number,但是在運行時,runnable這個變數的實際值其實是hkJournalist,因此應該先觸發第一個重載返回string。於是就產生了矛盾。
我們可以學習golang的interface,所有interface再套一層,一個value欄位指向真正的值,一個type欄位保存類型信息。但是這樣會導致編譯結果和源碼相差十萬八千里,和JS相互調用也非常非常困難(辦法總是有的),性能也會有問題。這就違背了TS的「JavaScript Superset」和"seamless interop"的設計原則。
因此,雖然沒有運行時類型信息是TS不做傳統重載的理由(借口)。但就算把這個借口去掉,其他設計仍然會限制重載的實現。實在是做不了,只能用這個compile type only overload + user provided dispatch implementaion來模擬JS特色的重載。
TypeScript不能自己寫重載函數,因為生成的代碼是不檢查參數類型的,所以那個東西只是一個給你繞過編譯器類型檢查的機會。同理,下標函數也不能自己寫,這個很傻逼。
譬如說你的函數是JavaScript寫的,你會自己檢查參數類型,而且函數的確也只接受有限的幾組類型,而且不能用"|"來表達(因為參數數量和順序可能不一樣),那這個時候就可以使用這個後門。
which就像官網例子說的那樣。
不得不說,我當初看到ts重載的內容的時候我也是題主的這樣想的。
但後來,仔細看一下ts官方的design goal 其實這並不是ts想要做的。 理論上來說,ts根據我們寫的內容,在生成的js里加入基本類型和參數個數判斷,然後生成一段相同功能的js,是有一定可行性的(呃。。不過如果是自己定義的類型,好像不好做判斷,畢竟ts里標註的類型並不完全是js運行時的類型),但是這不符合ts的設計初衷,產生的js代碼一點也不清真。根據這個design goal還有一些類似的問題,理論上ts可能可以做,但也不太會去做。
好比根據ts代碼,生成運行較快的JS代碼或是優化js代碼;加入運行時的代碼類型檢查;加入與ES提案無關的語法糖等等。我覺得這樣挺好的,專註於做type的事情,所以叫typescript嘛。類型重載, 實現不重載. js不自帶重載, ts的設計目標又希望runtime輕, 那隻能人類來手寫 (判斷是哪個重載) 的代碼.
對我來說最大的用處是...一些js庫的API要有這個重載才能寫出人能讀的類型. 比如jQuery.post
我自己很少寫, 因為如果一個方法的參數複雜到需要自己寫Overload, 我會覺得可能不應該作為一個方法出現, 然後就拆成幾個了.
似乎要寫多個export
我之前學習ts的時候,仗著我有c#背景,也是很輕視,以為會很簡單,但後來發現,他喵的要完全掌握ts的類型系統,比C#還要複雜啊。
學習ts,一定要理解ts的背景,它要解決的問題,和它面對的問題和妥協。
比如ts編譯成js,這些類型是都要丟棄的,也沒辦法再檢查了,js的原型鏈機制,也做不到一個鏈上帶幾個同名的function。
一群java轉javascript的重度魔障
TypeScript 的重載可以說是一個非常折中的方案。
詳細的可以看這裡,但這個 issue 最終也被標記為了 Out Of Scope.
Suggestion: Compile time function overloading · Issue #3442 · Microsoft/TypeScript
原因很簡單,TypeScript 最終會編譯成 JavaScript,但 TypeScript 里的類型和 JavaScript 運行時的類型是兩碼事,TypeScript 並不想成為侵入式的。
上一個 Google 在試驗的一個小眾語言(名字忘了,不是 Dart),也有類型,編譯後會在 JavaScript 注入整個自己的 runtime,想一想都覺得可怕,所以最後 Google 自己都放棄了,官網都寫著我們換了 TypeScript(就是想不起名字了)。
所以最好不要用 TypeScript 的 Overload,沒有太大意義。
推薦閱讀:
※如何看待json語法中不能加註釋的設計?
※JS中{}+[]和[]+{}的返回值情況是怎樣的?
※用d3.js或jfreechart在web上做圖表,哪個比較好,他們的區別在哪,哪個國內用的比較多?
※前端如何處理動態url?
※亞馬遜是如何反爬蟲的?
TAG:JavaScript | 編程語言 | 編程 | Nodejs | TypeScript |