類型

類型系統傳統上分為兩個截然不同的陣營:靜態類型系統,其中每個程式表達式都必須在程式執行前計算出一個類型,以及動態類型系統,其中在執行時間之前對類型一無所知,而這時程式所操作的實際值是可用的。物件導向允許在靜態類型語言中具備一些彈性,方法是讓程式碼在編譯時不必知道精確的值類型。撰寫可操作不同類型的程式碼的能力稱為多型性。經典動態類型語言中的所有程式碼都是多型的:只有在明確檢查類型時,或當物件在執行時無法支援運算時,任何值的類型才會受到限制。

Julia 的類型系統是動態的,但透過讓特定值具有特定類型,它獲得了靜態類型系統的一些優點。這在產生有效率的程式碼時可能很有幫助,但更重要的是,它允許在函式引數的類型上進行方法調用,並與語言深度整合。方法調用在 方法 中有詳細探討,但根源於此處所介紹的類型系統。

在 Julia 中,當省略類型時,預設行為是允許值具有任何類型。因此,人們可以在不使用明確類型的情況下撰寫許多有用的 Julia 函式。然而,當需要額外的表達能力時,很容易將明確的類型註解逐漸引入先前「未類型化」的程式碼中。加入註解有三個主要目的:利用 Julia 強大的多重調用機制、改善人類可讀性,以及捕捉程式設計師錯誤。

類型系統的術語來描述 Julia,它是:動態的、標稱的和參數化的。泛型類型可以參數化,類型之間的層次關係是明確宣告的,而不是由相容的結構暗示的。Julia 類型系統的一個特別顯著的特徵是具體類型可能不會互相子類型化:所有具體類型都是最終的,並且只能有抽象類型作為其超類型。雖然這乍看之下似乎過於嚴格,但它有許多有益的後果,而缺點卻出乎意料地少。事實證明,能夠繼承行為比能夠繼承結構重要得多,而繼承兩者在傳統的面向物件語言中會造成重大的困難。Julia 類型系統的其他高級別面向,應該在前面提到的是

  • 物件和非物件值之間沒有區別:Julia 中的所有值都是具有類型的真實物件,該類型屬於單一的、完全連接的類型圖,其所有節點作為類型都是平等的一等公民。
  • 沒有「編譯時類型」的有意義概念:值所具有的唯一類型是程式執行時的實際類型。在靜態編譯與多型結合使這種區別顯著的面向物件語言中,這稱為「執行時類型」。
  • 只有值,而不是變數,具有類型 - 變數只是繫結到值的變數,儘管為了簡化起見,我們可能會將「變數的類型」簡稱為「變數所引用的值的類型」。
  • 抽象和具體類型都可以由其他類型參數化。它們也可以由符號參數化,對於任何類型的值,isbits 返回 true(基本上,像數字和布林值之類的東西,它們像 C 類型或沒有指向其他對象的指標的 struct 那樣儲存),以及它們的元組。當不需要引用或限制類型參數時,可以省略它們。

Julia 的類型系統被設計為功能強大且富有表現力,同時清晰、直觀且不引人注目。許多 Julia 程式設計師可能永遠不需要撰寫明確使用類型的程式碼。但是,某些類型的程式設計透過宣告類型變得更清晰、更簡單、更快速且更強大。

類型宣告

:: 算子可用於將類型註解附加到程式中的表達式和變數。這樣做的主要原因有兩個

  1. 作為一個斷言,以幫助確認您的程式按照您的期望工作,以及
  2. 向編譯器提供額外的類型資訊,在某些情況下可以提高效能。

當附加到計算值的表達式時,:: 算子被讀為「是實例」。它可以在任何地方使用,以斷言左側表達式的值是右側類型的實例。當右側的類型具體時,左側的值必須具有該類型作為其實作——回想一下,所有具體類型都是最終的,因此沒有實作是任何其他類型的子類型。當類型是抽象時,值由抽象類型的子類型的具體類型實作就足夠了。如果類型斷言不為真,則會擲回例外,否則,將傳回左側值

julia> (1+2)::AbstractFloat
ERROR: TypeError: in typeassert, expected AbstractFloat, got a value of type Int64

julia> (1+2)::Int
3

這允許將類型斷言附加到任何原地的表達式。

當附加在賦值運算左邊的變數,或作為 local 宣告的一部分時,:: 運算子表示有點不同的東西:它宣告變數總是具有指定的類型,就像在靜態類型語言(例如 C)中的類型宣告。賦值給變數的每個值都會使用 convert 轉換為宣告的類型

julia> function foo()
           x::Int8 = 100
           x
       end
foo (generic function with 1 method)

julia> x = foo()
100

julia> typeof(x)
Int8

此功能有助於避免效能「陷阱」,這些陷阱可能會在變數的其中一個賦值意外變更其類型時發生。

此「宣告」行為只會在特定脈絡中發生

local x::Int8  # in a local declaration
x::Int8 = 10   # as the left-hand side of an assignment

並套用於整個目前範圍,甚至在宣告之前。

從 Julia 1.8 開始,類型宣告現在可以在全域範圍中使用,亦即可以將類型註解新增至全域變數,以使其存取類型穩定。

julia> x::Int = 10
10

julia> x = 3.5
ERROR: InexactError: Int64(3.5)

julia> function foo(y)
           global x = 15.8    # throws an error when foo is called
           return x + y
       end
foo (generic function with 1 method)

julia> foo(10)
ERROR: InexactError: Int64(15.8)

宣告也可以附加至函式定義

function sinc(x)::Float64
    if x == 0
        return 1
    end
    return sin(pi*x)/(pi*x)
end

從此函式傳回的行為就像賦值給具有宣告類型的變數:值總是轉換為 Float64

抽象類型

抽象類型無法實例化,僅作為類型圖中的節點,從而描述相關具體類型的集合:這些具體類型是其後代。我們從抽象類型開始,即使它們沒有實例化,因為它們是類型系統的骨幹:它們形成概念階層,使 Julia 的類型系統不僅僅是物件實作的集合。

回想在 整數與浮點數 中,我們介紹了各種具體的數值類型:Int8UInt8Int16UInt16Int32UInt32Int64UInt64Int128UInt128Float16Float32Float64。儘管它們具有不同的表示大小,但 Int8Int16Int32Int64Int128 都具有共同點,即它們是有符號整數類型。同樣地,UInt8UInt16UInt32UInt64UInt128 都是無符號整數類型,而 Float16Float32Float64 的不同之處在於它們是浮點類型,而不是整數。對於一段程式碼來說,如果其參數是某種整數,則它通常是有意義的,但實際上並不依賴於具體的整數類型。例如,最大公因數演算法適用於所有類型的整數,但對浮點數不起作用。抽象類型允許建立類型階層,提供具體類型可以適用的背景。這允許您輕鬆地對任何整數類型進行編寫程式,而無需將演算法限制為特定類型的整數。

使用 abstract type 關鍵字宣告抽象類型。宣告抽象類型的通用語法為

abstract type «name» end
abstract type «name» <: «supertype» end

abstract type 關鍵字會引入一個新的抽象類型,其名稱由 «name» 給定。此名稱可以選擇性地接續 <: 和一個已存在的類型,表示新宣告的抽象類型是此「父」類型的子類型。

當未給定父類型時,預設父類型為 Any – 一個預先定義的抽象類型,所有物件都是其實例,且所有類型都是其子類型。在類型理論中,Any 通常稱為「頂端」,因為它位於類型圖表的頂點。Julia 也有預先定義的抽象「底端」類型,位於類型圖表的最低點,寫作 Union{}。它是 Any 的完全相反:沒有物件是 Union{} 的實例,且所有類型都是 Union{} 的父類型。

讓我們考慮構成 Julia 數值階層的一些抽象類型

abstract type Number end
abstract type Real          <: Number end
abstract type AbstractFloat <: Real end
abstract type Integer       <: Real end
abstract type Signed        <: Integer end
abstract type Unsigned      <: Integer end

Number 類型是 Any 的直接子類型,而 Real 是它的子類型。反過來,Real 有兩個子類型(它有更多類型,但這裡只顯示兩個;我們稍後會介紹其他類型):IntegerAbstractFloat,將世界分為整數表示和實數表示。實數表示包括浮點類型,但也包括其他類型,例如有理數。AbstractFloat 只包括實數的浮點表示。整數進一步細分為 SignedUnsigned 類型。

<: 算子通常表示「是子類型」,並用於上述聲明中,宣告右手邊類型是新宣告類型的直接超類型。它也可以用在表達式中,作為子類型算子,當其左操作數是其右操作數的子類型時,會傳回 true

julia> Integer <: Number
true

julia> Integer <: AbstractFloat
false

抽象類型的重要用途是為具體類型提供預設實作。舉一個簡單的例子,請考慮

function myplus(x,y)
    x+y
end

首先要注意的是,上述參數宣告等同於 x::Anyy::Any。當呼叫此函數時,例如 myplus(2,5),分派器會選擇與給定參數相符的最具體方法,稱為 myplus。(有關多重分派的更多資訊,請參閱 方法。)

假設找不到比上述更具體的方法,Julia 接著會根據上述提供的泛函,內部定義並編譯一個專門針對兩個 Int 參數的方法,稱為 myplus,亦即,它會隱式定義並編譯

function myplus(x::Int,y::Int)
    x+y
end

最後,它會呼叫這個具體方法。

因此,抽象類型允許程式設計師撰寫一般函式,這些函式之後可以由許多具體類型的組合當作預設方法使用。由於多重分派,程式設計師可以完全控制使用預設方法或更具體的方法。

需要注意的一個重點是,如果程式設計師依賴於引數為抽象類型的函式,則不會損失效能,因為它會針對每個具體引數類型元組重新編譯,並使用該元組呼叫函式。(不過,如果函式引數是抽象類型的容器,則可能會出現效能問題;請參閱效能提示。)

基本類型

警告

幾乎總是建議將現有的基本類型封裝在新複合類型中,而不是定義您自己的基本類型。

此功能的存在是為了讓 Julia 能夠啟動 LLVM 支援的標準基本類型。一旦定義這些類型,定義更多類型的理由就非常少了。

基本類型是一種具體類型,其資料由純粹的位元組成。基本類型的經典範例是整數和浮點數值。與大多數語言不同,Julia 讓您可以宣告自己的基本類型,而不是只提供一組內建的基本類型。事實上,標準基本類型都是以語言本身定義的

primitive type Float16 <: AbstractFloat 16 end
primitive type Float32 <: AbstractFloat 32 end
primitive type Float64 <: AbstractFloat 64 end

primitive type Bool <: Integer 8 end
primitive type Char <: AbstractChar 32 end

primitive type Int8    <: Signed   8 end
primitive type UInt8   <: Unsigned 8 end
primitive type Int16   <: Signed   16 end
primitive type UInt16  <: Unsigned 16 end
primitive type Int32   <: Signed   32 end
primitive type UInt32  <: Unsigned 32 end
primitive type Int64   <: Signed   64 end
primitive type UInt64  <: Unsigned 64 end
primitive type Int128  <: Signed   128 end
primitive type UInt128 <: Unsigned 128 end

宣告基本類型的通用語法如下

primitive type «name» «bits» end
primitive type «name» <: «supertype» «bits» end

位元數表示類型所需的儲存空間,而名稱則為新類型命名。原始類型可以選擇宣告為某個超類型的子類型。如果省略超類型,則該類型預設以 Any 為其直接超類型。因此,上述 Bool 的宣告表示布林值需要八個位元儲存,且以 Integer 為其直接超類型。目前,僅支援 8 位元的倍數大小,而您可能會遇到 LLVM 錯誤,其大小與上述所使用的不同。因此,布林值雖然實際上只需要一個位元,但無法宣告小於八個位元。

類型 BoolInt8UInt8 都有相同的表示方式:它們是八位元記憶體區塊。然而,由於 Julia 的類型系統是命名式的,因此儘管它們具有相同的結構,但它們並不可互換。它們之間的基本差異在於它們具有不同的超類型:Bool 的直接超類型是 IntegerInt8 的是 Signed,而 UInt8 的是 UnsignedBoolInt8UInt8 之間的所有其他差異都是行為問題,也就是函數在給定這些類型物件作為引數時定義的行為方式。這就是為什麼需要命名式類型系統:如果結構決定類型,而類型又決定行為,那麼就不可能讓 Bool 的行為與 Int8UInt8 有任何不同。

複合類型

在各種語言中,複合類型 被稱為記錄、結構或物件。複合類型是一個命名欄位的集合,其一個實例可以被視為一個單一的值。在許多語言中,複合類型是唯一一種使用者可定義的類型,而且它們也是 Julia 中最常使用的使用者可定義類型。

在主流的物件導向語言,例如 C++、Java、Python 和 Ruby 中,複合型別也具有與之關聯的名稱函式,而組合稱為「物件」。在更純粹的物件導向語言,例如 Ruby 或 Smalltalk 中,所有值都是物件,無論它們是否為複合型別。在較不純粹的物件導向語言,包括 C++ 和 Java 中,某些值,例如整數和浮點數值,不是物件,而使用者定義複合型別的執行個體則是具有關聯方法的真實物件。在 Julia 中,所有值都是物件,但函式並未與它們運作的物件綑綁在一起。這是必要的,因為 Julia 會透過多重分派來選擇要使用的函式方法,表示在選擇方法時會考量函式所有引數的型別,而不仅仅是第一個引數(請參閱方法以取得有關方法和分派的更多資訊)。因此,函式「屬於」其第一個引數是不適當的。將方法整理到函式物件中,而不是在每個物件「內部」擁有具名稱的方法袋,最終成為語言設計中極有益的一面。

複合型別會透過struct關鍵字加上一區塊欄位名稱來引入,這些欄位名稱可選擇使用::運算子加上型別註解

julia> struct Foo
           bar
           baz::Int
           qux::Float64
       end

沒有型別註解的欄位預設為Any,因此可以容納任何型別的值。

透過將Foo型別物件套用在欄位值上,如同函式一樣,可以建立Foo型別的新物件

julia> foo = Foo("Hello, world.", 23, 1.5)
Foo("Hello, world.", 23, 1.5)

julia> typeof(foo)
Foo

當類型像函式一樣被套用時,它會被稱為建構函式。兩個建構函式會自動產生(這些稱為預設建構函式)。一個接受任何參數並呼叫 convert 將它們轉換成欄位的類型,另一個則接受與欄位類型完全匹配的參數。產生這兩個建構函式的理由是,這使得在不慎取代預設建構函式的情況下,更容易新增新的定義。

由於 bar 欄位的類型不受限制,因此任何值都可以。但是,baz 的值必須可以轉換成 Int

julia> Foo((), 23.5, 1)
ERROR: InexactError: Int64(23.5)
Stacktrace:
[...]

你可以使用 fieldnames 函式尋找欄位名稱的清單。

julia> fieldnames(Foo)
(:bar, :baz, :qux)

你可以使用傳統的 foo.bar 符號存取複合物件的欄位值

julia> foo.bar
"Hello, world."

julia> foo.baz
23

julia> foo.qux
1.5

使用 struct 宣告的複合物件是不可變的;它們在建構後無法修改。這乍看之下可能很奇怪,但它有幾個優點

  • 它可以更有效率。有些結構可以有效地封裝成陣列,在某些情況下,編譯器可以完全避免配置不可變的物件。
  • 不可能違反類型建構函式提供的約束。
  • 使用不可變物件的程式碼可能更容易理解。

不可變的物件可能包含可變的物件,例如陣列,作為欄位。這些包含的物件將保持可變;只有不可變物件本身的欄位無法變更為指向不同的物件。

在需要時,可以使用關鍵字 mutable struct 宣告可變的複合物件,這將在下一節中討論。

如果不可變結構的所有欄位都無法區分(===),則包含這些欄位的兩個不可變值也無法區分

julia> struct X
           a::Int
           b::Float64
       end

julia> X(1, 2) === X(1, 2)
true

關於如何建立複合類型的實例還有很多話可說,但這個討論取決於參數化類型方法,而且足夠重要,可以在其自己的區段中說明:建構函式

對於許多使用者定義的類型X,您可能想要定義方法Base.broadcastable(x::X) = Ref(x),以便該類型的實例作為 0 維度「純量」進行廣播

可變複合類型

如果複合類型使用mutable struct而不是struct宣告,則可以修改其實例

julia> mutable struct Bar
           baz
           qux::Float64
       end

julia> bar = Bar("Hello", 1.5);

julia> bar.qux = 2.0
2.0

julia> bar.baz = 1//2
1//2

可以在欄位和使用者之間提供額外的介面,透過實例屬性。這可以更進一步控制使用bar.baz表示法可以存取和修改的內容。

為了支援變異,此類物件通常會配置在堆疊上,並具有穩定的記憶體位址。可變物件就像一個小容器,可能會隨著時間而儲存不同的值,因此只能透過其位址來可靠地識別。相反地,不可變類型的實例與特定的欄位值關聯在一起 –- 僅欄位值就能告訴您有關物件的所有資訊。在決定是否讓類型可變時,請詢問具有相同欄位值的兩個實例是否會被視為相同,或者它們是否可能隨著時間獨立變更。如果它們被視為相同,則該類型可能應該是不可變的。

總而言之,兩個基本屬性定義了 Julia 中的不變性

  • 不允許修改不可變類型的值。
    • 對於位元類型,這表示值設定後,其位元模式將永遠不會改變,而該值就是位元類型的識別碼。
    • 對於複合類型,這表示其欄位的數值識別碼將永遠不會改變。當欄位為位元類型時,表示其位元將永遠不會改變;對於數值為陣列等可變類型之欄位,表示欄位將永遠參照相同的可變數值,即使該可變數值的內容本身可能會修改。
  • 具有不變類型的物件可以由編譯器自由複製,因為其不變性使得在程式設計上無法區分原始物件與副本。
    • 特別是,這表示像整數和浮點數等夠小的不變數值通常會在暫存器(或堆疊配置)中傳遞給函式。
    • 另一方面,可變數值會在堆疊中配置,並作為指向堆疊配置數值的指標傳遞給函式,除非編譯器確定無法得知這不是正在發生的事。

在已知某個可變結構體的一個或多個欄位是不變的情況下,可以使用 const 宣告這些欄位,如下所示。這會啟用部分(但不是全部)不變結構體的最佳化,並可強制執行標示為 const 的特定欄位的不變式。

Julia 1.8

為可變結構體的欄位加上 const 註解需要至少 Julia 1.8。

julia> mutable struct Baz
           a::Int
           const b::Float64
       end

julia> baz = Baz(1, 1.5);

julia> baz.a = 2
2

julia> baz.b = 2.0
ERROR: setfield!: const field .b of type Baz cannot be changed
[...]

宣告類型

前幾節討論的三種類型(抽象、原始、複合)實際上都密切相關。它們共用相同的關鍵屬性

  • 它們是明確宣告的。
  • 它們有名稱。
  • 它們有明確宣告的超類型。
  • 它們可能有參數。

由於這些共用屬性,這些類型在內部表示為相同概念的實例,DataType,這是任何這些類型的類型

julia> typeof(Real)
DataType

julia> typeof(Int)
DataType

DataType 可能為抽象或具體。如果它是具體的,它具有指定的大小、儲存配置,以及(選擇性地)欄位名稱。因此,基本類型是一個大小非零但沒有欄位名稱的 DataType。複合類型是一個具有欄位名稱或為空的 (大小為零) DataType

系統中的每個具體值都是某個 DataType 的實例。

類型聯集

類型聯集是一種特殊的抽象類型,它包含所有引數類型的實例作為物件,使用特殊 Union 關鍵字建構

julia> IntOrString = Union{Int,AbstractString}
Union{Int64, AbstractString}

julia> 1 :: IntOrString
1

julia> "Hello!" :: IntOrString
"Hello!"

julia> 1.0 :: IntOrString
ERROR: TypeError: in typeassert, expected Union{Int64, AbstractString}, got a value of type Float64

許多語言的編譯器具有用於推論類型的內部聯集建構;Julia 只是將它公開給程式設計人員。Julia 編譯器能夠在存在 Union 類型時產生有效率的程式碼,其中類型數量很少 [1],透過為每種可能的類型在不同的分支中產生專門的程式碼。

Union 類型特別有用的情況是 Union{T, Nothing},其中 T 可以是任何類型,而 Nothing 是單例類型,其唯一的實例是物件 nothing。這個模式是 Julia 等同於其他語言中的 NullableOptionMaybe 類型。將函數引數或欄位宣告為 Union{T, Nothing} 允許將其設定為類型 T 的值,或設定為 nothing 以表示沒有值。請參閱 此常見問題解答條目 以取得更多資訊。

參數化類型

Julia 類型系統的一個重要且強大的功能是它具有參數化:類型可以採用參數,因此類型宣告實際上會引入一整個新的類型家族,每個參數值組合都有對應的類型。有許多語言支援某種版本的泛型程式設計,其中資料結構和用於處理它們的演算法可以在不指定所涉及的精確類型的情況下指定。例如,ML、Haskell、Ada、Eiffel、C++、Java、C#、F# 和 Scala 中都存在某種形式的泛型程式設計,僅舉幾例。其中一些語言支援真正的參數多態性(例如 ML、Haskell、Scala),而另一些語言則支援泛型程式設計的臨時、基於範本的樣式(例如 C++、Java)。由於各種語言中存在如此多種不同的泛型程式設計和參數類型,我們甚至不會嘗試將 Julia 的參數類型與其他語言進行比較,而是專注於解釋 Julia 自身的系統。然而,我們會注意到,由於 Julia 是一種動態型別語言,不需要在編譯時做出所有型別決策,因此在靜態參數型別系統中遇到的許多傳統難題可以相對容易地處理。

所有宣告的類型(DataType 種類)都可以參數化,在每種情況下語法相同。我們將按以下順序討論它們:首先是參數複合類型,然後是參數抽象類型,最後是參數基本類型。

參數複合類型

類型參數在類型名稱之後立即引入,並用大括號括起來

julia> struct Point{T}
           x::T
           y::T
       end

此宣告定義了一個新的參數類型,Point{T},包含兩個類型為 T 的「座標」。有人可能會問,什麼是 T?嗯,這正是參數類型的重點:它可以是任何類型(或實際上任何位元類型的一個值,儘管在這裡它明顯用作一個類型)。Point{Float64} 是具體類型,等同於在 Point 定義中以 Float64 取代 T 所定義的類型。因此,這個單一宣告實際上宣告了無限數量的類型:Point{Float64}Point{AbstractString}Point{Int64} 等。這些現在都是可用的具體類型

julia> Point{Float64}
Point{Float64}

julia> Point{AbstractString}
Point{AbstractString}

類型 Point{Float64} 是其座標為 64 位元浮點值的點,而類型 Point{AbstractString} 是一個「點」,其「座標」是字串物件(請參閱 字串)。

Point 本身也是一個有效的類型物件,包含所有實例 Point{Float64}Point{AbstractString} 等作為子類型

julia> Point{Float64} <: Point
true

julia> Point{AbstractString} <: Point
true

當然,其他類型不是它的子類型

julia> Float64 <: Point
false

julia> AbstractString <: Point
false

具有不同 T 值的具體 Point 類型絕不會是彼此的子類型

julia> Point{Float64} <: Point{Int64}
false

julia> Point{Float64} <: Point{Real}
false
警告

最後一點非常重要:即使 Float64 <: Real,我們沒有 Point{Float64} <: Point{Real}

換句話說,在類型理論的術語中,Julia 的類型參數是不變的,而不是 協變(或甚至逆變)。這是出於實際原因:儘管 Point{Float64} 的任何實例在概念上可能也像 Point{Real} 的實例,但這兩種類型在記憶體中的表示方式不同

  • Point{Float64} 的實例可以緊湊且有效地表示為一對 64 位元值的立即值;
  • Point{Real} 的實例必須能夠容納任何一對 Real 實例。由於是 Real 實例的物件可以是任意大小和結構,因此在實務上,Point{Real} 的實例必須表示為一對指向個別配置的 Real 物件的指標。

能夠以立即值儲存 Point{Float64} 物件所獲得的效率在陣列的情況下會大幅放大:Array{Float64} 可以儲存為 64 位元浮點值的一段連續記憶體區塊,而 Array{Real} 必須是陣列指標,指向個別配置的 Real 物件,而這些物件很可能是 封裝 的 64 位元浮點值,但也可能是任意大的複雜物件,而這些物件宣告為 Real 抽象類型的實作。

由於 Point{Float64} 不是 Point{Real} 的子類型,因此下列方法無法套用於 Point{Float64} 類型的引數

function norm(p::Point{Real})
    sqrt(p.x^2 + p.y^2)
end

定義一個方法的正確方式,該方法接受所有 Point{T} 類型的引數,其中 TReal 的子類型:

function norm(p::Point{<:Real})
    sqrt(p.x^2 + p.y^2)
end

(等效地,可以定義 function norm(p::Point{T} where T<:Real)function norm(p::Point{T}) where T<:Real;請參閱 UnionAll 類型。)

稍後會在 方法 中討論更多範例。

要如何建構 Point 物件?可以為複合類型定義自訂建構函式,這將在 建構函式 中詳細討論,但在沒有任何特殊建構函式宣告的情況下,有兩種預設方式可以建立新的複合物件,一種是明確給定類型參數,另一種是從物件建構函式的引數中暗示類型參數。

由於類型 Point{Float64} 是具體類型,等同於使用 Float64 取代 T 宣告的 Point,因此可以相應地套用為建構函式

julia> p = Point{Float64}(1.0, 2.0)
Point{Float64}(1.0, 2.0)

julia> typeof(p)
Point{Float64}

對於預設建構函式,每個欄位必須提供一個引數

julia> Point{Float64}(1.0)
ERROR: MethodError: no method matching Point{Float64}(::Float64)
[...]

julia> Point{Float64}(1.0,2.0,3.0)
ERROR: MethodError: no method matching Point{Float64}(::Float64, ::Float64, ::Float64)
[...]

參數化類型只會產生一個預設建構函式,因為無法覆寫它。此建構函式接受任何引數,並將它們轉換為欄位類型。

在許多情況下,提供要建構的 Point 物件類型是多餘的,因為呼叫建構函式的引數類型已經隱含地提供了類型資訊。因此,只要參數類型 T 的暗示值明確,您也可以將 Point 本身套用為建構函式

julia> p1 = Point(1.0,2.0)
Point{Float64}(1.0, 2.0)

julia> typeof(p1)
Point{Float64}

julia> p2 = Point(1,2)
Point{Int64}(1, 2)

julia> typeof(p2)
Point{Int64}

Point 的情況下,只有當傳遞給 Point 的兩個引數具有相同類型時,才會明確暗示 T 的類型。如果不是這種情況,建構函式將會失敗,並產生 MethodError

julia> Point(1,2.5)
ERROR: MethodError: no method matching Point(::Int64, ::Float64)

Closest candidates are:
  Point(::T, !Matched::T) where T
   @ Main none:2

Stacktrace:
[...]

可以定義建構函式方法來適當地處理此類混合情況,但這將在 建構函式 中稍後討論。

參數化抽象類型

參數化抽象類型宣告宣告一組抽象類型,方式與

julia> abstract type Pointy{T} end

透過此宣告,Pointy{T} 對於 T 的每個類型或整數值都是一個不同的抽象類型。與參數化複合類型一樣,每個此類實例都是 Pointy 的子類型

julia> Pointy{Int64} <: Pointy
true

julia> Pointy{1} <: Pointy
true

參數化抽象類型是不變的,就像參數化複合類型一樣

julia> Pointy{Float64} <: Pointy{Real}
false

julia> Pointy{Real} <: Pointy{Float64}
false

符號 Pointy{<:Real} 可用於表示共變類型的 Julia 類比,而 Pointy{>:Int} 則表示反變類型的類比,但技術上這些表示類型集合(請參閱 UnionAll 類型)。

julia> Pointy{Float64} <: Pointy{<:Real}
true

julia> Pointy{Real} <: Pointy{>:Int}
true

就像一般的抽象類型用於在具體類型上建立有用的類型階層,參數化抽象類型對參數化複合類型也有相同的作用。例如,我們可以宣告 Point{T}Pointy{T} 的子類型,如下所示

julia> struct Point{T} <: Pointy{T}
           x::T
           y::T
       end

給定這樣的宣告,對於 T 的每個選擇,我們都有 Point{T} 作為 Pointy{T} 的子類型

julia> Point{Float64} <: Pointy{Float64}
true

julia> Point{Real} <: Pointy{Real}
true

julia> Point{AbstractString} <: Pointy{AbstractString}
true

這種關係也是不變的

julia> Point{Float64} <: Pointy{Real}
false

julia> Point{Float64} <: Pointy{<:Real}
true

Pointy 這樣的參數化抽象類型有什麼用?考慮如果我們建立一個類點的實作,它只需要一個座標,因為該點位於對角線 x = y

julia> struct DiagPoint{T} <: Pointy{T}
           x::T
       end

現在 Point{Float64}DiagPoint{Float64} 都是 Pointy{Float64} 抽象的實作,對於 T 類型的每個其他可能的選擇也一樣。這允許對所有 Pointy 物件共用的通用介面進行編程,並針對 PointDiagPoint 實作。然而,在我們在下一個區段 方法 中引入方法和調度之前,無法完全展示這一點。

在某些情況下,類型參數可能無法在所有可能的類型上自由變動。在這種情況下,可以像這樣限制 T 的範圍

julia> abstract type Pointy{T<:Real} end

有了這樣的宣告,使用任何是 Real 子類型的類型來取代 T 是可以接受的,但不是 Real 的子類型的類型

julia> Pointy{Float64}
Pointy{Float64}

julia> Pointy{Real}
Pointy{Real}

julia> Pointy{AbstractString}
ERROR: TypeError: in Pointy, in T, expected T<:Real, got Type{AbstractString}

julia> Pointy{1}
ERROR: TypeError: in Pointy, in T, expected T<:Real, got a value of type Int64

參數化複合類型的類型參數可以以相同的方式進行限制

struct Point{T<:Real} <: Pointy{T}
    x::T
    y::T
end

為了提供一個所有這些參數化類型機制如何有用的實際範例,以下是 Julia 的 Rational 不可變類型(在此為了簡潔起見,我們省略了建構函式)的實際定義,表示整數的精確比率

struct Rational{T<:Integer} <: Real
    num::T
    den::T
end

僅對整數值進行比率計算才有意義,因此參數類型 T 僅限於 Integer 的子類型,而整數比率表示實數線上的值,因此任何 Rational 都是 Real 抽象的實例。

Tuple 類型

Tuples 是函數參數的抽象,不包含函數本身。函數參數的顯著特徵是它們的順序和類型。因此,Tuple 類型類似於參數化不變類型,其中每個參數都是一個欄位的類型。例如,2 元素 Tuple 類型類似於下列不變類型

struct Tuple2{A,B}
    a::A
    b::B
end

但是,有三個主要差異

  • Tuple 類型可以有任意數量的參數。
  • Tuple 類型在參數中是協變的:Tuple{Int}Tuple{Any} 的子類型。因此 Tuple{Any} 被視為抽象類型,而 Tuple 類型僅在其參數為具體類型時才是具體的。
  • Tuples 沒有欄位名稱;欄位只能透過索引存取。

Tuple 值使用括號和逗號撰寫。建立 Tuple 時,會依需求產生適當的 Tuple 類型

julia> typeof((1,"foo",2.5))
Tuple{Int64, String, Float64}

請注意協變的含義

julia> Tuple{Int,AbstractString} <: Tuple{Real,Any}
true

julia> Tuple{Int,AbstractString} <: Tuple{Real,Real}
false

julia> Tuple{Int,AbstractString} <: Tuple{Real,}
false

直觀地說,這對應於函數參數的類型是函數簽章的子類型(當簽章相符時)。

Vararg Tuple 類型

Tuple 類型的最後一個參數可以是特殊值 Vararg,表示任意數量的尾隨元素

julia> mytupletype = Tuple{AbstractString,Vararg{Int}}
Tuple{AbstractString, Vararg{Int64}}

julia> isa(("1",), mytupletype)
true

julia> isa(("1",1), mytupletype)
true

julia> isa(("1",1,2), mytupletype)
true

julia> isa(("1",1,2,3.0), mytupletype)
false

此外,Vararg{T} 對應到零個或多個類型為 T 的元素。變數長元組類型用來表示變數長方法所接受的參數(請參閱 變數長函數)。

特殊值 Vararg{T,N}(當用於元組類型的最後一個參數時)對應到類型為 T 的正好 N 個元素。NTuple{N,T}Tuple{Vararg{T,N}} 的方便別名,亦即一個元組類型,其中包含類型為 T 的正好 N 個元素。

命名元組類型

命名元組是 NamedTuple 類型的實例,它有兩個參數:一個提供欄位名稱的符號元組,以及一個提供欄位類型的元組類型。為了方便起見,NamedTuple 類型使用 @NamedTuple 巨集來列印,它提供一個方便的類似於 struct 的語法,用於透過 key::Type 宣告來宣告這些類型,其中省略的 ::Type 對應到 ::Any

julia> typeof((a=1,b="hello")) # prints in macro form
@NamedTuple{a::Int64, b::String}

