Ruby'de blok, bir metot çağrısına eklediğiniz ve metodun istediği zaman çalıştırabildiği kod parçasıdır. Bloğu { ... } ya da do ... end ile yazarsınız; metot içinde yield bloğu çağırır, block_given? blok verilip verilmediğini söyler, &block parametresi ise bloğu bir Proc nesnesi olarak yakalar. İki yazım arasındaki tek gerçek fark önceliktir: do ... end süslü paranteze göre daha zayıf bağlanır.

Bloklar Ruby'nin en karakteristik özelliklerinden biridir; each, map ve select gibi metotların hepsi bloklarla çalışır. Bu yazıda kendi metodunuzda blok kabul etmeyi, bloğa değer gönderip sonucunu kullanmayı ve blokların değişkenlerle ilişkisini anlatıyoruz. Örnekler Ruby 4.0 ile çalıştırıldı. Blok parametrelerinin ayrıntıları (it, _1) için blok parametreleri yazısına, blokların nesne hâli için Proc ve lambda yazısına bakabilirsiniz.

Blok nasıl verilir?

Blok nasıl verilir?

Belgelere göre blok, çağıran kapsamdan metoda gönderilen bir closure'dır ve çağrıda her zaman en sonda yer alır. Blok parametreleri |...| arasına yazılır ve metot parametrelerine benzer kurallarla tanımlanır. Topluluk arasında yaygın alışkanlık, tek satırlık bloklarda süslü parantez, çok satırlılarda do ... end kullanmaktır; ancak bu bir dil kuralı değildir.

ruby
[1, 2, 3].each { |x| puts x * 10 }
# çıktı: 10
# çıktı: 20
# çıktı: 30

[1, 2].each do |x|
  kare = x * x
  puts "#{x} karesi #{kare}"
end
# çıktı: 1 karesi 1
# çıktı: 2 karesi 4

yield ile bloğu çağırmak

Kendi metodunuzda bloğu çalıştırmak için yield yazarsınız. yield istediğiniz kadar çağrılabilir; her çağrıda blok yeniden çalışır. yield argüman alırsa bunlar blok parametrelerine gider ve yield ifadesinin değeri, bloğun son ifadesinin değeridir. Bu sayede metot, bloğun hesapladığı sonucu kullanabilir.

ruby
def iki_kez
  yield
  yield
end
iki_kez { puts "merhaba" }
# çıktı: merhaba
# çıktı: merhaba

def kare_al(sayi)
  sonuc = yield(sayi)
  "blok #{sonuc} döndürdü"
end
p kare_al(4) { |x| x * x } # çıktı: "blok 16 döndürdü"

Belgeler, bloğu yalnızca çağıracaksanız ve onu başka bir yere aktarmayacaksanız, açık bir blok parametresi tanımlamak yerine yield kullanmayı önerir.

block_given? ile bloğun varlığını kontrol etmek

Blok olmadan yield çalışırsa Ruby LocalJumpError verir. Kernel#block_given?, o anki bağlamda yield çağrısının bir blok çalıştırıp çalıştıramayacağını söyler. Bloğu isteğe bağlı tutan metotlar bu kontrolle iki farklı davranış sunabilir.

ruby
def dene
  if block_given?
    yield
  else
    "blok yok"
  end
end
p dene              # çıktı: "blok yok"
p dene { "merhaba" } # çıktı: "merhaba"

def ver
  yield
end
begin
  ver
rescue LocalJumpError => e
  puts "#{e.class}: #{e.message}" # çıktı: LocalJumpError: no block given (yield)
end

Çekirdek kütüphanedeki pek çok metot bloksuz çağrıldığında bir Enumerator döndürür. Kendi metodunuzda bu davranışı block_given? ve to_enum ile nasıl kuracağınızı Enumerator ve Lazy yazısında bulabilirsiniz.

&block: bloğu nesne olarak yakalamak

&block: bloğu nesne olarak yakalamak

Parametre listesinin sonuna &ad eklerseniz, metoda verilen blok bir Proc nesnesine çevrilir ve bu adla erişilebilir hâle gelir. Böylece bloğu saklayabilir, daha sonra call ile çalıştırabilir ya da başka bir metoda &ad ile aktarabilirsiniz. Blok verilmezse parametrenin değeri nil olur. Blok parametresi her zaman listenin en sonunda olmalıdır.

ruby
def sakla(&blok)
  blok
end
b = sakla { |x| x * 10 }
p b.class   # çıktı: Proc
p b.call(3) # çıktı: 30

def her_birine(liste, &blok)
  liste.each(&blok)
end
her_birine([1, 2]) { |x| puts x }
# çıktı: 1
# çıktı: 2

def yok(&blok) = blok
p yok # çıktı: nil

Bloğu yalnızca başka bir metoda aktaracaksanız ona ad vermeniz de gerekmez: def her_birine(liste, &) = liste.each(&). Ruby 3.1 NEWS dosyasına göre anonim blok argümanı bu sürümle geldi. Çağrı tarafında & operatörü, bir proc ya da lambda nesnesini yeniden bloğa çevirir.

do/end ile süslü parantez farkı: öncelik

İki yazım aynı bloğu üretir, ama belgelerin vurguladığı gibi do ... end bloğu süslü paranteze göre daha düşük önceliklidir. Parantezsiz iç içe çağrılarda süslü parantezli blok en yakındaki (içteki) metoda, do ... end bloğu ise dıştaki metoda bağlanır. Parantez kullanırsanız blok yine dıştaki metoda gider.

ruby
def dis(arg = nil)
  block_given? ? "dis bloğu aldı" : "dis şunu aldı: #{arg}"
end

def ic
  block_given? ? "ic bloğu aldı" : "ic blok almadı"
end

a = dis ic { 1 }
b = dis ic do 1 end
c = dis(ic) { 1 }
p a # çıktı: "dis şunu aldı: ic bloğu aldı"
p b # çıktı: "dis bloğu aldı"
p c # çıktı: "dis bloğu aldı"

Bloklar closure olarak çalışır

Blok, tanımlandığı yerdeki yerel değişkenleri görür ve değiştirebilir. Bloğun içinde ilk kez oluşturulan bir değişken ise blok bitince kaybolur. Dıştaki aynı adlı değişkeni yanlışlıkla değiştirmek istemiyorsanız, belgelerde anlatılan blok-yerel değişken yazımını kullanabilirsiniz: parametre listesinde ; işaretinden sonra yazılan adlar yalnızca blok içinde geçerlidir.

ruby
sayac = 0
[1, 2, 3].each { |x| sayac += x }
p sayac # çıktı: 6

yer = "dünya"
[1].each do |x; yer|
  yer = "blok"
end
p yer # çıktı: "dünya"

[1].each { |x| yeni = x }
p defined?(yeni) # çıktı: nil

Blok içinde next, break ve return

Blok içinde next o turu bitirir ve verdiğiniz değer bloğun sonucu olur. break bloğu çağıran metodun tamamını durdurur; metot çağrısının değeri break ile verdiğiniz değer olur. return ise bloğun içinde bulunduğu metottan çıkar. Bu davranışların proc ve lambda için nasıl değiştiği Proc ve lambda yazısında ayrıntılı anlatılıyor.

ruby
r = [1, 2, 3, 4].map do |x|
  next 0 if x.even?
  x
end
p r # çıktı: [1, 0, 3, 0]

r = [1, 2, 3, 4].each do |x|
  break x * 100 if x == 3
end
p r # çıktı: 300

def ilk_buyuk(liste)
  liste.each { |x| return x if x > 2 }
  nil
end
p ilk_buyuk([1, 5, 3]) # çıktı: 5

Pratik kalıp: önce ve sonra çalışan kod

