型別

型別 在 Julia

1 個練習

關於 型別

到目前為止,課程大綱還沒怎麼提到型別,但型別顯然存在於 Julia 中:

julia> vals = (42, 4.3, π, "hello", 'Q')
(42, 4.3, π, "hello", 'Q')

julia> typeof(vals)
Tuple{Int64, Float64, Irrational{:π}, String, Char}

我們從未指定型別,但 Julia 還是照樣指定了它們。

  1. Julia 有types,型別是它設計的核心。
  2. Julia 通常能「猜」出型別,靠的是Type Inference。

JIT 編譯器會瀏覽(所有)程式碼,觀察變數怎麼被使用,然後_推論_出一個與該用法相容的合適預設型別。

型別提升

在先前幾個概念裡,已經有很多例子把數值型別強制轉成一致的形式,從最簡單的算術開始。

julia> 2 + 1.3
3.3

我們把一個整數(Int64)和一個浮點數(Float64)相加,得到的是 Float64 的結果。同樣地:

julia> nums = (3, 4.1, 1//4)
(3, 4.1, 1//4)

julia> typeof(nums)
Tuple{Int64, Float64, Rational{Int64}}

julia> [nums...]
3-element Vector{Float64}:
 3.0
 4.1
 0.25

在上面的例子中,元組保留了每個元素的型別,但轉成向量之後,全部都被改成一致的 Float64。

Julia 編譯器知道哪些型別轉換是可行的:整數轉浮點數沒問題,浮點數轉整數會失去精度,因此會引發 InexactError。 型別提升會把運算式中所有的值轉成一個共同的型別,這個型別要夠通用,才能與所有輸入相容。

同樣的轉換也可以用promote()函式明確地完成:

julia> promote(nums...)
(3.0, 4.1, 0.25)

型別指定

對解決簡單的教學練習來說,仰賴型別推論和型別提升沒問題,但程式一大,你大概就需要更精確的控制。

在多數現代處理器上,整數預設是 Int64。 我們在Numbers概念中看過,值可以轉換成某個非預設的型別。

julia> x = Int16(42)
42

julia> typeof(x)
Int16

不過,變數x仍然可以被重新指定成不同的型別:

julia> x = "changed"
"changed"

julia> typeof(x)
String

這就是type instability,它:

  • 在小型腳本裡非常方便。
  • 在較大的程式裡,對效能和可靠性都不好。

我們也可以改用::運算子來設定x的型別:

julia> y::Int16 = 42
42

julia> typeof(y)
Int16

julia> y = "changed"
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Int16
The function `convert` exists, but no method is defined for this combination of argument types.

現在y的型別是Int16,而且永遠都會是Int16。 如此一來,編譯器就知道要為它保留多少位元組,也能把其餘程式碼最佳化成依賴這個穩定型別的形式。 就這點來說,這種變數和 C 這類靜態型別語言裡的變數差不多。

型別斷言

前面介紹的型別指定,用在變數指定的左側,用來限制該變數的型別。

把::運算子用在某個值、或會求值成某個值的東西上,通常就是在_斷言_這個值必須是這種型別,否則就應該拋出錯誤。

julia> 42::Number
42

julia> "two"::Number
ERROR: TypeError: in typeassert, expected Number, got a value of type String

常見的用法是把這用在函式的回傳值上,當作簡單的最後一道檢查,確認函式行為符合預期。

注意,還有個@assert巨集可以用來做其他形式的斷言。

型別階層

Int64、Int16、String、Char:這些型別究竟是_「從哪來的」_?

在許多物件導向(OO)語言裡,每個型別都是一個類別,子類別化把它們排成類別階層,而類別方法則定義了各種行為。

Java 和 Ruby 是這種模式的明顯例子,但就連 Python 在內部也類似。

Julia 沒有類別。

官方文件給的理由是,物件導向的特性會干擾 JIT 編譯器,損害執行效能。

誰知道呢,也許他們也讀過這段話:

「物件導向程式設計是個格外糟糕的主意,這種主意只可能源自加州。」

這句話一般歸於 Edsger Dijkstra,他從 1950 年代起便是位傑出的電腦科學家,活躍數十年(雖然大家通常不認為他特別樂觀,也不覺得他特別擅長委婉的外交手腕)。

可是,看看這段程式碼:

julia> y::Int16 = 42
42

julia> typeof(y)
Int16

julia> supertype(Int16)
Signed

julia> supertypes(Int16)
(Int16, Signed, Integer, Real, Number, Any)

補充一些細節:

  • Int16 是一種型別,我們可以建立這種型別的變數。
  • Int16 是 Signed 的子型別,supertype() 函式會告訴我們這件事。
  • 型別有一個階層結構,往上會經過 Integer、Real 和 Number,最頂端是 Any,而 subtypes() 會為我們列出階層的這個分支。

所有分支最後都終止於 Any,而 Any 獨特的地方在於它是自己的超型別。

julia> supertypes(String)
(String, AbstractString, Any)

julia> supertype(Any)
Any

所以,Julia 沒有 class 階層,但_確實_有 type 階層。 想顯示整個階層會得到一棵巨大的樹,根本沒辦法真的看清楚。 至於只看其中一部分,則可以在線上討論裡找到。

之後我們會試著拆解這一切是怎麼運作的,不過在那之前還有更多東西要先探索。

測試型別

我們可以用typeof()照平常的方式測試相等性。

julia> typeof(11)
Int64

julia> typeof(11) == Int64
true

julia> typeof(11) == Number
false

型別相等必須完全一致,因為這種比較方式並不理解型別階層。

更有彈性的做法是用isa,它會告訴我們某個值的型別,是和比較對象的型別相同,還是它的子型別。 它可以用中綴或函式形式來使用。

julia> 12 isa Int64
true

julia> 12 isa Number
true

julia> isa(12, Number)
true

julia> 12 isa String
false

注意,isa 左邊要放的是 value,不是 type。

用這種方式比較兩個_型別_會得到意想不到的結果。 正確的運算子是 <:,之後的概念裡我們還會看到很多次。

julia> Int64 isa Number  ## Don't do this!
false

julia> Int64 <: Number
true

型別測試可以用來控制函式內部的流程,但在道地的 Julia 裡相對少見。

我們會在多重派發概念中看到,把型別加在函式引數上、讓 Julia 的派發機制處理這類邏輯,往往更有效率。 不過,在談到多重派發之前,我們還有好幾個和型別相關的概念要討論。

抽象型別與具體型別

我們看過,型別階層形成樹狀結構(以電腦科學的意義來說,根在頂端)。

樹裡的每一項都是一個node,而它們可以分成幾類:

  1. 有子型別的節點稱為abstract。
  2. 沒有子型別的葉節點則稱為concrete。
julia> subtypes(Integer)  # an abstract type
3-element Vector{Any}:
 Bool
 Signed
 Unsigned

julia> subtypes(Int64)  # a concrete type
Type[]

這是個重要的區別,因為只有具體型別可以被instantiated成變數。

julia> a::Int16 = 42
42

julia> typeof(a)
Int16

julia> b::Integer = 42
42

julia> typeof(b)
Int64

注意,使用抽象型別(在這裡)不會產生錯誤訊息,但編譯器會建立一個適當的具體型別:Int64 而不是 Integer。 至少它防止了變數被指定成非整數:

julia> f::Integer = "hello"
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Integer
The function `convert` exists, but no method is defined for this combination of argument types.

因此,用抽象型別做型別指定,就是對變數型別的_限制_,限定它必須是(以上例來說)Integer 的任一子型別。 這在程式語言的世界裡並不尋常:它比 C 的型別指定弱,比近年 Python 版本的型別提示強。

isabstracttype()和isconcretetype()這兩個函式可以用來測試。 注意,這兩個函式並不只是彼此的否定:我們之後的概念裡會看到,有些型別既不抽象也不具體。

julia> isconcretetype(Integer), isabstracttype(Integer)
(false, true)

julia> isconcretetype(Int64), isabstracttype(Int64)
(true, false)

# Vector is neither
julia> isconcretetype(Vector), isabstracttype(Vector)
(false, false)

雖然 Vector 不是具體型別,但它的元素是具體的。 eltype()(元素型別)函式會取出這個型別:

julia> eltype([1, 2.3])
Float64
透過 GitHub 編輯 連結會在新視窗或分頁中開啟

學習 型別