İçeriğe geç
academia.sh

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ük true, yazılmayan ilklendirme ve sonlandırma yalnız atlanıyor.
  • İfadesiz bir switch, örtük true ile karşılaştırılır ve her case kendi başına bir mantıksal ifadeye dönüşür.
  • if bloğu bir return ile bittiğinde, else yazılmadan da kalan kod bir else bloğu gibi çalışır.
  • Bir switch dalının sonu, yazılmayan bir breake 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: fallthrough sö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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat