Ders 06 / 18
Denetim Akışı
Beş biçim aynı soruyla ölçülüyor — koşulsuz for, tek koşullu for, ifadesiz switch, else'siz erken dönüş, kendiliğinden düşmeyen switch dalları — ve beşi de yazılmayan parçayı tanımlı bir karşılıkla dolduruyor. Sınırlayıcı ölçüm: yazılmayan break gerekmiyor, ama düşmeyi isteyen kod fazladan bir sözcük (fallthrough) yazmak zorunda.
İçindekiler
Önceki ders on beş yazılmamışlığı ölçtü ama hiçbiri bir denetim yapısının içinde
değildi — hepsi bir değişkenin, bir dönüşün ya da bir argümanın yokluğuyla ilgiliydi. Bu ders
aynı soruyu denetim akışına taşıyor: for neden bazen koşulsuz yazılabiliyor, switch neden
bazen ifadesiz yazılabiliyor, ve bir dalın sonunda yazılmayan break neyin yerini tutuyor?
Programlama Temelleri kursu (M01/K02) koşullu dallanmayı ve döngü denetimini Python’la kurdu;
orada döngü ve dallanma için ayrı anahtar sözcükler vardı (while, for, if/elif/else)
ve her birinin kendi sözdizimi sabitti. Bu kavramlar burada tekrar kurulmaz — farkı tek
cümleyle yazmak yeter: Go, döngü için tek bir anahtar sözcük (for) kullanıyor ve bu
sözcüğün hangi klasik döngü biçimine karşılık geldiği, kaynakta hangi bölümlerin yazılıp
yazılmadığına bağlı. Bu ders o bağlılığı ölçüyor.
Beş Biçim, Tek Soru
Go’nun denetim akışı iki anahtar sözcüğe dayanıyor: for ve switch (if üçüncüsü, ama
onda yazılmayan tek şey else). Diğer dillerde ayrı sözcüklerle karşılanan “sonsuz döngü”,
“koşullu döngü” ve “sayaçlı döngü” ayrımı, Go’da tek bir for sözcüğünün hangi bölümlerinin
yazıldığına indirgeniyor. Beş ayrı biçimi aynı çerçevede koşturuyoruz: her biri bir şeyi
yazmadan bırakıyor, dil o boşluğu kendi dolduruyor.
// akis.go — bes bicim, yazilmayan parcayi tanimli bir karsilikla dolduruyor
package main
import "fmt"
func siniflandir(x int) string {
if x < 0 {
return "negatif"
}
return "negatif degil"
}
func main() {
sayac := 0
for {
sayac++
if sayac == 5 {
break
}
}
fmt.Printf("%-28s %s\n", "kosulsuz for (durana kadar)", fmt.Sprint("sayac=", sayac))
i, toplam := 0, 0
for i < 5 {
toplam += i
i++
}
fmt.Printf("%-28s %s\n", "tek kosullu for", fmt.Sprint("toplam=", toplam, " son_i=", i))
puan := 72
var not string
switch {
case puan >= 90:
not = "AA"
case puan >= 70:
not = "BA"
default:
not = "FF"
}
fmt.Printf("%-28s %s\n", "ifadesiz switch", fmt.Sprint("not=", not))
fmt.Printf("%-28s %s\n", "else'siz erken donus", fmt.Sprint(siniflandir(-3), " / ", siniflandir(3)))
sonuc := ""
switch 2 {
case 1:
sonuc += "bir"
case 2:
sonuc += "iki"
case 3:
sonuc += "uc"
}
fmt.Printf("%-28s %s\n", "switch dusmuyor", fmt.Sprint("sonuc=", sonuc))
}
kosulsuz for (durana kadar) sayac=5 tek kosullu for toplam=10 son_i=5 ifadesiz switch not=BA else'siz erken donus negatif / negatif degil switch dusmuyor sonuc=iki
Beş satırın her biri, gövdesinden bağımsız okunabilecek bir sayıya dayanıyor. toplam=10,
0+1+2+3+4ün toplamı — döngü beş kez (i sıfırdan dörde) çalıştığını, i < 5 koşulu
sağlandığı sürece devam ettiğini doğruluyor; son_i=5 ise döngünün, koşul artık sağlanmadığı
an durduğunu gösteriyor. not=BA, 72 puanın puan >= 70 dalına düştüğünü, puan >= 90
dalının hiç denenmediğini kanıtlıyor. Beşi de birbirinden bağımsız kanıtlar, ama hepsi aynı
soruya yanıt veriyor: yazılmayan parça neyle dolduruluyor?
DT17. forın üç bölümü de (ilklendirme, koşul, sonlandırma) isteğe bağlıdır. İlk satırda
hiçbiri yazılmıyor; derleyici yazılmayan koşulun yerine örtük true koyuyor, bu yüzden
döngü kendiliğinden durmuyor — sayac, breake kadar sınırsız artmaya devam edeceğini
gösteriyor, biz onu beşte durduruyoruz. İkinci satırda yalnız koşul yazılıyor; ilklendirme ve
sonlandırma yazılmadığında dil onları atlıyor, inin döngüden önce tanımlanmış ve
gövde içinde elle artırılmış olması yeterli.
İki for biçimi de aynı iddiayı farklı uçlardan gösteriyor: yazılmayan bir bölüm, dilin
“bu bölüm yok sayılsın” demesiyle karşılanıyor — koşul yoksa hep doğru kabul ediliyor,
ilklendirme ve sonlandırma yoksa hiç çalıştırılmıyor. Üçü de aynı for sözcüğünün parçası;
hangi bölümlerin yazıldığı, döngünün hangi klasik biçime (sonsuz, koşullu, üç adımlı) karşılık
geldiğini belirliyor.
Bu, forın tek bir sözdizim ağacı taşıdığı, ama o ağacın üç dalının da isteğe bağlı olduğu
anlamına geliyor. Üç adımlı biçim (for i := 0; i < 5; i++ { }) hiçbir bölümü atlamaz ve
kaynakta en açık hâliyle durur; ikinci satırdaki tek koşullu biçim, ilklendirme ile
sonlandırmayı dışarıya, gövdenin öncesine ve içine taşır — i, toplam := 0, 0 döngüden önce,
i++ gövdenin son satırında. Derleyici için bu iki yazım aynı makine kodunu üretir; aradaki
fark tamamen okuyan tarafın neyi bir bakışta göreceğidir. Üç adımlı biçimde döngünün sınırları
tek satırda toplanır; tek koşullu biçimde sınırları görmek için gövdenin tamamını okumak
gerekir.
Bu ayrımın bir bedeli var: birinci satırdaki sayac döngüden önce sıfırlanıyor ve döngü
içinde artıyor, ama döngünün ne zaman duracağı bilgisi ne başlıkta ne de tek bir satırda —
gövdenin ortasındaki bir if bloğunda gizli. Koşulsuz for, en esnek biçimdir çünkü durma
koşulunu hiçbir yere sabitlemez; bu esneklik, durma koşulunu bulmak için gövdenin tamamını
okumayı gerektirir.
Bu esnekliğin bir bedeli olduğu kadar bir nedeni de var: bazı döngülerin durma koşulu, tek bir
karşılaştırmayla ifade edilemeyecek kadar karmaşıktır — birden fazla durum birlikte
sağlandığında ya da bir dış kaynaktan (bir kanaldan, bir okuyucudan) gelen bir sinyale bağlı
olarak durur. Böyle bir döngü için başlıkta tek bir koşul yazmak, gövdenin gerçek durma
mantığını başlığa sığdırmaya çalışmak olurdu; koşulsuz for, durma mantığını olduğu gibi
gövdeye bırakmanın yolu. Üç adımlı ve tek koşullu biçimler, durma koşulu tek bir karşılaştırmaya
indirgenebildiğinde tercih ediliyor; koşulsuz biçim, indirgenemediğinde.
DT18. Üçüncü satırda switch bir ifade almadan yazılıyor. Bir switch ifadesi
yazılmadığında derleyici onu örtük true ile karşılaştırıyor; her case de kendi
başına bir mantıksal ifadeye dönüşüyor. puan >= 70 doğru olduğu için not, "BA" oluyor —
tam olarak bir if/else if zincirinin yapacağı şey, yalnız switch sözdizimiyle.
Bu, switchin Go’da bir “eşitlik” yapısından çok bir “dallanma” yapısı olduğunu gösteriyor.
Bazı dillerde switch, yalnız tek bir değeri sabit değerlerle eşitleyebilir; Go’da bir ifade
yazılmadığında caseler serbestçe mantıksal koşullar taşıyabiliyor ve bu koşullar sırayla
denenip ilk doğru olan çalıştırılıyor — tıpkı bir if/else if zincirinde olduğu gibi. Yazılan
switch { } ile switch true { } arasında hiçbir fark yok; ikincisi yalnız ilkinin yazılmayan
kısmını açıkça gösteriyor.
Bu, switchin iki ayrı kullanımı tek bir sözdizimde birleştirdiğini gösteriyor. Bir ifade
yazıldığında (switch puan { case 90: ... }), her case o ifadeyle eşitlenen bir sabit
listesi taşıyabiliyor — çoklu değer eşleşmesi bunun bir örneği, kursun ilerleyen bir dersinde
tekrar görülecek. İfade yazılmadığında ise her case kendi başına tam bir mantıksal ifade
taşıyor ve eşitlik değil, doğruluk sınanıyor. Aynı anahtar sözcük, yazılan tek bir parçaya
(ifadenin kendisi) bağlı olarak iki farklı dallanma biçimine dönüşüyor.
Beşinci
satırda ise farklı bir yazılmamışlık ölçülüyor: hiçbir case sonunda break yok, ama
sonuc, yalnız eşleşen dalın (case 2) değerini taşıyor — case 3ün gövdesi hiç
çalışmıyor. Go’da bir casein sonu, dalın kendiliğinden kapanması demek; yazılmayan
breakin karşılığı, düşmenin hiç olmamasıdır.
Dördüncü satır farklı bir aileden: else hiç yazılmadan, if bloğu bir return ile bitiyor
ve kalan kod sanki bir else bloğuymuş gibi çalışıyor. siniflandir(-3), if dalına girip
"negatif" döndürüyor; siniflandir(3), if dalına hiç girmeden ikinci satıra düşüp
"negatif degil" döndürüyor. Yazılmayan elsenin karşılığı, kontrolün if bloğundan çıkan
her yolun (bir return ile ya da hiç girmeden) aynı yere, işlevin geri kalanına
varmasıdır.
Bu biçim, Go kaynaklarında “koruma cümlesi” (guard clause) diye anılan bir düzenek — bir
işlevin başında istisnai durumları erken return ile eleyip, geri kalan gövdeyi tek bir iç
içe geçmeden yazmanın yolu. if x < 0 { return "negatif" } satırından sonra else yazılsaydı,
işlevin geri kalanı bir kademe daha içeri kayardı; else yazılmadığı için ikinci return
işlevin ana gövdesiyle aynı girinti düzeyinde kalıyor. Yazılmayan elsenin karşılığı bu yüzden
yalnız bir davranış değil, aynı zamanda bir biçim kazancı: gövde, iç içe geçmiş bloklar
yerine düz bir sırayla okunuyor.
Bu düzenek, işlev tek bir istisnai durum yerine birden fazlasını elediğinde daha da belirgin
oluyor. Art arda birkaç koruma cümlesi yazıldığında (if a { return }, if b { return }, if c { return }), her biri kendi elsesini yazsaydı gövde her satırda bir kademe daha içeri
kayardı; else hiç yazılmadığında bütün koruma cümleleri aynı girinti düzeyinde yan yana
durur ve işlevin “asıl işi” en sonunda, en az girintili satırda kalır. Yazılmayan else,
burada tek bir satırın değil, işlevin tamamının okunurluğünü etkiliyor.
Sınırlayıcı Ölçüm: Yazılmayan Break, Yazılması Gereken Fallthrough
Beşinci satırdaki “düşmeme” davranışı kolaylık gibi görünüyor — bir sözcük (break) yazmaktan
kurtulduk. Ama tersini istediğimizde, yani bir dalın bilerek bir sonrakine düşmesini
istediğimizde, dil bunu bedelsiz vermiyor.
Bu ölçümün önemi, “yazılmayan bir şey her zaman en kısa yol mu” sorusuna verdiği yanıtta
yatıyor. Beşinci satırda gördüğümüz üzere, hiçbir case break yazmadı ve yine de yalnız
eşleşen dal çalıştı — bu bir kazanç gibi göründü. Ama bu kazanç, “düşme” davranışının dilde hiç
var olmadığı anlamına gelmiyor; yalnız varsayılan olmadığı anlamına geliyor. Aşağıdaki iki
switch, aynı üç dalı taşıyor ve aynı değeri (2) sınıyor; aralarındaki tek fark, ikincisinin
case 2 gövdesine bir sözcük eklemesi.
// sinirlayici.go — yazilmayan break gerekmiyor, ama dusmeyi isteyen kod fazladan sozcuk yaziyor
package main
import "fmt"
func main() {
yaziliDegil := ""
switch 2 {
case 1:
yaziliDegil += "bir"
case 2:
yaziliDegil += "iki"
case 3:
yaziliDegil += "uc"
}
yazili := ""
switch 2 {
case 1:
yazili += "bir"
case 2:
yazili += "iki"
fallthrough
case 3:
yazili += "uc"
}
fmt.Printf("%-28s %s\n", "break yazilmadi", fmt.Sprint("sonuc=", yaziliDegil))
fmt.Printf("%-28s %s\n", "fallthrough yazildi", fmt.Sprint("sonuc=", yazili))
}
break yazilmadi sonuc=iki fallthrough yazildi sonuc=ikiuc
DT19. İki switch da aynı değeri (2) sınıyor ve aynı üç dalı taşıyor; tek fark, ikinci
switchin case 2 gövdesine fallthrough sözcüğünün eklenmesi. Birinci satırda sonuç yalnız
"iki" — case 2 çalışıyor, case 3e hiç geçilmiyor. İkinci satırda sonuç "ikiuc" — açıkça
istenen bir düşme, case 2nin ardından case 3ü de çalıştırıyor.
Bu, “yazılmayanın karşılığı” sorusuna bir asimetri ekliyor. Varsayılan davranış (düşmeme)
hiçbir sözcük gerektirmiyor; varsayılanın tersi (düşme) ise adı konmuş, tek amaçlı bir
anahtar sözcük gerektiriyor. Bu asimetri kasıtlı: bir casein gövdesini okuyan biri, orada
fallthrough görmediği sürece, o dalın kendi başına bittiğinden emin olabiliyor. Yazılmayan
şey burada bir eksiklik değil, bir garanti — geçerli olmayan tek yorum kalıyor: dal
kendiliğinden kapanıyor, aksi yazılmadıkça.
fallthroughın taşıdığı bir kısıtlama daha var: yalnız bir sonraki casee düşebiliyor,
kendi koşulunu denetlemeden. İkinci switchte case 3ün kendi koşulu (3 değerine eşit
olmak) hiç sınanmıyor — fallthrough görüldüğü an, sıradaki dal koşulsuz çalıştırılıyor. Bu,
fallthroughı bir casein koşulunu tekrarlamak ile karıştırmamak gerektiğini gösteriyor:
fallthrough, “bir sonraki dalın koşulunu da sına” değil, “bir sonraki dalın gövdesini de
çalıştır” demek. Yazma anında ayırt edilmesi gereken bir ayrıntı — fallthroughın sıradaki
dalı sınamadan çalıştırdığını bilmeyen biri, düşen dalın koşulunun da doğrulandığını
sanabilir.
fallthroughın kendi de bir kısıtla geliyor: yalnız bir case gövdesinin son satırı
olabiliyor. Bir dalın ortasına yazılsaydı derleyici bunu reddederdi — düşme, dalın geri kalanı
çalıştıktan sonra gelen tek bir geçiş, dalın içine serpiştirilebilecek bir dallanma aracı
değil. Bu kısıtlama, fallthroughı sıradan bir gotodan ayırıyor: yalnız bitişik bir
sonraki dala geçebiliyor ve yalnız bir dalın en sonunda görünebiliyor.
Bu asimetri, kursun ilk dersinde ölçülen “reddetmek daha az yazmak demek değildir” gözlemine benziyor ama ondan farklı bir yerde duruyor. Orada reddedilen bir kalıbın karşılığı daha uzun bir yazımdı; burada hiçbir şey reddedilmiyor, yalnız varsayılan olmayan yol daha uzun yazılıyor. Varsayılan davranış hep en kısa yazımı alıyor, varsayılan dışı davranış hep bir sözcük daha fazla istiyor.
Beş biçimin tamamına birden bakıldığında ortak bir örüntü çıkıyor: Go’nun denetim akışında
yazılmayan her parça, en sık istenen davranışın karşılığı oluyor. Bir döngünün sonsuza
kadar sürmesi ender istenir, bu yüzden koşulsuz for özel bir sözdizim gerektirmiyor —
yalnız koşulun yokluğu yeterli. Bir switch dalının bir sonrakine düşmesi ender istenir, bu
yüzden düşmeme varsayılan; düşmek istendiğinde bu istisnai isteğin kendisi işaretlenmek
zorunda. Bir if bloğundan sonra kodun devam etmesi sık istenir, bu yüzden else yazmadan da
devam ediyor. Yazılmayan parça hep çoğunluğun yolu; yazılması gereken parça hep
azınlığın istisnası.
Beş biçimin tamamı, tek bir ölçüm programında yan yana koşturulabiliyor olması da kayda değer: hiçbiri diğerini etkilemiyor, hiçbiri özel bir kurulum istemiyor. Bu, denetim akışının Go’da birbirinden bağımsız, küçük kurallar kümesi olarak tasarlandığını gösteriyor — bir biçimi anlamak için ötekileri bilmek gerekmiyor, her biri kendi başına, tek bir yazılmamışlığa tek bir yanıt veriyor.
Özet
forın üç bölümü de isteğe bağlı; yazılmayan koşul örtüktrue, yazılmayan ilklendirme ve sonlandırma yalnız atlanıyor.- İfadesiz bir
switch, örtüktrueile karşılaştırılır ve hercasekendi başına bir mantıksal ifadeye dönüşür. ifbloğu birreturnile bittiğinde,elseyazılmadan da kalan kod birelsebloğu gibi çalışır.- Bir
switchdalının sonu, yazılmayan birbreake gerek kalmadan kendiliğinden kapanır; beş biçimin beşi de tanımlı bir karşılıkla doluyor. - Varsayılanın tersini (düşmeyi) istemek bedelsiz değil:
fallthroughsözcüğü açıkça yazılmak zorunda.
Sonraki Adım
Bu derste ölçülen “else’siz erken dönüş” biçimi, bir işlevin erken bittiği durumları
gösterdi ama işlevin dönüş değerinin kendisi hiç sorgulanmadı — siniflandir her zaman tek
bir dizgi döndürdü ve o dizgiyi hiç kimse yazılmadan bırakmadı. Sıradaki ders bunu soruyor: bir
işlev birden fazla değer döndürdüğünde bunlardan biri yazılmazsa ne oluyor, ve adlandırılmış
bir dönüş hiç dokunulmadan bırakıldığında hangi değeri taşıyor?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.