Ders 08 / 18
Değişken Sayıda Argüman
Hiç argüman verilmeyen bir değişken sayıda argüman parametresi nil bir dilim oluyor, tek tek yazılan argümanlar taze bir dilim üretiyor, mevcut bir dilimin ... ile açılması ise kopyalamadan aynı alt diziyi paylaşıyor — çağrılan tarafın yaptığı değişiklik çağırana yansıyor. Sınırlayıcı ölçüm: nil dilim ile boş dilim uzunlukça aynı ama == nil'de ayrışıyor, ve bu fark JSON karşılığında görünür oluyor.
İçindekiler
Önceki ders, bir dönüş değerinin bir ada bağlandığında kullanılmak zorunda olduğunu ölçtü. Bu ders aynı soruyu argüman tarafına taşıyor: bir işlev kaç argüman alacağını önceden bilmediğinde, o parametre hiç argüman verilmediğinde ne taşıyor, ve mevcut bir dilim bu parametreye açılarak geçirildiğinde aradaki ilişki nedir?
Programlama Temelleri kursu (M01/K02) değerle ve referansla aktarımı Python’la kurdu; orada bir listeyi bir işleve geçirmek, listenin kendisini paylaşmak anlamına geliyordu. Bu kavram burada tekrar kurulmaz — farkı tek cümleyle yazmak yeter: Go’da her şey değerle aktarılıyor, dilimler de dahil; bu dersin ölçtüğü, “değerle aktarım” ile “paylaşılan bir alt diziye işaret eden bir değerin aktarımı” arasındaki farkın değişken sayıda argümanlarda nasıl göründüğü.
Üç Çağrı Biçimi
Go’da bir parametrenin önüne ... konursa, o parametre değişken sayıda argüman kabul
eder ve işlev gövdesinde bir dilim gibi davranır. Çağıran taraf üç ayrı biçimde çağırabilir:
hiç argüman vermeden, argümanları tek tek sıralayarak, ya da elinde zaten duran bir dilimi
... ile açarak. Üçü de sözdizim olarak geçerli, ama imzanın arkasında üçü de aynı şeyi
üretmiyor.
Bu kursun kendisi, değişken sayıda argümanı en baştan beri kullanıyor: her fmt.Printf ve
fmt.Println çağrısı, standart kitaplığın değişken sayıda argüman alan işlevleri. Bir çağrıda
tek bir değer, başka bir çağrıda beş değer geçirebilmemiz, tam olarak bu dersin ölçtüğü
düzenek sayesinde. Bu ders, o düzeneğin kendi işlevlerimizi yazarken nasıl davrandığını
soruyor.
İki yardımcı işlev bu parametrenin davranışını iki yönden ölçüyor: ozellikleri, uzunluğunu
ve nil olup olmadığını döndürüyor; ilkiSifirla, ilk elemanını değiştirip bu değişikliğin
çağırana yansıyıp yansımadığını sınamamıza yarıyor.
// yardimci.go — degisken sayida arguman alan iki yardimci
package main
func ozellikleri(sayilar ...int) (uzunluk int, nilMi bool) {
return len(sayilar), sayilar == nil
}
func ilkiSifirla(sayilar ...int) {
if len(sayilar) > 0 {
sayilar[0] = 0
}
}
DT23. ozelliklerinin dönüş imzası önceki dersin adlandırılmış dönüşünü kullanıyor;
sayilar == nil karşılaştırması, bir dilimin nil olup olmadığını sınamanın standart yolu —
Sıfır Değerler dersinde ölçülen aynı karşılaştırma burada bir parametre üzerinde koşuyor.
ilkiSifirla de aynı dersten bir tekniği ödünç alıyor: len(sayilar) > 0 denetimi, nil
dilime indeksleme yaparak panikletmemek için — nil bir dilimin sıfırıncı elemanına erişmek,
tıpkı boş bir dilimde olduğu gibi, dizinin sınırları dışına taşar.
Üç çağrı biçimini yan yana koşturuyoruz: hiç argüman verilmeden, argümanlar tek tek yazılarak,
ve mevcut bir dilim ... ile açılarak.
// degisken.go — hic arguman verilmedigindeki dilim, dilimin acilmasi, acilan dilimin kopyalanmamasi
package main
import "fmt"
func main() {
u1, n1 := ozellikleri()
fmt.Printf("%-28s uzunluk=%d nil=%v\n", "hic arguman verilmedi", u1, n1)
u2, n2 := ozellikleri(1, 2, 3)
fmt.Printf("%-28s uzunluk=%d nil=%v\n", "uc arguman tek tek yazildi", u2, n2)
kaynak := []int{10, 20, 30}
u3, n3 := ozellikleri(kaynak...)
fmt.Printf("%-28s uzunluk=%d nil=%v\n", "mevcut dilim acildi (...)", u3, n3)
ilkiSifirla(1, 2, 3)
fmt.Printf("%-28s %s\n", "tek tek yazilan degisti mi", "hicbir sey gozlenemez, cagirana ait dilim yok")
ilkiSifirla(kaynak...)
fmt.Printf("%-28s kaynak=%v\n", "acilan dilim degisti mi", kaynak)
}
hic arguman verilmedi uzunluk=0 nil=true uc arguman tek tek yazildi uzunluk=3 nil=false mevcut dilim acildi (...) uzunluk=3 nil=false tek tek yazilan degisti mi hicbir sey gozlenemez, cagirana ait dilim yok acilan dilim degisti mi kaynak=[0 20 30]
DT24. İlk üç satır, aynı parametrenin üç farklı çağrı biçiminde ne taşıdığını gösteriyor.
Hiç argüman verilmediğinde (ozellikleri()), parametre nil bir dilim — uzunluğu 0 ve
kimliği boş. Argümanlar tek tek yazıldığında (ozellikleri(1, 2, 3)), derleyici bu üç değeri
tutacak taze bir dilim oluşturuyor; bu dilim nil değil, çünkü bir yerde gerçekten
ayrılmış. Mevcut bir dilim ... ile açıldığında (ozellikleri(kaynak...)), uzunluk ve
nillik ikinci satırla aynı çıkıyor — ama bu satırın asıl farkı, dördüncü ve beşinci
satırlarda ortaya çıkıyor.
Birinci satırdaki nil sonucu, ilk derste kurulan kalıbı doğruluyor: hiç argüman verilmemesi
bir hata değil, tanımlı bir sıfır değer. İkinci satır ise ilk kez, bir değişkenin değil, bir
çağrının kendisinin bir dilim ürettiğini gösteriyor — 1, 2, 3 yazmak, kaynakta hiçbir
dilim değişmezi ([]int{1, 2, 3}) görünmese de, çağrı anında bir dilim inşa ediyor.
İlk üç satırı yalnız uzunluk ve nil sütunlarına bakarak okursak, ikinci ve üçüncü satır
birbirinden ayırt edilemez: ikisi de uzunluk=3 nil=false. Bu, ölçümün bir sınırını gösteriyor
— ozellikleri işlevinin kendisi, kendisine geçirilen dilimin taze mi yoksa paylaşılan
mı olduğunu bilmiyor, çünkü bu bilgi dilim başlığının hiçbir alanında (uzunluk, kapasite,
işaretçi) doğrudan görünmüyor. Bu ayrımı görmek için başka bir ölçüme, ilkiSifirlaya
ihtiyaç var.
Açılan Dilim Kopyalanmıyor
Bu belirsizlik, “tanımlı” sınıfının bir başka sınırını daha gösteriyor. Sekiz tanımlı satırın her biri belirli bir soruya kesin bir yanıt veriyordu; burada soru başka — bir işlevin kendi parametresine bakarak, o parametrenin çağıranla ne kadar bağlantılı olduğunu anlayabilmesi mi soruluyor. Yanıt hayır: dilim başlığı bu bilgiyi taşımıyor, yalnız davranışı gözlemleyerek (değiştirip sonucu izleyerek) ortaya çıkarılabiliyor.
Dördüncü ve beşinci satırlar aynı işlevi (ilkiSifirla) iki farklı çağrı biçimiyle
çalıştırıyor. ilkiSifirla(1, 2, 3) çağrısında değiştirilen dilim, yalnız o çağrı için
oluşturulmuş taze bir dilim — çağıran tarafta bu dilime karşılık gelen hiçbir değişken yok,
bu yüzden “değişti mi” sorusunun bile bir anlamı yok. ilkiSifirla(kaynak...) çağrısında ise
durum farklı: kaynak, çağrıdan önce [10 20 30], çağrıdan sonra [0 20 30]. İşlevin içinde
sayilar[0] = 0 yazmak, çağıranın kendi kaynak değişkenini değiştirdi.
DT25. Bu, ... ile açılan bir dilimin kopyalanmadığını gösteriyor. Go, f(dilim...)
çağrısında yeni bir alt dizi ayırmıyor; dilimin kendi başlığını (uzunluk, kapasite, alt
diziye işaretçi) doğrudan parametreye geçiriyor. Parametre ile kaynak, aynı alt diziyi
paylaşıyor; parametre üzerinden yapılan bir eleman değişikliği, aynı alt diziyi okuyan
kaynak üzerinden de görünür oluyor.
Bu, “değerle aktarım” kuralını (Programlama Temelleri kursunda kavram olarak kuruldu, burada
tekrar kurulmuyor) çelişkili gibi gösterebilir — sanki dilim “referansla” geçiyormuş gibi
görünüyor. Ama aktarılan şey yine bir değerdir: dilim başlığının kendisi kopyalanıyor,
yalnız o başlığın işaret ettiği alt dizi kopyalanmıyor. ilkiSifirla(1, 2, 3) ile
ilkiSifirla(kaynak...) arasındaki fark, aktarım kuralında değil, hangi alt dizinin
paylaşıldığında: birincisinde parametre kendi taze alt dizisine işaret ediyor ve o alt diziyi
kimse başka bir yerden görmüyor; ikincisinde parametre kaynakın zaten var olan alt
dizisine işaret ediyor ve o alt diziyi kaynak da görmeye devam ediyor.
Bu ayrım, tek tek yazılan argümanların her zaman güvenli olduğu, açılan bir dilimin ise
bazen paylaşım riski taşıdığı anlamına geliyor. topla(1, 2, 3) gibi bir çağrıda, işlevin
argümanları değiştirmesi hiçbir zaman çağıranı etkilemez, çünkü paylaşılacak bir çağıran-taraf
dilimi yok. topla(dilim...) gibi bir çağrıda ise işlevin kendi parametresine yazması,
dilimin kendisini de değiştirebilir. Bu, bir işlevi yazarken sorulması gereken bir soruyu
ortaya çıkarıyor: değişken sayıda argüman alan bir işlev, kendi parametresine yazıyorsa,
bu yazmanın çağıranı da etkileyebileceği kaynak kodunda belirtilmeli — imza bu riski kendisi
göstermiyor, çünkü func ilkiSifirla(sayilar ...int) imzası, ilkiSifirla(1, 2, 3) ile
ilkiSifirla(kaynak...) çağrılarının ikisi için de aynı.
Bu, standart kitaplığın değişken sayıda argüman alan işlevlerinin neden neredeyse hiçbirinin
kendi parametresine yazmadığını açıklıyor: fmt.Println, argümanlarını yalnız okuyor, hiç
değiştirmiyor. Bir işlev yazarken kendi değişken sayıda argüman parametresine yazma ihtiyacı
duyuluyorsa, bu ders bir uyarı taşıyor — o yazma, çağıranın ... ile açtığı bir dilime
sessizce sızabiliyor.
Sınırlayıcı Ölçüm: Nil Dilim İle Boş Dilim Aynı Değildir
İlk satırda nil bir dilim, ikinci ve üçüncü satırlarda nil olmayan dilimler gördük.
Ama nil olmayan bir dilim, boş da olabilir. ozellikleri() ile ozellikleri([]int{}...)
karşılaştırıldığında bu fark netleşiyor: birincisinde hiçbir argüman yok, ikincisinde bir
argüman var — o argüman, []int{} ile kurulmuş, sıfır elemanlı ama gerçekten var olan bir
dilim.
// sinirlayici.go — nil dilim ile bos dilim ayni degildir
package main
import (
"encoding/json"
"fmt"
)
func main() {
uNil, nNil := ozellikleri()
uBos, nBos := ozellikleri([]int{}...)
fmt.Printf("%-28s uzunluk=%d nil=%v\n", "hic arguman (nil dilim)", uNil, nNil)
fmt.Printf("%-28s uzunluk=%d nil=%v\n", "bos dilim acildi ([]int{}...)", uBos, nBos)
var nilDilim []int
bosDilim := []int{}
bNil, _ := json.Marshal(nilDilim)
bBos, _ := json.Marshal(bosDilim)
fmt.Printf("%-28s json=%s\n", "nil dilimin json karsiligi", bNil)
fmt.Printf("%-28s json=%s\n", "bos dilimin json karsiligi", bBos)
}
hic arguman (nil dilim) uzunluk=0 nil=true
bos dilim acildi ([]int{}...) uzunluk=0 nil=false
nil dilimin json karsiligi json=null
bos dilimin json karsiligi json=[]
İlk iki satır, lenin ayırt edemediği bir farkı gösteriyor: ikisinin de uzunluğu 0, ama
biri nil, öbürü değil. []int{}, boş de olsa, bir yere ayrılmış bir dilim; hiçbir
elemanı yok ama kimliği var. nil dilim ise hiçbir yere ayrılmamış, kimliği de yok. len bu
ikisini ayırt etmiyor çünkü uzunluk, ayrılmış olup olmamayı değil, kaç eleman tutulduğunu
sayıyor.
Fark somut bir yerde görünüyor: encoding/json paketinin dönüştürücüsü. nil bir dilim
JSON’da null olarak yazılıyor, boş ama nil olmayan bir dilim ise [] olarak yazılıyor.
İkisi de “hiçbir eleman yok” bilgisini taşısa da, dönüştürücü bu iki durumu ayrı karşılıklar
olarak ele alıyor — biri “değer yok”, öbürü “değer var ama boş”. Standart kitaplığın bu
seçimi, nil ile boş dilim arasındaki farkı yalnız bir dil kuralı olmaktan çıkarıp, gerçek bir
programın çıktısında görünür bir sonuca dönüştürüyor.
Bu iki karşılık, aynı iki dilimin başka bir bağlamda tamamen aynı davranmasıyla da tezat
oluşturuyor. Bir for range döngüsüyle gezildiklerinde, nil dilim ile boş dilim arasında
hiçbir fark yok — ikisi de hiç yinelenmeden döngüyü bitiriyor. append çağrıldığında da ikisi
aynı davranıyor — ikisine de eleman eklenebiliyor. Fark yalnız kimlik sorulduğunda
(== nil) ya da kimliğe duyarlı bir kod (encoding/json gibi) devreye girdiğinde ortaya
çıkıyor. Bu, “tanımlı” sınıfının kendi içinde bir alt ayrımı daha olduğunu gösteriyor: bazı
işlemler bir dilimin kimliğine bakıyor, bazıları yalnız içeriğine.
Bu, kursun sıfır değer ölçümüne bir sınır daha ekliyor. nil dilim, Sıfır Değerler dersinde
ölçüldüğü gibi tanımlı bir sıfır değer taşıyor ve append gibi işlemlere açık. Ama nil
olmak ile “boş olmak” aynı şey değil, ve bu fark yalnız == nil karşılaştırmasında değil,
dilimi işleyen başka kodlarda (burada bir dönüştürücüde) da yüzeye çıkabiliyor. Bir işlevin
hiç argüman almadığını (nil) mi, yoksa boş bir dilimle çağrıldığını ([]int{}...) mı
bildiği, çağrılan tarafın davranışını değiştirebiliyor.
Sekiz dersin sonunda, açılış dersinde sorulan soruya geri dönebiliriz: bir dil, yazmadığın şeyin karşılığını nasıl belirler? A payı boyunca bu soru sekiz farklı yerde soruldu — bir kaynağın derlenebilmesi için ne gerektiğinde, bir değişkenin tipi çıkarılırken, bir sabit dizisi doldurulurken, bir denetim yapısının parçası atlanırken, bir dönüş değeri yazılmadığında, ve şimdi bir parametreye hiç argüman verilmediğinde. Yanıt hiçbir zaman tek biçimli değildi: kimi zaman dil tanımlı bir değer koydu, kimi zaman derleyici kaynağı hiç kabul etmedi, kimi zaman sonuç çalışma zamanına kaldı. Sıfır Değerler dersinde ölçülen 8/4/3 dağılımı bu üç yanıtın hiçbirinin baskın olmadığını, üçünün de dilin tasarımının bir parçası olduğunu gösterdi.
Bu derste ölçülen nil dilim, o dağılımın en görünür üyesi, çünkü bir dilim üzerinde
neredeyse her işlem (len, append, for range) nil durumunu sorunsuzca kaldırıyor — bir
programcı nil bir dilimle günlerce çalışıp hiç fark etmeyebilir. Bu görünmezlik, tam da bu
dersin gösterdiği gibi, bir sınırla bitiyor: kimlik sorulduğunda ya da kimliğe duyarlı bir kod
devreye girdiğinde, nil olmak aniden görünür hale geliyor.
A payının sekiz dersi boyunca ölçüm hep aynı düzeni izledi: iki hâli yan yana koymak, farkı koşturarak göstermek, sonra o farkın nerede kırıldığını bulmak. Bu son ders de aynı düzeni taşıyor — üç çağrı biçimi yan yana koşturuldu, aralarındaki fark (paylaşım) ölçüldü, ve o farkın kendisinin bile bir sınırı (nil ile boş dilim ayrımı) bulundu. Sıradaki konu, aynı düzeni tek bir işlevin parametresinden bütün bir tip ailesine taşıyor.
Özet
- Değişken sayıda argüman alan bir parametreye hiç argüman verilmezse, parametre
nilbir dilim olur. - Argümanlar tek tek yazıldığında derleyici taze bir dilim oluşturur; bu dilim
nildeğildir. - Mevcut bir dilim
...ile açıldığında kopyalanmaz — parametre, çağıranın dilimiyle aynı alt diziyi paylaşır ve çağrılan tarafın değişikliği çağırana yansır. nildilim ile boş dilimlenaçısından aynıdır (0) ama== nilaçısından ayrışır.- Bu fark yalnız karşılaştırmada kalmıyor:
encoding/jsonpaketinildiliminull, boş dilimi[]olarak yazıyor. for rangeveappendgibi işlemlernilile boş dilimi ayırt etmiyor; fark yalnız kimliğe bakan işlemlerde (== nil,encoding/json) ortaya çıkıyor.
Sonraki Adım
Hiç argüman verilmeyen bir parametrenin nil bir dilim olması, sıfır değerin bu kursta
gördüğümüz en görünür sonucu. A payı burada kapanıyor; sıradaki konu bu tipin kendisine
bakıyor — dilim, sözlük ve yapı yazılmayanı nasıl karşılıyor, ve bir değeri bir değişkene
atadığında ortaya çıkan şey bir kopya mı, yoksa bu derste gördüğümüz gibi bir
paylaşım mı?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.