İçeriğe geç
academia.sh

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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat