欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

詳解Go語言中空結(jié)構(gòu)體的慣用法

 更新時間:2024年11月05日 11:59:42   作者:江湖十年  
空結(jié)構(gòu)體在 Go 編程中有著廣泛的應(yīng)用,本文將詳細探討空結(jié)構(gòu)體的幾種典型用法,并解釋為何它們在特定場景下非常有用,希望對大家有所幫助

在 Go 語言中,空結(jié)構(gòu)體 struct{} 是一個非常特殊的類型,它不包含任何字段并且不占用任何內(nèi)存空間。雖然聽起來似乎沒什么用,但空結(jié)構(gòu)體在 Go 編程中實際上有著廣泛的應(yīng)用。本文將詳細探討空結(jié)構(gòu)體的幾種典型用法,并解釋為何它們在特定場景下非常有用。

空結(jié)構(gòu)體不占用內(nèi)存空間

首先我們來驗證下空結(jié)構(gòu)體是否占用內(nèi)存空間:

type Empty struct{}

var s1 struct{}
s2 := Empty{}
s3 := struct{}{}

fmt.Printf("s1 addr: %p, size: %d\n", &s1, unsafe.Sizeof(s1))
fmt.Printf("s2 addr: %p, size: %d\n", &s2, unsafe.Sizeof(s2))
fmt.Printf("s3 addr: %p, size: %d\n", &s3, unsafe.Sizeof(s3))
fmt.Printf("s1 == s2 == s3: %t\n", s1 == s2 && s2 == s3)

NOTE: 為了保持代碼邏輯清晰,這里只展示了代碼主邏輯。后文中所有示例代碼都會如此,完整代碼可以在文末給出的示例代碼 GitHub 鏈接中獲取。

在 Go 語言中,我們可以使用 unsafe.Sizeof 計算一個對象占用的字節(jié)數(shù)。

執(zhí)行以上示例代碼,輸出結(jié)果如下:

$ go run main.go
s1 addr: 0x1044ef4a0, size: 0
s2 addr: 0x1044ef4a0, size: 0
s3 addr: 0x1044ef4a0, size: 0
s1 == s2 == s3: true

根據(jù)輸出結(jié)果可知:

  • 多個空結(jié)構(gòu)體內(nèi)存地址相同。
  • 空結(jié)構(gòu)體占用字節(jié)數(shù)為 0,即不占用內(nèi)存空間。
  • 多個空結(jié)構(gòu)體值相等。

后面兩個結(jié)論很好理解,第一個結(jié)論有點反常識。為什么不同變量實例化的空結(jié)構(gòu)體內(nèi)存地址會相同?

真的是這樣嗎?我們可以看下另一個示例:

var (
 a struct{}
 b struct{}
 c struct{}
 d struct{}
)

println("&a:", &a)
println("&b:", &b)
println("&c:", &c)
println("&d:", &d)

println("&a == &b:", &a == &b)
x := &a
y := &b
println("x == y:", x == y)

fmt.Printf("&c(%p) == &d(%p): %t\n", &c, &d, &c == &d)

這段代碼中定義了 4 個空結(jié)構(gòu)體,依次打印它們的內(nèi)存地址,然后又分別對比了 a 與 b 的內(nèi)存地址和 c 與 d 的內(nèi)存地址兩兩是否相等。

執(zhí)行示例代碼,輸出結(jié)果如下:

$ go run -gcflags='-m -N -l' main.go
# command-line-arguments
./main.go:11:3: moved to heap: c
./main.go:12:3: moved to heap: d
./main.go:23:12: ... argument does not escape
./main.go:23:50: &c == &d escapes to heap
&a: 0x1400010ae84
&b: 0x1400010ae84
&c: 0x104ec74a0
&d: 0x104ec74a0
&a == &b: false
x == y: true
&c(0x104ec74a0) == &d(0x104ec74a0): true

在 Go 語言中使用 go run 命令時,可以通過 -gcflags 選項向 Go 編譯器傳遞多個標(biāo)志,這些標(biāo)志會影響編譯器的行為。

  • -m 標(biāo)志用于啟動編譯器的內(nèi)存逃逸分析。
  • -N 標(biāo)志用于禁用編譯器優(yōu)化。
  • -l 標(biāo)志用于禁用函數(shù)內(nèi)聯(lián)。

根據(jù)輸出可以發(fā)現(xiàn),變量 c 和 d 發(fā)生了內(nèi)存逃逸,并且最終二者的內(nèi)存地址相同,相等比較結(jié)果為 true。

而 a 和 b 兩個變量的輸出結(jié)果就比較有意思了,兩個變量沒有發(fā)生內(nèi)存逃逸,并且二者打印出來的內(nèi)存地址相同,但內(nèi)存地址相等比較結(jié)果卻為 false。

所以,我們可以推翻之前的結(jié)論,新結(jié)論為:「多個空結(jié)構(gòu)體內(nèi)存地址可能相同」。

在 Go 官方的語言規(guī)范中 Size and alignment guarantees 部分對關(guān)于空結(jié)構(gòu)體內(nèi)存地址進行了說明:

A struct or array type has size zero if it contains no fields (or elements, respectively) that have a size greater than zero. Two distinct zero-size variables may have the same address in memory.

大概意思是說:如果一個結(jié)構(gòu)體或數(shù)組類型不包含任何占用內(nèi)存大小大于零的字段(或元素),那么它的大小為零。兩個不同的零大小變量可能在內(nèi)存中具有相同的地址

注意,這里說的是可能may have the same。所以前文所述「多個空結(jié)構(gòu)體內(nèi)存地址相同」的結(jié)論并不準(zhǔn)確。

NOTE: 本文示例執(zhí)行結(jié)果基于 Go 1.22.0 版本,對于多個空結(jié)構(gòu)體內(nèi)存地址打印結(jié)果既存在相同情況,也存在不同情況,這跟 Go 編譯器實現(xiàn)有關(guān),后續(xù)實現(xiàn)可能會有變化。

另外,對于嵌套的空結(jié)構(gòu)體,其表現(xiàn)結(jié)果與普通空結(jié)構(gòu)體相同:

type Empty struct{}

type MultiEmpty struct {
 A Empty
 B struct{}
}

s1 := Empty{}
s2 := MultiEmpty{}
fmt.Printf("s1 addr: %p, size: %d\n", &s1, unsafe.Sizeof(s1))
fmt.Printf("s2 addr: %p, size: %d\n", &s2, unsafe.Sizeof(s2))

執(zhí)行示例代碼,輸出結(jié)果如下:

$ go run main.go
s1 addr: 0x1044ef4a0, size: 0
s2 addr: 0x1044ef4a0, size: 0

空結(jié)構(gòu)體影響內(nèi)存對齊

空結(jié)構(gòu)體也并不是什么時候都不會占用內(nèi)存空間,比如空結(jié)構(gòu)體作為另一個結(jié)構(gòu)體字段時,根據(jù)位置不同,可能因內(nèi)存對齊原因,導(dǎo)致外層結(jié)構(gòu)體大小不一樣:

type A struct {
 x int
 y string
 z struct{}
}

type B struct {
 x int
 z struct{}
 y string
}

type C struct {
 z struct{}
 x int
 y string
}

a := A{}
b := B{}
c := C{}
fmt.Printf("struct a size: %d\n", unsafe.Sizeof(a))
fmt.Printf("struct b size: %d\n", unsafe.Sizeof(b))
fmt.Printf("struct c size: %d\n", unsafe.Sizeof(c))

以上示例中,定義了三個結(jié)構(gòu)體 A、B、C,并且都定義了三個字段,類型分別是 int、string、struct{},空結(jié)構(gòu)體字段分別放在最后、中間、最前面不同的位置。

執(zhí)行示例代碼,輸出結(jié)果如下:

$ go run main.go
struct a size: 32
struct b size: 24
struct c size: 24

可以發(fā)現(xiàn),當(dāng)空結(jié)構(gòu)體放在另一個結(jié)構(gòu)體最后一個字段時,會觸發(fā)內(nèi)存對齊。

此時外層結(jié)構(gòu)體會占用更多的內(nèi)存空間,所以如果你的程序?qū)?nèi)存要求比較嚴(yán)格,則在使用空結(jié)構(gòu)體作為字段時需要考慮這一點。

NOTE: 這里先挖個坑,我會再寫一篇 Go 中結(jié)構(gòu)體內(nèi)存對齊的文章,分析下為什么 struct{} 放在結(jié)構(gòu)體字段最后會出現(xiàn)內(nèi)存對齊現(xiàn)象,敬請期待。防止迷路,

空結(jié)構(gòu)體用法

根據(jù)前文的講解,我們對 Go 中空結(jié)構(gòu)體的特性和一些使用時注意事項已經(jīng)有所了解,是時候探索空結(jié)構(gòu)體的用處了。

實現(xiàn) Set

首先,空結(jié)構(gòu)體最常用的地方,就是用來實現(xiàn) set(集合) 類型了。

我們知道 Go 語言在語法層面沒有提供 set 類型。不過我們可以很方便的使用 map + struct{} 來實現(xiàn) set 類型,代碼如下:

// Set 基于空結(jié)構(gòu)體實現(xiàn) set
type Set map[string]struct{}

// Add 添加元素到 set
func (s Set) Add(element string) {
 s[element] = struct{}{}
}

// Remove 從 set 中移除元素
func (s Set) Remove(element string) {
 delete(s, element)
}

// Contains 檢查 set 中是否包含指定元素
func (s Set) Contains(element string) bool {
 _, exists := s[element]
 return exists
}

// Size 返回 set 大小
func (s Set) Size() int {
 return len(s)
}

// String implements fmt.Stringer
func (s Set) String() string {
 format := "("
 for element := range s {
  format += element + " "
 }
 format = strings.TrimRight(format, " ") + ")"
 return format
}

s := make(Set)

s.Add("one")
s.Add("two")
s.Add("three")

fmt.Printf("set: %s\n", s)
fmt.Printf("set size: %d\n", s.Size())
fmt.Printf("set contains 'one': %t\n", s.Contains("one"))
fmt.Printf("set contains 'onex': %t\n", s.Contains("onex"))

s.Remove("one")

fmt.Printf("set: %s\n", s)
fmt.Printf("set size: %d\n", s.Size())

執(zhí)行示例代碼,輸出結(jié)果如下:

$ go run main.go
set: (one two three)
set size: 3
set contains 'one': true
set contains 'onex': false
set: (three two)
set size: 2

使用 map 和空結(jié)構(gòu)體非常容易實現(xiàn) set 類型。map 的 key 實際上與 set 不重復(fù)的特性剛好一致,一個不需要關(guān)心 value 的 map 即為 set。

也正因為如此,空結(jié)構(gòu)體類型最適合作為這個不需要關(guān)心的 value 的 map 了,因為它不占空間,沒有語義。

也許有人會認(rèn)為使用 any 作為 map 的 value 也可以實現(xiàn) set。但其實 any 是會占用空間的。

示例如下:

s := make(map[string]any)
s["t1"] = nil
s["t2"] = struct{}{}
fmt.Printf("set t1 value: %v, size: %d\n", s["t1"], unsafe.Sizeof(s["t1"]))
fmt.Printf("set t2 value: %v, size: %d\n", s["t2"], unsafe.Sizeof(s["t2"]))

執(zhí)行示例代碼,輸出結(jié)果如下:

$ go run main.go
set t1 value: <nil>, size: 16
set t2 value: {}, size: 16

可以發(fā)現(xiàn),any 類型的 value 是有大小的,所以并不合適。

日常開發(fā)中,我們還會用到一種 set 的慣用法:

s := map[string]struct{}{
 "one":   {},
 "two":   {},
 "three": {},
}
for element := range s {
 fmt.Println(element)
}

這種用法也比較常見,無需聲明一個 set 類型,直接通過字面量定義一個 value 為空結(jié)構(gòu)體的 map,非常方便。

申請超大容量 Array

基于空結(jié)構(gòu)體不占內(nèi)存空間的特性,我們可以考慮創(chuàng)建一個容量為 100 萬的 array

var a [1000000]string
var b [1000000]struct{}

fmt.Printf("array a size: %d\n", unsafe.Sizeof(a))
fmt.Printf("array b size: %d\n", unsafe.Sizeof(b))

執(zhí)行示例代碼,輸出結(jié)果如下:

$ go run main.go
array a size: 16000000
array b size: 0

使用空結(jié)構(gòu)體創(chuàng)建的 array 其大小依然為 0。

申請超大容量 Slice

我們還以考慮創(chuàng)建一個容量為 100 萬的 slice

var a = make([]string, 1000000)
var b = make([]struct{}, 1000000)
fmt.Printf("slice a size: %d\n", unsafe.Sizeof(a))
fmt.Printf("slice b size: %d\n", unsafe.Sizeof(b))

執(zhí)行示例代碼,輸出結(jié)果如下:

$ go run main.go
slice a size: 24
slice b size: 24

當(dāng)然,可以發(fā)現(xiàn),其實不管是否使用空結(jié)構(gòu)體,slice 只占用 header 的空間。

信號通知

空結(jié)構(gòu)體另一個我經(jīng)常使用的方法是與 channel 結(jié)合當(dāng)作信號來使用,示例如下:

done := make(chan struct{})

go func() {
    time.Sleep(1 * time.Second) // 執(zhí)行一些操作...
    fmt.Printf("goroutine done\n")
    done <- struct{}{} // 發(fā)送完成信號
}()

fmt.Printf("waiting...\n")
<-done // 等待完成
fmt.Printf("main exit\n")

這段代碼中聲明了一個長度為 0 的 channel,其類型為 chan struct{}

然后啟動一個 goroutine 執(zhí)行業(yè)務(wù)邏輯,主協(xié)程等待信號退出,二者使用 channel 進行通信。

執(zhí)行示例代碼,輸出結(jié)果如下:

$ go run main.go
waiting...
goroutine done
main exit

主協(xié)程先輸出 waiting...,然后等待 1s,goroutine 輸出 goroutine done,接著主協(xié)程收到退出信號,輸出 main exit 程序執(zhí)行完成。

由于 struct{} 并不占用內(nèi)存,所以實際上 channel 內(nèi)部只需要將計數(shù)器加一即可,不涉及數(shù)據(jù)傳輸,故沒有額外內(nèi)存開銷。

這段代碼還有另一種實現(xiàn):

done := make(chan struct{})

go func() {
 time.Sleep(1 * time.Second) // 執(zhí)行一些操作...
 fmt.Printf("goroutine done\n")
 close(done) // 不需要發(fā)送 struct{}{},直接 close,發(fā)送完成信號
}()

fmt.Printf("waiting...\n")
<-done // 等待完成
fmt.Printf("main exit\n")

這里 goroutine 中都不需要發(fā)送空結(jié)構(gòu)體,直接對 channel 進行 close 就行了,struct{} 在這里起到的作用更像是一個「占位符」的作用。

在 Go 語言 context 源碼中也使用了 struct{} 作為完成信號:

type Context interface {
 Deadline() (deadline time.Time, ok bool)
 // See https://blog.golang.org/pipelines for more examples of how to use
 // a Done channel for cancellation.
 Done() <-chan struct{}
 Err() error
 Value(key any) any
}

context.Context 的 Done 方法返回值即為 chan struct{}

無操作的方法接收器

有時候,我們需要“組合”一些方法,并且這些方法內(nèi)部并不會用到方法接收器,這時就可以使用 struct{} 作為方法接收器。

type NoOp struct{}

func (n NoOp) Perform() {
 fmt.Println("Performing no operation.")
}

方法中代碼并沒有引用 n,如果換成其他類型則會占用內(nèi)存空間。

在實際開發(fā)過程中,有時候代碼寫到一半,為了編譯通過,我們也會寫出這種代碼,先寫出代碼整體框架,再實現(xiàn)內(nèi)部細節(jié)。

作為接口實現(xiàn)

用 struct{} 作為方法接收器,還有另一個用途,就是作為接口的實現(xiàn)。常用于忽略不需要的輸出,和單元測試。啥意思呢?往下看。

我們知道 Go 中有個 io.Writer 接口:

type Writer interface {
 Write(p []byte) (n int, err error)
}

我們還知道,Go 的 io 包中有個 io.Discard 變量,它的主要作用是提供一個“黑洞”設(shè)備,任何寫入到 io.Discard 的數(shù)據(jù)都會被消耗掉而不會有任何效果(這類似于 Unix 中的 /dev/null 設(shè)備)。

io.Discard 定義如下:

// Discard is a [Writer] on which all Write calls succeed
// without doing anything.
var Discard Writer = discard{}

type discard struct{}

func (discard) Write(p []byte) (int, error) {
 return len(p), nil
}

io.Discard 代碼定義極其簡單,它實現(xiàn)了 io.Writer 接口,并且這個 Writer 方法的實現(xiàn)也極其簡單,什么都沒做直接返回。

根據(jù)注釋也能發(fā)現(xiàn),Writer 方法的目的就是啥都不做,所有調(diào)用都會成功,所以可以類比為 Unix 系統(tǒng)中的 /dev/null。

io.Discard 可以用于忽略日志:

// 設(shè)置日志輸出為 `io.Discard`,忽略所有日志
log.SetOutput(io.Discard)
// 這條日志不會在任何地方顯示
log.Println("This log will not be shown anywhere")

此外,我曾寫過一篇文章《在 Go 語言單元測試中如何解決 MySQL 存儲依賴問題》。里面有這樣一段示例代碼:

type UserStore interface {
 Create(user *User) error
 Get(id int) (*User, error)
}

...

type fakeUserStore struct{}

func (f *fakeUserStore) Create(user *store.User) error {
 return nil
}

func (f *fakeUserStore) Get(id int) (*store.User, error) {
 return &store.User{ID: id, Name: "test"}, nil
}

這就是空結(jié)構(gòu)體作為接口實現(xiàn)的另一種用途,編寫測試用 fake object 時非常有用。

即我們定義一個 struct{} 類型 fakeUserStore,然后實現(xiàn) UserStore 接口,這樣在單元測試代碼中,就可以用 fakeUserStore 來替換真實的 UserStore 實例對象,以此來解決對象間的依賴問題。

標(biāo)識符

最后,我們再來介紹一種空結(jié)構(gòu)體比較好玩的用法。

相信很多同學(xué)都直接或間接的使用過 Go 中的 sync.Pool,其定義如下:

type Pool struct {
 noCopy noCopy

 local     unsafe.Pointer
 localSize uintptr

 victim     unsafe.Pointer
 victimSize uintptr

 New func() any
}

其中有一個 noCopy 屬性,其定義如下:

type noCopy struct{}

func (*noCopy) Lock()   {}
func (*noCopy) Unlock() {}

noCopy 即為一個空結(jié)構(gòu)體,其實現(xiàn)也非常簡單,僅定義了兩個空方法。

而這個 noCopy 屬性看似沒什么用,實際上卻有著大作用。這個字段的主要作用是阻止 sync.Pool 被意外復(fù)制。它是一種通過編譯器靜態(tài)分析來防止結(jié)構(gòu)體被不當(dāng)復(fù)制的技巧,以確保正確的使用和內(nèi)存安全性。

可以通過 go vet 命令檢測出 sync.Pool 是否被意外復(fù)制。

在這里,noCopy 屬性對當(dāng)前結(jié)構(gòu)體本身沒有作用,但可以將其作為一個是否允許復(fù)制的標(biāo)識符,有了這個標(biāo)記,就代表結(jié)構(gòu)體不能被復(fù)制,go vet 命令就可以檢查出來。

我們自定義的 struct 也可以通過嵌入 noCopy 屬性來實現(xiàn)禁止復(fù)制:

package main

type noCopy struct{}

func (*noCopy) Lock()   {}
func (*noCopy) Unlock() {}

func main() {
 type A struct {
  noCopy noCopy
  a      string
 }

 type B struct {
  b string
 }

 a := A{a: "a"}
 b := B{b: "b"}

 _ = a
 _ = b
}

使用 go vet 命令檢查是否存在意外的結(jié)構(gòu)體復(fù)制:

$ go vet main.go

# command-line-arguments
# [command-line-arguments]
./main.go:21:6: assignment copies lock value to _: command-line-arguments.A contains command-line-arguments.noCopy

可以發(fā)現(xiàn),go vet 已經(jīng)檢測出我們通過 _ = a 復(fù)制了 noCopy 結(jié)構(gòu)體 A。

總結(jié)

空結(jié)構(gòu)體 struct{} 在 Go 中雖小卻有著巧妙的用途。

