İçeriğe geç
academia.sh

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 nil bir dilim olur.
  • Argümanlar tek tek yazıldığında derleyici taze bir dilim oluşturur; bu dilim nil değ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.
  • nil dilim ile boş dilim len açısından aynıdır (0) ama == nil açısından ayrışır.
  • Bu fark yalnız karşılaştırmada kalmıyor: encoding/json paketi nil dilimi null, boş dilimi [] olarak yazıyor.
  • for range ve append gibi işlemler nil ile 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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat