Ders 09 / 18
Diziler ve Dilimler
Aynı atama satırı dizide kopya, dilimde paylaşım üretiyor: dizinin uzunluğu tipin parçasıdır ve atandığında bütün öğeler kopyalanır, dilimin atanması ise yalnızca başlığı kopyalar ve alt diziyi kaynakla paylaşır. Sınırlayıcı ölçüm dilimin de bir değer olduğunu gösteriyor: paylaşım, append çağrısı kapasiteyi aştığında kesiliyor, çünkü kopyalanan başlık artık ayrı bir alt diziyi gösteriyor.
İçindekiler
Değişken Sayıda Argüman dersi, hiç argüman verilmeyen bir parametrenin nil bir dilim
olduğunu ölçtü — sıfır değerin en görünür sonuçlarından biriydi bu. Bu ders aynı tipin
kendisine döner: dilim, sözlük ve yapı yazılmayanı nasıl karşılıyor, ve daha önce hiç
sorulmamış bir soru — bir değeri bir değişkene atadığında kopya mı oluşuyor, paylaşım mı?
Veri Yapıları kursu diziyi ve listeyi soyut veri tipi olarak kurdu ve gerçekleştirim
seçeneklerini karmaşıklıkla karşılaştırdı; o karşılaştırma burada tekrarlanmıyor. Burada
ölçülen Go’nun kendi iki tipinin — dizi ve dilim — atama sırasında gözlemlenebilir
davranışıdır: aynı = işareti, sağdaki değerin tipine göre iki ayrı şey yapıyor.
Dizi, dilim, sözlük ve yapı bu kursta bileşik tip ortak adıyla anılıyor, çünkü dördü de
başka tiplerden kurulu: bir dizi intlerden, bir yapı adlandırılmış alanlardan. Bileşik
tipler arasındaki fark, atama sırasında ne kadarının kopyalandığında yatıyor — ve bu ders o
farkın en temel iki üyesiyle, dizi ile dilimle, başlıyor.
Dizi: Sabit Uzunluk ve Kopyalanan Atama
Go’da bir dizinin uzunluğu tipin parçasıdır; [4]int ile [3]int iki ayrı tiptir, tıpkı
int ile stringin iki ayrı tip olması gibi. Bu, “dört elemanlık bir dizi” gibi gevşek
bir tanım değil, derleyicinin sözdiziminden okuduğu kesin bir bilgidir.
BT1. — Uzunluğu farklı iki dizi tipi arasında doğrudan atama yapan bir kaynağın
derlenmediği burada bir derleyici çağrısıyla koşturulmuyor; ölçüm bunun yerine %T
biçimlendirme fiiliyle aynı bilgiyi dolaylı gösteriyor — basılan tip adının kendisi
uzunluğu içeriyor.
// bilesik.go — dizi atamasi kopyalar, tip adi uzunlugu tasir
package main
import "fmt"
func main() {
kaynakDizi := [4]int{10, 20, 30, 40}
kopyaDizi := kaynakDizi
kopyaDizi[0] = 99
fmt.Println("tip :", fmt.Sprintf("%T", kaynakDizi))
fmt.Println("kaynakDizi :", kaynakDizi)
fmt.Println("kopyaDizi :", kopyaDizi)
}
tip : [4]int kaynakDizi : [10 20 30 40] kopyaDizi : [99 20 30 40]
Aynı kaynakta kopyaDizi := kaynakDizi satırının hemen üstüne var kopyaDizi [3]int
yazılsaydı derleme burada dururdu; [3]int ile [4]int birbirinin yerine geçmeyen iki tip
olduğu için atama reddedilirdi. Bu ders bunu ayrı bir derleyici çağrısıyla göstermiyor,
çünkü Go’nun Tasarım Amaçları dersi örtük tip dönüşümünün reddini zaten ölçmüştü; buradaki
fark yalnızca uzunluğun da tip kimliğinin bir parçası olduğudur.
Basılan tip [4]inttir, yalnızca int değil — uzunluk tip adının içinde durur.
kopyaDizi := kaynakDizi satırından sonra ilk öğeyi değiştirmek yalnızca kopyaDiziyi
etkiliyor; kaynakDizi değişmeden kalıyor. Dizi atandığında dört tam sayının hepsi yeni bir
değişkene kopyalanıyor — sözdizimi tek bir eşittir işareti olsa da, altında dört ayrı
kopyalama işlemi var.
Bu kopyalama yalnızca değişkenden değişkene atamada değil, bir fonksiyona argüman
geçirilirken de işliyor; parametreler de birer atamadır. Uzunluğu tipin parçası olmasının
bir başka sonucu da buradan çıkıyor: iki dizi, öğe tipleri karşılaştırılabilir olduğu
sürece, == ile doğrudan karşılaştırılabiliyor — çünkü karşılaştırılan şey bir işaretçi
değil, dizinin taşıdığı içeriğin kendisi. Aynı özellik dizinin bir sözlük anahtarı
olarak kullanılabilmesini de açıklıyor; Sözlükler dersinde görüleceği gibi bir sözlüğün
anahtar tipi karşılaştırılabilir olmak zorundadır, ve dizi bu koşulu doğal olarak sağlar.
Aynı kural aşağı doğru da işliyor: bir alanı dilim olan bir yapı da karşılaştırılamaz hâle
gelir, çünkü yapının karşılaştırılabilirliği alanlarının karşılaştırılabilirliğine bağlıdır
— bu, Yapılar dersinde tekrar karşımıza çıkacak.
Dilim: Uzunluk, Kapasite ve Paylaşılan Alt Dizi
Dilim bir dizi değildir; dilimin kendisi üç parçadan oluşan küçük bir yapıdır: alt diziye
giden bir işaretçi, geçerli uzunluk ve kapasite. []int{10, 20, 30, 40} yazımı bu üçünü
bir kerede kurar — arkasında dört öğelik bir alt dizi üretir ve dilim o alt diziyi gösterir.
// paylasim.go — dilim atamasi basligi kopyalar, alt dizi paylasilir
package main
import "fmt"
func main() {
kaynakDilim := []int{10, 20, 30, 40}
payDilim := kaynakDilim
payDilim[0] = 99
fmt.Println("kaynakDilim :", kaynakDilim)
fmt.Println("payDilim :", payDilim)
fmt.Println("uzunluk :", len(kaynakDilim), " kapasite:", cap(kaynakDilim))
}
kaynakDilim : [99 20 30 40] payDilim : [99 20 30 40] uzunluk : 4 kapasite: 4
Aynı satır — payDilim := kaynakDilim — dizideki gibi bir kopya üretmiyor.
payDilim[0] = 99 yazıldığında kaynakDilim de değişiyor, çünkü ikisi aynı alt diziyi gösteriyor; atanan
şey alt dizinin kendisi değil, ona giden işaretçiyi taşıyan küçük başlıktır. uzunluk ve
kapasite burada eşit çıkıyor çünkü dilim değişmezi tam olarak dört öğeyle kuruldu; bir
dilim dilimlemeyle türetildiğinde bu ikisi ayrışabilir — bu ayrım Dilim İşlemleri dersinin
konusu.
Başlığın küçük ve sabit boyutlu olmasının doğrudan bir sonucu var: bir dilimi kopyalamanın maliyeti, alt dizinin kaç öğe tuttuğundan bağımsızdır. On öğelik bir dilimi atamak da on milyon öğelik bir dilimi atamak da aynı üç alanı kopyalar; kopyalanan hiçbir zaman öğelerin kendisi olmuyor. Diziyi atamanın maliyeti ise uzunlukla birlikte büyür, çünkü her öğe tek tek kopyalanıyor.
Bu ayrım iki tipin nerede kullanılacağını da belirliyor. Uzunluğu koddan önce bilinen ve hiç değişmeyecek küçük veriler — dört baytlık bir IPv4 adresi, sabit uzunluklu bir sağlama toplamı — dizi olarak tanımlanır; boyutu tip imzasına yazmak, o boyutun asla değişmeyeceğini derleyiciye söylemenin bir yoludur. Uzunluğu koşum sırasında değişecek her şey — okunan satır sayısı, biriktirilen sonuç kümesi — dilim olarak tutulur, çünkü dilimin uzunluğu tipin değil değerin bir alanıdır.
Dilimin == ile karşılaştırılamaması da aynı köke bağlanıyor. Bir dilim karşılaştırması
“içerik aynı mı” sorusuna yanıt vermeye kalksaydı derleyicinin her karşılaştırmada alt
diziyi baştan sona gezmesi gerekirdi; dil bunun yerine dilimi yalnızca nil ile
karşılaştırmaya izin veriyor, iki dilimi birbiriyle değil. Aynı == işleci dizide
derleniyor, dilimde derlenmiyor — çünkü dizi taşıdığı içeriğin kendisidir, dilim ise ona
giden bir göstericidir.
BT2. — Bu ayrım kaynak bir derleyici çağrısıyla koşturulur; basılan iki satır yalnızca “derlendi” ya da “derlenmedi” sınıfını taşır, derleyicinin hata metni okunmaz.
// karsilastirma.go — dizi == ile karsilastirilir, dilim karsilastirilamaz
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
)
func gecerMi(kaynak string) bool {
dizin, err := os.MkdirTemp("", "sinama")
if err != nil {
return false
}
defer os.RemoveAll(dizin)
os.WriteFile(filepath.Join(dizin, "go.mod"), []byte("module sinama\n\ngo 1.24\n"), 0o644)
os.WriteFile(filepath.Join(dizin, "kod.go"), []byte(kaynak), 0o644)
cmd := exec.Command("go", "build", "./...")
cmd.Dir = dizin
cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local")
return cmd.Run() == nil
}
func main() {
diziEsit := `package sinama
func F() bool {
a := [3]int{1, 2, 3}
b := [3]int{1, 2, 3}
return a == b
}`
dilimEsit := `package sinama
func F() bool {
a := []int{1, 2, 3}
b := []int{1, 2, 3}
return a == b
}`
fmt.Println("iki dizi == ile karsilastirilir :", gecerMi(diziEsit))
fmt.Println("iki dilim == ile karsilastirilir:", gecerMi(dilimEsit))
}
iki dizi == ile karsilastirilir : true iki dilim == ile karsilastirilir: false
Fonksiyona Geçerken Aynı Kural İşler
Parametre geçişi de bir atamadır ve yukarıdaki kural burada da aynen işliyor: dizi
parametresi kendi kopyasını alır, dilim parametresi aynı başlığı — dolayısıyla aynı alt
diziyi — paylaşır. Bu, iki değişken arasındaki atamayla fonksiyon çağrısı arasında Go’nun
hiçbir ayrım yapmadığının bir kanıtı: parametre bağlama da = işaretiyle aynı kuralı
izliyor.
// gecis.go — fonksiyona gecis de bir atamadir
package main
import "fmt"
func diziDegistir(d [4]int) {
d[0] = 999
}
func dilimElemanDegistir(d []int) {
d[0] = 999
}
func dilimYenidenAta(d []int) {
d = append(d, 777, 777, 777, 777, 777) // kapasiteyi asar, yeni alt diziye gecer
}
func main() {
dizi := [4]int{1, 2, 3, 4}
diziDegistir(dizi)
fmt.Println("dizi fonksiyona gecti :", dizi)
dilim := []int{1, 2, 3, 4}
dilimElemanDegistir(dilim)
fmt.Println("dilim eleman yazma :", dilim)
dilim2 := []int{1, 2, 3, 4}
dilimYenidenAta(dilim2)
fmt.Println("dilim yeniden atama :", dilim2)
}
dizi fonksiyona gecti : [1 2 3 4] dilim eleman yazma : [999 2 3 4] dilim yeniden atama : [1 2 3 4]
İlk satır beklendiği gibi: diziDegistir kendi kopyası üzerinde çalışıyor, çağıranın
dizisi değişmiyor. İkinci satır paylaşımı doğruluyor: dilimElemanDegistir parametresinin
gösterdiği alt diziye yazıyor ve bu yazma çağırana geri yansıyor. Üçüncü satır ise ince bir
noktayı gösteriyor: dilimYenidenAta parametreyi append ile büyütüp yeniden atadığında
yalnızca kendi yerel kopyasının başlığı değişiyor; çağırandaki dilim2nin başlığı hâlâ eski
alt diziyi gösteriyor ve fonksiyon döndüğünde o değişiklik kayboluyor. Fonksiyona bir dilim
geçirmek alt diziyi paylaştırır ama başlığı paylaştırmaz — bu, Sınırlayıcı Ölçüm
bölümündeki header ayrılmasının fonksiyon çağrısı bağlamındaki karşılığıdır.
Standart kitaplığın kendi append işlevi de bu yüzden bir sonuç döndürür ve çağıranın onu
geri atamasını bekler; işlev parametre olarak aldığı dilimi büyütüp kendi içinde tutsaydı
bile bu, çağırandaki değişkeni etkilemezdi. dilimYenidenAta işlevinin üçüncü satırdaki
davranışı, appendin neden s = append(s, x) biçiminde çağrıldığının doğrudan kanıtıdır:
sonucu geri atamayı unutan kod, kapasite aşıldığında sessizce eski dilimle kalır.
Sınırlayıcı Ölçüm: Dilim Başlığı Ne Zaman Ayrılır
Dilimin paylaştığı şey alt dizidir, başlığın kendisi değil. Başlık da bir değerdir ve atandığında kopyalanır — üç alanı (işaretçi, uzunluk, kapasite) birlikte. Bu fark, iki değişken aynı alt diziyi göstermeyi bıraktığı anda görünür oluyor.
BT3. — Buradaki kapasite make ile açıkça istenmiyor; []int{1, 2, 3} bir dilim
değişmezidir ve derleyici kapasiteyi üç öğeye eşitliyor. Ölçüm bu kapasiteyi sabit
varsaymıyor, koşumdan cap ile okuyor.
// baslik.go — dilim basligi ne zaman kopyadan ayrilir
package main
import "fmt"
func main() {
temel := []int{1, 2, 3}
pay := temel
pay[0] = 100
fmt.Println("eleman yazmadan sonra temel:", temel, " pay:", pay)
oncekiKapasite := cap(temel)
temel = append(temel, 4)
pay[1] = 200
fmt.Println("append sonrasi temel:", temel, " pay:", pay)
fmt.Println("kapasite (bu kosumda) onceki=", oncekiKapasite, " sonraki=", cap(temel))
}
eleman yazmadan sonra temel: [100 2 3] pay: [100 2 3] append sonrasi temel: [100 2 3 4] pay: [100 200 3] kapasite (bu kosumda) onceki= 3 sonraki= 6
İlk satırda pay[0] = 100 hem temeli hem payi değiştiriyor — ikisi hâlâ aynı alt
diziyi gösteriyor, tıpkı önceki bölümdeki gibi. Sonra temel = append(temel, 4) çağrısı
kapasiteyi aşıyor (üç öğelik alt dizide dördüncü öğeye yer yok); bu durumda dilin çalışma
zamanı yeni bir alt dizi ayırıyor, eski verileri oraya kopyalıyor ve temelin başlığını
o yeni alt diziyi gösterecek şekilde güncelliyor. payin başlığı bu güncellemeden habersiz
kalıyor — hâlâ eski alt diziyi gösteriyor. Bu yüzden pay[1] = 200 yalnızca payi
değiştiriyor; temel üzerinde hiçbir iz bırakmıyor.
Bu geçişin öngörülemezliği asıl riski taşıyor. Kaynak koda bakan biri pay[1] = 200
satırının temeli etkileyip etkilemeyeceğini, yalnızca satırın kendisinden okuyarak
söyleyemez — cevap, o satıra gelene kadar pay ile temelin kaç kez ortak bir append
sınırını aştığına bağlıdır. İki dilim aynı alt diziyi paylaşırken biri büyütülüp diğeri
büyütülmediğinde, ikisi arasındaki paylaşım metinde hiçbir iz bırakmadan sona eriyor.
BT4. — Basılan iki kapasite sayısı (3 ve 6) bu koşumun gözlemidir, dilin sözü değildir.
Go belirtimi appendin kapasiteyi aştığında yeni bir alt dizi ayıracağını garanti eder ama
yeni kapasitenin ne kadar büyüyeceğini belirtmez; büyüme oranı bir gerçekleştirim
ayrıntısıdır ve derleyici sürümleri arasında değişebilir. Kod bu satırı bir sabit gibi
okumamalı — yalnızca “kapasite aşıldı, yeni bir alt dizi geldi” bilgisini okumalı.
Kopyalanan şey burada iyice açık: temel ve pay hiçbir zaman aynı değişken olmadı,
ikisi de baştan beri kendi başlığına sahipti. Paylaşım, iki başlığın aynı alt diziyi
göstermesinden geliyordu; append alt diziyi değiştirdiğinde paylaşım bitiyor. Dilimin
kendisi bir değerdir — tıpkı dizinin bir değer olması gibi — ama taşıdığı değer alt dizinin
verisi değil, ona giden bir göstericidir.
Bunun pratikteki karşılığı şu: bir fonksiyon aldığı dilime append ile öğe ekliyorsa ve bu
büyümenin çağırana da yansımasını istiyorsa, büyümüş dilimi geri döndürmek zorundadır —
tıpkı appendin kendi imzasının yaptığı gibi. Parametre olarak gelen dilim değişkeninin
kendisini büyütmek, yalnızca o yerel değişkeni günceller; çağırandaki başlık bundan
etkilenmez. Dizi ile dilim arasındaki fark böylece tek bir cümleye iniyor: dizi bir
değerdir ve tamamıyla kopyalanır, dilim de bir değerdir ama taşıdığı şey paylaşılan bir alt
diziye giden küçük bir başlıktır — ikisi de “referansla aktarım” değil, farklı içerikli bir
değerle aktarımdır. Bu ayrımın işaretçilerle ne ilgisi olduğu — bir değişkenin adresini
almanın ne zaman gerektiği, ne zaman gereksiz olduğu — İşaretçiler dersinde ayrıca ölçülüyor;
burada kurulan yalnızca dilimin kendi başlığının bir kopyalama biriminden ibaret olduğu.
Özet
- Dizinin uzunluğu tipin parçasıdır (
[4]int); dizi atandığında bütün öğeler kopyalanır ve iki değişken birbirinden bağımsız hâle gelir. - Dilim üç alanlı bir başlıktır: alt diziye giden işaretçi, uzunluk, kapasite. Dilim atandığında bu başlık kopyalanır ama alt dizi kopyalanmaz; iki değişken aynı alt diziyi paylaşır.
- Paylaşılan alt diziye yazılan bir değişiklik her iki değişkenden de görünür, çünkü ikisi de aynı belleği gösterir.
appendçağrısı kapasiteyi aştığında yeni bir alt dizi ayrılır ve yalnızca çağrının sonucunu tutan değişken o yeni alt diziye geçer; paylaşım burada kesilir.- Kapasitenin büyüme oranı (3’ten 6’ya gibi) bir gerçekleştirim ayrıntısıdır; dilin sözü yalnızca “kapasite aşılınca yeni bir alt dizi ayrılır” ifadesidir.
Sonraki Adım
Bu derste paylaşım, yalnızca iki değişken aynı dilimi doğrudan taşıdığında görüldü —
payDilim := kaynakDilim gibi bir atamayla, ya da bir fonksiyona parametre olarak
geçirilerek. Ama bir dilim başka bir dilimden dilimlenerek türetildiğinde de aynı alt
dizi paylaşılır — ve bu durumda paylaşımın ne zaman süreceği, ne zaman keseceği yazılan
koda değil, o türetilen dilimin kapasitesine bağlıdır. Sıradaki ders bu bağımlılığı ölçer:
aynı append çağrısı, aynı kaynak metinle, iki ayrı kapasitede iki ayrı sonuç verir.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.