Julia編程基礎(五):數值與運算

本文是《Julia 編程基礎》開源版本第五章:數值與運算。本書旨在幫助編程愛好者和專業程序員快速地熟悉 Julia 編程語言,並能夠在夯實基礎的前提下寫出優雅、高效的程序。這一系列文章由 郝林 採用 CC BY-NC-ND 4.0知識共享 署名-非商業性使用-禁止演繹 4.0 國際 許可協議)進行許可,請在轉載之前仔細閱讀上述許可協議。

本書的示例項目名爲Programs.jl,地址在這裏。其中會包含本書所講的大部分代碼,但並不是那些代碼的完全拷貝。這個示例項目中的代碼旨在幫助本書讀者更好地記憶和理解書中的要點。它們算是對書中代碼的再整理和補充。

我們在上一章介紹了 Julia 中的主要類型,其中包括了屬於具體類型的原語類型和複合類型。我們用數值類型舉了一些例子,還展示了一幅數值類型的層次結構圖。這個結構圖中的很多類型都是可以被實例化的具體類型。

接下來,我們就從那些具體的數值類型開始,介紹 Julia 編程中最常用的對象。

5.1 數值的類型

Julia 中具體的數值類型一共有 19 個。羅列如下。

常用的數值類型:

  • 布爾類型:Bool
  • 有符號整數類型:BigIntInt8Int16Int32Int64Int128
  • 無符號整數類型:UInt8UInt16UInt32UInt64UInt128
  • 浮點數類型:BigFloatFloat16Float32Float64

更多的數值類型:

  • 複數類型:Complex
  • 有理數類型:Rational
  • 無理數類型:Irrational

在那些常用的數值類型中,除了BigIntBigFloat之外的其他類型都屬於原語類型。而前兩個類型以及後面的ComplexRationalIrrational都屬於複合類型。

5.2 整數

我們在前面說過,整數類型又被分爲有符號類型和無符號類型。後兩者分別包含了 6 種和 5 種具體類型。我們爲了表示這些類型的值而輸入的內容又被稱爲整數字面量。比如,上一章示例中的1242020都是整數字面量。

5.2.1 類型與取值

整數類型的名稱大都很直觀,並且它們的寬度也都已經體現在名稱中了。詳見下表。

表 5-1 整數類型及其取值

類型名 是否有符號? 其值佔用的比特數 最小值 最大值
Int8 8 -2^7 2^7 - 1
UInt8 8 0 2^8 - 1
Int16 16 -2^15 2^15 - 1
UInt16 16 0 2^16 - 1
Int32 32 -2^31 2^31 - 1
UInt32 32 0 2^32 - 1
Int64 64 -2^63 2^63 - 1
UInt64 64 0 2^64 - 1
Int128 128 -2^127 2^127 - 1
UInt128 128 0 2^128 - 1

我們最好記住該表中各個類型的最小值和最大值。這並不困難,因爲它們是有規律可循的。不過,實在記不住也沒有關係。通過調用typemin函數和typemax函數,我們可以分別獲得某一個整數類型能夠表示的最小值和最大值,例如:

julia> typemin(Int8), typemax(Int8)
(-128, 127)

julia> typemin(UInt8), typemax(UInt8)
(0x00, 0xff)

julia> 

嚴格來說,Bool類型也屬於整數類型。因爲它與SignedUnsigned一樣,也是Integer類型的直接子類型。Bool類型的寬度(也就是其值佔用的比特數)是8,最小值是0(即false),最大值是1(即true)。

此外,Julia 還定義了IntUIntInt代表了有符號整數的默認類型。在 32 位的計算機系統中,它們分別是Int32UInt32的別名。而在 64 位的計算機系統中,它們分別是Int64UInt64的別名。如此一來,我們在 REPL 環境中隨便輸入一個整數字面量,就能猜出它的類型:

julia> typeof(2020) # 在 32 位的計算機系統中 
Int32 

julia> 
julia> typeof(2020) # 在 64 位的計算機系統中 
Int64

julia> 

這完全取決於我們的計算機系統的位數(或者說字寬)。順便說一句,我們可以通過訪問常量Sys.WORD_SIZE來獲知自己的計算機系統的字寬。

不過,對於較大的整數,Julia 會自動使用寬度更大的整數類型,例如:

julia> typeof(1234567890123456789)
Int64

julia> typeof(12345678901234567890)
Int128

julia> 

注意,在這個例子中,整數字面量的類型是否爲Int128,依據的不是字面量的長度,而是字面量表示的整數是否大於Int64類型的最大值。

5.2.2 表示方法

與前面的有符號整數不同,無符號整數會使用以0x爲前綴的十六進制形式來表示。比如:

julia> UInt32(2020)
0x000007e4

julia> UInt64(2020)
0x00000000000007e4

julia> 

我們都知道,在這些十六進制整數中,字母af分別代表了十進制整數1015,並且大寫這些字母也是可以的。注意,無符號整數值的類型會由字面量本身決定:

julia> typeof(0x01)
UInt8

julia> typeof(0x001)
UInt16

julia> typeof(0x00001)
UInt32

julia> typeof(0x000000001)
UInt64

julia> typeof(0x00000000000000001)
UInt128

julia> 

無符號整數值0x01只需佔用 8 個比特(1 位的十六進制數相當於 4 位的二進制數),因此使用UInt8類型就足夠了。無符號整數值0x001佔用的比特是 12 個,超出了UInt8類型的位數,所以就需要使用UInt16類型。而0x00001的佔位是 20 個,所以需要使用UInt32類型。以此類推。總之,一個無符號整數值的默認類型將會是能夠容納它的那個寬度最小的類型。

除了十六進制之外,我們還可以使用二進制或八進制的形式來表示無符號整數值。比如:

julia> 0b00000001
0x01

julia> 0o001
0x01

julia> 

0b爲前綴的整數字面量就是以二進制形式表示的整數,而以0o爲前綴的整數字面量則是以八進制形式表示的整數。在這裏,數字1至少需要 8 位的二進制數或 3 位的八進制數或 2 位的十六進制數來表示。即使我們輸入的位數不夠也沒有關係,Julia 會自動幫我們在高位補0以填滿至相應類型的位數(這裏是 8 個比特):

julia> 0b001
0x01

julia> 0o01
0x01

julia> 0x1
0x01

julia>

對於更大的無符號整數值的字面量來說也是類似的。

注意,二進制、八進制和十六進制的字面量可以表示無符號的整數值,但不能表示有符號的整數值。雖然我們可以在這些字面量的前面添加負號-,但是它們表示的依然是無符號的整數值。例如:

julia> -0x01, typeof(-0x01), Int16(-0x01)
(0xff, UInt8, 255)

julia> 

不要被字面量-0x01中的負號迷惑,它表示的值的類型仍然是UInt80xff實際上是負的0x01(也就是-1)的補碼。但由於十六進制字面量表示的整數值只能是無符號的,所以 Julia 會把它視爲一個無符號整數值的原碼。如此一來,字面量-0x01代表的整數值就是255

順便說一下,我們可以使用下劃線_作爲數值字面量中的數字分隔符。至於劃分的具體間隔,Julia 並沒有做硬性的規定。例如:

julia> 2_020, 0x000_01, 0b000_000_01, -0x0_1
(2020, 0x00000001, 0x01, 0xff)

julia> 

5.2.3 關於溢出

我們已知每個整數類型的最小值和最大值。當一個整數值超出了其類型的取值範圍時,我們就說這個值溢出(overflow)了。

以 64 位的計算機系統爲例,Julia 對整數值溢出有兩種處理措施,具體如下:

  • 對於其類型的寬度小於64的整數值,值不變,其類型會被提升到Int64
  • 對於其類型的寬度等於或大於64的整數值,其類型不變,對值採取環繞式(wraparound)處理。

也就是說,對於Int8Int16Int32‌、UInt8UInt16UInt32這 6 個類型,Julia 會把溢出值的類型自動地轉換爲Int64。這樣的話,這些值就不再是溢出的了。

對於寬度更大的整數類型,Julia 會採取不同的應對措施——環繞式(wraparound)處理。這是什麼意思呢?比如,當一個Int64類型的整數值比這個類型的最大值還要大1的時候,該值就會變成這個類型的最小值。相對應的,當這個類型的整數值比其最小值還要小1的時候,該值就會變成這個類型的最大值。示例如下:

julia> int1 = typemax(Int64)
9223372036854775807

julia> int2 = int1 + 1
-9223372036854775808

julia> int2 == typemin(Int64)
true

julia> int3 = int2 - 1
9223372036854775807

julia> int3 == typemax(Int64)
true

julia> 

可以想象一下,對於一個寬度小於64的整數類型,它的所有可取值共同形成了一根又長又直的棍子。棍子上的值以由小到大的順序從左到右排列。棍子的最左端是它的最小值,而最右端是它的最大值。

但對於像Int64這樣的整數類型來說,其所有可取值共同形成的就不再是一根棍子了,而是一個圓環。這就好像把原來的棍子掰彎並首尾相接了一樣。當該類型的值從它的最大值變更爲最大值再加1時,就好似從圓環接縫的右側移動一格,到了接縫左側。相對應的,當該類型的值從它的最小值變更爲最小值再減1時,就好像從圓環接縫的左側移動一格,到了接縫右側。這樣的處理方式就叫做對整數溢出的環繞式處理。

總之,對於Int64Int128UInt64UInt128這 4 個類型,Julia 會對溢出值做環繞式處理。

如果你需要的是不會溢出的整數類型,那麼可以使用BigInt。它的值的大小隻受限於當前計算機的內存空間。

5.2.4 BigInt

BigInt類型屬於有符號的整數類型。它表示的數值可以是非常大的正整數,也可以是非常小的負整數。由此,我們可以說,它的值可以是任意精度的。

與其他的整數類型一樣,其實例的構造函數與類型擁有相同的名稱。並且,我們還可以使用一種非常規的字符串來構造它的值。例如:

julia> BigInt(1234567890123456789012345678901234567890)
1234567890123456789012345678901234567890

julia> typeof(ans)
BigInt

julia> big"1234567890123456789012345678901234567890"
1234567890123456789012345678901234567890

julia> typeof(ans)
BigInt

julia>

可以看到,我們把一串很長的數字傳給了BigInt函數,並由此構造了一個BigInt類型的值。實際上,BigInt函數接受的唯一參數可以是任意長度的整數字面量,也可以是任何其他整數類型的值。

甚至,這個構造函數的參數值還可以是像big"1234"這樣的非常規字符串。不過,我們沒有必要這麼做。因爲big"1234"本身就能夠表示一個BigInt類型的實例。更寬泛地講,在一個內容形似整數的字符串前添加big這 3 個字母就可以把它變成一個BigInt類型的值。

另外,任何溢出的整數值的類型都不會被自動地轉換成BigInt。如有需要,我們只能手動地進行類型轉換。

最後,你需要了解的是,雖然BigInt直接繼承自Signed類型,但它是一個比較特殊的整數類型。它被定義在了Base.GMP包中,而其他的整數類型的定義都在Core包中。GMP 指的是 GNU Multiple Precision Arithmetic Library,可以翻譯爲多精度算術庫。Julia 中的Base.GMP包實際上只是對這個庫的再次封裝而已。雖然如此,這樣一個類型的值卻可以直接與其他類型的數值一起做數學運算。這主要得益於 Julia 中數值類型的層次結構,以及它的類型提升和轉換機制。

5.3 浮點數

浮點數可以用來表示小數。在抽象類型AbstractFloat之下,有 4 個具體的浮點數類型。它們是BigFloatFloat16Float32Float64

我們先說後 3 個類型。

5.3.1 精度與換算

這 3 種通常的浮點數類型分別對應着 3 種不同精度的浮點數。詳見下表。

表 5-2 浮點數類型及其取值

類型名 精度 其值佔用的比特數
Float16 半(half) 16
Float32 單(single) 32
Float64 雙(double) 64

對於這 3 種精度的浮點數,最新的 IEEE 754 技術標準中都有所描述。簡單來說,一個浮點數在存儲時會由 3 個部分組成,即:正負號部分(sign,簡稱S)、指數部分(exponent,簡稱E)和尾數部分(trailing significand,簡稱T)。例如,一個Float32類型的值會佔用 32 個比特,其中的正負號會使用 1 個比特,指數部分會使用 8 個比特,而尾數部分會用掉剩下的 23 個比特。

在通常情況下,這 3 個部分會依照下面的公式來共同表示一個浮點數:

-1^S x 2^E-bias x (1 + 2^1-p x T)

這裏的bias指的是偏移量,它會是指數部分的比特序列所能表示的最大正整數。注意,指數部分本身也是有符號的。而p代表的則是正負號部分和尾數部分共佔用的比特數。