Blokların en yaygın kullanım biçimlerinden biri, bir işin öncesinde ve sonrasında hep aynı adımların çalışmasını sağlamaktır: bir kaynağı açıp kapatmak, süre ölçmek ya da log yazmak gibi. Metot hazırlığı yapar, asıl işi yield ile bloğa bırakır ve temizliği ensure bölümünde yapar. Metot gövdesi örtük bir begin bloğu gibi davrandığı için ayrıca begin yazmanız gerekmez. Blok bir hata fırlatsa bile ensure çalışır, metodun dönüş değeri ise bloğun sonucu olur.

ruby
def islem(ad)
  puts "#{ad}: başla"
  yield
ensure
  puts "#{ad}: bitir"
end

p islem("rapor") { 42 }
# çıktı: rapor: başla
# çıktı: rapor: bitir
# çıktı: 42

begin
  islem("yedek") { raise "disk dolu" }
rescue => e
  puts "yakalandı: #{e.message}"
end
# çıktı: yedek: başla
# çıktı: yedek: bitir
# çıktı: yakalandı: disk dolu

Ruby'nin dosya işlemlerinde gördüğünüz bloklu kullanım da aynı fikre dayanır; ayrıntılar için dosya ve IO işlemleri yazısına bakabilirsiniz.

Bloklarla ilgili araçlar bir bakışta

AraçNeredeNe işe yarar
yieldMetot gövdesiVerilen bloğu çalıştırır, değerini döndürür
block_given?Metot gövdesiBlok verilmiş mi kontrol eder
&blokParametre listesiBloğu Proc nesnesi olarak yakalar
&Parametre ve çağrıBloğu adsız olarak aktarır (3.1+)
&nesneÇağrıProc veya to_proc destekleyen nesneyi bloğa çevirir
{ }ÇağrıYüksek öncelikli blok, en yakın metoda bağlanır
do endÇağrıDüşük öncelikli blok, dıştaki metoda bağlanır
|x; y|Blok parametresiy blok-yerel değişkendir

Sık yapılan hatalar

Sık yapılan hatalar: Bloğu kontrol etmeden yield çağırmak., puts ile do/end bloğu kullanmak., Blok parametresini listenin ortasına koymak., Blok içinde oluşturulan değişkeni...
  • Bloğu kontrol etmeden yield çağırmak. Blok isteğe bağlıysa block_given? kullanın, yoksa LocalJumpError alırsınız.
  • puts ile do/end bloğu kullanmak. Blok yanlış metoda bağlanır; ya süslü parantez kullanın ya da sonucu önce bir değişkene atayın.
  • Blok parametresini listenin ortasına koymak. &blok her zaman en sonda olmalıdır.
  • Blok içinde oluşturulan değişkeni dışarıda kullanmaya çalışmak. Değişkeni bloktan önce tanımlayın.
  • map içinde break ile değer biriktirmeyi beklemek. break metodun tamamını keser ve yalnızca o değeri döndürür.

Sık Sorulan Sorular

Bir metoda birden fazla blok verilebilir mi?

Hayır, her çağrıya en fazla bir blok verilebilir. Birden fazla kod parçası geçirmeniz gerekiyorsa bunları proc ya da lambda nesneleri olarak normal argüman şeklinde verin.

yield mi &block mı kullanmalıyım?

Bloğu yalnızca çalıştıracaksanız yield belgelerin önerdiği yoldur. Bloğu saklamanız, başka bir metoda aktarmanız ya da nesne olarak incelemeniz gerekiyorsa &block kullanın.

Blok bir nesne midir?

Blok kendi başına bir nesne değildir; &block ile yakalandığında ya da proc ile oluşturulduğunda Proc nesnesine dönüşür. Ayrıntılar için Proc ve lambda farkı yazısına bakın.

Bloklar döngülerin yerine mi geçer?

Ruby'de koleksiyonlar üzerinde gezinmek için genellikle each gibi bloklu metotlar kullanılır. while ve until döngüleriyle karşılaştırma için Ruby döngüleri yazısına bakabilirsiniz.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Ruby öğrenme rehberi · Blok parametreleri, it ve _1 · Proc ve lambda farkı · Enumerator ve Lazy · Enumerable: map, select, reduce · Ruby döngüleri