CodeGym /Kurslar /Go SELF /Təkrar — göstəricilər və dəyər semantikası

Təkrar — göstəricilər və dəyər semantikası

Go SELF
Səviyyə, Dərs
Mövcuddur

1. Əsas mantra: “Go-da hər şey dəyərlə ötürülür”

Əgər nə vaxtsa Go-da göstəricilərin “bəzən işləyən, bəzən də sancan sehr” olduğunu hiss etmisinizsə, problem adətən göstəricilərdə deyil. Problem gözləntilərdədir: bizə elə gəlir ki, “funksiyaya ötürdüm — orada dəyişdi”. Go-da əsas qayda başqadır: funksiyaya həmişə arqumentin surəti gəlir. Və yalnız sonra biz dəqiqləşdiririk ki, dəqiq nəyi köçürmüşük: rəqəmi, slice başlığını, map başlığını, yoxsa ünvanı.

Bunu yadda saxlamaq üçün ən rahat yol sadə sxemdir:

flowchart TD
    A["f(x) çağırışı"] --> B["x dəyəri köçürülür"]
    B --> C["Funksiya surət ilə işləyir"]
    C --> D["Kənardan yalnız surətin işarə etdiyi şey dəyişir (əgər işarə edirsə)"]

Bu qayda fundamentdir. Göstəricilər isə sadəcə “surətin çölə işarə etməsi” üçün üsullardan biridir.

2. Go-da göstərici: ünvan & və dereferens *

“Göstərici” deyəndə dərhal “qorxulu C və yaddaş sızması” təsəvvür etmək istəyirik. Amma Go-da göstərici daha gündəlik bir şeydir: başqa bir dəyərin ünvanını saxlayan dəyər. Siz ünvanı & operatoru ilə alırsınız, ünvan üzrə dəyərə çatmaq üçün isə * (dereferens) istifadə edirsiniz. Bu, &x formasında Scan zamanı artıq gördüyünüz eyni operator cütüdür, sadəcə indi nə etdiyimizi şüurlu şəkildə anlayırıq.

Elanlarda və ifadələrdə sintaksis qəsdən oxşardır: p *int — “int-ə göstərici”, *p isə — “p-nin işarə etdiyi dəyər”.

Mini nümunə: niyə “sadəcə int ötürmək” dəyişiklik etmir

Burada “arqumentin surəti” qaydası görünür:

package main

import "fmt"

func inc(n int) {
	n++
}

func main() {
	x := 10
	inc(x)
	fmt.Println(x) // 10
}

inc x-in surətini artırdı. Orijinal olduğu kimi qaldı.

Mini nümunə: *int vasitəsilə orijinalı necə dəyişirik

İndi funksiyaya rəqəm yox, ünvan ötürək:

package main

import "fmt"

func inc(p *int) {
	*p++
}

func main() {
	x := 10
	inc(&x)
	fmt.Println(x) // 11
}

Biz göstəricinin surətini ötürdük, amma həmin göstərici ilkin x-ə işarə edir, buna görə *p vasitəsilə edilən dəyişiklik kənardan görünür.

3. Göstəricilər həqiqətən harada faydalıdır

Başlanğıc səviyyədə olan adam çox vaxt “hər şeyi göstərici ilə etmək” istəyir ki, “mütləq işləsin”. Bu normaldır: beyin universal alət axtarır. Problem ondadır ki, göstəricilər nöqtəvi alətdir. Onlar o zaman yaxşıdır ki, siz birbaşa nə üçün ünvanın lazım olduğunu formalaşdıra bilirsiniz, dəyərin yox.

Funksiyadan kənarda dəyişən dəyişmək lazımdır

Bu ən dürüst haldır: dəyişən var və funksiya onu yeniləməlidir. Məsələn — sayğac, pul qalığı, “növbəti ID”, statistik toplanma.

Bir az sonra bunu tədris mini tətbiqimizdə (task-list) nextID vasitəsilə tətbiq edəcəyik.

“Dəyər yoxdur” demək lazımdır