從節(jié)省內(nèi)存的角度看,它是表示空概念的理想選擇。從語義上考慮,使用 struct{} 語義更明確,就是不關(guān)注值。

由于內(nèi)存對齊的影響,空結(jié)構(gòu)體字段順序可能影響外層結(jié)構(gòu)體的大小,建議將空結(jié)構(gòu)體放在外層結(jié)構(gòu)體的第一個字段。

無論是作使用空結(jié)構(gòu)體實現(xiàn)集合、信號通知、方法載體還是占位符等,struct{} 都顯示了其獨特的價值。

以上就是詳解Go語言中空結(jié)構(gòu)體的慣用法的詳細內(nèi)容,更多關(guān)于Go語言空結(jié)構(gòu)體的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 一文總結(jié)Go語言切片核心知識點和坑

    一文總結(jié)Go語言切片核心知識點和坑

    都說Go的切片用起來絲滑得很,Java中的List怎么用,切片就怎么用,作為曾經(jīng)的Java選手,因為切片的使用不得當(dāng),喜提缺陷若干,本文就給大家總結(jié)一下Go語言切片核心知識點和坑,需要的朋友可以參考下
    2023-06-06
  • go通過benchmark對代碼進行性能測試詳解

    go通過benchmark對代碼進行性能測試詳解

    在開發(fā)中我們要想編寫高性能的代碼,或者優(yōu)化代碼的性能時,你首先得知道當(dāng)前代碼的性能,在go中可以使用testing包的benchmark來做基準(zhǔn)測試 ,文中有詳細的代碼示例,感興趣的小伙伴可以參考一下
    2023-04-04
  • go語言題解LeetCode1299將每個元素替換為右側(cè)最大元素

    go語言題解LeetCode1299將每個元素替換為右側(cè)最大元素

    這篇文章主要為大家介紹了go語言LeetCode刷題1299將每個元素替換為右側(cè)最大元素示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-01-01
  • 使用Go實現(xiàn)郵箱驗證碼API功能

    使用Go實現(xiàn)郵箱驗證碼API功能

    本文將帶你了解一個項目如何實現(xiàn)一個郵箱驗證接口,即一個可用的發(fā)送郵箱驗證碼API和驗證驗證碼是否正確功能,對Go實現(xiàn)郵箱驗證碼API詳細過程感興趣的朋友一起看看吧
    2024-06-06
  • Go語言七篇入門教程三函數(shù)方法及接口

    Go語言七篇入門教程三函數(shù)方法及接口

    這篇文章主要為大家介紹了Go語言的函數(shù)方法及接口的示例詳解,本文是Go語言七篇入門系列文章,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步
    2021-11-11
  • Golang并發(fā)繞不開的重要組件之Goroutine詳解

    Golang并發(fā)繞不開的重要組件之Goroutine詳解

    Goroutine、Channel、Context、Sync都是Golang并發(fā)編程中的幾個重要組件,這篇文中主要為大家介紹了Goroutine的相關(guān)知識,需要的可以參考一下
    2023-06-06
  • Go中過濾范型集合性能示例詳解

    Go中過濾范型集合性能示例詳解

    這篇文章主要為大家介紹了Go中過濾范型集合性能示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-03-03
  • golang1.21泛型函數(shù)全面講解

    golang1.21泛型函數(shù)全面講解

    在Go編程語言中,泛型一直是一個備受期待的特性,隨著Go?1.21的發(fā)布,本文旨在提供Go?1.21中泛型的詳細探索,闡明它們的優(yōu)點、語法、實現(xiàn)和最佳實踐,希望對大家有所幫助
    2023-09-09
  • go中string、int、float相互轉(zhuǎn)換方式

    go中string、int、float相互轉(zhuǎn)換方式

    這篇文章主要介紹了go中string、int、float相互轉(zhuǎn)換方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-07-07
  • 一文帶你了解如何正確理解和使用Golang中nil

    一文帶你了解如何正確理解和使用Golang中nil

    在?Golang?中,nil?是一個預(yù)定義的標(biāo)識符,在不同的上下文環(huán)境中有不同的含義,但通常表示“無”、“空”或“零值”,本文主要來帶大家了解下nil的正確使用,需要的可以參考下
    2023-12-12

最新評論