julia> NamedTuple{(:a, :b), Tuple{Int64, String}} # long form of the type
@NamedTuple{a::Int64, b::String}

@NamedTuple 巨集的 begin ... end 形式允許將宣告拆分到多行(類似於結構宣告),但其他部分是等效的

julia> @NamedTuple begin
           a::Int
           b::String
       end
@NamedTuple{a::Int64, b::String}

NamedTuple 類型可以用作建構函式,接受一個單一的元組參數。建構的 NamedTuple 類型可以是具體類型(指定兩個參數),也可以是僅指定欄位名稱的類型

julia> @NamedTuple{a::Float32,b::String}((1, ""))
(a = 1.0f0, b = "")

julia> NamedTuple{(:a, :b)}((1, ""))
(a = 1, b = "")

如果指定欄位類型,則會轉換參數。否則,直接使用參數的類型。

參數化基本類型

基本類型也可以參數化宣告。例如,指標表示為基本類型,在 Julia 中會這樣宣告

# 32-bit system:
primitive type Ptr{T} 32 end

# 64-bit system:
primitive type Ptr{T} 64 end

這些宣告與典型的參數化複合類型相比,有一個稍微奇怪的特徵,就是類型參數 T 沒有用在類型本身的定義中,它只是一個抽象標籤,基本上定義了一個具有相同結構的類型家族,只用它們的類型參數來區分。因此,Ptr{Float64}Ptr{Int64} 是不同的類型,即使它們具有相同的表示方式。當然,所有特定指標類型都是傘型 Ptr 類型的子類型

julia> Ptr{Float64} <: Ptr
true

julia> Ptr{Int64} <: Ptr
true

UnionAll 類型

我們已經說過,像 Ptr 這樣的參數化類型會作為其所有實例 (Ptr{Int64} 等) 的超類型。這是怎麼運作的?Ptr 本身不能成為一個正常的資料類型,因為在不知道所參考資料的類型的情況下,這個類型顯然不能用於記憶體操作。答案是 Ptr (或其他參數化類型,例如 Array) 是一種稱為 UnionAll 類型的不同類型。這種類型表示某些參數的所有值的反覆聯集

UnionAll 類型通常使用關鍵字 where 來撰寫。例如,Ptr 可以更準確地寫成 Ptr{T} where T,意思是所有類型為 Ptr{T} 的值,其中 T 的值為某個值。在此背景下,參數 T 也經常稱為「類型變數」,因為它就像一個遍歷類型的變數。每個 where 都會引入一個單一類型變數,因此對於具有多個參數的類型,這些表達式會巢狀,例如 Array{T,N} where N where T

型別應用程式語法 A{B,C} 需要 AUnionAll 型別,且首先將 A 中最外層的型別變數替換為 B。預期結果為另一個 UnionAll 型別,然後將 C 替換到其中。因此 A{B,C} 等於 A{B}{C}。這說明了為何可以部分實例化型別,例如 Array{Float64}:第一個參數值已固定,但第二個參數值仍會遍歷所有可能的值。使用明確的 where 語法,可以固定任何參數子集。例如,所有 1 維陣列的型別可以寫成 Array{T,1} where T

型別變數可以用子型別關係來限制。Array{T} where T<:Integer 指涉所有元素型別為某種 Integer 的陣列。語法 Array{<:Integer}Array{T} where T<:Integer 的便捷縮寫。型別變數可以同時有下界和上界。Array{T} where Int<:T<:Number 指涉所有能夠包含 IntNumber 陣列(因為 T 必須至少與 Int 一樣大)。語法 where T>:Int 也可以用來只指定型別變數的下界,而 Array{>:Int} 等於 Array{T} where T>:Int

由於 where 表達式會巢狀,因此型別變數界限可以指涉外層型別變數。例如 Tuple{T,Array{S}} where S<:AbstractArray{T} where T<:Real 指涉 2 元組,其第一個元素是某個 Real,而其第二個元素是任何一種元素型別包含第一個元組元素型別的陣列的 Array

where 關鍵字本身可以巢狀在更複雜的宣告中。例如,考慮下列宣告所建立的兩個類型

julia> const T1 = Array{Array{T, 1} where T, 1}
Vector{Vector} (alias for Array{Array{T, 1} where T, 1})

julia> const T2 = Array{Array{T, 1}, 1} where T
Array{Vector{T}, 1} where T

類型 T1 定義一維陣列的一維陣列;每個內部陣列包含相同類型的物件,但此類型可能因內部陣列而異。另一方面,類型 T2 定義一維陣列的一維陣列,其所有內部陣列都必須具有相同的類型。請注意,T2 是一個抽象類型,例如,Array{Array{Int,1},1} <: T2,而 T1 是具體類型。因此,T1 可以使用零參數建構函式 a=T1() 建構,但 T2 則不能。

有一個方便的語法可用於命名此類類型,類似於函式定義語法的簡短形式

Vector{T} = Array{T, 1}

這等於 const Vector = Array{T,1} where T。撰寫 Vector{Float64} 等於撰寫 Array{Float64,1},而傘式類型 Vector 的實例為所有 Array 物件,其中第二個參數(陣列維度數)為 1,而與元素類型無關。在參數化類型必須始終完整指定的語言中,這並非特別有幫助,但在 Julia 中,這允許只為抽象類型撰寫 Vector,包括任何元素類型的所有一維密集陣列。

單例類型

沒有欄位的不可變複合類型稱為單例。正式來說,如果

  1. T 是不可變複合類型(即使用 struct 定義),
  2. a isa T && b isa T 暗示 a === b

然後 T 是單例類型。[2] Base.issingletontype 可用於檢查類型是否是單例類型。 抽象類型 在建構時無法成為單例類型。

從定義中可以得知,此類型的實例只有一個

julia> struct NoFields
       end

julia> NoFields() === NoFields()
true

julia> Base.issingletontype(NoFields)
true

=== 函數確認 NoFields 的建構實例實際上是同一個。

當上述條件成立時,參數化類型可以是單例類型。例如,

julia> struct NoFieldsParam{T}
       end

julia> Base.issingletontype(NoFieldsParam) # Can't be a singleton type ...
false

julia> NoFieldsParam{Int}() isa NoFieldsParam # ... because it has ...
true

julia> NoFieldsParam{Bool}() isa NoFieldsParam # ... multiple instances.
true

julia> Base.issingletontype(NoFieldsParam{Int}) # Parametrized, it is a singleton.
true

julia> NoFieldsParam{Int}() === NoFieldsParam{Int}()
true

函數類型

每個函數都有自己的類型,它是 Function 的子類型。

julia> foo41(x) = x + 1
foo41 (generic function with 1 method)

julia> typeof(foo41)
typeof(foo41) (singleton type of function foo41, subtype of Function)

請注意 typeof(foo41) 如何列印為它本身。這只是一個列印慣例,因為它是一個可以像任何其他值一樣使用的第一類物件

julia> T = typeof(foo41)
typeof(foo41) (singleton type of function foo41, subtype of Function)

julia> T <: Function
true

在頂層定義的函數類型是單例。必要時,您可以使用 === 來比較它們。

閉包 也有自己的類型,通常會列印出以 #<number> 結尾的名稱。在不同位置定義的函數的名稱和類型是不同的,但不能保證在各個工作階段中列印方式相同。

julia> typeof(x -> x + 1)
var"#9#10"

閉包類型不一定都是單例。

julia> addy(y) = x -> x + y
addy (generic function with 1 method)

julia> typeof(addy(1)) === typeof(addy(2))
true

julia> addy(1) === addy(2)
false

julia> Base.issingletontype(typeof(addy(1)))
false

Type{T} 類型選擇器

對於每個類型 TType{T} 是一個抽象參數化類型,其唯一的實例是物件 T。在我們討論 參數化方法轉換 之前,很難解釋此建構的用途,但簡而言之,它允許您將函數行為專門用於特定類型作為。這對於撰寫方法(特別是參數化方法)很有用,其行為取決於作為明確引數給出的類型,而不是由其一個引數的類型暗示的類型。

由於定義有點難以解析,我們來看一些範例

julia> isa(Float64, Type{Float64})
true

julia> isa(Real, Type{Float64})
false

julia> isa(Real, Type{Real})
true

julia> isa(Float64, Type{Real})
false

換句話說,isa(A, Type{B}) 僅在 AB 為同一個物件,且該物件為型別時為真。

特別是,由於參數化型別為不變,我們有

julia> struct TypeParamExample{T}
           x::T
       end

julia> TypeParamExample isa Type{TypeParamExample}
true

julia> TypeParamExample{Int} isa Type{TypeParamExample}
false

julia> TypeParamExample{Int} isa Type{TypeParamExample{Int}}
true

沒有參數時,Type 僅為一抽象型別,其所有實例皆為型別物件

julia> isa(Type{Float64}, Type)
true

julia> isa(Float64, Type)
true

julia> isa(Real, Type)
true

任何非型別物件皆非 Type 的實例

julia> isa(1, Type)
false

julia> isa("foo", Type)
false

雖然 Type 與任何其他抽象參數化型別一樣,皆為 Julia 型別階層的一部分,但除了在某些特殊情況下,它通常不會用於方法簽章之外。Type 的另一個重要用途是強化欄位型別,否則這些欄位型別的捕捉精度會較低,例如以下範例中的 DataType,其中預設建構函式可能會導致依賴於精確包裝型別的程式碼效能問題(類似於抽象型別參數)。

julia> struct WrapType{T}
       value::T
       end

julia> WrapType(Float64) # default constructor, note DataType
WrapType{DataType}(Float64)

julia> WrapType(::Type{T}) where T = WrapType{Type{T}}(T)
WrapType

julia> WrapType(Float64) # sharpened constructor, note more precise Type{Float64}
WrapType{Type{Float64}}(Float64)

型別別名

有時,為已可表達的型別引入新名稱會比較方便。這可以用一個簡單的賦值陳述式來完成。例如,UInt 別名為 UInt32UInt64,視系統上指標的大小而定

# 32-bit system:
julia> UInt
UInt32

# 64-bit system:
julia> UInt
UInt64

這是透過 base/boot.jl 中的下列程式碼來完成的

if Int === Int64
    const UInt = UInt64
else
    const UInt = UInt32
end

當然,這取決於 Int 的別名是什麼,但預設為正確的型別,即 Int32Int64

(請注意,與 Int 不同,Float 並不存在作為特定大小 AbstractFloat 的型別別名。與整數暫存器(其中 Int 的大小反映該機器上原生指標的大小)不同,浮點暫存器大小是由 IEEE-754 標準指定的。)

類型上的運算

由於 Julia 中的類型本身就是物件,因此一般的函式可以對它們進行運算。有些函式對於處理或探索類型特別有用,例如 <: 算子,它表示其左操作數是否為其右操作數的子類型。

isa 函式測試物件是否為特定類型,並傳回 true 或 false

julia> isa(1, Int)
true

julia> isa(1, AbstractFloat)
false

typeof 函式在範例中已於手冊中使用,傳回其引數的類型。由於如上所述,類型是物件,它們也有類型,我們可以詢問它們的類型是什麼

julia> typeof(Rational{Int})
DataType

julia> typeof(Union{Real,String})
Union

如果我們重複這個過程會怎樣?類型的類型是什麼?事實上,類型都是複合值,因此都具有 DataType 類型

julia> typeof(DataType)
DataType

julia> typeof(Union)
DataType

DataType 是其自己的類型。

另一個適用於某些類型的運算為 supertype,它會顯示類型的超類型。只有宣告的類型 (DataType) 具有明確的超類型

julia> supertype(Float64)
AbstractFloat

julia> supertype(Number)
Any

julia> supertype(AbstractString)
Any

julia> supertype(Any)
Any

如果您將 supertype 應用於其他類型物件 (或非類型物件),則會引發 MethodError

julia> supertype(Union{Float64,Int64})
ERROR: MethodError: no method matching supertype(::Type{Union{Float64, Int64}})
Closest candidates are:
[...]

自訂美化列印

通常,我們希望自訂顯示類型實例的方式。這是透過覆載 show 函式來完成的。例如,假設我們定義一個類型以極座標形式表示複數

julia> struct Polar{T<:Real} <: Number
           r::T
           Θ::T
       end

julia> Polar(r::Real,Θ::Real) = Polar(promote(r,Θ)...)
Polar

在此,我們新增了一個自訂建構函數,以便它能接收不同 Real 類型的參數,並將它們提升為一個共同的類型(請參閱 建構函數轉換和提升)。(當然,我們也必須定義許多其他方法才能讓它像一個 Number,例如 +*onezero、提升規則等等。)預設情況下,此類型的執行個體顯示得相當簡單,包含類型名稱和欄位值等資訊,例如 Polar{Float64}(3.0,4.0)