Go-da ayrıca “nullable int” tipi yoxdur (dilin əsas hissəsində). Buna görə bəzən belə edirlər: *int əvəzinə int. Onda nil “dəyər yoxdur” deməkdir. Bu xüsusilə 0 valid rəqəm olduqda və siz “0” ilə “təyin edilməyib”i qarışdırmaq istəmədikdə çox rahatdır.

Kiçik nümunə:

package main

import "fmt"

func printLimit(limit *int) {
	if limit == nil {
		fmt.Println("limit: not set") // limit: not set
		return
	}
	fmt.Println("limit:", *limit)
}

func main() {
	printLimit(nil)

	x := 5
	printLimit(&x) // limit: 5
}

Burada başa düşmək vacibdir ki, nil müqavilənin bir hissəsidir. Əgər müqavilə “nil ola bilər” deyirsə, deməli funksiyanın daxilində yoxlama olmalıdır.

4. Göstəricilər lazım olmayan yer: slice və map artıq “istinad edir”

İndi çoxlarını çaşdıran vacib məqam: “amma mən göstərici ötürmədim, yenə də dəyişdi”. Bu, adətən slice və map ilə baş verir.

Slice massiv deyil, “başlıq + data”dır

Slice daxilində data massivinə göstərici, uzunluq və tutum saxlanır. İki fərqli slice eyni massivə baxa bilər.

Bu, eyni anda iki şeyi deməkdir:

Birincisi: əgər funksiya slice-in element-ini dəyişirsə, bu kənardan görünür, çünki element ümumi data massivində yerləşir.

İkincisi: əgər funksiya slice-in uzunluğunu dəyişirsə (append, Delete və s. ilə), yeni slice qaytarmalıdır, çünki başlıq dəyişir (len/cap və bəzən göstərici). Məhz buna görə append dəyər qaytarır.

Mini nümunə: slice elementinin dəyişməsi göstəricisiz də görünür

package main

import "fmt"

func setFirst(s []int) {
	s[0] = 99
}

func main() {
	a := []int{1, 2, 3}
	setFirst(a)
	fmt.Println(a) // [99 2 3]
}

Biz başlığın surətini ötürdük, amma başlıq eyni data massivinə işarə edir.

Mini nümunə: append “slice-i dəyişir”, ona görə nəticəni saxlamaq lazımdır

package main

import "fmt"

func addOne(s []int) []int {
	return append(s, 10)
}

func main() {
	a := []int{1, 2}
	a = addOne(a)
	fmt.Println(a) // [1 2 10]
}

Əgər nəticəni saxlamasanız, köhnə uzunluqla və ya daha pis halda köhnə başlıqla yaşamağa davam edə bilərsiniz.

Bu fikir slices.Delete kimi funksiyalara da aiddir: onlar yeni slice qaytarır və nəticəni görməzdən gəlmək səhvdir.

5. new, make və nil: “yoxluğu” necə yaratmaq və saxlamaq olar

Koda new, make və ya nil daxil olanda, başlanğıcda çox vaxt “hə, sehr başladı” rejimi işə düşür. Əslində fərq çox praktikdir: bunlar fərqli şeylər və fərqli müqavilələr üçündür.

new və make: qısa və mənaca

new(T) T tipində dəyər üçün yaddaş ayırır, ora zero value qoyur və *T qaytarır.

make xüsusi “daxili konteynerlər” üçün işə hazır dəyər yaradır: []T, map[K]V, chan T. (Kanallar indilik lazım deyil, amma qaydanı yadda saxlayaq.)

Fərqi birbaşa kodla görmək rahatdır:

package main

import "fmt"

func main() {
	p := new(int)          // *int, daxilində 0
	s := make([]int, 0, 2) // []int, append üçün hazır
	m := make(map[string]int)

	fmt.Println(*p)        // 0
	fmt.Println(len(s))    // 0
	fmt.Println(len(m))    // 0
}

Qısa “sağlamlıq yoxlaması” istəyirsinizsə: new göstərici qaytarır, make — YOX.

nil: bir dəyər, amma müxtəlif rollarda

