Calvert's murmur

在 Golang 中使用常數

2019-11-06

約 6259 字 / 需 34 分鐘閱讀

原文:CalliCoderWorking with Constants in Golang

常數

在 Golang 中,我們使用 constant 語法來表示固定(不改變)的值,如 51.34true"Hello" 等。

定數是常數

Golang 中的所有定數,如整數 51000、浮點數 4.761.89、布林值 truefalse 或字串 "Hello""Jhon" 都是常數

常數 範例
整數常數 100067413
浮點數常數 4.56128.372
布林常數 truefalse
表示字元的常數 'C''ä'
複數常數 2.7i3 + 5i
字串常數 "Hello""Rajeev"

宣告常數

定數是沒有名稱的常數。你可以使用 const 關鍵字來要宣告一個常數並幫它命名:

const myFavLanguage = "Python"
const sunRisesInTheEast = true

你也可以像這樣在宣告時指定型別:

const a int = 1234
const b string = "Hi"

在單一陳述式中也可以有多個宣告:

const country, code = "India", 91

const (
	employeeId string  = "E101"
	salary     float64 = 50000.0
)

正如你所期望的,常數無法更改。也就是說,你無法在初始化常數後重新分派給它:

const a = 123
a = 321 // Compiler Error (Cannot assign to constant)

已型別化和未型別化常數

Golang 中的常數很特別。它們的運作方式與其他語言的運作方式不同。要了解它們為何如此特別以及它們如何正常運作,我們需要關於 Go 型別系統的背景知識。讓我們直接進入正題吧!

背景知識

Go 是一種靜態型別的程式語言。也就是說編譯器在編譯時期就知道或推斷了每個變數的型別。

但是它的型別系統更進一步的不允許你執行混合數字型別的操作。舉例來說,你不能將 float64 變數加到 int,也不能將 int64 變數加到 int

var myFloat float64 = 21.54
var myInt int = 562
var myInt64 int64 = 120

var res1 = myFloat + myInt // Not Allowed (Compiler Error)
var res2 = myInt + myInt64 // Not Allowed (Compiler Error)

要使上述操作正常運作,你需要明確地轉換變數,讓它們都屬於相同型別:

var res1 = myFloat + float64(myInt) // Works
var res2 = myInt + int(myInt64)     // Works

如果你使用過其他靜態型別的語言,如 C、C++ 或 Java,那麼你必須知道,只要在任何操作中將它們混合使用,它們會自動將較小的型別轉換為較大的型別。舉例來說,int 可以自動轉換為 longfloatdouble

所以顯而易見的問題是:為什麼 Go 不會做同樣的事?它為什麼不像 C、C++ 或 Java 一樣執行隱式型別轉換?

以下是 Go 設計師所說的(節錄自 Golang 的官方文件):

C 語言籠罩在數字型別間便利的自動轉換所引起的混亂中。表達式什麼時候是無號數?可容納多大數值範圍?它會溢位嗎?結果是否可移植、不受執行它的機器影響?這也使得編譯器複雜化;「一般算術轉換」不容易實作且跨架構不一致。出於可移植性的原因,我們決定以程式碼中明確地型別轉換作為代價,讓事情變得清晰明瞭。

好吧!因此 Go 不提供隱式型別轉換,它要求我們在混合操作多個型別的變數時,必須使用明確地型別轉換。

但是 Go 的型別系統如何與常數一起使用?以下所有陳述式在 Golang 中皆有效:

var myInt32 int32 = 10
var myInt int = 10
var myFloat64 float64 = 10
var myComplex complex64 = 10

在以上範例中,常數值 10 的型別是什麼?此外,如果 Golang 中沒有隱式型別轉換,那麼我們就不需要像這樣寫上面的陳述式:

var myInt32 int32 = int32(10)
var myFloat64 float64 = float64(10)
// etc..

好吧!所有這些問題的答案都在於 Golang 處理常數的方式。因此,讓我們找出它們的處理方式。

未型別化常數

除非明確地給定型別,否則 Golang 中任何已命名或未命名的常數都是未型別化的。舉例來說,以下所有常數都是未型別化的:

1       // untyped integer constant
4.5     // untyped floating-point constant
true    // untyped boolean constant
"Hello" // untyped string constant

即使你給它們名字,它們還是未型別化:

const a = 1
const f = 4.5
const b = true
const s = "Hello"

現在,你可能會想說我使用的是如 integer 常數、string 常數之類的語法,而我還說它們是未型別化。

是的,1 是整數、4.5 是浮點數以及 "Hello" 是字串。但它們只是數值,還沒有給它們如 int32float64string固定型別,這會迫使它們遵守 Go 的嚴格型別規則。

事實上,1 是未型別化的,這讓我們可以將其分派給型別與整數相容的任何變數:

var myInt int = 1
var myFloat float64 = 1
var myComplex complex64 = 1

要注意的是,雖然 1 未型別化,但它是未型別化的整數。因此,它只能在允許整數的地方使用。舉例來說,你不能將它分派給 stringboolean 變數。

同樣地,可以在任何允許使用浮點數的地方使用如 4.5 之類的未型別化浮點數常數:

var myFloat32 float32 = 4.5
var myComplex64 complex64 = 4.5

現在來看一個未型別化字串常數的範例:

在 Golang 中,你可以使用 type 關鍵字來建立型別別名:

type RichString string // Type alias of `string`

鑑於 Golang 的強型別特性,你不能將 string 變數分派給 RichString 變數:

var myString string = "Hello"
var myRichString RichString = myString // Won't work.

但是,你可以將一個未型別化的字串常數分派給 RichString 變數,因為它與字串相容:

const myUntypedString = "Hello"
var myRichString RichString = myUntypedString // Works

常數和型別推斷:預設型別

Go 支援型別推斷。也就是說,它可以從初始化變數的值推斷出變數的型別。因此,你可以宣告一個具有初始值但沒有任何型別資訊的變數,而 Go 會自動決定型別:

var a = 5 // Go compiler automatically infers the type of the variable `a`

但是它是如何運作的呢?鑑於 Golang 中的常數是未型別化的,在上面的範例中,變數 a 的型別是什麼?是 int8int16int32int64 還是 int

好吧,事實證明,Golang 中每個未型別化的常數都有一個預設型別。當我們將常數分派給沒有任何明確型別的變數時,將使用預設型別。

以下是 Golang 中各種常數的預設型別:

常數 預設型別
整數(10、76) int
浮點數(3.14、7.92) float64
複數(3 + 5i) complex128
字元('a''♠' rune
布林值(true, false) bool
字串(”Hello”) string

因此,在陳述式 var a = 5 中,由於沒有明確地型別資訊,整數常數的預設型別將決定 a 的型別,即 int

已型別化常數

在 Golang 中,當你像這樣在宣告常數時明確地指定型別,常數是已型別化的:

const typedInt int = 1 // Typed constant

就像變數一樣,Go 型別系統的所有規則都適用於已型別化的常數。舉例來說,你不能分派已型別化的整數常數到浮點數變數:

var myFloat64 float64 = typedInt // Compiler Error

使用已型別化常數,你將失去未型別化常數帶來的所有靈活性,例如將它們分派給相容型別的任何變數,或將它們混合在算術運算中。因此,僅在絕對必要時,才應該為常數宣告型別。否則,只宣告沒有型別的常數。

常數表達式

常數是未型別化的(除非明確地給定型別),這讓你可以在任何表達式中自由混合它們。

因此,你可以擁有一個混合各種未型別化常數的表達式,只要這些未型別化的常數彼此相容:

const a = 5 + 7.5 // Valid
const b = 12 / 5  // Valid
const c = 'z' + 1 // Valid

const d = "Hey" + true // Invalid (untyped string constant and untyped boolean constant are not compatible with each other)

對常數表達式及其結果的評估遵循某些規則。讓我們看看這些規則:

常數表達式的規則

  • 比較兩個未型別化的常數,會輸出未型別化的布林值常數。

    const a = 7.5 > 5       // true (untyped boolean constant)
    const b = "xyz" < "uvw" // false (untyped boolean constant)
  • 對於其他運算(除了位移):

    • 如果兩個運算元型別相同(例如,皆為未型別化的整數常數),則結果也是相同型別。舉例來說,表達式 25 / 2 的結果是 12,不是 12.5。由於兩個運算元均為未型別化整數,因此結果將被截斷為整數。

    • 如果兩個運算元型別不同,則結果是該運算元的型別且根據規則取較大的:integer < rune < floating-point < complex

    const a = 25 / 2       // 12 (untyped integer constant)
    const b = (6 + 8i) / 2 // (3 + 4i) (untyped complex constant)
  • 位移運算規則有點複雜。首先,有一些必要條件:

    • 位移表達式右側的運算元必須是未型別化的整數型別,或是可以表示 uint 型別的未型別化常數。

    • 左側的運算元必須是整數型別,或是可以表示 int 型別的未型別化常數。

    規則:如果位移表達式左側的運算元是未型別化常數,則結果是未型別化的整數常數;否則結果與左側運算元的型別相同。

    const a = 1 << 5         // 32 (untyped integer constant)
    const b = int32(1) << 4  // 16 (int32)
    const c = 16.0 >> 2      // 4 (untyped integer constant) - 16.0 can represent a value of type `int`
    const d = 32 >> 3.0      // 4 (untyped integer constant) - 3.0 can represent a value of type `uint`
    
    const e = 10.50 << 2     // ILLEGAL (10.50 can't represent a value of type `int`)
    const f = 64 >> -2       // ILLEGAL (The right operand must be an unsigned int or an untyped constant compatible with `uint`)

常數表達式範例

讓我們來看一些常數表達式的範例:

package main

import "fmt"

func main() {
	var result = 25 / 2
	fmt.Printf("result is %v which is of type %T\n", result, result)
}
# Output
result is 12 which is of type int

由於 252 都是未型別化的整數常數,因此結果會被截斷為未型別化的整數 12

若要得到正確的結果,你可以執行以下任一操作:

// Use a float value in numerator or denominator
var result = 25.0 / 2
// Explicitly cast the numerator or the denominator
var result = float64(25) / 2

讓我們來看另一個範例:

package main

import "fmt"

func main() {
	var result = 4.5 + (10-5)*(3+2)/2
	fmt.Println(result)
}

以上程式會產生什麼結果?

嗯,不是 17。以上程式的實際結果是 16.5。我們來看一下表達式的求值順序,以了解為什麼結果是 16.5

4.5 + (10 - 5) * (3 + 2)/24.5 + (5) * (3 + 2)/24.5 + (5) * (5)/24.5 + (25)/24.5 + 1216.5

了解了嗎?結果是錯誤的,因為表達式 25/2 的計算結果是 12

要得到正確的結果,可以執行以下任一操作:

// Use a float value in the numerator or denominator
var result = 4.5 + (10-5)*(3+2)/2.0
// Explicitly cast numerator or the denominator
var result = 4.5 + float64((10-5)*(3+2))/2

結論

未型別化常數是 Go 創造者做出的驚人設計決策。儘管 Go 有強大的型別系統,但你可以使用未型別化的常數來擺脫 Go 的型別系統,並在處理任何表達式中的混合資料型別時具有更強大的靈活性。

Tags: Golang