Ders 13 / 18
İşaretçiler
nil bir işaretçiden okumak açıkta kalıyor: dil bir şey koymuyor, derleyici de reddetmiyor, sonuç panik. Dört çağrının yalnızca ikisi çağıranın değerini değiştiriyor — adres alınmadan yazılan bir değer, fonksiyon parametresinde de yöntem alıcısında da yalnızca kendi kopyası üzerinde çalışıyor. Sınırlayıcı ölçüm işaretçinin de bir değer olduğunu gösteriyor: bir işaretçiyi yeniden atamak yalnızca yerel kopyayı değiştiriyor, gösterdiği yere yazmak çağıranı değiştiriyor.
İçindekiler
Yapılar dersi bir yapının atandığında tamamen kopyalandığını, bir işlevin kendisine geçirilen kopya üzerinde çalışırsa çağırandaki hiçbir şeyi değiştiremediğini kapattı. Bu ders o sınırın nasıl aşıldığına bakıyor: bir değişkenin adresini almak, çağrılan tarafa kopya yerine o adresi geçirmek anlamına geliyor — ve adres de kendi başına bir değer.
İşaretçi (*T), Go’da bir değişkenin bellekteki yerini tutan bir tiptir. &x yazımı
xin adresini üretir, *p yazımı bir işaretçinin gösterdiği yeri okur ya da oraya yazar.
Bu iki işleç — adres alma ve gösterileni çözme — bu dersin ölçtüğü her şeyin altında
duruyor: bir çağrının çağıranı değiştirip değiştirmemesi, hangisinin ne zaman yazıldığına
bağlı.
Ne Zaman Adres Almak Gerekir
Bu konudaki dört tip aynı soruya dört ayrı yanıt veriyor: bir işleve geçirilen değer değiştirildiğinde, bu değişiklik çağırana adres almadan mı ulaşıyor, yoksa adres şart mı?
// adres.go — dizi, dilim ve yapida adres almanin gerekip gerekmedigi
package main
import "fmt"
type Kayit struct {
Sayi int
}
func degistirDizi(d [3]int) { d[0] = 99 }
func degistirDiziPtr(d *[3]int) { d[0] = 99 }
func yazDilimeElemanla(s []int) { s[0] = 99 }
func yazSozluge(s map[string]int) { s["a"] = 99 }
func degistirYapi(k Kayit) { k.Sayi = 99 }
func degistirYapiPtr(k *Kayit) { k.Sayi = 99 }
func main() {
dizi := [3]int{1, 2, 3}
degistirDizi(dizi)
fmt.Println("dizi, adressiz :", dizi)
dizi2 := [3]int{1, 2, 3}
degistirDiziPtr(&dizi2)
fmt.Println("dizi, adresle :", dizi2)
dilim := []int{1, 2, 3}
yazDilimeElemanla(dilim)
fmt.Println("dilim, adressiz :", dilim)
sozluk := map[string]int{"a": 1}
yazSozluge(sozluk)
fmt.Println("sozluk, adressiz :", sozluk["a"])
yapi := Kayit{Sayi: 1}
degistirYapi(yapi)
fmt.Println("yapi, adressiz :", yapi)
yapi2 := Kayit{Sayi: 1}
degistirYapiPtr(&yapi2)
fmt.Println("yapi, adresle :", yapi2)
}
dizi, adressiz : [1 2 3]
dizi, adresle : [99 2 3]
dilim, adressiz : [99 2 3]
sozluk, adressiz : 99
yapi, adressiz : {1}
yapi, adresle : {99}
Altı satır dört bileşik tipin aynı soruya verdiği farklı yanıtı gösteriyor. Dizi ve yapı,
adres alınmadan geçirildiğinde değişmiyor — ikisi de tamamen kopyalanan değerler, Diziler
ve Dilimler ile Yapılar derslerinde kurulan kuralın aynısı. Dilim ve sözlük ise adres
hiç alınmadan değişebiliyor: yazDilimeElemanla ile yazSozluge yalnızca dilimin ve
sözlüğün kendisini (başlığını) aldı, işaretçi almadı, ama ikisi de çağırandaki veriyi
değiştirdi — çünkü değişen şey başlık değil, başlığın gösterdiği paylaşılan alt dizi ya da
karma tabloydu. Dilim İşlemleri ve Sözlükler derslerinde ölçülen paylaşım, burada adres
almanın neden gereksiz olduğunu açıklıyor: ikisi de kendi içinde zaten bir gösterici
taşıyor.
Dizi ve yapı için adres almanın gerekmesinin nedeni tam tersi: ikisi hiçbir göstericiyi
kendi içinde taşımıyor, taşıdıkları içeriğin kendisi. İşaretçi almak, bu içeriğin
kopyalanmasını engellemenin tek yolu. Dört tip böylece iki gruba ayrılıyor: dizi ve
yapı kendi içeriğini taşıyor ve değiştirilmek istendiğinde adres istiyor; dilim ve
sözlük bir göstericiyi taşıyor ve değiştirilmek istendiğinde kendi kopyası bile
yeterli oluyor — tabii büyümenin kendisi (append, ya da sözlüğün karma tablosunun ilk
kez kurulması) ayrı bir konu, çünkü o durumda değişen şey içerik değil başlığın kendisi.
nil İşaretçiden Okumak Açıkta Kalır
Bir işaretçi ilk değeri verilmeden tanımlandığında nil olur; bu, Sıfır Değerler
dersindeki bağlayıcı dağılımın açıkta kalan üç durumundan biriydi. nil işaretçinin
kendisini okumak (adresin ne olduğuna bakmak) sorun değil, ama gösterdiği yere gitmeye
çalışmak — alanına erişmek — çalışma zamanını durduruyor.
BT14. — Ölçüm iki ayrı soruyu birbirinden ayırıyor: işaretçinin kendisi nil mi
(güvenle sorulabilen bir soru), ve işaretçinin gösterdiği yerdeki alan ne (güvenle
sorulamayan bir soru). İkisi aynı satırda karıştırılmıyor.
// nil.go — nil isaretciden okumak acikta kalir
package main
import "fmt"
type Kayit struct {
Sayi int
}
func panikliyorMu(f func()) (p bool) {
defer func() {
if recover() != nil {
p = true
}
}()
f()
return false
}
func main() {
var isaretci *Kayit
fmt.Println("isaretcinin kendisi nil mi:", isaretci == nil)
okumaPanikledi := panikliyorMu(func() { _ = isaretci.Sayi })
fmt.Println("isaretci.Sayi okumasi : panik=", okumaPanikledi)
}
isaretcinin kendisi nil mi: true isaretci.Sayi okumasi : panik= true
isaretci == nil sorusunun kendisi güvenle yanıtlanıyor, çünkü bu, işaretçinin kendi
değerini okumak — bir adresi, ya da adresin yokluğunu. Ama isaretci.Sayi yazımı önce o
adrese gitmeyi, sonra oradaki Sayi alanını okumayı gerektiriyor; gidilecek adres
yoksa bu adım çalışma zamanında durduruyor. Sözlükler dersinde ölçülen nil sözlüğe yazma
panikleri ile buradaki fark: sözlükte panikleyen yalnızca yazmaktı, okumak tanımlıydı;
işaretçide panikleyen okumanın kendisi, çünkü sözlükte “aranan yer yok” tanımlı bir
sonuca (sıfır değer) bağlanabiliyordu, işaretçide “gidilecek yer yok”un tanımlı bir
karşılığı bulunmuyor.
Bu fark, iki tipin nil durumunu neden farklı yorumladığını gösteriyor. Sözlüğün nil
durumu “henüz hiçbir karma tablom yok ama arama yapabilirim” demekti — arama bulunamayan
bir anahtar için zaten tanımlı bir yanıt (sıfır değer, false) üretiyordu. İşaretçinin
nil durumu ise “gösterdiğim hiçbir yer yok” demek; burada dönülecek tanımlı bir sıfır
değer yok, çünkü işaretçinin okunması gereken şey kendi değeri değil, gösterdiği yerdeki
değer. Sıfır Değerler dersindeki üç açıkta durumun ortak noktası tam olarak bu: tanımlı
bir karşılık, ancak sorulan sorunun yapısı buna izin verdiğinde var oluyor.
Dört Çağrı: Adres mi, Kopya mı
İşlevlere ek olarak, bir tipin yöntemleri de değer alıcı ya da işaretçi alıcı ile
tanımlanabilir; ikisi de nokta sözdizimiyle çağrılıyor ve çağrı yazımından hangisinin
kullanıldığı görünmüyor. Bir yöntem tanımı func (k Kayit) Ad() biçimindeyse alıcı
değerdir, func (k *Kayit) Ad() biçimindeyse alıcı işaretçidir; parantez içindeki bu tek
karakter (*), az önce ölçülen fonksiyon parametresi ayrımının birebir aynısını yöntemler
için de kuruyor. Ölçüm burada bilinçli olarak Yapılar dersindeki tek alanlı Kayit tipini
yeniden kullanıyor; farkı yaratan şeyin alanların sayısı değil, yalnızca alıcının tanımı
olduğunu görünür kılmak için.
// dortcagri.go — dort cagri, ikisi cagiranin degerini degistiriyor
package main
import "fmt"
type Kayit struct {
Sayi int
}
func kopyalaDegistir(k Kayit) { k.Sayi = 99 }
func pointerDegistir(k *Kayit) { k.Sayi = 99 }
func (k Kayit) DegistirV() { k.Sayi = 99 }
func (k *Kayit) DegistirP() { k.Sayi = 99 }
func main() {
k1 := Kayit{Sayi: 1}
kopyalaDegistir(k1)
fmt.Println("deger parametreli fonksiyon :", k1.Sayi)
k2 := Kayit{Sayi: 1}
pointerDegistir(&k2)
fmt.Println("isaretci parametreli fonksiyon:", k2.Sayi)
k3 := Kayit{Sayi: 1}
k3.DegistirV()
fmt.Println("deger alicili yontem :", k3.Sayi)
k4 := Kayit{Sayi: 1}
k4.DegistirP()
fmt.Println("isaretci alicili yontem :", k4.Sayi)
}
deger parametreli fonksiyon : 1 isaretci parametreli fonksiyon: 99 deger alicili yontem : 1 isaretci alicili yontem : 99
Dört çağrının ikisi çağıranın değerini değiştiriyor; ölçümün amacı bu ikiyi bulmak
değil, çağıranı değiştirmenin çağrı yazımından değil alıcı tanımından geldiğini
göstermek. İlk ikisi beklenen kalıbı
tekrarlıyor: değer parametresi kendi kopyasını alır, işaretçi parametresi çağırana geri
ulaşır. Son ikisi aynı kalıbı yöntem çağrısı biçiminde gösteriyor: k3.DegistirV() ile
k4.DegistirP() sözdizimsel olarak neredeyse özdeş — ikisi de değişkenAdı.YöntemAdı()
biçiminde — ama biri değer alıcı, öteki işaretçi alıcı ile tanımlı ve sonuçları farklı.
Çağrıya bakarak hangisinin çağırılacağı bilinmiyor; bunu belirleyen, yöntemin kendi
tanımındaki alıcı tipi.
k4.DegistirP() yazımının kendisi bir işaretçi içermiyor, ama derleyici k4ün
adresini kendisi alıp (&k4).DegistirP()e çeviriyor — çünkü k4 adreslenebilir bir
değişken. Bu otomatik dönüşüm, işaretçi alıcılı bir yöntemi sıradan bir değer üzerinden
çağırmayı olanaklı kılıyor; adres alma adımı yazılmıyor ama gerçekleşiyor.
Bu otomatik dönüşümün bir sınırı var: yalnızca adreslenebilir değerler için işliyor.
Bir değişken adreslenebilir, çünkü bellekte kalıcı bir yeri var; bir işlevin dönüş
değeri gibi geçici bir değer değil. k4.DegistirP() çalıştı çünkü k4 bir değişkendi;
Kayit{Sayi: 1}.DegistirP() gibi bir çağrı, süslü ayraçla kurulan değerin kendisi
adreslenebilir olmadığı için derlenmezdi. Aynı otomatik dönüşüm tersine de işliyor:
işaretçi alıcılı bir tipin nil olmayan bir işaretçisi üzerinden değer alıcılı bir
yöntem çağrıldığında, derleyici işaretçiyi kendiliğinden çözüyor.
Bu iki yönlü otomatik dönüşüm, bir tipin yöntemlerini hep aynı alıcı türüyle tanımlamayı
pratik bir alışkanlık hâline getiriyor: bir tipin bazı yöntemleri değer, bazıları işaretçi
alıcılıysa, çağıranın hangi çağrının çağırdığı değeri değiştirip hangisinin değiştirmediğini
yöntem yöntem ezberlemesi gerekiyor. Yukarıdaki DegistirV/DegistirP çifti bu karışıklığı
bilinçli olarak sergiliyor; gerçek bir tipte ikisinin bir arada bulunması, tam da bu ölçümün
gösterdiği belirsizliği üretir.
Alıcı seçimi yalnızca “çağıranı değiştirmek isteyip istememek” sorusuna bağlı değil. Bir
değer alıcı, yöntem her çağrıldığında o değerin tam bir kopyasını çıkarıyor — Kayit
gibi tek bir tam sayı taşıyan küçük bir tip için bu maliyet önemsiz, ama Yapılar dersindeki
gibi çok alanlı ya da başka yapıları gömen büyük bir tip için her çağrıda bütün alanları
kopyalamak anlamına geliyor. İşaretçi alıcı bu kopyalamayı, taşınan tek şey bir adres
olduğu için, yapının büyüklüğünden bağımsız kılıyor. Çağıranı değiştirmek istemeyen bir
yöntem bile, yalnızca kopyalama maliyetinden kaçınmak için işaretçi alıcı seçebiliyor.
Sınırlayıcı Ölçüm: İşaretçi de Bir Değerdir
İşaretçinin gösterdiği yere yazmakla, işaretçinin kendisini yeniden atamak farklı şeyler. Go’da her şey değerle aktarılır (Programlama Temelleri dersinde kavram olarak kuruldu, burada tekrarlanmıyor) — işaretçi de bir istisna değil, kendisi de bir değer, ve bir işaretçi parametresi de kopyalanıyor.
BT15. — İki işlev de birer tam sayı işaretçisi alıyor ve ikisi de kaynakta neredeyse aynı görünüyor; fark tek bir satırda. Ölçüm bu iki satırı bilinçli olarak yan yana koyuyor ki fark yalnızca yazıdan okunabilsin, çalıştırmaya gerek kalmasın — sonra ikisi ayrı ayrı koşturulup bu okumanın doğru olduğu doğrulanıyor.
// deger.go — isaretcinin kendisi de kopyalanan bir degerdir
package main
import "fmt"
func yenidenAta(p *int) {
yeni := 99
p = ¥i // yalnizca yerel kopyayi degistirir
}
func degerYaz(p *int) {
*p = 99 // isaretcinin gosterdigi yere yazar
}
func main() {
x := 1
yenidenAta(&x)
fmt.Println("isaretciyi yeniden atamak :", x)
y := 1
degerYaz(&y)
fmt.Println("isaretcinin gosterdigine yazmak:", y)
}
isaretciyi yeniden atamak : 1 isaretcinin gosterdigine yazmak: 99
yenidenAta çağrıldığında xin adresi pye kopyalanıyor; işlev içinde p = ¥i
yazıldığında değişen yalnızca pnin kendi kopyası — artık başka bir adresi gösteriyor,
ama çağırandaki x hiç etkilenmiyor, çünkü xin kendisi hiçbir zaman yeninin adresini
almadı. degerYaz ise pnin gösterdiği adrese *p = 99 ile doğrudan yazıyor; bu adres
hâlâ xinki ile aynı olduğu için (bu çağrıda y), yazma çağırana ulaşıyor.
Bu fark, dört çağrı ölçümündeki “işaretçi alınca değişir” kuralının neden doğru olduğunu açıklıyor. İşaretçi parametreli bir fonksiyonun çağıranı değiştirmesi, işaretçinin sihirli bir “referans” olmasından değil, işaretçinin taşıdığı adresin kopyalanıp kopyalanan adrese yazılmasından geliyor. Adres kopyalansa bile gösterdiği yer aynı kaldığı sürece, o yere yapılan yazma her kopyadan görünür — tıpkı dilimin başlığı kopyalandığında alt dizinin paylaşılmaya devam etmesi gibi. İşaretçi, kopyalandığında paylaşımı sürdüren bir değerdir; farkı, dilimin aksine, gösterdiği şeyin bir alt dizi değil tek bir değer olmasıdır.
Bu ders boyunca ölçülen dört tip — dizi, dilim, sözlük, yapı — ve işaretçinin kendisi, aynı tek kuralın beş farklı görünümüdür: Go’da atama, parametre geçişi ve dönüş hep aynı işi yapar, bir değeri kopyalar. Görünürdeki fark, kopyalanan değerin ne taşıdığından geliyor. Dizi ve yapı içeriği taşıyor, kopya içeriği de kopyalıyor. Dilim ve sözlük bir göstericiyi taşıyor, kopya göstericiyi de kopyalıyor ama gösterilen yeri değil — bu yüzden paylaşım sürüyor. İşaretçi de aynı kalıba uyuyor: taşıdığı tek şey bir adres, kopya adresi kopyalıyor, adresin gösterdiği yeri değil. “Referansla aktarım” hiçbir zaman ayrı bir düzenek olarak var olmadı; var olan, bazı değerlerin bir gösterici taşıması ve bazılarının taşımamasıydı.
Özet
- Dizi ve yapı adres alınmadan geçirildiğinde değişmez, çünkü ikisi de tamamen kopyalanır; dilim adres almadan da değişebilir, çünkü kendi içinde zaten bir gösterici taşır.
nilbir işaretçinin kendisini okumak güvenlidir; gösterdiği yere gitmeye çalışmak (bir alana erişmek) çalışma zamanını durdurur — Sıfır Değerler dersindeki açıkta sınıfının bir örneği.- Dört çağrının (değer parametresi, işaretçi parametresi, değer alıcılı yöntem, işaretçi alıcılı yöntem) yalnızca ikisi çağıranın değerini değiştirir; ayrım nokta sözdiziminden değil, alıcının tanımından okunur.
- İşaretçi kendisi de bir değerdir ve kopyalanır: bir işaretçiyi yeniden atamak yalnızca
yerel kopyayı değiştirir, gösterdiği yere yazmak (
*p = ...) çağırana ulaşır.
Sonraki Adım
Bu derste ölçülen tipler — dizi, dilim, yapı, işaretçi — hep sayı ya da adlandırılmış alan taşıyordu; hiçbiri kendi içinde metin taşımıyordu. Metin bunlardan farklı bir bileşik tip: bir dizgi, dışarıdan bakıldığında tek parça görünen ama içeride bir bayt dizisi olan bir değer. Dizgi de değiştirilemez olduğu için burada ölçülen kopya/paylaşım ayrımının dışında kalıyor, ama kendi yazılmayan sorusunu taşıyor. Sıradaki ders şu soruyu soruyor: bir dizginin uzunluğu neyi sayıyor, bir dizgiyi indekslemek neyi döndürüyor, ve bu ikisi “karakter” sözcüğünün sezgisiyle ne zaman ayrışıyor?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.