nil ilə bağlı hekayə belədir: söz birdir, amma mənalar bir neçədir və buna görə insanlar səhv tutur. nil “göstərici yoxdur”, “map yoxdur”, “slice yoxdur” mənasında ola bilər. Amma davranış fərqlidir — və bu normaldır: müqavilələr fərqlidir.

nil göstərici: dereferens etmək olmaz

Əgər sizdə var p *int = nil varsa, onda *p panikadır. Burada fəlsəfi heç nə yoxdur: ünvan yoxdur, oxumağa heç nə yoxdur.

nil slice: adətən təhlükəsizdir

nil slice len==0 mənasında boş kimi davranır və append işləyir. O sadəcə data massivi yaradır və yeni slice başlığı qaytarır.

nil map: oxumaq olar, yazmaq olmaz

nil map-dan oxu zero value verir, onun üzərində range sadəcə icra olunmur. Amma yazı (m["x"]=1]) panika yaradacaq, çünki map-in daxili strukturu yoxdur.

Həyatda bu, çox sadə qaydaya çevrilir: yazı üçün map-i make ilə ilkinləşdirmək lazımdır.

6. Göstərici tələləri: “ünvan var, amma doğru deyil”

Göstərici ilə bağlı səhvlər nadir hallarda “*p sintaksisini başa düşmədim” kimi görünür. Daha çox belə görünür: “niyə bütün göstəricilərim eynidir?” və ya “niyə göstərici ilə dəyişdim, amma slice-də dəyişmədi?”. Bu artıq sintaksis deyil, ömür müddəti və dəqiq nəyin ünvanını aldığınız məsələsidir.

Dövrədə yenidən istifadə olunan bir dəyişənin ünvanı

İndi Go-da (1.25) for , v := range ... üçün ən məşhur hal düzəldilib (orada dəyişən adətən hər iterasiya üçün ayrı olur), amma dəyişən dövrədən kənarda elan edilibsə, tələ hələ də asanlıqla yaranır.

package main

import "fmt"

func main() {
	nums := []int{10, 20, 30}

	var v int
	ptrs := make([]*int, 0, len(nums))

	for _, n := range nums {
		v = n
		ptrs = append(ptrs, &v) // eyni ünvan
	}

	fmt.Println(*ptrs[0], *ptrs[1], *ptrs[2]) // 30 30 30
}

Problem append-də deyil, range-də deyil, &v-nin həmişə eyni olmasındadır.

Əgər sizə həqiqətən slice elementlərinin ünvanları lazımdırsa, çox vaxt daha düzgün yol &nums[i] götürməkdir:

package main

import "fmt"

func main() {
	nums := []int{10, 20, 30}
	ptrs := make([]*int, 0, len(nums))

	for i := range nums {
		ptrs = append(ptrs, &nums[i])
	}

	fmt.Println(*ptrs[0], *ptrs[1], *ptrs[2]) // 10 20 30
}

Slice elementi üçün göstərici və append: “yeni massivə köçdülər”

Bu çox Go-vari haldır: append data massivini böyüdə və elementləri yeni yerə köçürə bilər. Onda elementlərə köhnə göstəricilər köhnə massivə işarə edəcək. Slice artıq yeni massivə baxır, sizin göstərici isə köhnəyə. Heç kim günahkar deyil, sadəcə siz “köçdən əvvəlki” ünvana tutmusunuz.

Bunu köçün mütləq baş verməsi üçün belə göstərək: cap=1 edək.

package main

import "fmt"

func main() {
	s := make([]int, 1, 1)
	s[0] = 10

	p := &s[0]
	s = append(s, 20) // cap çatmır -> yeni massiv

	*p = 99
	fmt.Println(s[0], *p) // 10 99
}

Bu nə “random”dur, nə də “Go xarabdır”. Bu, slice quruluşunun nəticəsidir: slice başlığı data massivinə işarə edir və yeni yerə işarə etməyə başlaya bilər.

Niyə &m[k] olmaz