下面舉一個例子。Float32類型的浮點數-0.75如果用二進制形式來表示就是這樣的:

julia> bitstring(Float32(-0.75))
"10111111010000000000000000000000"

julia> 

在 REPL 環境回顯的這個比特串中,最左邊的那個1就代表了S。緊挨在S右邊的 8 個比特是01111110,轉換成十進制數就是126,這就是E。而在E右邊的 23 個比特則代表T,即十進制數4194304。另外,對於Float32類型來說,bias就是127,而p則是24。把這些都代入前面的公式就可以得到:

-1^1 x 2^-1 x (1 + 0.5)

最終得出-0.75。這就是浮點數與其底層存儲之間的換算過程。

對於Float16Float64類型的浮點數來說,公式是一樣的。只是它們存儲那 3 個部分所佔用的比特數都會不同。不過,對於一些特殊的浮點數(如正無窮、負無窮等),這個公式就不適用了。至於怎樣換算,我們就不在此討論了。如果你有興趣,可以去閱讀最新的 IEEE 754 技術標準。

上面示例中的函數bitstring會把一個數值中的所有比特完全平鋪開,並把它們原封不動地塞到一個字符串當中。這樣的字符串就叫做比特串。

順便說一句,如果我們想獲取一個浮點數在底層存儲上的指數部分,可以調用exponent函數。該函數會以返回一個Int64類型的值。相關的,significand函數用於獲取一個浮點數在底層存儲上的尾數部分,其結果值的類型是Float64

5.3.2 值的表示

我們可以使用數學中的標準形式來寫入一個浮點數的字面量,例如:

julia> -0.75
-0.75

julia> 2.718281828
2.718281828

julia> 

如果浮點數的整數部分或小數部分只包含0的話,我們還可以把這個0省略掉:

julia> -.5
-0.5

julia> 1.
1.0

julia> 

另外,我們還可以使用科學計數法(E-notation)來表示浮點數,如:

julia> 1e8
1.0e8

julia> 0.5e-6
5.0e-7

julia> 0.25e-2
0.0025

julia> 

注意,這裏的e表示的是以10爲底的冪運算(或者說指數運算)。緊挨在它右邊的那個整數就是指數。因此,0.25e-2就相當於0.25 * 10^-2

Julia 的 REPL 環境在必要的時候也會使用科學計數法回顯浮點數:

julia> 0.000025
2.5e-5

julia> 2500000.0
2.5e6

julia> 

對於我們在上面寫入的這些浮點數,Julia 都會把它們識別爲Float64類型的值。如果你想把一個浮點數轉換爲Float32類型的,那麼有兩種方式。一種方式是,使用該類型對應的構造函數。另一種方式是,把科學計數法中的e改爲f。比如:

julia> Float32(0.000025)
2.5f-5

julia> typeof(2.5f-5)
Float32

julia> 

注意,這裏的f表示的同樣是以10爲底的冪運算。只不過由它參與生成的浮點數一定是Float32類型的。

對於Float16類型的浮點數來說,我們使用科學計數法表示的時候會有些特殊。它由 3 個部分組成,即:一個用十六進制形式表示的整數、一個代表了以2爲底的冪運算的字母p,以及一個代表指數的整數。示例如下:

julia> 0x1p0
1.0

julia> 0x1p1
2.0

julia> 0x1p3
8.0

julia> 0x1p-2
0.25

julia> 

可以看到,在我們改動代表指數的那個整數時,浮點數是以20.5的倍數來改變的。顯然,使用這種方式表示的浮點數在精度上會比較低。這主要是由於在p左邊的只能是整數。

Float16的這種特殊性不僅在於表示形式。它的底層實現也是比較特殊的。由於在傳統的計算機硬件中並沒有半精度浮點數這一概念,所以這種浮點數可能無法在硬件層面直接參與運算。Julia 只能採用軟實現的方式來支持Float16,並且在運算的時候把這類值的類型轉換成Float32

5.3.3 特殊的浮點數

特殊的浮點數包括正零、負零、正無窮、負無窮,以及 NaN。

正零(positive zero)和負零(negative zero)雖然在數學邏輯上是相同的,但是在底層存儲上卻是不同的。請看下面的代碼:

julia> 0.0 == -0.0 
true

julia> bitstring(0.0)
"0000000000000000000000000000000000000000000000000000000000000000"

julia> bitstring(-0.0)
"1000000000000000000000000000000000000000000000000000000000000000"

julia> 

在默認情況下,0.0-0.0都是Float64類型的值,但在這裏並不重要。重要的是,在存儲時,它們的指數部分和尾數部分都是0。這是 IEEE 754 技術標準中針對這兩個浮點數的特殊二進制表示法。在這種情況下,如果正負號部分是0,那麼它就代表0.0,否則就代表-0.0

與正零和負零相比,正無窮(positive infinity)、負無窮(negative infinity)和 NaN(Not a Number) 就更加特殊了。並且,它們對應於不同的浮點數類型都有着不同的標識符。請看下面這張表。

表 5-3 非常特殊的 3 種浮點數

Float16 Float32 Float64 含義 說明
Inf16 Inf32 Inf 正無窮(positive infinity),統稱 Inf 大於所有有限浮點數的值
-Inf16 -Inf32 -Inf 負無窮(negative infinity),統稱 -Inf 小於所有有限浮點數的值
NaN16 NaN32 NaN 非數(not a number),統稱 NaN 不等於任何浮點數(包括它本身)的值

Julia 爲這 3 種非常特殊的浮點數一共定義了 9 個常量。它們的名稱分別在此表格最左側的 9 個單元格中。由於浮點數字面量默認都是Float64類型的,所以這些常量的名稱也是以Float64下的名稱爲基準。

Inf16Inf32Inf代表的都是正無窮。它們一定都大於所有的有限浮點數。因此,我們像下面這樣調用typemax函數就可以得到對應類型的正無窮:

julia> typemax(Float16), typemax(Float32), typemax(Float64)
(Inf16, Inf32, Inf)

julia> 

相對應的,-Inf16-Inf32-Inf都代表負無窮。它們一定都小於所有的有限浮點數。所以:

julia> typemin(Float16), typemin(Float32), typemin(Float64)
(-Inf16, -Inf32, -Inf)

julia> 

NaN16NaN32NaN的含義都是非數(或者說不是數)。因此,一些無效操作的結果值以及無法確切定義的浮點數就都歸於它們的名下了。比如:

julia> 0 / 0
NaN

julia> Inf - Inf
NaN

julia> Inf16 - Inf16
NaN16

julia> -Inf - -Inf
NaN

julia> Inf / Inf
NaN

julia> Inf32 / Inf32
NaN32

julia> -Inf / Inf
NaN

julia> 0 * Inf
NaN

julia> 

這些運算規則都遵循了 IEEE 754 技術標準中的描述。所以我們也不用專門記憶。等到真正需要的時候再去查閱相關文檔就好了。

5.3.4 BigFloat

BigFloatBase.MPFR包中定義的一個類型。MPFR 本身是一個具有正確舍入(rounding)功能的用於多精度浮點計算(multiple-precision floating-point computations)的 C 語言程序庫。而Base.MPFR包只是對這個庫再次封裝。

BigFloat類型代表着任意精度的浮點數。示例如下:

julia> BigFloat(-0.75^68) / 3
-1.064252443341024990056571709262760635124796711655411248405774434407552083333339e-09

julia> typeof(ans)
BigFloat

julia> 

BigInt一樣,我們使用以big爲前綴的非常規字符串也可以構造出BigFloat的值,比如:

julia> big"-0.75"
-0.75

julia> typeof(ans)
BigFloat

julia> big"2.718281828"
2.718281828000000000000000000000000000000000000000000000000000000000000000000015

julia> typeof(ans)
BigFloat

julia> 

另外,我們都知道,通常的浮點數類型都有着固定的精度。而且,在默認情況下,Julia 對浮點數的舍入模式是四捨五入(由於計算機無法精確地表示所有小數,而且浮點數的位數有限,所以舍入必然存在,舍入模式也是必須要有的)。然而,對於BigFloat類型,我們可以自己設定它的精度和舍入模式。

通過調用setprecisionsetround函數,我們可以更改BigFloat類型值在參與運算時的默認精度和舍入模式。但要注意,這種更改是全局的!也就是說,更改一旦發生,它就會影響到當前 Julia 程序中所有相關的後續操作。不過,我們可以利用do代碼塊,讓這種更改只在當前的代碼塊中有效。下面是一些示例:

julia> BigFloat(1.01) + parse(BigFloat, "0.2")
1.210000000000000008881784197001252323389053344726562500000000000000000000000007

julia> setrounding(BigFloat, RoundDown)
MPFRRoundDown::MPFRRoundingMode = 3

julia> BigFloat(1.01) + parse(BigFloat, "0.2")
1.21000000000000000888178419700125232338905334472656249999999999999999999999999

julia> setprecision(35) do 
           BigFloat(1.01) + parse(BigFloat, "0.2") 
       end
1.2099999999

julia> BigFloat(1.01) + parse(BigFloat, "0.2") 
1.21000000000000000888178419700125232338905334472656249999999999999999999999999

julia> 

示例中的函數parse可以幫助我們把一個字符串值轉換成某個數值類型的值。不過,轉換是否能夠成功就要看字符串的具體內容了。如果不能成功轉換,這個函數就會報錯。

至於都有哪些舍入模式,我們可以參看Base.Rounding.RoundingMode類型的文檔。我們在前面說的默認舍入模式是由常量Base.Rounding.RoundNearest代表的。另外,我們在後面討論控制流的時候會對do代碼塊進行詳細說明。

5.4 複數和有理數

5.4.1 複數

Julia 預定義的複數類型是Complex。它是Number的直接子類型。爲了構造出複數的虛部,Julia 還專門定義了一個常量im。這裏的 im 是 imaginary 的縮寫。它使用起來是這樣的:

julia> 1 + 2im; typeof(1+2im)
Complex{Int64}

julia> 1.1 + 2.2im; typeof(1.1+2.2im)
Complex{Float64}

julia> 

可以看到,Complex是一個參數化的類型。因爲在其名稱的右側還有一個由花括號包裹的類型參數。這個類型參數會是一個代表了某個類型的標識符。關於參數化類型,我們在下下一章就會講到。

爲了使常見的數學公式和表達式更加清晰,Julia 允許在變量之前緊挨一個數值字面量,以表示兩個數相乘。比如,如果變量x的值是整數8,那麼2x^3就表示2乘以83次方。又比如,2^3x表示224次方。在這種情況下,變量x就被稱爲數值字面量係數(numeric literal coefficient)。

正因爲如此,我們才需要特別注意,上例中的2im2.2im雖然看起來與這種表示法非常相似,但其含義卻是完全不同的。整數或浮點數的字面量與常量im共同組成的是一個複數的虛部。而且還要注意,在構造複數的虛部時,我們就不能再使用數值字面量係數了。因爲這肯定會產生歧義。比如,1 + 2xim就是不合法的,除非已經存在一個名爲xim的變量,但如此一來這表示的就不是一個複數了。如果必須有變量參與複數的構造,那麼我們可以使用complex函數,例如:complex(1, 2x)

Julia 允許複數參與標準的數學運算。所以,下面的這些數學表達式是合法的:

julia> (1 + 2im) + (3 + 4im)
4 + 6im

julia> (1 + 2im) - (3 + 4im)
-2 - 2im

julia> (1 + 2im) * (3 + 4im)
-5 + 10im

julia> (1 + 2im) / (3 + 4im)
0.44 + 0.08im

julia> 3(1 + 2im)^8
-1581 + 1008im

julia> 

例子中的圓括號代表着對運算次序的設定。這與它在數學中的一般含義是一致的。

要想分別得到一個複數的實部和虛部,我們就需要調用real函數和imag函數。示例如下:

julia> com1 = 1 + 2im 
1 + 2im

julia> real(com1), imag(com1)
(1, 2)

julia> 

另外,我們還可以利用conj函數求出一個複數的共軛(conjugate),以及使用abs函數計算出一個複數與0之間的距離,等等。總之,Julia 預定義的很多數學函數都可以應用於複數。

5.4.2 有理數

我們在前面說過,浮點數無法精確地表示所有小數。比如,1/3是一個無限循環小數,但用浮點數表示的話只能是這樣的:

julia> 1/3
0.3333333333333333

julia> typeof(ans)
Float64

julia> 

嚴格來說,1/3並不是一個浮點數。因爲浮點數會對無限循環小數做舍入,這會損失精度。但是,它肯定是一個有理數。

在 Julia 中,有理數用於表示兩個整數之間的精確比率。有理數的類型是Rational。它的值可以由操作符//來構造。代碼如下:

