Ko'rsatkichlar (pointers)
Ko'rsatkich (pointer) o'zgaruvchining xotiradagi manziliga murojaat qilish imkonini beradi. U bitta qiymatni turli joydan o'zgartirish, katta tuzilmani nusxalamasdan uzatish imkonini beradi.
& va * operatorlari
&x—xo'zgaruvchisining manzilini oladi.*p—pmurojaat qilayotgan qiymatni o'qish yoki o'zgartirish imkonini beradi.*int—intturidagi qiymatga ko'rsatkich turini bildiradi.
package main
import "fmt"
func main() {
x := 10
p := &x
fmt.Println("Qiymat:", x)
fmt.Println("Ko'rsatkich orqali:", *p)
*p = 20
fmt.Println("Yangi qiymat:", x)
}
Natija:
pning turi *int. *p = 20 yangi ko'rsatkich yaratmaydi. Bu amal p murojaat qilayotgan x qiymatini o'zgartiradi.
Manzilni &p bilan chiqarish mumkin, ammo uning aniq qiymati dastur har ishga tushirilganda o'zgarishi mumkin. Dastur
mantig'ini bu manzilga bog'lamaslik kerak.
Ko'rsatkich orqali qiymatni o'zgartirish
Oddiy o'zgaruvchiga yangi qiymat berilganda faqat o'sha o'zgaruvchining qiymati o'zgaradi:
package main
import "fmt"
func main() {
son := 10
nusxa := son
nusxa++
fmt.Println("son:", son)
fmt.Println("nusxa:", nusxa)
}
Natija:
nusxa := son yozilganda sonning qiymati boshqa o'zgaruvchiga nusxalanadi. Shundan keyin nusxa va son ikkita
alohida o'zgaruvchi hisoblanadi. nusxani o'zgartirish songa ta'sir qilmaydi.
Ko'rsatkich bilan esa boshqa o'zgaruvchining o'ziga murojaat qilish mumkin:
Natija:
p := &son yozuvi son o'zgaruvchisining manzilini pga saqlaydi. *p esa shu manzilda turgan qiymatga murojaat
qiladi.
Shuning uchun:
yozuvi:
bilan bir xil ma'noni anglatadi. Ya'ni p murojaat qilayotgan qiymat bittaga oshiriladi.
Bu yerda muhim farq bor:
qiymatni nusxalaydi.
Quyidagi kod esa:
sonning o'ziga murojaat qilish imkonini beradi.
Ko'rsatkich har doim ham kerak emas. Agar ikkita mustaqil qiymat kerak bo'lsa, oddiy nusxa tushunarliroq. Ko'rsatkich bir xil qiymatni boshqa joydan ko'rish yoki o'zgartirish zarur bo'lganda foydali bo'ladi, bu bilan ortiqcha xotira ajratilmaydi.
Ko'rsatkich va qiymatning yashash muddati
Ko'rsatkich mavjud ekan, u murojaat qilayotgan qiymat ham yaroqli bo'lishi kerak.
Masalan:
Natija:
son ichki blok ichida yaratilgan bo'lsa ham, uning manzili p orqali tashqarida ishlatilmoqda.
Go kompilyatori qiymat qancha vaqt saqlanishi kerakligini aniqlaydi. Zarur bo'lsa, qiymatning yashash muddati undan foydalanayotgan ko'rsatkichga mos ravishda boshqariladi.
Dasturchi bunday xotirani C tilidagidek qo'lda bo'shatmaydi. Go xotirani avtomatik boshqaradi.
Bu xususiyat:
kabi kodni xavfsiz ishlatish imkonini beradi.
Ko'rsatkichlarning tengligi
Ikki ko'rsatkich bir xil o'zgaruvchiga murojaat qilsa, ular teng bo'ladi:
package main
import "fmt"
func main() {
x := 5
p1 := &x
p2 := &x
y := 5
p3 := &y
fmt.Println(p1 == p2)
fmt.Println(p1 == p3)
}
Natija:
p1 va p2 bir xil x o'zgaruvchisining manzilini saqlaydi. Shu sabab:
natijasi true.
x va yning qiymatlari bir xil:
lekin ular xotirada ikkita alohida o'zgaruvchi.
Shuning uchun:
natijasi false.
Bu yerda ko'rsatkichlarning tengligi ular murojaat qilayotgan qiymatlarning tengligini emas, balki bir xil joyga murojaat qilayotganini bildiradi.
Ko'rsatkichni mos turdagi boshqa ko'rsatkich yoki nil bilan taqqoslash mumkin.
nil ko'rsatkich
Ko'rsatkichning nol qiymati nil.
Masalan:
Natija:
Quyidagi qism:
pni *int turidagi ko'rsatkich sifatida e'lon qiladi, ammo unga hali hech qanday int o'zgaruvchisining manzili berilmagan.
Shuning uchun uning qiymati:
bo'ladi.
Keyinchalik unga yaroqli manzil berish mumkin:
package main
import "fmt"
func main() {
var p *int
son := 13
p = &son
fmt.Println(p == nil)
fmt.Println(*p)
}
Natija:
nil ko'rsatkich orqali qiymatga murojaat qilish mumkin emas.
Masalan:
dastur ishlayotgan vaqtda panic keltirib chiqaradi.
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x0 pc=0x7ff781d44b96]
Shu sabab ko'rsatkich nil bo'lishi mumkin bo'lgan holatda avval uni tekshirish kerak:
nil ko'rsatkich "hozircha hech qanday qiymatga murojaat qilmayapman" degan holatni ifodalash imkonini beradi.
new orqali ko'rsatkich yaratish
new(T) T turining nol qiymatiga ega yangi o'zgaruvchi yaratib, unga *T turidagi ko'rsatkich beradi.
Masalan:
Natija:
int turining nol qiymati 0 bo'ladi.
Shuning uchun:
yozilganidan keyin:
qiymati 0 bo'ladi.
Keyin:
orqali shu qiymat o'zgartirildi.
newning natijasi oddiy qiymat emas, ko'rsatkich:
bu yerda pning turi:
Ko'pincha oddiy lokal o'zgaruvchi yaratib, uning manzilini olish tushunarliroq:
Bu kod:
bilan o'xshash natija beradi.
new va make bir xil vazifani bajarmaydi.
new(T):
turidagi ko'rsatkich beradi.
make esa faqat:
- slice;
- map;
- channel
uchun ishlatiladi va foydalanishga tayyor qiymat yaratadi.
Goda pointer arifmetikasi yo'q
C va C++ kabi ayrim tillarda ko'rsatkich manzilini arifmetik amallar yordamida o'zgartirish mumkin.
Masalan, Goda odatiy ko'rsatkich bilan quyidagilarni yozib bo'lmaydi:
yoki:
Go odatiy kodda pointer arifmetikasiga ruxsat bermaydi.
Bu cheklov xotira manzillari bilan noto'g'ri ishlash natijasida yuzaga kelishi mumkin bo'lgan ayrim xatolarning oldini oladi.
Past darajadagi maxsus vazifalar uchun Go'da unsafe paketi mavjud. Ammo kundalik dasturlarda undan foydalanish kerak
emas.
Ko'rsatkich ishlatilgani qiymat albatta heap xotirada saqlanadi degani ham emas.
Go kompilyatori escape analysis orqali qiymat qayerda saqlanishini aniqlaydi. Qiymat vaziyatga qarab stackda qolishi
yoki heapga joylashtirilishi mumkin.
Shuning uchun:
"Pointer ishlatsam, kod albatta tezroq ishlaydi"
degan fikr to'g'ri emas.
Ko'rsatkichni tanlashda birinchi navbatda muamo hisobga olinadi. Ishlash tezligi muhim bo'lgan joyda esa taxmin qilish o'rniga o'lchash to'g'ri bo'ladi.
Slice, map va ko'rsatkich
Slice oddiy massivning o'zi emas. U asosiy massivdagi ma'lumotlarga murojaat qiluvchi tavsifni saqlaydi.
Masalan:
package main
import "fmt"
func main() {
sonlar := []int{10, 20, 30}
boshqa := sonlar
boshqa[0] = 100
fmt.Println(sonlar)
fmt.Println(boshqa)
}
Natija:
boshqa := sonlar yozilganda slice tavsifi nusxalanadi. Ammo ikkala slice ham bir xil asosiy massivdagi elementlarga
murojaat qiladi.
Shuning uchun:
o'zgarishi sonlar orqali ham ko'rinadi. Shu sababli slice elementlarini o'zgartirish uchun odatda *[]int kabi
slice ko'rsatkichi kerak emas.
mapda ham shunga o'xshash holat mavjud:
package main
import "fmt"
func main() {
lugat := map[string]int{
"go": 2009,
}
boshqa := lugat
boshqa["python"] = 1991
fmt.Println(lugat)
}
lugat va boshqa bir xil map ma'lumotlariga murojaat qiladi. Shuning uchun boshqa orqali qo'shilgan element lugat
orqali ham ko'rinadi.
Slice bilan append ishlatilganda esa alohida jihat bor:
package main
import "fmt"
func main() {
sonlar := []int{10, 20, 30}
sonlar = append(sonlar, 40)
fmt.Println(sonlar)
}
append natijasida yangi slice qiymati olinadi. Yetarli sig'im bo'lmasa, Go yangi asosiy massiv ham ajratishi mumkin.
Shu sabab slice bilan ishlashda uning yangi holatini:
ko'rinishida saqlab olish odatiy usul hisoblanadi.
slice yoki mapning o'z imkoniyatlari yetarli bo'lgan joyda ularga qo'shimcha ko'rsatkich ishlatish kodni keraksiz
murakkablashtirishi mumkin.
Qachon ko'rsatkich ishlatmaslik kerak?
Ko'rsatkich imkoniyat beradi, ammo har bir qiymat uchun undan foydalanish shart emas.
Quyidagi holatlarda oddiy qiymat yetarli bo'lishi mumkin:
- qiymat kichik va uni nusxalash arzon bo'lsa.
- ikkita mustaqil qiymat kerak bo'lsa.
- asl qiymatni boshqa joydan o'zgartirish zarur bo'lmasa.
nilholati kodga keraksiz murakkablik olib kirsa.slice,mapyokichannelning o'z xususiyati kerakli murojaat semantikasini bersa.
Masalan:
agar x va y mustaqil o'zgarishi kerak bo'lsa, kerakli usul.
Ammo:
yozilganda p orqali xning o'zini o'zgartirish mumkin.
Har bir o'zgaruvchini ko'rsatkichga aylantirish kodni avtomatik ravishda tezlashtirmaydi. Aksincha, bir xil qiymat bir nechta joydan o'zgartirilishi mumkin bo'lsa, ma'lumot qayerda o'zgarganini kuzatish qiyinlashadi. Shuning uchun ko'rsatkichni shunchaki "pointer ishlatish yaxshi" degan fikr bilan emas, uning imkoniyati kerak bo'lganda ishlatish kerak.
Misollar
1. Ikki qiymatni pointer orqali almashtirish
package main
import "fmt"
func main() {
x, y := 10, 20
px := &x
py := &y
*px, *py = *py, *px
fmt.Println(x, y)
}
Natija:
px xga, py esa yga murojaat qiladi.
Quyidagi yozuv:
ko'rsatkichlar murojaat qilayotgan ikkita qiymatni o'zaro almashtiradi.
Ko'rsatkichlarning o'zi almashtirilmaydi. O'zgarayotgan narsa ular murojaat qilayotgan x va y qiymatlaridir.
2. Ikki natijani ko'rsatkichlar orqali saqlash
package main
import "fmt"
func main() {
a, b := 17, 5
var bolinma int
var qoldiq int
pBolinma := &bolinma
pQoldiq := &qoldiq
*pBolinma = a / b
*pQoldiq = a % b
fmt.Println(bolinma, qoldiq)
}
Natija:
pBolinma va pQoldiq ikkita alohida o'zgaruvchiga murojaat qiladi.
bolinmaning qiymatini o'zgartiradi.
esa qoldiqning qiymatini o'zgartiradi.
Bu misol bir nechta ko'rsatkich turli qiymatlarga murojaat qilib, ularni alohida o'zgartira olishini ko'rsatadi.
3. Ixtiyoriy qiymatni nil bilan ifodalash
Ba'zan 0 qiymati va "qiymat umuman belgilanmagan" holatini bir-biridan ajratish kerak bo'lishi mumkin.
package main
import "fmt"
func main() {
var chegirma *int
if chegirma == nil {
fmt.Println("Chegirma belgilanmagan")
}
nol := 0
chegirma = &nol
if chegirma != nil {
fmt.Println("Chegirma:", *chegirma)
}
}
Natija:
Birinchi holatda:
ya'ni chegirma qiymati umuman belgilanmagan.
Ikkinchi holatda esa ko'rsatkich mavjud va u 0 qiymatiga murojaat qilmoqda.
Demak:
va:
bir xil holat emas.
Ko'rsatkich "qiymat yo'q" va "qiymat bor, uning qiymati nol" holatlarini bir-biridan ajratish imkonini beradi.
4. new bilan turli qiymatlar yaratish
package main
import "fmt"
func main() {
faol := new(bool)
nom := new(string)
fmt.Println(*faol)
fmt.Println(*nom)
*faol = true
*nom = "Go"
fmt.Println(*faol, *nom)
}
Dastlab:
false qiymatiga ega bool uchun ko'rsatkich yaratadi.
esa bo'sh satr qiymatiga ega string uchun ko'rsatkich yaratadi.
Keyin:
orqali ko'rsatkichlar murojaat qilayotgan qiymatlar o'zgartiriladi.
5. Pointer nusxasi bir xil qiymatga qaraydi
package main
import "fmt"
func main() {
son := 5
birinchi := &son
ikkinchi := birinchi
*ikkinchi = 25
fmt.Println(*birinchi, son)
}
Natija:
Quyidagi yozuv:
sonning qiymatini nusxalamaydi.
Bu yerda ko'rsatkichning qiymati, ya'ni manzil nusxalanadi.
Natijada:
va:
bir xil son o'zgaruvchisiga murojaat qiladi.
Shuning uchun:
yozilgandan keyin *birinchi orqali ham 25 ko'rinadi.
6. Pointerlar bir obyektga qarashini tekshirish
package main
import "fmt"
func main() {
son := 13
a := &son
b := a
c := new(int)
*c = 13
fmt.Println(a == b)
fmt.Println(a == c)
}
Natija:
a va b bir xil son o'zgaruvchisiga murojaat qiladi.
Shuning uchun:
natijasi true.
c ham 13 qiymatiga murojaat qiladi, ammo bu boshqa o'zgaruvchi.
Shuning uchun:
natijasi false.
Pointerlarning tengligi ular murojaat qilayotgan qiymatlar tengligini emas, bir xil obyektga murojaat qilayotganini tekshiradi.
7. Slice elementiga pointer olish
package main
import "fmt"
func main() {
sonlar := []int{10, 20, 30}
p := &sonlar[1]
*p = 99
fmt.Println(sonlar)
}
Natija:
Quyidagi yozuv:
slice ichidagi ikkinchi elementning manzilini oladi.
Keyin:
shu elementning asl qiymatini o'zgartiradi.
Shuning uchun o'zgarish sonlar slice'ida ham ko'rinadi.
Bu yerda muhim jihat bor. Keyinchalik append ishlatilganda slice uchun yangi asosiy massiv ajratilishi mumkin:
Agar yangi massiv yaratilsa, sonlar yangi massivga murojaat qila boshlaydi. Oldin olingan p esa avvalgi massivdagi
elementga murojaat qilishda davom etishi mumkin.
Shuning uchun slice elementlariga uzoq muddat saqlanadigan ko'rsatkich olishda uning keyinchalik qanday o'zgarishini ham hisobga olish kerak.
8. Bog'langan ro'yxat tuguni
Ko'rsatkichlar yordamida bir obyekt boshqasiga murojaat qiladigan ma'lumot tuzilmalarini yaratish mumkin.
Masalan:
package main
import "fmt"
type Tugun struct {
Qiymat int
Keyingi *Tugun
}
func main() {
uchinchi := &Tugun{
Qiymat: 30,
}
ikkinchi := &Tugun{
Qiymat: 20,
Keyingi: uchinchi,
}
birinchi := &Tugun{
Qiymat: 10,
Keyingi: ikkinchi,
}
fmt.Println(birinchi.Qiymat)
fmt.Println(birinchi.Keyingi.Qiymat)
fmt.Println(birinchi.Keyingi.Keyingi.Qiymat)
fmt.Println(birinchi.Keyingi.Keyingi.Keyingi)
}
Natija:
Har bir Tugun o'z qiymatidan tashqari keyingi tugunga ko'rsatkich saqlaydi:
birinchi.Keyingi ikkinchi tugunga murojaat qiladi.
ikkinchi.Keyingi esa uchinchi tugunga murojaat qiladi.
uchinchi tugunda Keyingi qiymati berilmagan. *Tugun ko'rsatkichining nol qiymati nil bo'lgani uchun:
qiymati nil.
Shu tarzda bir obyekt boshqasiga ko'rsatkich orqali ulanadi. Bog'langan ro'yxat kabi ma'lumot tuzilmalarining asosiy g'oyalaridan biri ham shundan iborat.