在JavaScript中,如果執行"1" + 2,結果會是"12",因為JavaScript是…嗯…弱型別(Weakly-typed)語言?

在Python中,執行"1" + 2會發生TypeError,因為Python是強型別(Strongly-typed)語言?

在Java中執行"1" + 2,結果會是"12",這樣可以說Java就是弱型別語言嗎?

這樣的思考方向似乎不太對啊?

強型別與弱型別?

如大多數開發者所知的,在語言的分類上,會有所謂強型別、弱型別語言。

以"1" + 2這個運算式來說,"1"的型態string,2是個number,卻能進行+操作成為"12",這是因為執行時,2被轉為string,並進行字串串接而來。在JavaScript中,有許多這類無需開發者介入,就會自動發生型態轉換的操作,例如"2" - 1也是會得到1的結果,因而一般來說,傾向於將JavaScript歸類在弱型別語言。

然而在Python中,執行"1" + 2會發生TypeError,因為"1"的型態是str,2是個int,顯然地,"1"不會自動轉換為1,2也不會自動轉換為"2",因此,無法直接使用+進行int與int相加,或者是str與str的串接而發生錯誤,因而一般來說,傾向於將Python歸類在弱型別語言,真要進行運算,開發者得明確地使用"1" + str(2)得到"12",或者是int("1") + 2得到3。

一個單純的結論是,偏向強型別的語言,多數情況下必須明確進行型態轉換或剖析,避免了許多非預期的自動型態轉換造成的錯誤,然而這會帶來語法上的冗長。

弱型別語言則相反,取得了語法簡潔的優點,但必須多注意非預期型態轉換帶來的問題,JavaScript應該是後者的代表,你可以將number、string、[]、{}等混合運算,得到許多神奇的結果。這很好玩,不過,若在實際應用程式中,因此發生非預期結果,就一點都不好玩了!

強型別與弱型別本身就沒有嚴謹的定義,實際上,有許多呼籲別再使用這兩個名詞的聲音,以免在一些場合造成誤會。單看是否要明確進行型態轉換來區分語言,也有問題。正如在Java中執行"1" + 2,結果會是"12"就說Java是弱型別,相信許多開發者都不能接受,而且,JavaScript也不是沒有要明確進行型態轉換的例子,畢竟,你得使用parseInt("1") + 2,才能得到3的結果。

型態轉換潛規則有多少?

或許,當開發者以強型別、弱型別來分類一門語言時,真正想表達的是,這門語言在型態轉換上,預設的潛規則多或者是少。JavaScript就是屬於預設潛規則極多的語言,雖說若是能掌握,程式碼撰寫上會簡潔許多,然而,每個開發者掌握的程度不一致時,合作開發時,就可能發生許多彼此難以捉摸的問題。

崇尚明確總比隱含好的Python,就有著一組最小的潛規則,然而,由於語言亦追求簡單總比複雜好,這看似矛盾,實際上,可透過「明確地定義型態轉換潛規則」來解決。

舉例來說,可以定義__add__、__sub__、__mul__、__truediv__等方法,讓一個類別的實例,可以直接使用+、-、*、/等運算子,除了語言預設的潛規則之外,若發現這些運算子,可用於自訂類別上,就可實際查看類別原始碼,瞭解轉換的規則。

身為靜態定型語言,但努力追求簡潔語法的Scala,除了可直接定義+、-、*、/等方法來重載運算子之外,還可以透過implicit關鍵字,定義從A型態到B型態的隱式轉換(Implicit conversion),舉例來說,相對於撰寫:

val old = "oz"
val young = new StringBuilder(old).insert(1, "r")

開發者可以使用implicit,定義String至StringBuilder的型態轉換函式,達到撰寫簡潔程式碼的目的,例如:

implicit def stringToBuilder(s: String) = new StringBuilder(s)
val old = "oz"
val young = old.insert(1, "r")

雖然程式碼撰寫時,型態轉換看似是隱含的,實際上可以在某個地方查閱到轉換規則,例如Scala在scala.Predef中,就定義了許多implicit方法,對語言最小潛規則外的型態轉換,開發者能有明確載明規則的文件可供查詢。

Go的Typed與Untyped常數

近來在學習Go的過程中,我有個有趣的發現,Go對型態轉換抱持著極為嚴格的態度。在其他語言中,若x為1,y為3.14,基本上x + y這類的運算式是允許的;然而在Go中,卻會發生mismatched types的編譯錯誤,實際上更嚴格,只要型態不符合,就算都是整數,也無法通過編譯,例如:

var x int32 = 1
var y int64 = 2
fmt.Println(x + y) // mismatched types 編譯錯誤

解決的方式,是令運算元的型態一致,例如x + int32(y)或int64(x) + y,不過,這就帶來了一個問題,像是x + 1到底能不能通過編譯呢?是不是要寫成x + int32(1)?

為了解決這個問題,Go將常數區分為Typed與Untyped,如果開發者單純寫下1這個常數,那麼它是個整數,不過,其型態未定(Untyped),也就是,它並非Go中定義的int8、int16、int32、int64等任何一種型態。

只有在程式上下文(context)中能判定型態,1才能決定它的型態,因此,若x是int32,Go寫下x + 1,這時1會是int32;類似地,const Pi = 3.14宣告中,Pi是個型態未定的浮點數。若是在宣告常數時指定型態,例如,const Pi float64 = 3.14,那浮點數Pi的型態為float64。不過,這也又留下另一個問題,那麼1 + 3.14呢?

這是個常數運算式,Go會根據運算元是整數、rune(單引號括住的常數)、浮點數或複數的順序,來決定編譯時期的值,1 + 3.14的運算元是整數與浮點數,因此,結果會是浮點數4.14,不過其型態未定。同樣地,const C = 1 + 3.14的話,C常數值是浮點數4.14,然而型態未定,因為沒有程式上下文可確定C的型態,然而,若是撰寫const C float64 = 1 + 3.14的話,那麼C會是float64型態。

清楚規範型態轉換規則

乍看Go的Typed與Untyped常數,覺得似乎是特立獨行的規範,實際上,卻是讓型態轉換的規則最小化了:「常數只有在程式上下文可獲取型態資訊時,才會是已定義型態,只要有型態,若運算時必須轉換型態,一律要明確指定」,據稱,Go語言是個以軟體工程為目標而設計出來的語言,如此嚴謹得規範型態轉換,可見型態轉換是個不容輕忽的大問題。

反過來思考,在其他語言中,開發者往往容易忽略型態轉換規則,然而,搞清楚規則絕對是必要的。我曾在Java中遇過的真實案例,是life / (365 * 24 * 60 * 60 * 1000))、Integer.MAX_VALUE + 1,或者是誤以為Integer.MIN_VALUE == -Integer.MIN_VALUE為true的錯誤。

使用如JavaScript這類潛規則極多的語言,也應建立一組清楚的型態轉換規則或慣例,採用嚴格或廣為人知的規則,或者是明確定義函式進行型態轉換。例如使用toBoolean(value),而不是!!value來取得真正的boolean值。

因此,"1" + 2要思考的,不僅只是強、弱型別二分法的問題,它代表的是其實是開發者或團隊成員,是否對型態系統有清楚或一致的認識。

作者簡介


Advertisement

更多 iThome相關內容