map-in elementinin ünvanını almaq olmaz: &m[k] kompilyasiya olunmur. Səbəb sadə və praktikdir: map elementləri struktur daxilində böyümə/yenidən hash zamanı yerlərini dəyişə bilər və dil “elementin sabit ünvanı”na zəmanət vermir.

Dəyəri map-də yeniləmək istəyirsinizsə, bunu təyinetmə ilə edin:

package main

import "fmt"

func main() {
	m := map[string]int{"a": 1}

	x := m["a"]
	x++
	m["a"] = x

	fmt.Println(m["a"]) // 2
}

7. Tədris mini tətbiqi nümunəsində “dəyər semantikası”

Bütün bunların yalnız nəzəriyyə olaraq qalmaması üçün gəlin, giriş, sətirlər, səhvlər, slice-lər və map-ləri məşq etmək üçün artıq bir neçə dəfə istifadə etdiyimiz tədris task-list-i üçün kiçik loqika hissəsi yığaq. İndi fokus CLI və komandalar üzərində yox, dövlətin funksiyalara necə ötürülməsi və qarışdırılmaması üzərində olacaq.

Tapşırıqları strukturlar olmadan saxlayacağıq (strukturlara kursun başqa hissəsində çatacağıq), ona görə minimal dəst götürək:

  • titles map[int]string — ID üzrə tapşırıq başlığı
  • done map[int]bool — ID üzrə icra statusu
  • nextID int — növbəti veriləcək ID

Niyə map-i “dəyər kimi” ötürürük və *map etmirik

Map özü “istinad edən şey” kimi davranır: siz başlığın surətini ötürürsünüz, amma o, eyni daxili cədvələ işarə edir. Buna görə funksiya map-ə rahatlıqla yaza bilər və bu kənardan görünəcək — heç bir *map olmadan.

Budur tapşırıq əlavə edən funksiya: göstərici bizə yalnız nextID üçün lazımdır, çünki bu sadə rəqəmdir və biz onu “çöldə” artırmaq istəyirik.

package main

import "fmt"

func addTask(nextID *int, titles map[int]string, done map[int]bool, title string) int {
	id := *nextID
	*nextID++

	titles[id] = title
	done[id] = false
	return id
}

func main() {
	titles := make(map[int]string)
	done := make(map[int]bool)
	nextID := 1

	id := addTask(&nextID, titles, done, "buy milk")
	fmt.Println(id, titles[id], done[id]) // 1 buy milk false
}

Diqqət edin: map-i göstəricisiz ötürürük, nextID-ni isə göstərici ilə. Bu, məhz “dəyər semantikası”nın işdə necə göründüyüdür: kənar rəqəmi dəyişmək lazım olanda — ünvan; ümumi strukturu istinadla dəyişəndə — dəyər kifayətdir.

Slice dövlət kimi: nə vaxt yeni slice qaytarmaq lazımdır

İndi təsəvvür edək ki, tapşırıqları ardıcıllıqla çap etmək üçün əlavə olunma sırası üzrə ID siyahımız var: order []int. ID əlavə edərkən append edirik. Və burada yenə slice başlığı qaydası üzə çıxır: uzunluq dəyişirsə, funksiya yeni slice qaytarmalıdır.

package main

import "fmt"

func addToOrder(order []int, id int) []int {
	return append(order, id)
}

func main() {
	order := make([]int, 0, 2)

	order = addToOrder(order, 1)
	order = addToOrder(order, 2)

	fmt.Println(order) // [1 2]
}

Əgər order = ... hissəsini unutsanız, proqram formal olaraq bəzi hallarda “işləyir” kimi görünə bilər, sonra isə qəfil qəribə davranmağa başlaya bilər. Məhz buna görə Go-da slice-lər ətrafında “append-in nəticəsini həmişə saxlayın” mövzusunda bu qədər danışılır. Bu, boşboğazlıq yox, real səhv qorumasıdır.

Yeri gəlmişkən, eyni şey slices paketindəki Delete kimi funksiyalara da aiddir: onlar yeni slice qaytarır və nəticəni görməzlikdən gəlmək səhvdir.

“Slice içində gözlənilməz nil” — yanlış istifadənin nəticəsi

Başqasının kodunu oxuyarkən faydalı olan başqa bir maraqlı məqam: slice-in uzunluğunu dəyişən funksiyalardan səhv istifadə etsəniz, datada “gözlənilməz sıfırlar” əldə edə bilərsiniz (göstəricilər üçün nil). Go-nun müasir versiyalarında slice üçün standart funksiyalar “quyruğu sıfırlamağa” (clear tail) aktiv şəkildə çalışır və testlər əvvəllər təsadüfən keçdiyi yerlərdə indi uğursuz ola bilər.

Hətta hələ hər gün slices.Delete istifadə etməsəniz də, bu ümumi ideyanı vurğulayır: uzunluğu dəyişən əməliyyatlardan sonra köhnə slice-i “köhnəlmiş” saymaq daha yaxşıdır.

8. Tipik səhvlər

Səhv №1: int ötürməyin kənarda dəyəri dəyişməsinə imkan verəcəyini düşünmək.
Varsayılan olaraq funksiyaya arqumentlər dəyərlə ötürülür — surət yaradılır. Əgər funksiyanın daxilində int tipli parametri dəyişirsinizsə, kənarda heç nə dəyişməyəcək. Əgər məna etibarilə çağıran kodun dəyişənini dəyişmək lazımdırsa — *int ötürün. Əgər lazım deyilsə — int saxlayın və müqaviləni mürəkkəbləşdirməyin.

Səhv №2: “hər şeyi göstərici ilə müalicə etmək”, o cümlədən slice və map-i.
Slice və map artıq daxili dataya istinad kimi davranır: surət çıxaranda siz bütün elementləri yox, başlığı köçürürsünüz. Slice elementlərinin dəyişməsi göstəricisiz də görünür, amma uzunluğun dəyişməsi yeni slice qaytarmağı tələb edir (başlıq dəyişir). Başınızda davamlı olaraq slice modelini pointer + len + cap kimi saxlayın.

Səhv №3: dövrədə eyni dəyişən üçün göstərici toplamaq.
Əgər dəyişən dövrədən kənarda elan olunubsa və siz hər iterasiyada onun ünvanını götürürsünüzsə, bir ünvanın üstündə yığılan göstəricilər dəsti alacaqsınız. Kod kompilyasiya olunur, amma sonda bütün dəyərlər eyni olur. Həll — real elementin ünvanını götürmək (&nums[i]) və ya hər iterasiyada yeni dəyişən yaratmaqdır.

Səhv №4: slice elementinə göstərici saxlamaq və sonra append etmək.
Əgər append tutumu artırsa və massiv yeni yaddaşa köçürsə, köhnə göstərici “köhnə” massivə işarə etməyə davam edəcək. Kiçik nümunələrdə bu görünməyə bilər (əgər cap kifayət edirsə), amma real kodda çətin tutulan səhvlərə gətirəcək. Qayda sadədir: slice elementlərinə olan göstəriciləri cari massivin etibarlı ömründən uzun saxlamayın.

Səhv №5: nil göstəricini dereferens etmək.
nil-in dereferensi zəmanətli panikadır. Burada “bəlkə alınar” yoxdur: ünvan yoxdursa, oxumağa və yazmağa yer yoxdur. Göstəricidən fərqli olaraq, nil slice çox vaxt təhlükəsizdir, nil map isə yalnız oxuma üçün təhlükəsizdir. Map-ə yazmaq üçün onu əvvəlcədən make ilə ilkinləşdirmək lazımdır.

1
Tapşırıq
Go SELF, səviyyə, dərs
Bağlanıb
Yığılan məbləğ
Yığılan məbləğ
1
Tapşırıq
Go SELF, səviyyə, dərs
Bağlanıb
Kupon yoxdursa
Kupon yoxdursa
1
Tapşırıq
Go SELF, səviyyə, dərs
Bağlanıb
Müsbət siqnallar
Müsbət siqnallar
1
Tapşırıq
Go SELF, səviyyə, dərs
Bağlanıb
Yerində vurma
Yerində vurma
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION