Yineleyici
Go'da belirli veri yapılarını yinelemek için kullanılan anahtar kelime for range'dir. Önceki bölümlerde bazı uygulamaları zaten tanıtılmıştı. Sadece dilin yerleşik birkaç veri yapısı üzerinde çalışabilir
- Dizi
- Slice
- String
- map
- chan
- Tamsayı değeri
Bu durumda kullanım çok esnek değildir, genişletilebilirlik yoktur, özel türler için neredeyse desteklenmez. Ancak go1.23 sürümü güncellendikten sonra, for range anahtar kelimesi range over func'u destekler. Bu şekilde özel yineleyiciler mümkün hale gelir.
Tanıma
Aşağıdaki örnekle yineleyiciyi ilk kez tanıyalım. Fonksiyon bölümünde anlatılan kapalı fonksiyonla Fibonacci dizisini çözen örneği hatırlıyor musunuz bilmiyorum. Uygulama kodu şu şekildedir
func Fibonacci(n int) func() (int, bool) {
a, b, c := 1, 1, 2
i := 0
return func() (int, bool) {
if i >= n {
return 0, false
} else if i < 2 {
f := i
i++
return f, true
}
a, b = b, c
c = a + b
i++
return a, true
}
}Bunu yineleyiciye dönüştürebiliriz. Aşağıdaki gibi gösterildiği gibi, kod miktarının biraz azaldığını görebilirsiniz
func Fibonacci(n int) func(yield func(int) bool) {
a, b, c := 0, 1, 1
return func(yield func(int) bool) {
for range n {
if !yield(a) {
return
}
a, b = b, c
c = a + b
}
}
}Go'nun yineleyicisi range over func stilindedir. Doğrudan for range anahtar kelimesini kullanarak kullanabiliriz. Kullanımı eskisinden daha uygundur
func main() {
n := 8
for f := range Fibonacci(n) {
fmt.Println(f)
}
}Çıktı şu şekildedir
0
1
1
2
3
5
8
13Yukarıda gösterildiği gibi, yineleyici bir kapalı fonksiyondur. Parametre olarak bir geri çağırma fonksiyonu kabul eder. Hatta içinde yield gibi ifadeler görebilirsiniz. Python yazan kişiler buna aşinadır, Python'daki oluşturuculara benzer. Go'nun yineleyicisi herhangi bir yeni anahtar kelime veya sözdizimi özelliği eklemez. Yukarıdaki örnekte yield sadece bir geri çağırma fonksiyonudur, bir anahtar kelime değildir. Resmi bu adı anlamayı kolaylaştırmak için seçmiştir.
İtme Tarzı Yineleyici
Yineleyicinin tanımı hakkında, iter kütüphanesinde aşağıdaki açıklamayı bulabiliriz
An iterator is a function that passes successive elements of a sequence to a callback function, conventionally named yield.
Yineleyici, bir dizinin ardışık elemanlarını geleneksel olarak yield olarak adlandırılan bir geri çağırma fonksiyonuna ileten bir fonksiyondur.
Buradan kesin olarak öğrenebileceğimiz bir nokta, yineleyicinin bir fonksiyon olduğu ve parametre olarak bir geri çağırma fonksiyonu kabul ettiğidir. Yineleme sürecinde dizinin elemanlarını tek tek yield geri çağırma fonksiyonuna iletir. Önceki örnekte yineleyiciyi aşağıdaki şekilde kullanıyorduk
for f := range Fibonacci(n) {
fmt.Println(f)
}Resmi tanıma göre, yukarıdaki Backward yineleyici örneğinin kullanımı aşağıdaki kod ile eşdeğerdir
Fibonacci(n)(func(f int) bool {
fmt.Println(f)
return true
})Döngü gövdesi yineleyicinin yield geri çağırma fonksiyonudur. Fonksiyon true döndürdüğünde yineleyici yinelemeye devam eder, aksi takdirde durur.
Ayrıca, iter standart kütüphanesi yineleyici türü iter.Seq'i tanımlar. Türü fonksiyondur.
type Seq[V any] func(yield func(V) bool)
type Seq2[K, V any] func(yield func(K, V) bool)iter.Seq'ün geri çağırma fonksiyonu sadece bir parametre kabul eder. Yineleme sırasında for range'in sadece bir dönüş değeri vardır. Aşağıdaki gibi
for v := range iter {
// body
}iter.Seq2'nin geri çağırma fonksiyonu iki parametre kabul eder. Yineleme sırasında for range'in iki dönüş değeri vardır. Aşağıdaki gibi
for k, v := range iter {
// body
}Standart kütüphanede 0 parametreli Seq tanımlanmamış olsa da, bu tamamen izin verilir. Şuna eşdeğerdir
func(yield func() bool)Kullanımı aşağıdaki gibi gösterilir
for range iter {
// body
}Geri çağırma fonksiyonunun parametre sayısı sadece 0 ila 2 olabilir. Fazlası derlenemez.
Kısacası, for range'deki döngü gövdesi yineleyicideki yield geri çağırma fonksiyonudur. for range kaç değer döndürürse, ilgili yield fonksiyonunun o kadar giriş parametresi vardır. Her yineleme turunda, yineleyici yield fonksiyonunu çağırır, yani döngü gövdesindeki kodu yürütür ve dizinin elemanlarını aktif olarak yield fonksiyonuna iletir. Elemanları aktif olarak ileten bu tür yineleyicilere genellikle itme tarzı yineleyici (pushing iterator) denir. Diğer dillerdeki foreach gibi tipik bir örnektir. Örneğin js
let arr = [1, 2, 3, 4, 5];
arr
.filter((e) => e % 2 === 0)
.forEach((e) => {
console.log(e);
});Go'daki表现形式 range tarafından yineelenen elemanların döndürülmesidir.
for index, value := range iterator() {
fmt.Println(index, value)
}Bazı dillerde (Java gibi) başka bir adı vardır: veri akışı işleme.
Döngü gövdesindeki kod geri çağırma fonksiyonu olarak yineleyiciye iletildiğinden ve büyük olasılıkla bir kapalı fonksiyon olduğundan, Go'nun bir kapalı fonksiyonun defer, return, break, goto gibi anahtar kelimeleri kullanırken normal döngü gövdesi kod segmenti gibi davranmasını sağlaması gerekir. Aşağıdaki birkaç durumu düşünün.
Örneğin yineleyici döngüsünde return kullanıldığında, yield geri çağırma fonksiyonunda bu return nasıl işlenmelidir?
for index, value := range iterator() {
if value > 10 {
return
}
fmt.Println(index, value)
}Geri çağırma fonksiyonunda doğrudan return yapılamaz. Bu sadece yinelemeyi durdurur, return etkisine ulaşamaz
iterator()(func(index int, value int) bool {
if value > 10 {
return false
}
fmt.Println(index, value)
})Yineleyici döngüsünde defer kullanıldığında da aynı durum geçerlidir
for index, value := range iterator() {
defer fmt.Println(index, value)
}Geri çağırma fonksiyonunda doğrudan defer kullanılamaz. Çünkü bu şekilde geri çağırma fonksiyonu bittiğinde doğrudan ertelenmiş çağrı yapılır
iterator()(func(index int, value int) bool {
defer fmt.Println(index, value)
})Diğer birkaç anahtar kelime break, continue, goto da benzerdir. Neyse ki Go bu durumları bizim için halletmiştir. Sadece kullanmamız yeterlidir. Şimdilik bunlarla ilgilenmemize gerek yoktur. Eğer ilgileniyorsanız rangefunc/rewrite.go içindeki kaynak kodu kendiniz inceleyebilirsiniz.
Çekme Tarzı Yineleyici
İtme tarzı yineleyici (pushing iterator) yineleme mantığını yineleyicinin kontrol etmesi, kullanıcının pasif olarak elemanları almasıdır. Tam tersi, çekme tarzı yineleyici (pulling iterator) kullanıcının yineleme mantığını kontrol etmesi, dizinin elemanlarını aktif olarak almasıdır. Genel olarak, çekme tarzı yineleyiciler next(), stop() gibi belirli fonksiyonlara sahiptir. Yinelemenin başlatılması veya sonlandırılması için kullanılır. Bir kapalı fonksiyon veya yapı olabilir.
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line, err := scanner.Text(), scanner.Err()
if err != nil {
fmt.Println(err)
return
}
fmt.Println(line)
}Yukarıda gösterildiği gibi, Scanner Text() yöntemi ile dosyadaki bir sonraki metin satırını alır, Scan() yöntemi ile yinelemenin bitip bitmediğini gösterir. Bu da çekme tarzı yineleyicinin bir modudur. Scanner durumu kaydetmek için yapı kullanır. iter kütüphanesinde tanımlanan çekme tarzı yineleyici durumu kaydetmek için kapalı fonksiyon kullanır. iter.Pull veya iter.Pull2 fonksiyonu ile standart bir itme tarzı yineleyiciyi çekme tarzı yineleyiciye dönüştürebiliriz. iter.Pull ile iter.Pull2 arasındaki fark ikincisinin iki dönüş değeri olmasıdır. İmza şu şekildedir
func Pull[V any](seq Seq[V]) (next func() (V, bool), stop func())
func Pull2[K, V any](seq Seq2[K, V]) (next func() (K, V, bool), stop func())Her ikisi de parametre olarak bir yineleyici kabul eder, ardından yinelemeyi devam ettirmek ve durdurmak için kullanılan iki fonksiyon next() ve stop() döndürür.
func next() (V, bool)
func stop()next yineelenen elemanı ve geçerli değerin geçerli olup olmadığını gösteren bir boolean değer döndürür. Yineleme bittiğinde next fonksiyonu elemanın sıfır değerini ve false döndürür. stop fonksiyonu yineleme sürecini sonlandırır. Çağrıcı yineleyiciyi artık kullanmadığında, yinelemeyi sonlandırmak için stop fonksiyonunu kullanmalıdır. Bu arada, birden fazla goroutine'in aynı yineleyicinin next fonksiyonunu çağırması yanlış bir uygulamadır. Çünkü eşzamanlı güvenli değildir.
Aşağıdaki örnekle gösterelim. İşlevi önceki Fibonacci yineleyicisini çekme tarzı yineleyiciye dönüştürmektir. Aşağıdaki gibi
func main() {
n := 10
next, stop := iter.Pull(Fibonacci(n))
defer stop()
for {
fibn, ok := next()
if !ok {
break
}
fmt.Println(fibn)
}
}Çıktı
0
1
1
2
3
5
8
13
21
34Bu şekilde yineleme mantığını next ve stop fonksiyonları ile manuel olarak kontrol edebiliriz. Belki bunu yapmanın gereksiz olduğunu düşünebilirsiniz. Böyle yapmak istiyorsanız neden en baştaki kapalı fonksiyon sürümünü doğrudan kullanmıyorsunuz? Aynı şekilde yinelemeyi kendiniz kontrol edebilirsiniz. Kapalı fonksiyonun kullanımı şu şekildedir
func main() {
fib := Fibonacci(10)
for {
n, ok := fib()
if !ok {
break
}
fmt.Prinlnt(n)
}
}Dönüşüm süreci: kapalı fonksiyon → yineleyici → çekme tarzı yineleyici. Kapalı fonksiyon ile çekme tarzı yineleyicinin kullanımı neredeyse aynıdır, düşünce tarzları aynıdır. İkincisi çeşitli işlemler nedeniyle performans kaybına neden olabilir. Dürüst olmak gerekirse bu gerçekten gereksizdir. Uygulama senaryosu gerçekten çok fazla değildir. Ancak iter.pull, iter.Seq için vardır. Yani itme tarzı yineleyiciyi çekme tarzı yineleyiciye dönüştürmek için vardır. Sadece bir çekme tarzı yineleyici istiyorsanız, özel olarak bunun için bir itme tarzı yineleyici uygulamayı düşünün. Dönüşüm için karmaşıklığı ve performansı gözden geçirin. Tıpkı bu Fibonacci dizisi örneğinde olduğu gibi, bir tur dönüp tekrar başa dönüyorsunuz. Tek avantajı resmi yineleyici standardına uygun olması olabilir.
Hata İşleme
Yineleme sırasında hata oluşursa ne olur? Bunu yield fonksiyonuna ileterek for range'in döndürmesini sağlayabiliriz. Çağrıcının işlemesini sağlayabiliriz. Tıpkı bu satır yineleyici örneğinde olduğu gibi
func ScanLines(reader io.Reader) iter.Seq2[string, error] {
scanner := bufio.NewScanner(reader)
return func(yield func(string, error) bool) {
for scanner.Scan() {
if !yield(scanner.Text(), scanner.Err()) {
return
}
}
}
}::: ipucu
Dikkat edilmesi gereken, ScanLines yineleyicisinin tek kullanımlık olduğu, dosya kapatıldıktan sonra tekrar kullanılamayacağıdır.
:::
İkinci dönüş değerinin error türü olduğunu görebilirsiniz. Kullanımı şu şekildedir
for line, err := range ScanLines(file) {
if err != nil {
fmt.Println(err)
break
}
fmt.Println(line)
}Bu şekilde işlemek sıradan hata işleme ile aynıdır. Çekme tarzı yineleyici de aynıdır
next, stop := iter.Pull2(ScanLines(file))
defer stop()
for {
line, err, ok := next()
if err != nil {
fmt.Println(err)
break
} else if !ok {
break
}
fmt.Println(line)
}Eğer panic oluşursa, her zamanki gibi recovery kullanın.
defer func() {
if err := recover(); err != nil {
fmt.Println("panic:", err)
os.Exit(1)
}
}()
for line, err := range ScanLines(file) {
if err != nil {
fmt.Println(err)
break
}
fmt.Println(line)
}Çekme tarzı yineleyici hala aynıdır, burada göstermeyeceğiz.
Standart Kütüphane
Birçok standart kütüphane de yineleyiciyi destekler. En yaygın kullanılanlar slices ve maps standart kütüphaneleridir. Aşağıda birkaç pratik işlev tanıtılacaktır.
slices.All
func All[Slice ~[]E, E any](s Slice) iter.Seq2[int, E]slices.All slice'ı bir slice yineleyicisine dönüştürür
func main() {
s := []int{1, 2, 3, 4, 5}
for i, n := range slices.All(s) {
fmt.Println(i, n)
}
}Çıktı
0 1
1 2
2 3
3 4
4 5slices.Values
func Values[Slice ~[]E, E any](s Slice) iter.Seq[E]slices.Values slice'ı bir slice yineleyicisine dönüştürür, ancak indeksiz
func main() {
s := []int{1, 2, 3, 4, 5}
for n := range slices.Values(s) {
fmt.Println(n)
}
}Çıktı
1
2
3
4
5slices.Chunk
func Chunk[Slice ~[]E, E any](s Slice, n int) iter.Seq[Slice]slices.Chunk fonksiyonu bir yineleyici döndürür. Bu yineleyici n elemanlı slice'ları çağrıcıya iletir
func main() {
s := []int{1, 2, 3, 4, 5}
for chunk := range slices.Chunk(s, 2) {
fmt.Println(chunk)
}
}Çıktı
[1 2]
[3 4]
[5]slices.Collect
func Collect[E any](seq iter.Seq[E]) []Eslices.Collect fonksiyonu slice yineleyicisini bir slice olarak toplar
func main() {
s := []int{1, 2, 3, 4, 5}
s2 := slices.Collect(slices.Values(s))
fmt.Println(s2)
}Çıktı
[1 2 3 4 5]maps.Keys
func Keys[Map ~map[K]V, K comparable, V any](m Map) iter.Seq[K]maps.Keys map'in tüm anahtarlarını yineleyen bir yineleyici döndürür. slices.Collect ile işbirliği yaparak doğrudan bir slice olarak toplanabilir.
func main() {
m := map[string]int{"one": 1, "two": 2, "three": 3}
keys := slices.Collect(maps.Keys(m))
fmt.Println(keys)
}Çıktı
[three one two]Map sırasız olduğundan, çıktı da sabit değildir
maps.Values
func Values[Map ~map[K]V, K comparable, V any](m Map) iter.Seq[V]maps.Values map'in tüm değerlerini yineleyen bir yineleyici döndürür. slices.Collect ile işbirliği yaparak doğrudan bir slice olarak toplanabilir.
func main() {
m := map[string]int{"one": 1, "two": 2, "three": 3}
keys := slices.Collect(maps.Values(m))
fmt.Println(keys)
}Çıktı
[3 1 2]Map sırasız olduğundan, çıktı da sabit değildir
maps.All
func All[Map ~map[K]V, K comparable, V any](m Map) iter.Seq2[K, V]maps.All bir map'i bir map yineleyicisine dönüştürür
func main() {
m := map[string]int{"one": 1, "two": 2, "three": 3}
for k, v := range maps.All(m) {
fmt.Println(k, v)
}
}Genellikle doğrudan bu şekilde kullanılmaz, diğer veri akışı işleme fonksiyonları ile birlikte kullanılır.
maps.Collect
func Collect[K comparable, V any](seq iter.Seq2[K, V]) map[K]Vmaps.Collect bir map yineleyicisini bir map olarak toplayabilir
func main() {
m := map[string]int{"one": 1, "two": 2, "three": 3}
m2 := maps.Collect(maps.All(m))
fmt.Println(m2)
}collect fonksiyonu genellikle veri akışı işlemenin sonlandırma fonksiyonu olarak kullanılır.
Zincirleme Çağrı
Yukarıda standart kütüphane tarafından sağlanan fonksiyonlar aracılığıyla, bunları veri akışını işlemek için birleştirebiliriz. Örneğin veri akışını sıralamak gibi. Aşağıdaki gibi
sortedSlices := slices.Sorted(slices.Values(s))Go'nun yineleyicisi kapalı fonksiyon kullanır. Sadece bu şekilde iç içe fonksiyon çağrıları yapılabilir. Kendisi zincirleme çağrı yapamaz. Çağrı zinciri uzadıktan sonra okunabilirlik çok zayıf olur. Ancak yapıyı kaydetmek için yineleyiciyi kendimiz uygulayabiliriz. Zincirleme çağrıyı gerçekleştirebiliriz.
demo
Basit bir zincirleme çağrı demosu aşağıda gösterilmiştir. Filter, Map, Find, Some gibi yaygın işlevleri içerir.
package iterx
import (
"iter"
"slices"
)
type SliceSeq[E any] struct {
seq iter.Seq2[int, E]
}
func (s SliceSeq[E]) All() iter.Seq2[int, E] {
return s.seq
}
func (s SliceSeq[E]) Filter(filter func(int, E) bool) SliceSeq[E] {
return SliceSeq[E]{
seq: func(yield func(int, E) bool) {
// İndeksi yeniden düzenle
i := 0
for k, v := range s.seq {
if filter(k, v) {
if !yield(i, v) {
return
}
i++
}
}
},
}
}
func (s SliceSeq[E]) Map(mapFn func(E) E) SliceSeq[E] {
return SliceSeq[E]{
seq: func(yield func(int, E) bool) {
for k, v := range s.seq {
if !yield(k, mapFn(v)) {
return
}
}
},
}
}
func (s SliceSeq[E]) Fill(fill E) SliceSeq[E] {
return SliceSeq[E]{
seq: func(yield func(int, E) bool) {
for i, _ := range s.seq {
if !yield(i, fill) {
return
}
}
},
}
}
func (s SliceSeq[E]) Find(equal func(int, E) bool) (_ E) {
for i, v := range s.seq {
if equal(i, v) {
return v
}
}
return
}
func (s SliceSeq[E]) Some(match func(int, E) bool) bool {
for i, v := range s.seq {
if match(i, v) {
return true
}
}
return false
}
func (s SliceSeq[E]) Every(match func(int, E) bool) bool {
for i, v := range s.seq {
if !match(i, v) {
return false
}
}
return true
}
func (s SliceSeq[E]) Collect() []E {
var res []E
for _, v := range s.seq {
res = append(res, v)
}
return res
}
func (s SliceSeq[E]) Sort(cmp func(x, y E) int) []E {
collect := s.Collect()
slices.SortFunc(collect, cmp)
return collect
}
func (s SliceSeq[E]) SortStable(cmp func(x, y E) int) []E {
collect := s.Collect()
slices.SortStableFunc(collect, cmp)
return collect
}
func Slice[S ~[]E, E any](s S) SliceSeq[E] {
return SliceSeq[E]{seq: slices.All(s)}
}Sonra zincirleme çağrı ile işleyebiliriz. Birkaç kullanım örneğine bakalım.
Eleman değerini işle
func main() {
s := []string{"apple", "banana", "cherry"}
all := iterx.Slice(s).Map(strings.ToUpper).All()
for i, v := range all {
fmt.Printf("index: %d, value: %s\n", i, v)
}
}Çıktı
index: 0, value: APPLE
index: 1, value: BANANA
index: 2, value: CHERRYBelirli bir değeri bul
func main() {
s := []int{1, 2, 3, 4, 5}
result := iterx.Slice(s).Find(func(i int, e int) bool {
return e == 3
})
fmt.Println(result)
}Çıktı
3Slice'ı doldur
func main() {
s := []int{1, 2, 3, 4, 5}
result := iterx.Slice(s).Fill(6).Collect()
fmt.Println(result)
}Çıktı
[6 6 6 6 6]Elemanları filtrele
func main() {
s := []int{1, 2, 3, 4, 5}
filter := iterx.Slice(s).Filter(func(i int, e int) bool {
return e%2 == 0
}).All()
for i, v := range filter {
fmt.Printf("Index: %d, Value: %d\n", i, v)
}
}Çıktı
Index: 0, Value: 2
Index: 1, Value: 4Go'nun henüz kısaltılmış anonim fonksiyonları desteklemediğini belirtmek gerekir. js, rust, java'daki ok fonksiyonları gibi. Aksi takdirde zincirleme çağrı daha özlü ve zarif olabilir.
Performans
Go yineleyici için birçok işlem yaptığından, performansı kesinlikle yerel for range döngüsü kadar iyi değildir. En basit slice yinelemesinin performans farkını test edelim. Aşağıdaki birkaç türe ayrılır
- Yerel for döngüsü
- İtme tarzı yineleyici
- Çekme tarzı yineleyici
Test kodu şu şekildedir. Test slice uzunluğu 1000'dir.
package main
import (
"iter"
"slices"
"testing"
)
var s []int
const n = 10000
func init() {
for i := range n {
s = append(s, i)
}
}
func testNaiveFor(s []int) {
for i, n := range s {
_ = i
_ = n
}
}
func testPushing(s []int) {
for i, n := range slices.All(s) {
_ = i
_ = n
}
}
func testPulling(s []int) {
next, stop := iter.Pull2(slices.All(s))
for {
i, n, ok := next()
if !ok {
stop()
return
}
_ = i
_ = n
}
}
func BenchmarkNaive_10000(b *testing.B) {
for range b.N {
testNaiveFor(s)
}
}
func BenchmarkPushing_10000(b *testing.B) {
for range b.N {
testPushing(s)
}
}
func BenchmarkPulling_10000(b *testing.B) {
for range b.N {
testPulling(s)
}
}Test sonucu şu şekildedir
goos: windows
goarch: amd64
pkg: golearn
cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
BenchmarkNaive_10000
BenchmarkNaive_10000-16 492658 2398 ns/op 0 B/op 0 allocs/op
BenchmarkPushing_10000
BenchmarkPushing_10000-16 315889 3707 ns/op 0 B/op 0 allocs/op
BenchmarkPulling_10000
BenchmarkPulling_10000-16 2016 574509 ns/op 440 B/op 14 allocs/op
PASS
ok golearn 4.029sSonuçlardan görebileceğimiz gibi, itme tarzı yineleyici ile yerel for range döngüsü arasında çok büyük bir fark yoktur. Ancak çekme tarzı yineleyici önceki ikisinden neredeyse iki kat daha yavaştır. Kullanırken kendi durumunuza göre düşünebilirsiniz.
Özet
Jenerik durumuna benzer şekilde, Go yineleyicisi de tartışmalıdır. Bazı kişilerin görüşüne göre yineleyici çok fazla karmaşıklık getirir, Go'nun sadelik felsefesine aykırıdır. Bu tür yineleyici kapalı fonksiyon kodu çoğaldıktan sonra, hata ayıklama zor olabilir, okuma daha da sinir bozucu olur.
Yineleyici hakkında şiddetli tartışmaları birçok yerde görebilirsiniz
- Why People are Angry over Go 1.23 Iterators, bir yabancı abi tarafından yineleyici hakkında değerlendirme, okumaya değer
- golang/go · Discussion #56413, rsc tarafından başlatılan topluluk tartışması, birçok kişi görüşünü belirtti
Go yineleyicisine mantıklı bir şekilde bakalım. Gerçekten kod yazmayı daha uygun hale getirir. Özellikle slice türlerini işlerken. Ancak aynı zamanda biraz karmaşıklık getirir. Yineleyici bölümünün kod okunabilirliği azalır. Ancak genel olarak, bence bu gerçekten pratik bir özelliktir.
