Unity C# Delegate ve Event’ler

Yayınlandı: 29 Ekim 2019 yasirkula tarafından Oyun Tasarımı, UNITY 3D içinde

Yeniden merhabalar,

Bu derste C#‘taki delegate ve event türlerinden bahsedip, ilaveten event’lerin Unity‘e has bir başka sürümü olan UnityEvent‘i göstereceğim. Son olarak da, konuyla epey alakalı olduğu için lambda expression konseptinden bahsedeceğim.

Hazırsanız başlayalım!

Delegate ve event; daha önce hiç karşılaşmadıysanız isimleri bile korkutucu olabilir. Ancak kullanımları nispeten basit ve C# evreninde çok fazla yerde kullanılıyorlar. Örnek vereyim mi? AdMob kullanıyorsanız, oradaki OnAdClosed ve OnAdRewarded gibi değişkenler aslında birer event. Peki nedir bu event ve delegate’ler? Cevabı basit: içerisinde değer olarak fonksiyon tutan veri türleri. Yani nasıl int içerisinde bir tamsayı, bool içerisinde true ya da false tutuyorsa, delegate ve event’ler de içerisinde A fonksiyonunu, B fonksiyonunu vs. tutar. Ardından delegate/event vasıtasıyla bu fonksiyon(lar)ı istediğimiz zaman çağırabiliriz.

Delegate

Burada bilmemiz gereken iki şey var: delegate tanımlamak (declaration) ve delegate objesi oluşturmak (instantiation). Tanımlama esnasında, bu delegate’e değer olarak verebileceğimiz fonksiyonların almaları gereken parametreleri ve döndürmeleri gereken veri türünü (void, int vs.) belirliyoruz. O delegate’ten türeyen objelere sadece o parametrelere sahip fonksiyonları değer olarak verebiliriz. Basit bir örnek görelim:

using UnityEngine;

public class DelegateTest : MonoBehaviour 
{
	// Delegate tanımlaması
	public delegate void SayiDelegate( int sayi );

	// Delegate objesi oluşturmak
	public SayiDelegate delegateObjesi;
}

Delegate tanımlamaları 5 parçadan oluşur:

  1. Delegate’in erişilebilirliği: public, protected, private. Tıpkı class’larda olduğu gibi, bu değer delegate’e dışarıdan erişip erişemeyeceğimizi belirler. Verdiğim örnekte SayiDelegate‘in erişilebilirlik değeri public olduğu için herhangi bir class içerisinde bu delegate’in objelerini oluşturabiliriz ama diyelim private olsaydı DelegateTest sınıfı haricinde SayiDelegate’e erişemezdik.
  2. delegate kelimesi, tanımlama esnasında bunu koymak zorundasınız.
  3. Hedef fonksiyonların döndürdüğü veri türü: SayiDelegate objelerine değer olarak verebileceğimiz fonksiyonların ne döndürmesi gerektiğini belirler.
  4. Delegate’in ismi: Nasıl her class’ın eşsiz bir ismi olması gerekiyorsa, her delegate’in de eşsiz bir ismi olmak zorunda. Bizim örneğimizde delegate’in ismi SayiDelegate. Delegate isimlerine büyük harfle başlamak standarttır.
  5. Hedef fonksiyonların aldıkları parametreler: SayiDelegate objelerine değer olarak verebileceğimiz fonksiyonların hangi parametreleri almaları gerektiğini belirler. Bizim örneğimizde bu fonksiyonlar bir int parametre almak zorunda.

Delegate objesi oluşturmak kısmı ise oldukça basit; burada sanki tanımladığımız delegate’i bir class’mış gibi düşünebiliriz: nasıl normalde “public GameObject go;” diye değişken tanımlıyorduysak, aynı şekilde “public Delegateİsmi delegateObjesiAdı;” şeklinde delegate objesi oluşturuyoruz. Değer olarak fonksiyon tutan şeyler işte bu delegate objeleri (örnekteki delegateObjesi değişkeni).

Şu anda delegate objemize değer olarak bir fonksiyon vermedik, o yüzden şu anda bu obje bir işimize yaramıyor. Dilerseniz nasıl bu objeye değer olarak bir fonksiyon verebileceğimize bakalım:

using UnityEngine;

public class DelegateTest : MonoBehaviour
{
	// Delegate tanımlaması
	public delegate void SayiDelegate( int sayi );

	// Delegate objesi oluşturmak
	public SayiDelegate delegateObjesi;

	private void Start()
	{
		delegateObjesi = SayiTest1; // Çalışır
		delegateObjesi = SayiTest2; // Çalışır
		//delegateObjesi = SayiTest3; // Hata: döndürdüğü veri türü yanlış
		//delegateObjesi = SayiTest4; // Hata: aldığı parametre türü yanlış
		//delegateObjesi = SayiTest5; // Hata: aldığı parametreler yanlış
	}

	private void SayiTest1( int deger ) // Parametrenin ismi delegate ile aynı olmak zorunda değil!
	{
		Debug.Log( deger );
	}

	public static void SayiTest2( int i ) // Fonksiyon static olabilir
	{
		i = i * 2;
		Debug.Log( i );
	}

	public bool SayiTest3( int sayi )
	{
		return sayi == 0;
	}

	public void SayiTest4( float f )
	{
		Debug.Log( f );
	}

	public void SayiTest5( int sayi, bool debugLog )
	{
		if( debugLog )
			Debug.Log( sayi );
	}
}

Gördüğünüz üzere, tıpkı değişkenlere “=” ile değer verdiğimiz gibi, delegate objelerine de “delegateObjesi = Fonksiyonİsmi;” şeklinde değer veriyoruz. İlk 2 fonksiyon delegateObjesi’ne sorunsuz bir şekilde değer olarak verilebilirken, diğer 3 fonksiyon hata vermekte. Bunlara sırayla bakacak olursak:

  • SayiTest1: Fonksiyon void döndürüyor ve bir int parametre alıyor, o yüzden delegate objesi için geçerli bir fonksiyon. Burada şunları öğreniyoruz: fonksiyon public olmak zorunda değil ve parametrenin ismi, delegate tanımlamasında verdiğimiz isimle aynı olmak zorunda değil. Delegate tanımlarken SayiDelegate’i public yaptık ama fonksiyon private diyebilirsiniz; SayiDelegate’in public olup olmaması sadece hangi class’larda SayiDelegate objesi oluşturabileceğimizi etkiler, ona değer olarak verdiğimiz fonksiyonların public olup olmamasını etkilemez/kısıtlamaz.
  • SayiTest2: Delegate’lere static fonksiyonları da değer olarak verebiliriz, o yüzden bu da geçerli bir fonksiyon.
  • SayiTest3: void değil bool döndürdüğü için geçersiz bir fonksiyon.
  • SayiTest4: int değil float parametre aldığı için geçersiz bir fonksiyon.
  • SayiTest5: 1 değil 2 parametre aldığı için geçersiz bir fonksiyon.

Delegate objemize değer olarak fonksiyon verdik, peki bunu nasıl çağıracağız? Çok basit:

private void Start()
{
	delegateObjesi = SayiTest1;
	delegateObjesi = SayiTest2;

	delegateObjesi( 5 );
}

Tıpkı bir fonksiyon çağırır gibi! Burada parametre olarak girdiğimiz 5 değeri, delegate objesine değer olarak verdiğimiz fonksiyonun parametresine aktarılır. Yani şu anda SayiTest2(5); ile aynı şeyi yapmış oluyoruz.

Kodu test edecek olursanız konsola “10” yazdırıldığını göreceksiniz. Peki SayiTest1’in Debug.Log yapması gereken “5” niye yazdırılmadı? Çünkü “delegateObjesi = birFonksiyon;” ibaresi kullanınca, delegate objesinin içerisinde başka bir fonksiyon varsa o fonksiyon değer olmaktan çıkar ve yeni değer birFonksiyon olur. Ama endişelenmeyin çünkü delegate objelerine birden çok fonksiyonu değer olarak verebiliriz:

private void Start()
{
	delegateObjesi += SayiTest1;
	delegateObjesi += SayiTest2;

	delegateObjesi( 5 );
}

Tek yaptığımız “=” ibaresini “+=” ile değiştirmek oldu! Artık konsola önce “5”, sonra “10” yazdırılıyor. Benzer şekilde, “-=” ile de bir fonksiyonu delegate objesinden çıkarabilirsiniz. Eğer çıkarmak istediğiniz fonksiyon halihazırda delegate objesine eklenmemişse hiçbir şey olmaz, bir hata oluşmaz.

Aynı fonksiyonu bir delegate objesine birden çok kez ekleyebilirsiniz. Bu durumda o fonksiyonu delegate’e kaç kere eklediyseniz, fonksiyon o kadar kez çağrılır. Bu yüzden bir fonksiyonun bir delegate objesine sadece bir kere eklendiğinden %100 emin olmak istiyorsanız, önce -= ve ardından += yapabilirsiniz:

