Ders 10 / 18
Dilim İşlemleri
Aynı append çağrısı, aynı kaynak metinle, iki ayrı kapasitede iki ayrı sonuç veriyor: kapasite sınırının içindeyse paylaşım sürüyor ve türetilen dilime yazmak kaynağı değiştiriyor, kapasite aşıldığında paylaşım kesiliyor ve kaynak dokunulmadan kalıyor. Bu açıklığı kapatan iki yazım var: copy ile alınan bağımsız kopya ve kapasiteyi açıkça sınırlayan üç indeksli dilimleme.
İçindekiler
Önceki ders bir soruyla kapandı: aynı append çağrısı, aynı kaynak metinle, iki ayrı
kapasitede iki ayrı sonuç verir. Diziler ve Dilimler dersi paylaşımı yalnızca doğrudan
atamada gösterdi — payDilim := kaynakDilim gibi bir satırda. Bu ders paylaşımın daha
sık karşılaşılan biçimine bakıyor: bir dilimden dilimlenerek türetilen başka bir
dilime.
Dilimleme, Go kaynağında en sık görülen işlemlerden biridir: bir dosyanın satırlarını gezerken, bir ağ paketinin başlığını gövdesinden ayırırken, bir metnin belirli bir bölümünü alırken hep aynı sözdizim kullanılır. Bu yaygınlık, paylaşımın nasıl işlediğini bilmemenin bedelini de büyütüyor — çünkü her dilimleme satırı, görünmeyen bir bağ kuruyor.
Yazılmayan Dilim Sınırlarının Varsayılanı
Bir dilimleme ifadesi kaynak[baslangic:bitis] biçimindedir ve iki sınırdan biri ya da
ikisi de yazılmayabilir. Sıfır Değerler dersinin ölçtüğü on beş yazılmamışlığın biri tam
olarak buydu: dilim sınırlarının yazılmaması tanımlıdır, başlangıç yazılmadığında
0’a, bitiş yazılmadığında dilimin uzunluğuna karşılık gelir.
// sinirlar.go — dilimleme sinirlarinin yazilmayan uclari
package main
import "fmt"
func main() {
kaynak := []int{10, 20, 30, 40}
fmt.Println("kaynak[:] :", kaynak[:])
fmt.Println("kaynak[1:]:", kaynak[1:])
fmt.Println("kaynak[:2]:", kaynak[:2])
}
kaynak[:] : [10 20 30 40] kaynak[1:]: [20 30 40] kaynak[:2]: [10 20]
Üçü de aynı kaynaktan türedi ve üçü de aynı alt diziyi gösteriyor; hiçbiri kopya
almıyor. kaynak[:] iki sınırı da varsayılana bırakıp bütün dilimi kapsıyor, kaynak[1:]
yalnızca başlangıcı, kaynak[:2] yalnızca bitişi yazıyor. Bu üç satırın ürettiği sayılar
Sıfır Değerler dersindeki bağlayıcı tabloyla aynıdır; orada 15 yazılmamışlığın 8’i
tanımlı sınıfına giriyordu ve dilim sınırları bunlardan biriydi.
Yazılmayan bir sınırla, yanlış yazılmış bir sınır aynı şey değildir. Bitiş boş bırakıldığında dilimin uzunluğuna düşüyordu; bitiş açıkça yazılıp kapasiteyi aştığında ise sonuç tanımlı bir değer değil, bir çalışma zamanı durmasıdır.
// asim.go — yazilmayan sinir tanimli, kapasiteyi asan sinir acikta
package main
import "fmt"
func panikliyorMu(f func()) (p bool) {
defer func() {
if recover() != nil {
p = true
}
}()
f()
return false
}
func main() {
kaynak := []int{10, 20, 30, 40}
gecerliPanikledi := panikliyorMu(func() { _ = kaynak[1:3] })
fmt.Println("kaynak[1:3] (gecerli aralik) panik=", gecerliPanikledi)
asimPanikledi := panikliyorMu(func() { _ = kaynak[1:9] })
fmt.Println("kaynak[1:9] (kapasiteyi asan bitis) panik=", asimPanikledi)
}
kaynak[1:3] (gecerli aralik) panik= false kaynak[1:9] (kapasiteyi asan bitis) panik= true
Boş bırakılan sınır ile fazla yazılan sınır burada iki ayrı sınıfa düşüyor: birincisi dilin kendi doldurduğu tanımlı bir değer, ikincisi derleyicinin yakalayamadığı ve çalışma zamanına bırakılan bir hata. Aradaki fark yazılıp yazılmaması değil, yazılan değerin kapasitenin içinde kalıp kalmamasıdır.
Türetilen Dilime Yazmak Kaynağı Değiştirir
Dilimleme yeni bir alt dizi üretmez; türetilen dilim, kaynağın gösterdiği aynı alt diziyi, farklı bir uzunluk ve başlangıç noktasıyla gösterir. Bu yüzden türetilen dilime yazmak kaynağı da değiştirir.
// turev.go — dilimlenerek turetilen dilime yazmak kaynagi degistirir
package main
import "fmt"
func main() {
kaynak := []int{10, 20, 30, 40}
turev := kaynak[1:3]
turev[0] = 99
fmt.Println("turev :", turev)
fmt.Println("kaynak:", kaynak)
}
turev : [99 30] kaynak: [10 99 30 40]
turev := kaynak[1:3] ikinci ve üçüncü öğeleri (20, 30) kapsayan iki elemanlı bir
dilim üretiyor. turev[0] = 99 yazıldığında değişen şey turevin kendi verisi değil,
kaynak ile paylaşılan alt dizinin ikinci öğesi; bu yüzden kaynak[1] de 99 oluyor. Bu,
Diziler ve Dilimler dersinde doğrudan atamada görülen paylaşımın aynısıdır — yalnızca
dilimin nasıl elde edildiği değişti, kuralın kendisi değişmedi.
Bu paylaşım ikiden fazla dilim arasında da sürüyor, çünkü hepsi aynı alt diziye bağlı kalıyor. İki dilim kaynaktan örtüşerek türetildiğinde, örtüşen konuma yapılan bir yazma ikisinden de görünüyor.
// ortusen.go — kaynaktan ortusen iki dilim, ortak konuma yazma ikisinden de gorunur
package main
import "fmt"
func main() {
kaynak := []int{10, 20, 30, 40, 50}
sol := kaynak[0:3]
sag := kaynak[2:5]
sol[2] = 999 // sol'un son ogesi, kaynagin ucuncu ogesi, sag'in ilk ogesidir
fmt.Println("sol :", sol)
fmt.Println("sag :", sag)
fmt.Println("kaynak:", kaynak)
}
sol : [10 20 999] sag : [999 40 50] kaynak: [10 20 999 40 50]
sol ilk üç öğeyi, sag son üç öğeyi kapsıyor; ikisi üçüncü konumda (kaynak[2])
örtüşüyor. sol[2] = 999 yazıldığında değişen tek bellek konumu, üçünün de — sol,
sag ve kaynakın — paylaştığı o tek konum; bu yüzden değişiklik üçünde de görünüyor.
Kaç değişkenin aynı alt diziyi gösterdiğinin bir önemi yok, önemli olan yalnızca hangi
konumların örtüştüğü.
Aynı Çağrı, İki Kapasite, İki Sonuç
Buraya kadarki paylaşım öngörülebilirdi: bir dilim var oldukça, ondan türetilen her dilim
onunla aynı alt diziyi paylaşıyordu. append bu öngörülebilirliği bozuyor, çünkü
kapasiteyi aşıp aşmadığına göre iki farklı şey yapıyor: aşmıyorsa var olan alt diziye
yazıyor ve paylaşım sürüyor; aşıyorsa yeni bir alt dizi ayırıyor ve paylaşım kesiliyor.
Kaynak metin ikisinde de birebir aynı olabiliyor.
BT5. — İki dilim de aynı üç öğeyle, aynı sırayla dolduruluyor; farkı yaratan tek
değişken kapasitedir. makee ikinci argüman olarak kapasite açıkça veriliyor ki fark
koşumdan koşuma rastlantısal olmasın.
// buyume.go — ayni append cagrisi, iki kapasite, iki sonuc
package main
import "fmt"
func ekle(s []int, deger int) []int {
return append(s, deger)
}
func main() {
bosluklu := make([]int, 3, 5) // kapasitede iki fazladan yer var
copy(bosluklu, []int{1, 2, 3})
dolu := make([]int, 3, 3) // kapasitede fazladan yer yok
copy(dolu, []int{1, 2, 3})
bosluklu2 := ekle(bosluklu, 4)
bosluklu2[0] = 100
fmt.Println("bosluklu (cap=5) sonra bosluklu:", bosluklu, " bosluklu2:", bosluklu2)
dolu2 := ekle(dolu, 4)
dolu2[0] = 100
fmt.Println("dolu (cap=3) sonra dolu:", dolu, " dolu2:", dolu2)
}
bosluklu (cap=5) sonra bosluklu: [100 2 3] bosluklu2: [100 2 3 4] dolu (cap=3) sonra dolu: [1 2 3] dolu2: [100 2 3 4]
ekle işlevi ikisine de aynı kaynak metinle çağrıldı: append(s, deger). bosluklu
kapasitesinde iki fazladan yer taşıyordu; append bu yeri kullandı, alt diziyi büyütmedi
ve döndürdüğü bosluklu2 hâlâ aynı alt diziyi gösteriyor. Bu yüzden bosluklu2[0] = 100
yazıldığında bosluklu da değişti — paylaşım sürdü. dolu ise tam kapasiteydi; append
yeni bir alt dizi ayırdı ve dolu2 o yeni alt diziyi gösterdi. dolu2[0] = 100 bu kez
yalnızca dolu2yi değiştirdi, dolu dokunulmadan kaldı.
Bu, kursun en net açıkta kalan satırıdır: dil appendin kapasiteyi aştığında yeni bir
alt dizi ayıracağını garanti eder, ama sonucun kaynakla paylaşılıp paylaşılmayacağını
kaynak koddan okumak mümkün değildir. Aynı satır — append(s, deger) — bir çağrıda
paylaşımı sürdürüyor, bir çağrıda kesiyor; okuyan biri kapasiteyi bilmeden hangisinin
olacağını söyleyemez.
Bu belirsizliğin pratikte en çok karıştığı yer, bir dilimi büyüten işlevlerdir. Bir işlev
parametre olarak aldığı dilime append ile öğe ekleyip sonucu geri döndürmezse, çağırandaki
değişken kapasiteye bakılmaksızın hiç güncellenmez — kapasite yeterliyse bile.
BT6. — kapın kapasitesi burada bilinçli olarak bol tutuluyor (on öğelik yer, üç öğe
eklenecek); amaç kapasite yetersizliğini olası açıklamalardan çıkarmak. Kapasite dar
tutulsaydı sonucun neden değişmediği iki nedene birden bağlanabilirdi, ölçüm belirsizleşirdi.
// biriktir.go — sonucu dondurmeyen append, cagirani hicbir kosulda guncellemez
package main
import "fmt"
func ekleGuvenliDegil(hedef []int, kaynak []int) {
for _, v := range kaynak {
hedef = append(hedef, v)
}
fmt.Println("islev icinde hedef:", hedef)
}
func main() {
kap := make([]int, 0, 10) // bol kapasiteli, bos bir birikec
ekleGuvenliDegil(kap, []int{1, 2, 3})
fmt.Println("cagirandaki kap :", kap)
}
islev icinde hedef: [1 2 3] cagirandaki kap : []
kapın kapasitesi ondu, üç öğe için fazlasıyla yeterliydi — önceki ölçümün bosluklu
örneğindeki gibi paylaşım sürebilirdi. Ama ekleGuvenliDegil her append sonucunu kendi
yerel hedef değişkenine atıyor ve hiçbir şey geri döndürmüyor; işlev döndüğünde
kapın başlığı hiç değişmemiş oluyor, len(kap) hâlâ sıfır. Kapasitenin yeterli olması
bile bu durumda paylaşımı kurtarmıyor, çünkü sorun alt dizide değil, güncellenen başlığın
hiçbir zaman çağırana geri taşınmamasında.
copy ve Üç İndeksli Dilimleme: Açıklığı Kapatan İki Yazım
Bu öngörülemezlik iki yazımla kapatılabiliyor. Birincisi copy yerleşik işlevi: hedef ve
kaynağı ayrı alt dizilere sahip iki dilim olarak alır, öğeleri tek tek kopyalar ve kopyalanan
öğe sayısını döndürür.
// kopya.go — copy ile bagimsiz kopya, uc indeksli dilimleme ile sinirli kapasite
package main
import "fmt"
func ekle(s []int, deger int) []int {
return append(s, deger)
}
func main() {
kaynak := []int{10, 20, 30, 40}
bagimsiz := make([]int, len(kaynak))
adet := copy(bagimsiz, kaynak)
bagimsiz[0] = 7777
fmt.Println("copy adet:", adet, " bagimsiz:", bagimsiz, " kaynak:", kaynak)
sinirli := kaynak[1:3:3]
fmt.Println("sinirli uzunluk=", len(sinirli), " kapasite=", cap(sinirli))
sinirli2 := ekle(sinirli, 999)
sinirli2[0] = 8888
fmt.Println("sinirli2:", sinirli2, " kaynak:", kaynak)
}
copy adet: 4 bagimsiz: [7777 20 30 40] kaynak: [10 20 30 40] sinirli uzunluk= 2 kapasite= 2 sinirli2: [8888 30 999] kaynak: [10 20 30 40]
copy(bagimsiz, kaynak) çağrısından sonra bagimsiz kendi alt dizisine sahip; ilk öğesini
değiştirmek kaynakı hiç etkilemiyor. BT7. — Dönen adet değeri hiçbir yerde
varsayılmıyor, doğrudan basılıyor; copy hedefin ya da kaynağın uzunluğundan küçüğünü
kopyalar, bu örnekte ikisi de dört olduğu için dördü kopyalıyor.
copynin hedef ve kaynağı bağımsız iki alt dizide olmak zorunda değil; ikisi aynı alt
diziyi gösterse bile doğru çalışıyor. Bu, öğeleri bir dilim içinde kaydırmak için işe
yarıyor.
// kaydirma.go — copy ayni alt dizi icinde ortusen araliklarda da dogru calisir
package main
import "fmt"
func main() {
dizi := []int{1, 2, 3, 4, 5}
kaydirilan := copy(dizi[0:], dizi[1:4]) // son iki ogeyi bir konum sola kaydir
fmt.Println("kaydirilan adet:", kaydirilan, " dizi:", dizi)
}
kaydirilan adet: 3 dizi: [2 3 4 4 5]
dizi[1:4] ile dizi[0:] aynı alt dizinin örtüşen iki dilimidir; copy üçüncü öğeye
kadar olan değerleri bir konum sola kaydırıyor ve son öğe (5) yerinde kalıyor, çünkü
kaydırma yalnızca kopyalanan üç öğeyi kapsadı. Bu davranış rastlantı değil, copynin
kendi sözünün bir parçası: kaynak ve hedef örtüştüğünde bile sonuç, öğelerin sırayla değil
doğru kopyalandığı bir sonuçtur. Aynı işlemi tek tek dizin ilerleterek elle yazan bir
döngü, kaydırma yönüne göre öğelerin üzerine erken yazıp veriyi bozabilirdi; copy bu
riski taşımıyor.
İkinci yazım üç indeksli dilimlemedir: kaynak[1:3:3] yalnızca başlangıç ve bitişi değil,
üçüncü bir değerle kapasiteyi de belirtiyor. sinirlinin kapasitesi burada uzunluğuna
eşitleniyor — fazladan yer bırakılmıyor. Bu yüzden sinirliye yapılan append çağrısı
kesinlikle kapasiteyi aşıyor ve yeni bir alt dizi ayırıyor; sinirli2[0] = 8888
yazıldığında kaynak dokunulmadan kalıyor. Önceki bölümdeki dolu örneğinde kapasitenin
tam olması bir rastlantıydı (üç öğelik değişmez tam üç kapasiteyle kuruldu); üç indeksli
dilimleme bunu rastlantı olmaktan çıkarıp bir yazım kararına dönüştürüyor — kapasiteyi
istenen sınıra kilitliyor.
BT8. — İki kapatma yazımı iki ayrı amaca hizmet ediyor: copy verinin tamamen ayrı bir
kopyasını istiyorsa kullanılır, üç indeksli dilimleme ise kaynakla aynı alt diziyi paylaşmayı
sürdürüp yalnızca sonraki bir appendin paylaşımı kesmesini garanti altına almak
istendiğinde kullanılır. Birincisi paylaşımı baştan bitirir, ikincisi paylaşımı büyümenin
keseceğini önceden belirler.
Bu iki yazımın ortak noktası, ikisinin de kapasiteyi kod okuyucusunun tahminine
bırakmaması. copy kapasiteyi konudan tamamen çıkarıyor — hedefin kendi kapasitesi neyse
o kullanılıyor, kaynağınkiyle hiçbir ilişkisi kalmıyor. Üç indeksli dilimleme ise kapasiteyi
sıfırlamıyor, tam tersine onu daraltıyor: normalde kaynak[1:3] yazımı kalan bütün
kapasiteyi (kaynağın sonuna kadar) devralırdı, üçüncü indeks bu devralmayı keserek dilimi
kendi büyüklüğüne hapsediyor. İkisi de aynı sonuca varıyor — sonraki bir appendin kaynağa
dokunmayacağından emin olmak — ama farklı maliyetlerle: copy bir kopyalama işlemi
gerektirir, üç indeksli dilimleme hiçbir veri taşımaz, yalnızca bir sayıyı sınırlar.
Üç bölümün ölçtüğü şey tek bir cümleye iniyor: dilimleme her zaman paylaşır, append
kapasiteye göre paylaşır ya da paylaşmaz, copy ve üç indeksli dilimleme ise paylaşımı
yazım düzeyinde bir karara dönüştürür. Kaynak metne bakarak “bu satır paylaşıyor mu”
sorusunun yanıtı, üçüncü durumda önceden bilinir; ilk ikisinde bilinmez, yalnızca
koşturularak öğrenilir.
Özet
- Dilimleme ifadesinin yazılmayan sınırları tanımlıdır: başlangıç yazılmazsa
0, bitiş yazılmazsa dilimin uzunluğu kullanılır; bu, Sıfır Değerler dersindeki bağlayıcı 8/4/3 dağılımının tanımlı sınıfına giren bir örnektir. - Dilimlemeyle türetilen bir dilim kaynakla aynı alt diziyi gösterir; türetilen dilime yazmak kaynağı da değiştirir.
appendkapasiteyi aşmadığı sürece var olan alt diziye yazar ve paylaşım sürer; kapasiteyi aştığında yeni bir alt dizi ayırır ve paylaşım kesilir. Aynı kaynak metin iki ayrı kapasitede iki ayrı sonuç verebilir.copyhedefe ait bağımsız bir alt diziye öğeleri tek tek kopyalar ve kopyalanan öğe sayısını döndürür; kaynakla hiçbir paylaşım bırakmaz.- Üç indeksli dilimleme (
kaynak[baslangic:bitis:kapasite]) kapasiteyi açıkça sınırlar ve sonraki birappendin kaynaktan ayrılacağını yazım düzeyinde garanti eder.
Sonraki Adım
Bu iki derste ölçülen paylaşım dizi ve dilim gibi sıralı yapılar arasındaydı; sıradaki konu anahtar-değer eşleşmesine bakıyor. Sözlük de bir başlıktır ve bir alt yapıyı paylaştırır, ama sorduğu soru farklıdır: olmayan bir anahtara erişildiğinde ne olur, ve bu yanıt tek bir değerle mi yoksa iki değerle mi geliyor? Sıradaki ders bu ikisini ayırıyor — ve gezinme sırasının neden hiçbir zaman doğrudan yazdırılmadığını da.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.