julia> 1//3
1//3

julia> typeof(ans)
Rational{Int64}

julia> 

在操作符//左側的被稱爲分子,而在它右側的被稱爲分母。注意,這兩個數都只能是整數,而不能是浮點數。

如果在分子和分母之間存在公因數,那麼 Julia 會自動地把它們化爲最小項並讓分母變爲非負整數。例如:

julia> 3//9
1//3

julia> 3//-9
-1//3

julia> 42//126
1//3

julia> 

函數numeratordenominator可以讓我們分別得到一個有理數的分子和分母:

julia> numerator(rat1)
1

julia> denominator(rat1)
3

julia> 

有理數可以參與標準的數學運算。比如,我們可以拿一個有理數與一個整數、浮點數或者其他有理數進行比較。又比如,我們可以對有理數進行加減乘數等運算。另外,有理數也可以很容易地被轉換爲浮點數。例如:

julia> float(1//3) 
0.3333333333333333

julia> 

我在前面也說了,這實際上會存在精度上的損失。不過,爲了運算方便,Julia 會把分子和分母分別相同的有理數和浮點數視爲同一個數:

julia> float(1//3) == 1/3
true

julia> 

除非它們的分子和分母都爲0。這主要是因爲0//0是不合法的,會引發一個錯誤。況且,0/0會得到NaN,而從技術標準的角度講,NaN不與任何東西(包括它自己)相等。

5.5 常用的數學運算

Julia 中的一些操作符可以用於數學運算或位運算(也就是比特運算)。這樣的操作符也可以被稱爲運算符。因此,我們就有了數學運算符和位運算符這兩種說法。

5.5.1 數學運算符

可用於數學運算的運算符請見下表。

表 5-4 數學運算符

運算名稱 運算符 示意表達式 說明
一元加 + +x 求 x 的原值
一元減 - -x 求 x 的相反數,相當於 0 - x
平方根 √x 求 x 的平方根
二元加 + x + y 求 x 和 y 的和
二元減 - x - y 求 x 與 y 的差
* x * y 求 x 和 y 的積
/ x / y 求 x 與 y 的商
逆向除 \ x \ y 相當於 y / x
整除 ÷ x ÷ y 求 x 與 y 的商且只保留整數
求餘運算 % x % y 求 x 除以 y 後得到的餘數
冪運算 ^ x ^ y 求 x 的 y 次方

可以看到,Julia 中通用的數學運算符共有 9 個。其中,與+-一樣,也是一個一元運算符。它的含義是求平方根。在 REPL 環境中,我們可以通過輸入\sqrt[Tab]寫出這個符號。我們還可以用函數調用sqrt(x)來替代表達式√x

所謂的一元運算是指,只有一個數值參與的運算,比如√x。更寬泛地講,根據參與操作的對象的數量,操作符可被劃分爲一元操作符(unary operator)、二元操作符(binary operator)或三元操作符(ternary operator)。其中,參與操作的對象又被稱爲操作數(operand)。

除上述的運算符之外,Julia 還有一個專用於Bool類型值的一元運算符!,稱爲求反運算符。它會將true變爲false,反之亦然。

這些數學運算符都是完全符合數學邏輯的。所以我在這裏就不再展示它們的示例了。

5.5.2 位運算符

我們都知道,任何值在底層都是根據某種規則以二進制的形式存儲的。數值也不例外。我們把以二進制形式表示的數值簡稱爲二進制數。所謂的位運算,就是針對二進制數中的比特(或者說位)進行的運算。這種運算可以逐個地控制數中每個比特的具體狀態(01)。

Julia 中的位運算符共有 7 個。如下表所示。

表 5-5 位運算符

運算名稱 運算符 示意表達式 簡要說明
按位求反 ~ ~x 求 x 的反碼,相當於每一個二進制位都變反
按位求與 & x & y 逐個對比 x 和 y 的每一個二進制位,只要有0就爲0,否則爲1
按位求或 | x | y 逐個對比 x 和 y 的每一個二進制位,只要有1就爲1,否則爲0
按位異或 x ⊻ y 逐個對比 x 和 y 的每一個二進制位,只要不同就爲1,否則爲0
邏輯右移 >>> x >>> y 把 x 中的所有二進制位統一向右移動 y 次,並在空出的位上補0
算術右移 >> x >> y 把 x 中的所有二進制位統一向右移動 y 次,並在空出的位上補原值的最高位
邏輯左移 << x << y 把 x 中的所有二進制位統一向左移動 y 次,並在空出的位上補0

利用bitstring函數,我們可以很直觀地見到這些位運算符的作用。例如:

julia> x = Int8(-10)
-10

julia> bitstring(x)
"11110110"

julia> bitstring(~x)
"00001001"

julia> 

可以看到,按位求反的運算符~會把x中的每一個比特的狀態都變反(由0變成1或由1變成0)。這也是 Julia 中唯一的一個只需一個操作數的位運算符。因此,它與前面的+-一樣,都可以被稱爲一元運算符。

我們再來看按位求與和按位求或:

julia> y = Int8(17)
17

julia> bitstring(x)
"11110110"

julia> bitstring(y)
"00010001"

julia> bitstring(x & y)
"00010000"

julia> bitstring(x | y)
"11110111"

julia>

我們定義變量y,並由它來代表Int8類型的整數17y的二進制表示是00010001。對比變量x的二進制表示11110110,它們只在左數第 4 位上都爲1。因此,x & y的結果就是00010000。另一方面,它們只在右數第 4 位上都爲0,所以x | y的結果就是11110111

按位異或的運算符看起來很特別。因爲在別的編程語言中沒有這個操作符。在 REPL 環境中,我們可以通過輸入\xor[Tab]\veebar[Tab]寫出這個符號。我們還可以用函數調用xor(x, y)來替代表達式x ⊻ y

我們在前表中的也說明了,x ⊻ y的含義就是逐個對比xy的每一個二進制位,只要不同就爲1,否則爲0。示例如下:

julia> bitstring(x), bitstring(y), bitstring(x ⊻ y)
("11110110", "00010001", "11100111")

julia> 

Julia 提供了 3 種位移運算,分別是邏輯右移、算術右移和邏輯左移。下面是演示代碼:

julia> bitstring(x)
"11110110"

julia> bitstring(x >>> 3)
"00011110"

julia> bitstring(x >> 3)
"11111110"

julia> bitstring(x << 3)
"10110000"

julia>

在位移運算的過程中,數值的寬度(或者說佔用的比特數)是不變的。我們可以把承載一個數值的存儲空間看成一條板凳,而數值的寬度就是這條板凳的寬度。現在,有一條板凳承載了x變量代表的那個整數,並且寬度是8。也就是說,這條板凳上有 8 個位置,可以坐 8 個比特(假設比特是某種生物)。

每一次位移,板凳上的 8 個比特都會作爲整體向左或向右移動一個位置。在移動完成後,總會有 1 個比特被擠出板凳而沒有位置可坐,並且也總會有 1 個位置空出來。比如,如果向右位移一次,那麼最右邊的那個比特就會被擠出板凳,同時最左邊會空出一個位置。沒有位置可坐的比特會被淘汰,而空出來的位置還必須引進 1 個新的比特。

好了,我們現在來看從1111011000011110的運算過程。後者是前者邏輯右移三次之後的結果。按照前面的描述,在向右移動三次之後,最右邊的 3 個比特被淘汰了。因此,這時的二進制數就變爲了11110。又由於,邏輯右移運算會爲所有的空位都填補0(狀態爲0的比特),所以最終的二進制數就是00011110

與邏輯右移相比,算術右移只有一點不同,那就是:它在空位上填補的不是0,而是原值的最高位。什麼叫最高位?其實它指代的就是位置最高的那個比特。對於一個二進制數,最左邊的那個位置就是最高位,而最右邊的那個位置就是最低位。x的值11110110的最高位是1。因此,在算術右移三次之後,我們得到的新值就是11111110

與右移運算不同,左移運算只有一種。我們把它稱爲邏輯左移。這主要是因爲該運算也會爲空位填補0。所以,11110110經過邏輯左移三次之後就得到了10110000

5.5.3 運算同時賦值

Julia 中的每一個二元的數學運算符和位運算符都可以與賦值符號=聯用,可稱之爲更新運算符。聯用的含義是把運算的結果再賦給參與運算的變量。例如:

julia> x = 10; x %= 3
1

julia>

REPL 環境回顯的1就是變量x的新值。但要注意,這種更新運算相當於把新的值與原有的變量進行綁定,所以原有變量的類型可能會因此發生改變。示例如下:

julia> x = 10; x /= 3
3.3333333333333335

julia> typeof(x)
Float64

julia> 

顯然,x變量原有的類型肯定是某個整數類型(Int64Int32)。但更新運算使它的值變成了一個Float64類型的浮點數。因此,該變量的類型也隨之變爲了Float64

所有的更新運算符羅列如下:

+= -= *= /= \= ÷= %= ^= &= |= ⊻= >>>= >>= <<=

前 8 個屬於數學運算,後 6 個屬於位運算。

5.5.4 數值的比較

理所應當,數值與數值之間是可以比較的。在 Julia 中,這種比較不但可以發生在同類型的值之間,還可以發生在不同類型的值之間,比如整數和浮點數。通常,比較的結果會是一個Bool類型的值。

對於整數之間的比較,我們就不多說了。它與數學中的標準定義沒有什麼兩樣。至於浮點數,相關操作仍然會遵循 IEEE 754 技術標準。這裏存在 4 種互斥的比較關係,即:小於(less than)、等於(equal)、大於(greater than)和無序的(unordered)。

具體的浮點數比較規則如下:

  • 只要參與比較的兩個數值中有一個是 NaN,比較的結果就必然是false。因爲 NaN 不與任何東西相等,包括它自己。或者說,這種情況下的所有比較關係都是無序的。
  • Inf 等於它自己,並且一定大於除了 NaN 之外的任何數。
  • -Inf 等於它自己,並且一定小於除了 NaN 之外的任何數。
  • 正零(0.0)和負零(-0.0)是相等的。儘管它們在底層存儲上是不同的。
  • 其他情況下的有限浮點數比較將會按照數學中的標準定義進行。

Julia 中標準的比較操作符如下表。

表 5-6 比較操作符

操作符 含義
== 等於
!= ≠ 不等於
< 小於
<= ≤ 小於或等於
> 大於
>= ≥ 大於或等於

注意,對於不等於、小於或等於以及大於或等於,它們都有兩個等價的操作符可用。表中已用空格將它們分隔開了。

這些比較操作符都可以用於鏈式比較,例如:

julia> 1 < 3 < 5 > 2
true

julia> 

只有當鏈式比較中的各個二元比較的結果都爲true時,鏈式比較的結果纔會是true。注意,我們不要揣測鏈中的比較順序,因爲 Julia 未對此做出任何定義。

在這些比較操作符當中,我們需要重點關注一下==。我們之前使用過一個用於判斷相等的操作符===。另外,還有一個名叫isequal的函數也可以用於判等。我們需要明確這三者之間的聯繫和區別。

首先,操作符===代表最深入的判等操作。我們在前面說過,對於可變的值,這個操作符會比較它們在內存中的存儲地址。而對於不可變的值,該操作符會逐個比特地比較它們。

其次是操作符==。它完全符合數學中的判等定義。它只會比較數值本身,而不會在意數值的類型和底層存儲方式。對於浮點數,這種判等操作會嚴格遵循 IEEE 754 技術標準。順便說一句,在判斷兩個字符串是否相等時,它會逐個字符地進行比較,而忽略其底層編碼。

函數isequal用於更加淺表的判等。在大多數情況下,它的行爲都會依從於操作符==。在不涉及浮點數的時候,它會直接返回==的判斷結果。那爲什麼說它更加淺表呢?這是因爲,對於那些特殊的浮點數值,它只會去比較字面量。它同樣會判斷兩個 Inf(或者兩個 -Inf)是相等的,但也會判斷兩個 NaN 是相等的,還會判斷0.0-0.0是不相等的。這些顯然並未完全遵從 IEEE 754 技術標準中的規定。下面是相應的示例:

julia> isequal(NaN, NaN)
true

julia> isequal(NaN, NaN16)
true

julia> isequal(Inf32, Inf16)
true

julia> isequal(-Inf, -Inf32)
true

julia> isequal(0.0, -0.0)
false

julia> 

另外,===isequal無論如何都會返回一個Bool類型的值作爲結果。操作符==在絕大多數情況下也會如此。但當至少有一方的值是missing時,它就會返回missingmissing是一個常量,也是是類型Missing的唯一實例。它用於表示當前值是缺失的。

下面的代碼展示了上述 3 種判等操作在涉及missing時的判斷結果:

julia> missing === missing
true

julia> missing === 0.0
false

julia> missing == missing
missing

julia> missing == 0.0
missing

julia> isequal(missing, missing)
true

julia> isequal(missing, 0.0)
false

julia> 

最後,對於不同類型數值之間的比較,Julia 一般會貼合數學上的定義。比如:

julia> 0 == 0.0
true

julia> 1/3 == 1//3
false

julia> 1 == 1+0im
true

julia> 

5.5.5 操作符的優先級

Julia 對各種操作符都設定了特定的優先級。另外,Julia 還規定了它們的結合性。操作符的優先級越高,它涉及的操作就會越提前進行。比如:對於運算表達式10 + 3^2來說,由於運算符^的優先級比作爲二元運算符的+更高,所以冪運算3^2會先進行,然後纔是求和運算。

操作符的結合性主要用於解決這樣的問題:當一個表達式中存在且僅存在多個優先級相同的操作符時,操作的順序應該是怎樣的。一個操作符的結合性可能是,從左到右的、從右到左的或者未定義的。像我們在前面說的比較操作符的結合性就是未定義的。

下表展示了本章所述運算符的優先級和結合性。上方運算符的優先級會高於下方的運算符。

表 5-7 運算符的優先級和結合性

操作符 說明 結合性
+ - √ ~ ^ 一元的數學運算和位運算,以及冪運算 從右到左的
<< >> >>> 位移運算 從左到右的
* / \ ÷ % & 乘法、除法和按位與 從左到右的
+ - | ⊻ 加法、減法、按位或和按位異或 從左到右的
== != < <= > >= === !== 比較操作 未定義的
= += -= *= /= \= ÷= %= ^= &= |= ⊻= >>>= >>= <<= 賦值操作和更新運算 從右到左的

此外,數值字面量係數(如-3x+1中的x)的優先級略低於那幾個一元運算符。因此,表達式-3x會被解析爲(-3) * x,而表達式√4x則會被解析爲(√4) * x。可是,它與冪運算符的優先級卻是相當的。所以,表達式3^2x2x^3會被分別解析爲3^(2x)2 * (x^3)。也就是說,它們之間會依照從右到左的順序來結合。

對於運算表達式,我們理應更加註重正確性和(人類)可讀性。因此,我們總是應該在複雜的表達式中使用圓括號來明確運算的順序。比如,表達式(2x)^3的運算順序就一定是先做乘法運算再做冪運算。不過,過多的括號有時也會降低可讀性。所以我們往往需要對此做出權衡。如有必要,我們可以分別定義表達式的各個部分,然後再把它們組合在一起。

5.6 數值類型的提升

Julia 中有一個輔助系統,叫做類型提升系統。它可以將數學運算符操作的多個值統一地轉換爲某個公共類型的值,以便運算的順利進行。我們下面就簡要地說明一下這個輔助系統的應用和作用。關於公共類型的解釋也會在其中。

在 Julia 中,數學運算符其實也是用函數實現的。就拿用於二元加的運算符+來說,它的一個衍生方法的定義是這樣的:

+(x::Float64, y::Float64) = add_float(x, y)

這個定義向我們揭示了兩個細節。第一個細節就是我剛剛說的,數學運算符是由函數實現的。不僅如此,針對每一類可操作的數值,Julia 還定義了相應的衍生方法。第二個細節是,數學運算符操作的多個值必須是同一個類型的。你可能會有疑問,那爲什麼我們編寫的像1 + 2.0這樣的運算依然可以順利進行呢?實際上,這恰恰得益於 Julia 的類型提升系統。我們來看該系統中的一個定義:

+(x::Number, y::Number) = +(promote(x,y)...)

這個衍生方法的兩個參數的類型都是Number。這就意味着,只要參與二元加的操作數都是數值且它們的類型不同,該運算就會被分派到這個方法上。顯然,如果類型相同,那麼二元加運算就會被分派到像前一個定義那樣的方法上。

請注意,這個衍生方法的定義中有一個對promote函數的調用。這個函數其實就代表了類型提升系統的核心算法。我們可以在 REPL 環境中輸入表達式promote(1, 2.0)並回車。其結果如下:

julia> promote(1, 2.0)
(1.0, 2.0)

julia> typeof(ans)
Tuple{Float64,Float64}

julia> 

我們都知道,在 64 位的計算機系統中,字面量1的類型一定是Int64,而字面量2.0的類型肯定是Float64。由此,在那個調用promote函數後得到的元組中,包含了轉換自參數值1的、Float64類型的數值1.0,以及保持原樣的、Float64類型的數值2.0。這正是類型提升系統所起到的作用。它一般會先找到能夠無損地表示輸入值的某個公共類型,然後把這些值都轉換爲此公共類型的值(通常通過調用convert函數實現),最後輸出這些類型統一的值。

在一般情況下,如果參數值列表中只包含了整數和有理數,那麼promote函數就會把這些參數值都轉換爲有理數。倘若參數值列表中存在浮點數(但不存在複數),那麼這個函數就會把這些參數值都轉換爲適當類型的浮點數。一旦參數值列表中有複數,那該函數就一定會返回適當類型的複數的元組。另一方面,如果這些參數值的類型只是在寬度上所有不同(如Int64Int8Float16Float32等等),那麼promote函數就會把它們都轉換爲寬度較大的那個類型的值。

我們倒是不用死記硬背這些規則。因爲有一個名叫promote_type的函數,它可以接受若干個類型字面量並返回它們的公共類型。例如:

julia> promote_type(Int64, Float64)
Float64

julia> promote_type(Int64, Int8)
Int64

julia> promote_type(Float16, Float32)
Float32

julia> 

不論細節如何,經過前文所述的處理之後,這些數值就可以交給普通的運算符實現方法進行操作了。就像這樣:

julia> +(promote(1, 2.0)...)
3.0

julia> 

這裏對+函數的調用會被分派到我們在前面展示的那個針對Float64類型的衍生方法上。

解釋一下,符號...的作用是,把緊挨在它左邊的那個值中的所有元素值(如元組(1.0, 2.0)中的1.02.0)都平鋪開來,並讓這些元素值都成爲傳入外層函數(如+函數)的獨立參數值。所以,調用表達式+((1.0, 2.0)...)就相當於+(1.0, 2.0)

至於什麼是元組,你現在可以簡單地把它理解爲由圓括號包裹的、可承載若干值的容器。函數在同時返回多個值的時候通常就會用這種數據結構呈現。在後面講參數化類型的那一章裏有對元組的詳細說明。

回到正題。除了以上講的這些,Julia 的類型提升系統還有一個很重要的作用,那就是:讓我們可以編寫自己的類型提升規則,以自定義數學運算的部分行爲,尤其是在操作數的類型不同的時候。例如,若我們想讓整數和浮點數的運算結果變成BigFloat類型的值,則可以這樣做:

julia> import Base.promote_rule

julia> promote_rule(::Type{Int64}, ::Type{Float64}) = BigFloat
promote_rule (generic function with 137 methods)

julia>

第一行代碼是一條導入語句。簡單來說,我們在編寫某個函數的衍生方法的時候必須先導入這個函數。第二行代碼就是我編寫的衍生方法。由於與之相關的一些背景知識我們還沒有講到,所以你看不太懂也沒有關係。在這裏,你只要關注這行代碼中的Int64Float64BigFloat就可以了。前兩個都代表了操作數的類型,而後一個則代表了它們的公共類型。這正是在定義操作數類型和公共類型的對應關係。

現在,我們再次執行之前的代碼:

julia> promote(1, 2.0)
(1.0, 2.0)

julia> typeof(ans)
Tuple{BigFloat,BigFloat}

julia> 

可以看到,這次調用promote函數後得到的元組包含了兩個BigFloat類型的值。這就說明我們剛剛編寫的類型提升規則已經生效了。當然,修改 Julia 內置的類型提升規則是比較危險的。因爲這可能會改變已有代碼的基本行爲,並且會明顯地降低程序的穩定性,所以還是要謹慎爲之。但對於我們自己搭建的數值類型體系來講,這一特性的潛力是非常可觀的。

總之,Julia 的類型提升系統輔助維護着數學運算的具體實現。其中有着大量的默認規則,並確保着常規運算的有效性。但同時,它也允許我們自定義類型提升的規則,以滿足自己的特殊需要。

5.7 數學函數速覽

Julia 預定義了非常豐富的數學函數。一些常用的函數如下:

  • 數值類型轉換: 主要有T(x)convert(T, x)。其中,T代表目的類型,x代表源值。
  • 數值特殊性判斷:isequalisfiniteisinfisnan
  • 舍入: 有四捨五入的round(T, x)、向正無窮舍入的ceil(T, x)、向負無窮舍入的floor(T, x),以及總是向0舍入的trunc(T, x)
  • 除法:cld(x, y)fld(x, y)div(x, y),它們分別會將商向正無窮、負無窮和0做舍入。其中的x代表被除數,y代表除數。另外,與之相關的還有取餘函數rem(x, y)和取模函數mod(x, y),等等。
  • 公約數與公倍數: 函數gcd(x, y...)用於求取最大正公約數,而函數lcm(x, y...)則用於求取最小正公倍數。圓括號中的...的意思是,除了xy,函數還允許傳入更多的數值。但要注意,這裏的數值都應該是整數。
  • 符號獲取: 函數sign(x)signbit(x)都用於獲取一個數值的符號。但不同的是,前者對於正整數、0和負整數會分別返回10-1,而後者會分別返回falsefalsetrue
  • 絕對值獲取: 用於獲取絕對值的函數是abs(x)。一個相關的函數是,用於求平方的abs2(x)
  • 求根: 函數sqrt(x)用於求取x的平方根,而函數cbrt(x)則用於求取x的立方根。
  • 求指數: 函數exp(x)會求取x的自然指數。另外還有expm1(x),爲接近0x計算exp(x)-1
  • 求對數: log(x)會求取x的自然對數,log(b, x)會求以b爲底的x的對數,而log2(x)log10(x)則會分別以210爲底求對數。另外還有log1p(x),爲接近0x計算log(1+x)

除了以上函數之外,Julia 的Base包中還定義了很多三角函數和雙曲函數,比如sincosatanhacoth等等。另外,在SpecialFunctions.jl裏還有許多特殊的數學函數。不過這個包就需要我們手動下載了。

5.8 小結

在本章,我們主要探討了 Julia 中的數值及其運算方式。

這些數值的具體類型共有 19 種。常用的有,布爾類型、有符號整數類型、無符號整數類型和浮點數類型。另外還有複數類型、有理數類型和無理數類型。我們重點討論了整數類型和浮點數類型,其中還涉及到兩種可以代表任意精度數值的類型。

對於整數,我們需要注意無符號整數的表示形式和整數的溢出行爲。即使我們在無符號整數字面量的最左側添加了負號,它也會表示爲一個正整數。這與我們的直覺是不同的。而整數的溢出行爲,取決於整數類型的寬度是否小於當前計算機系統的字寬。

對於浮點數,Julia 擁有 3 種不同精度的常規類型。我們在表示其值的時候可以用一些方式加以區分。我們需要注意那些浮點數中的特殊值,並記住它們在運算過程的作用和影響。

我們還討論了針對這些數值的數學運算方式,介紹了數學運算符、位運算符、更新運算符、比較操作符,以及這些操作符的優先級和結合性。我們應該重點關注其中會影響到運算的表達和正確性的那些內容。

此外,我們也闡釋了數學運算的一些細節。這涉及到 Julia 的類型提升系統。有了它,我們才能將不同類型的值放在同一個運算表達式中。這個系統以及其中的默認規則在數學運算的過程中起到了很重要的作用。並且,它還允許我們對已有的規則進行修改,或對現有的規則進行擴充。

最後,爲了方便你進一步探索,我還簡單地羅列了一些有用的數學函數。雖然並不完全,但這些函數都是我們在編程時最常用到的。

Julia 中的數值類型確實有不少。但如果依照它們的命名規律(如寬度的大小、有無符號等),我們還是很容易記住它們的。我們應該按需取材,使用恰當類型的數值來存儲各種數據。這方面通常需要考慮便捷性、存儲空間、程序性能、傳輸效率等等因素。

原文鏈接:

https://github.com/hyper0x/JuliaBasics/blob/master/book/ch05.md

系列文章:

Julia編程基礎(一):初識Julia,除了性能堪比C語言還有哪些特性?

Julia編程基礎(二):開發Julia項目必須掌握的預備知識

Julia編程基礎(三):一文掌握Julia的變量與常量

Julia 編程基礎(四):如何用三個關鍵詞搞懂 Julia 類型系統

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章