delegateObjesi -= SayiTest1;
delegateObjesi += SayiTest1;

DelegateTest örneğinde sadece Start’ta delegate objesine değer veriyoruz, o yüzden burada buna gerek yok ama bazen çok daha karmaşık senaryolarla uğraşıyor ve bir fonksiyonu bir delegate’e birden çok yerde değer olarak veriyor olabilirsiniz, işte bu durumlarda bu -= ve += olayı önem kazanabilir.

NOT: İçerisinde hiç bir fonksiyon tutmayan bir delegate objesini delegateObjesi(5); şeklinde çağırmaya kalkarsanız NullReferenceException alırsınız, bunun için bir delegate’i çağırmadan önce daima delegate objesinin null olup olmadığına bakın:

if( delegateObjesi != null )
	delegateObjesi( 5 );

Delegate tanımlarken void ile kısıtlı olmadığımızdan bahsetmiştim. Burada akla şu soru gelebilir: int döndüren bir delegate’im varsa ve bu delegate objesine iki fonksiyon eklediysem, “int sonuc = delegateObjesi(5);” yaptığımda hangi fonksiyonun döndürdüğü değere erişeceğim? İşte bu güzel bir soru. Cevap: belirsiz. Eğer delegate objesindeki her iki fonksiyonun da döndürdüğü değerlere ayrı ayrı erişmek istiyorsanız kodunuzda biraz değişiklik yapmanız gerekecek:

using UnityEngine;

public class DelegateTest : MonoBehaviour
{
	// Delegate tanımlaması
	public delegate int SayiDelegate2( string yazi );

	// Delegate objesi oluşturmak
	public SayiDelegate2 delegateObjesi;

	private void Start()
	{
		delegateObjesi += SayiTest1;
		delegateObjesi += SayiTest2;

		if( delegateObjesi != null ) // Aslında delegateObjesi'nin boş olmadığını bildiğimiz için burada gerek yok
		{
			System.Delegate[] fonksiyonlar = delegateObjesi.GetInvocationList();
			for( int i = 0; i < fonksiyonlar.Length; i++ )
			{
				int sonuc = ( (SayiDelegate2) fonksiyonlar[i] ).Invoke( "Test" );
				Debug.Log( sonuc );
			}
		}
	}

	private int SayiTest1( string yazi )
	{
		return yazi.Length;
	}

	public static int SayiTest2( string y )
	{
		return -1;
	}
}

Öncelikle delegateObjesi.GetInvocationList(); ile, delegate objesindeki tüm fonksiyonları bir array’de tutuyoruz. Bu array’in türü System.Delegate; aslında tüm delegate objeleri arkaplanda birer System.Delegate sınıfı objeleridir. Fonksiyonlara eriştikten sonra, bir for döngüsü içerisinde bu fonksiyonları Invoke vasıtasıyla tek tek çağırıyoruz. Invoke kullanabilmek için öncelikle System.Delegate objelerimizi SayiDelegate2‘ye typecast yapmak zorundayız.

En neticede yukarıdaki kodu çalıştırırsanız, konsola “4” ve “-1” yazdırıldığını göreceksiniz.

Bu şekilde delegate’leri kabaca noktalamış olduk. Ama dipnot olarak şunları da ekleyelim:

  • Delegate objeleri tıpkı birer değişken gibi oldukları için, onları fonksiyonlarınıza parametre olarak verebilirsiniz.
  • Delegate tanımlamalarını bir class içerisinde yapmak zorunda değilsiniz. Örneğin yukarıdaki SayiDelegate2’yi DelegateTest sınıfı içerisinde tanımladığımız için ona diğer sınıflardan DelegateTest.SayiDelegate2 şeklinde erişmek zorundayız. Ama tanımlama işlemini class’ın dışında yaparsak, her yerden direkt SayiDelegate2 diye erişebiliriz.
  • Bir objenin fonksiyon(lar)ını bir delegate objesine eklerseniz ve daha sonra bu obje yok olursa (mesela Destroy(gameObject) ile), objenin fonksiyonları delegate objesinden otomatik olarak silinmez. Buna dikkat etmezseniz çok başınız ağrıyabilir. Bu yüzden obje yok olmadan önce elle fonksiyonları delegate objesinden çıkarmalısınız. Script’lerinizde bu işlemi OnDestroy() içerisinde yapabilirsiniz.
  • Bir kod incelerken zaman zaman System.Action veya System.Func türünde değişkenlere denk gelebilirsiniz. Bunlar da aslında birer delegate tanımlamalarıdır. Dilerseniz isimli bir delegate tanımlamak yerine (SayiDelegate), direkt bu delegate tanımlamalarını da kullanabilirsiniz. Şöyle ki:
public delegate void SayiDelegate( int sayi );
public delegate int SayiDelegate2( string yazi );
	
//public SayiDelegate delegateObjesi; // SayiDelegate ile
public System.Action<int> delegateObjesi; // System.Action ile

//public SayiDelegate2 delegateObjesi2; // SayiDelegate2 ile
public System.Func<string, int> delegateObjesi2; // System.Func ile
  • System.Action‘da “<” ve “>” işaretleri (generic) arasına, delegate’in aldığı parametreleri yazabilirsiniz. System.Action delegate’ler void döndürürler. System.Func delegate’ler ise void harici bir şey döndürürler ve bu döndürdükleri türü, “<” ve “>” işaretlerinin son parametresi olarak alırlar; ondan önceki parametreler ise, fonksiyonun aldığı parametreleri belirtir.

Event

Event’ler hemen hemen delegate objeleri ile aynı şeylerdir. İkisi de bir delegate tanımlamasına ihtiyaç duyarlar, ikisi de içerisinde değer olarak fonksiyonlar tutarlar. Ama aralarında başlıca 3 fark vardır:

  1. Event objesi tanımlarken event kelimesi kullanılmalıdır:
// Delegate tanımlaması
public delegate void SayiDelegate( int sayi );

// Delegate objesi oluşturmak
public SayiDelegate delegateObjesi;

// Event objesi oluşturmak
public event SayiDelegate eventObjesi;
  1. Event objelerine fonksiyonları sadece “+=” ile ekleyebilirsiniz, “=” yapmaya kalkarsanız hata alırsınız. Delegate’lerdeki “-=” olayı event’lerde de bulunmaktadır.
  2. Event objesindeki fonksiyonları eventObjesi() şeklinde çağırma işlemini sadece event objesinin tanımlandığı sınıfta yapabilirsiniz. Örneğin eventObjesi’ni A sınıfında tanımladıysanız, A sınıfı harici bir sınıf eventObjesi(); kodunu çalıştıramaz.

UnityEvent

Hiç UI Button objesinin On Click event’ine ve bu event’e nasıl Inspector’dan fonksiyonlar ekleyebildiğinize dikkat ettiniz mi? On Click aslında bir UnityEvent’tir. İşte UnityEvent’in event’e göre en büyük artısı budur: kendisine illa koddan değil, Inspector’dan da fonksiyon ekleyebilirsiniz. Ama bunun yanında önemli bir eksisi de vardır: System.Func gibi delegate’lerin aksine, UnityEvent’ler void harici bir şey döndürmeyi desteklemezler.

İsterseniz daha önceki SayiDelegate örneğimizi UnityEvent’e çevirmeyi görelim:

using UnityEngine;
using UnityEngine.Events; // <-- Buraya dikkat!

public class DelegateTest : MonoBehaviour
{
	// Delegate tanımlaması
	public delegate void SayiDelegate( int sayi );

	// UnityEvent tanımlaması
	[System.Serializable] public class SayiUnityEvent : UnityEvent<int> { }

	// Delegate objesi
	public SayiDelegate delegateObjesi;

	// UnityEvent objesi
	public SayiUnityEvent unityEventObjesi;

	private void Start()
	{
		// Fonksiyon eklemek
		delegateObjesi += SayiTest1;
		unityEventObjesi.AddListener( SayiTest1 );

		// Fonksiyonları çağırmak
		if( delegateObjesi != null )
			delegateObjesi( 5 );

		if( unityEventObjesi != null )
			unityEventObjesi.Invoke( 5 );

		// Fonksiyon çıkarmak
		delegateObjesi -= SayiTest1;
		unityEventObjesi.RemoveListener( SayiTest1 );

		// Tüm fonksiyonları çıkarmak
		delegateObjesi = null;
		unityEventObjesi.RemoveAllListeners();
	}

	private void SayiTest1( int deger )
	{
		Debug.Log( deger );
	}
}
  • UnityEvent’lerle çalışırken script’inizin başına “using UnityEngine.Events;” eklemelisiniz.
  • UnityEvent tanımlaması, UnityEvent class’ından türeyen ve System.Serializable attribute‘üne sahip yeni bir sınıf oluşturarak yapılır.
  • +=“in karşılığı AddListener, “-=“in karşılığı RemoveListener‘dır.
  • UnityEvent’e atalı fonksiyonlar Invoke ile çağrılır.

Bu noktalara dikkat ederseniz, Inspector’dan da üzerine fonksiyon eklemesi yapabildiğiniz event’lere sahip olacaksınız:

Lambda Expression

Kulağa delegate ve event’ten daha korkutucu gelen bir şey varsa herhalde o da lambda expression’dır. Ancak aslında kullanımı çok kolaydır. Kabaca bahsedecek olursak, lambda expression dediğimiz şey, kod içerisinde dinamik olarak isimsiz fonksiyonlar oluşturmaya yarar. C# dünyasında lambda expression’lar, delegate’ler ile birlikte kullanılırlar. O halde bodoslama dalış yapalım:

using UnityEngine;

public class DelegateTest : MonoBehaviour
{
	public delegate void SayiDelegate( int sayi );

	public SayiDelegate delegateObjesi;

	private void Start()
	{
		delegateObjesi += ( int deger ) =>
		{
			Debug.Log( deger );
		};

		delegateObjesi( 5 );
	}
}

Burada, SayiTest1 fonksiyonumuzu şu şekilde bir lambda expression’a çevirdik:

( int deger ) =>
{
	Debug.Log( deger );
};

Lambda expression’lar şu şekilde tanımlanır:

  1. Normal bir fonksiyon tanımlar gibi, parantezler açılır ve içerisine parametreler girilir.
  2. Lambda expression’lara has “=>” (lambda işleci) konulur.
  3. Süslü parantezler içerisine fonksiyonun kodu yazılır.

İstersek bu lambda expression’ı şu şekilde de yazabiliriz: “( deger ) => Debug.Log( deger );“. Çünkü tek satırlık gövdeye sahip lambda expression’larında süslü parantezler zorunlu değildir. İlaveten, lambda expression’ı değer olarak verdiğimiz delegateObjesi‘nin int parametre aldığı zaten bilindiği için, parametrenin türünü lambda expression’da tekrardan yazmak zorunda değiliz.

Lambda expression’ları çok güçlü yapan özelliklerden birisi, kendisini çevreleyen fonksiyonun parametrelerine ve değişkenlerine erişim imkanı olmasıdır:

using UnityEngine;

public class DelegateTest : MonoBehaviour
{
	public delegate void SayiDelegate( int sayi );

	public SayiDelegate delegateObjesi;

	private void Start()
	{
		int sayi = 10;
		delegateObjesi += ( deger ) => Debug.Log( deger * sayi );

		delegateObjesi( 5 );
	}
}

Bu kodu çalıştırınca konsola “50” bastırılır. Burada dikkat etmeniz gereken bir nokta, for loop’larında sıklıkla kullanılan i değişkenini lambda expression’a dahil etmek istediğinizde karşınıza çıkar:

private void Start()
{
	for( int i = 1; i <= 5; i++ )
	{
		delegateObjesi += ( deger ) => Debug.Log( deger * i );
	}

	delegateObjesi( 5 );
}

Bu kodu çalıştırırsanız, konsola “5”, “10”, “15”, “20”, “25” bastırılmasını beklerken onun yerine beş defa “25” bastırılır (C#’ın yeni sürümlerinde bu sorun giderilmiş olabilir). Bu sorunu şu şekilde çözmek zorundasınız:

for( int i = 1; i <= 5; i++ )
{
	int iKopya = i;
	delegateObjesi += ( deger ) => Debug.Log( deger * iKopya );
}

Yani for‘un içinde i değişkeninin bir kopyasını oluşturup, lambda expression’da bu kopya değişkeni kullanmalısınız.

Lambda expression’larla ilgili çok dikkat edilmesi gereken bir nokta, “-=” kullanımıdır. Lambda expression’lar bunu doğrudan desteklemez. Örneğin şu koddaki -= satırı bir işe yaramaz:

delegateObjesi += ( deger ) => Debug.Log( deger * iKopya );
delegateObjesi -= ( deger ) => Debug.Log( deger * iKopya );

Çünkü her yeni lambda expression, aslında arkaplanda ayrı bir fonksiyondur. Lambda expression’larda “-=“i ancak şu şekilde destekleyebilirsiniz:

private void Start()
{
	SayiDelegate lambdaObjesi = ( deger ) => Debug.Log( deger );
	delegateObjesi += lambdaObjesi;

	delegateObjesi( 5 );

	delegateObjesi -= lambdaObjesi;
}

Yani önce lambda expression’ı bir delegate objesine (lambdaObjesi) değer olarak vermeli, ardından artık bu delegate objesini “+=” ve “-=” ile kullanmalısınız.

Bonus: Örnek Kod

Yazıya son vermeden önce, dilerseniz lambda expression’ları ciddi bir örnek üzerinde görelim. Bu örnekte GecikmeliCalistir.Geciktir isminde bir fonksiyon yer almakta; bu fonksiyon, kendisine parametre olarak girilen delegate objesini birkaç saniye gecikmeli olarak çağırmaya yarıyor. Örnek kodda bu fonksiyonu çağırırken, delegate objesine değer olarak bir lambda expression giriyoruz. Bu şekilde o lambda expression’ın gecikmeli olarak çağrılmasını sağlıyoruz:

using System.Collections;
using UnityEngine;

public class GecikmeliCalistir : MonoBehaviour
{
	// Bu class singleton prensibi üzerine kurulu, yani bu class'tan oyun esnasında sadece bir tane olacak
	private static GecikmeliCalistir instance;

	// "fonksiyon" delegate objesini saniye kadar geciktirerek çalıştırır
	// Delegate tanımlamamız System.Action, yani parametre almayan ve void döndüren fonksiyonlar
	public static void Geciktir( float saniye, System.Action fonksiyon )
	{
		// Eğer singleton objemiz henüz yoksa, yeni bir obje oluşturup onu singleton olarak ayarla
		if( instance == null )
		{
			instance = new GameObject().AddComponent<GecikmeliCalistir>();

			// Singleton objenin sahneler arası geçişlerde yok olmasını önle
			DontDestroyOnLoad( instance.gameObject );
		}

		// Delegate objesi null değilse bir coroutine başlat
		if( fonksiyon != null )
			instance.StartCoroutine( SaniyeliGeciktirCoroutine( saniye, fonksiyon ) );
	}

	// "saniye" kadar bekledikten sonra "fonksiyon" delegate objesini çağıran coroutine
	// Coroutine'ler hakkında daha fazla bilgi için: https://yasirkula.com/2018/11/20/unity-3d-coroutineler/
	private static IEnumerator SaniyeliGeciktirCoroutine( float saniye, System.Action fonksiyon )
	{
		// "saniye" kadar bekle
		yield return new WaitForSeconds( saniye );

		// "fonksiyon" delegate objesindeki fonksiyonları çağır
		fonksiyon();
	}
}
void Start()
{
	// Delegate objesi (bu örnekte aşağıdaki lambda expression) çağrılmadan önceki süreyi (Time.time) depola
	float geciktiktenOnceZaman = Time.time;

	// Lambda expression'ı 2.5 saniye gecikmeli çalıştır
	GecikmeliCalistir.Geciktir( 2.5f, () =>
	{
		// Bu kod her ne kadar Start fonksiyonu içerisinde yer alsa da, aslında Start fonksiyonundan
		// 2.5 saniye sonra çağrılıyor. Bunu göstermek için tekrar Time.time'ı bir değişkene at
		// ve iki Time.time değerini konsola yazdır
		float geciktiktenSonraZaman = Time.time;
		Debug.Log( "Önce: " + geciktiktenOnceZaman + ", Sonra: " + geciktiktenSonraZaman );
	} );
}

Ve bu şekilde bu yazımı noktalıyorum. Biraz uzun oldu ama umarım faydalı olmuştur. Sonraki derslerde görüşmek ümidiyle!

Cevap Yazın

Aşağıya bilgilerinizi girin veya oturum açmak için bir simgeye tıklayın:

WordPress.com Logosu

WordPress.com hesabınızı kullanarak yorum yapıyorsunuz. Çıkış  Yap /  Değiştir )

Google fotoğrafı

Google hesabınızı kullanarak yorum yapıyorsunuz. Çıkış  Yap /  Değiştir )

Twitter resmi

Twitter hesabınızı kullanarak yorum yapıyorsunuz. Çıkış  Yap /  Değiştir )

Facebook fotoğrafı

Facebook hesabınızı kullanarak yorum yapıyorsunuz. Çıkış  Yap /  Değiştir )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.