如果我們希望它顯示為 3.0 * exp(4.0im),我們會定義下列方法,將物件印出到指定的輸出物件 io(代表檔案、終端機、緩衝區等等;請參閱 網路和串流

julia> Base.show(io::IO, z::Polar) = print(io, z.r, " * exp(", z.Θ, "im)")

可以更精細地控制 Polar 物件的顯示方式。特別是,有時需要同時使用詳細的多行列印格式(用於在 REPL 和其他互動式環境中顯示單一物件)和更精簡的單行格式(用於 print 或在另一個物件中顯示物件,例如在陣列中)。雖然預設情況下,show(io, z) 函數會在兩種情況下都被呼叫,但您可以透過覆載 show 的三個參數形式(將 text/plain MIME 類型作為其第二個參數)來定義一個不同的多行格式,用於顯示物件(請參閱 多媒體 I/O),例如

julia> Base.show(io::IO, ::MIME"text/plain", z::Polar{T}) where{T} =
           print(io, "Polar{$T} complex number:\n   ", z)

(請注意,此處的 print(..., z) 會呼叫 2 個參數的 show(io, z) 方法。)這會產生

julia> Polar(3, 4.0)
Polar{Float64} complex number:
   3.0 * exp(4.0im)

julia> [Polar(3, 4.0), Polar(4.0,5.3)]
2-element Vector{Polar{Float64}}:
 3.0 * exp(4.0im)
 4.0 * exp(5.3im)

其中單行的 show(io, z) 形式仍用於 Polar 值的陣列。技術上來說,REPL 會呼叫 display(z) 來顯示執行一行的結果,其預設值為 show(stdout, MIME("text/plain"), z),而後者又預設為 show(stdout, z),但您不應定義新的 display 方法,除非您正在定義一個新的多媒體顯示處理常式(請參閱 多媒體 I/O)。

此外,您還可以為其他 MIME 類型定義 show 方法,以便在支援此功能的環境中更豐富地顯示物件(HTML、影像等),例如 IJulia。例如,我們可以透過下列方式定義 Polar 物件的格式化 HTML 顯示,包含上標和斜體字

julia> Base.show(io::IO, ::MIME"text/html", z::Polar{T}) where {T} =
           println(io, "<code>Polar{$T}</code> complex number: ",
                   z.r, " <i>e</i><sup>", z.Θ, " <i>i</i></sup>")

Polar 物件會在支援 HTML 顯示的環境中自動使用 HTML 顯示,但如果您需要取得 HTML 輸出,可以手動呼叫 show

julia> show(stdout, "text/html", Polar(3.0,4.0))
<code>Polar{Float64}</code> complex number: 3.0 <i>e</i><sup>4.0 <i>i</i></sup>

HTML 渲染器會將其顯示為:Polar{Float64} 複數:3.0 e4.0 i

根據經驗法則,單行 show 方法應該印出一個有效的 Julia 表達式,用於建立顯示的物件。當這個 show 方法包含中綴運算子時,例如上述 Polar 單行 show 方法中的乘法運算子 (*),當印出作為另一個物件的一部分時,可能會無法正確解析。為了了解這一點,請考慮表達式物件(請參閱 程式表示),它會取我們 Polar 類型的特定執行個體的平方

julia> a = Polar(3, 4.0)
Polar{Float64} complex number:
   3.0 * exp(4.0im)

julia> print(:($a^2))
3.0 * exp(4.0im) ^ 2

由於運算子 ^ 的優先順序高於 *(請參閱 運算子優先順序和結合性),此輸出並未忠實地表示表達式 a ^ 2,它應該等於 (3.0 * exp(4.0im)) ^ 2。為了解決這個問題,我們必須為 Base.show_unquoted(io::IO, z::Polar, indent::Int, precedence::Int) 建立自訂方法,表達式物件在印出時會在內部呼叫此方法

julia> function Base.show_unquoted(io::IO, z::Polar, ::Int, precedence::Int)
           if Base.operator_precedence(:*) <= precedence
               print(io, "(")
               show(io, z)
               print(io, ")")
           else
               show(io, z)
           end
       end

julia> :($a^2)
:((3.0 * exp(4.0im)) ^ 2)

上述定義的方法會在呼叫 show 的優先順序高於或等於乘法的優先順序時,在呼叫周圍加上括號。此檢查允許在列印時省略解析正確且不帶括號的表達式(例如 :($a + 2):($a == 2)

julia> :($a + 2)
:(3.0 * exp(4.0im) + 2)

julia> :($a == 2)
:(3.0 * exp(4.0im) == 2)

在某些情況下,根據內容調整 show 方法的行為會很有用。這可透過 IOContext 類型來達成,它允許將內容屬性與封裝的 IO 串流一起傳遞。例如,當 :compact 屬性設定為 true 時,我們可以在 show 方法中建立較短的表示法,如果屬性為 false 或不存在,則改用較長的表示法

julia> function Base.show(io::IO, z::Polar)
           if get(io, :compact, false)::Bool
               print(io, z.r, "ℯ", z.Θ, "im")
           else
               print(io, z.r, " * exp(", z.Θ, "im)")
           end
       end

當傳遞的 IO 串流是設定 :compact 屬性的 IOContext 物件時,將會使用此新的簡潔表示法。特別是在列印具有多個欄的多維陣列時(橫向空間有限)會用到

julia> show(IOContext(stdout, :compact=>true), Polar(3, 4.0))
3.0ℯ4.0im

julia> [Polar(3, 4.0) Polar(4.0,5.3)]
1×2 Matrix{Polar{Float64}}:
 3.0ℯ4.0im  4.0ℯ5.3im

請參閱 IOContext 文件,以取得可調整列印的常見屬性清單。

"值類型"

在 Julia 中,您無法對 truefalse進行分派。不過,您可以對參數類型進行分派,而 Julia 允許您將「純位元」值(類型、符號、整數、浮點數、元組等)包含為類型參數。一個常見的範例是 Array{T,N} 中的維度參數,其中 T 是類型(例如 Float64),但 N 僅為 Int

您可以建立自己的自訂類型,將值作為參數,並使用它們來控制自訂類型的調度。為了說明這個概念,讓我們介紹參數類型 Val{x},以及它的建構函數 Val(x) = Val{x}(),它作為一種慣用的方式來利用此技術,適用於不需要更精細層級的情況。

Val 定義為

julia> struct Val{x}
       end

julia> Val(x) = Val{x}()
Val

Val 的實作沒有比這更多。Julia 標準函式庫中的一些函式接受 Val 實例作為引數,您也可以使用它來撰寫自己的函式。例如

julia> firstlast(::Val{true}) = "First"
firstlast (generic function with 1 method)

julia> firstlast(::Val{false}) = "Last"
firstlast (generic function with 2 methods)

julia> firstlast(Val(true))
"First"

julia> firstlast(Val(false))
"Last"

為了在 Julia 中保持一致,呼叫端應該總是傳遞一個 Val 實例,而不是使用一個 類型,也就是說,使用 foo(Val(:bar)) 而不是 foo(Val{:bar})

值得注意的是,極容易誤用包含 Val 在內的參數「值」類型;在不利的狀況下,您很容易讓您的程式碼效能變得更 。特別是,您永遠不會想要撰寫如上所示的實際程式碼。有關 Val 的正確(和不正確)用法的更多資訊,請閱讀 效能秘訣中的更廣泛討論

  • 1「小」由 max_union_splitting 組態定義,目前預設為 4。
  • 2一些流行的語言有單例類型,包括 Haskell、Scala 和 Ruby。