18 Ocak 2018 Perşembe

C# Reflection (Yansıma) - Yazı Serisi - 2


Merhaba, bir önceki yazımızda Runtime Type Identification - RTTI konusunu ele almıştık. Bu yazımızda da C#'ın en güçlü özelliklerinden biri olan Reflection konusunu inceleyeceğiz.

Reflection Nedir?


Reflection, bir programın runtime sırasında dinamik olarak tip oluşturması, yüklemesi ve kullanması işlemidir. Reflection'a neden ihtiyaç var derseniz; daha büyük projeler yaptıkça göreceksiniz ki, daha dinamik, daha esnek yapılara ihtiyaç duyacaksınız. Bu ihtiyaçlardan biri de Reflection'dur. Reflection, çalışma zamanında (runtime) bir tip hakkında bilgi edinebilmemizi ve bu tipin özelliklerine göre davranabilmemizi sağlar. Bu özellik C#'ın çok güçlü bir özelliğidir. Yansıma denmesinin sebebi, reflection ile bir tipin aynadaki yansıması benzetmesidir. Burda ayna görevi gören yapı Type nesnesidir. Örneklerle konuyu detaylandırmaya çalışacağız.

Reflection'un en önemli yapılarından birisi System.Type nesnesidir. Yukarda bahsettiğimiz bir tip hakkında bilgi edinme işlemi System.Type nesnesi sayesinde yapılır. Type nesnesi System.Reflection.MemberInfo adında bir abstract sınıftan türemiştir.

MemberInfo önemli olduğundan, sahip olduğu özellikleri aşağıda belirtelim:

Type DeclaringType: Üyenin tanımlandığı class veya interfacein tipini elde eder
MemberTypes MemberType: Üyenin türünü elde eder. Örneğin; özellik mi, metot mu gibi bilgileri elde eder.
int MetaDataToken: Metadata iliştirilmiş bir değeri elde eder.
string Name: Üyenin adını elde eder.
Type ReflectedType: Reflection yapılan nesnenin tipini elde eder.

Bunların yanında MemberInfo'nun nitelikler (attributes) ile ilgili iki tane de abstract metodu vardır: GetCustomAttributes() ve IsDefined(). İlki, adından da anlaşılacağı üzere ilgili nesneye iliştirilmiş attributelerin listesini getirir, diğeri de ilgili nesne için tanımlanmış attribute var mı yok mu diye kontrol eder. Nitelikler konusunu bir sonraki yazıda ele alacağız.

Bunun gibi daha birçok metot ve özellik vardır. Hepsini burda ele almak konuyu haddinden fazla uzatacaktır. Hepsi de ismiyle müsemma anlaşılabilir cinstendir. Örneğin; reflectionda kullanılmış bir GetParameters() metodu görürseniz bilin ki bu metodun parametrelerini getirecektir.

Şimdi gelelim bu öğelerin kullanımına.. Bir örnekle başlayalım:

using System; using System.Reflection; class MyClass { int x; int y; public MyClass(int i, int j) { this.y = j; this.x = i; } public int Sum() { return x + y; } public bool IsBetween(int i) { if (x < i && i < y) return true; return false; } public string DummyMethod(int a, int b) { return "Bu da 2 parametreli metot"; } public void Show() { Console.WriteLine("x değeri: " + x + " y değeri: " + y); } } class ReflectionTest { static void Main() { Type t = typeof(MyClass); //Burda MyClass'ın görüntüsü alınır MethodInfo[] mi = t.GetMethods();//Burda MyClass'ın metot listesi getirilir Console.WriteLine("Nesnenin adı: " + t.Name); Console.WriteLine("Nesnenin desteklediği metotlar:"); foreach (MethodInfo m in mi) { //Metot dönüş tipi ve adı ParameterInfo[] pi = m.GetParameters(); Console.WriteLine("Metot Adı: " + m.Name + "Dönüş tipi: " + m.ReturnType.Name); //Parametre varsa parametrelerini göster if (pi.Length <= 0) Console.WriteLine("Parametre yok"); for (int i = 0; i < pi.Length; i++) { Console.Write(i + 1 + ". parametre: Dönüş Değeri: " + pi[i].ParameterType.Name + " Adı: " + pi[i].Name + "\n"); } Console.WriteLine(); } Console.Read(); } }

Bu programın çıktısı aşağıdaki gibi olacaktır:

Nesnenin adı: MyClass
Nesnenin desteklediği metotlar:
Metot Adı: SumDönüş tipi: Int32
Parametre yok

Metot Adı: IsBetweenDönüş tipi: Boolean
1. parametre: Dönüş Değeri: Int32 Adı: i

Metot Adı: DummyMethodDönüş tipi: String
1. parametre: Dönüş Değeri: Int32 Adı: a
2. parametre: Dönüş Değeri: Int32 Adı: b

Metot Adı: ShowDönüş tipi: Void
Parametre yok

Metot Adı: ToStringDönüş tipi: String
Parametre yok

Metot Adı: EqualsDönüş tipi: Boolean
1. parametre: Dönüş Değeri: Object Adı: obj

Metot Adı: GetHashCodeDönüş tipi: Int32
Parametre yok

Metot Adı: GetTypeDönüş tipi: Type
Parametre yok
.........................

Bu örnekte neler yaptık inceleyelim: Öncelikle MyClass'ın görüntüsünü elde ettik. Bunu Type t = typeof(MyClass) ile yaptık. Artık t nesnesi MyClass'ın sahip olduğu özelliklere sahiptir. Ama bu new ile oluşturulan nesnelere benzemez. Bu işlem, reflectionda kullanılabilecek bir tip oluşturur. Zaten amacımız MyClass nesnesi oluşturmak olsaydı bunu şu şekilde yapardık: MyClass mc = new MyClass(); ama amacımız bu değil, amacımız runtime sırasında istediğimiz tipi elde etmek. Type ile MyClass'ın görüntüsünü elde ettikten sonra, sahip olduğu metotları listeledik (MethodInfo[] mi = t.GetMethods()). Daha sonra her bir metodun parametrelerini aldık (ParameterInfo[] pi = m.GetParameters()). Program adım adım incelenirse, gayet anlaşılır ve kolay niteliktedir.

Yukardaki örnekle ilgili son olarak, çıktıya dikkat ederseniz MyClass içinde tanımlanmayan ToString, Equals.. gibi metotlar gözükmekte. Bunun nedeni, bütün tiplerin kalıtım yoluyla object'ten türetilmiş olmasıdır.

GetMethods()'un başka bir kullanım şekli daha var:

MethodInfo[] GetMethods(BindingFlags bindingAttr).

BindingFlags enumerator tipindedir. En çok kullanılan tipler ise şunlardır:

DeclaredOnly: Yalnız belirtilen class tarafından tanımlanan metotları getirir. Kalıtım yoluyla aktarılan metotları getirmez.
NonPublic: nonpublic metotları getirir.
Public: public metotları getirir.
Static: static metotları getirir.

Bu kullanım şeklinde birden fazla tipi aynı anda kullanmamız mümkündür. Bu durumda aralarına "|" işaretini koymak gerekiyor. Bunun faydalarından biri de object'ten gelen metotların ya da kalıtım yoluyla gelen başka metotların listesini almamak içindir.

Yukardaki ilgili satırı aşağıdaki gibi değiştirin:

MethodInfo[] mi = t.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public);

Çıktı aşağıdaki gibi olacaktır:

Nesnenin adı: MyClass
Nesnenin desteklediği metotlar:
Metot Adı: SumDönüş tipi: Int32
Parametre yok

Metot Adı: IsBetweenDönüş tipi: Boolean
1. parametre: Dönüş Değeri: Int32 Adı: i

Metot Adı: DummyMethodDönüş tipi: String
1. parametre: Dönüş Değeri: Int32 Adı: a
2. parametre: Dönüş Değeri: Int32 Adı: b

Metot Adı: ShowDönüş tipi: Void
Parametre yok
.........................

Görüldüğü üzere object metotları gelmemiş oldu.

Olay basit aslında. Type nesnesine MyClass'ın görüntüsünü alsın diye bir görev yükledik (Type t = typeof(MyClass)). Artık burdaki nesne MyClass'ın görüntüsünü elde edecektir ve sahip olduğu tüm metotları elde edebilecek ve kullanabilecektir. Programcıkta görüldüğü üzere, t nesnesi MyClass'ın bütün özelliklerini kendine yükledi.

Şimdiiiii gelelim bu yazıyı merakla okuyanların daha da merak edeceği bir şeye: Metotları reflection ile çağırmak..

Metotları Reflection İle Çağırma


Yukardaki örneklerde bahsettiğimiz gibi Type ile bir tipin metotlarını elde ettikten sonra Invoke() ile bu metotları çağırabiliriz. Invoke()'nin prototipi aşağıdaki gibidir:

object Invoke(object nesne, object[] argümanlar)

Bunu bir örnekle inceleyelim: Örneğin uzunluğuna takılmayın, yukardaki koda birkaç satır daha ilave ettik. Kopuk olmasın diye ya da bunu uygulamak isteyen biri hepsini alıp kullanabilsin diye tekrar tekrar yazdım:

using System; using System.Reflection; class MyClass { int x; int y; public MyClass(int i, int j) { this.y = j; this.x = i; } public int Sum() { return x + y; } public bool IsBetween(int i) { if (x < i && i < y) return true; return false; } public void Set(int a, int b) { Console.WriteLine("Set(int, int) metodu çağrıldı"); x = a; y = b; Show(); } public void Set(double a, double b) { Console.WriteLine("Set(double, double) metodu çağrıldı"); x = (int)a; y = (int)b; Show(); } public void Show() { Console.WriteLine("x değeri: " + x + " y değeri: " + y); } } class InvokeTest { static void Main() { Type t = typeof(MyClass); MyClass mc = new MyClass(10, 20); int res; Console.WriteLine(t.Name + " metotları Reflection ile çağrılıyor..."); Console.WriteLine("Örnek: MyClass mc = new MyClass(10, 20)"); Console.WriteLine(); MethodInfo[] mi = t.GetMethods(); foreach (MethodInfo m in mi) { //Metodun parametrelerini alalım ParameterInfo[] pi = m.GetParameters(); if (m.Name.Equals("Set", StringComparison.Ordinal) && pi[0].ParameterType == typeof(int)) { object[] args = new object[2]; args[0] = 3; args[1] = 4; m.Invoke(mc, args); } else if (m.Name.Equals("Set", StringComparison.Ordinal) && pi[0].ParameterType == typeof(double)) { object[] args = new object[2]; args[0] = 6.2; args[1] = 9.7; m.Invoke(mc, args); } else if (m.Name.Equals("Sum", StringComparison.Ordinal)) { res = (int)m.Invoke(mc, null); Console.WriteLine("Sum metodu çağrıldı. Sonuç: " + res); } else if (m.Name.Equals("IsBetween", StringComparison.Ordinal)) { object[] args = new object[1]; args[0] = 13; bool isBetween = (bool)m.Invoke(mc, args); if (isBetween) Console.WriteLine("IsBetween metodu çağrıldı. 13 sayısı 10 ile 20 arasındadır"); } else if (m.Name.Equals("Show", StringComparison.Ordinal)) { m.Invoke(mc, null); } } Console.Read(); } }

Bu programın çıktısı aşağıdaki gibi olacaktır:

MyClass metotları Reflection ile çağrılıyor...
Örnek: MyClass mc = new MyClass(10, 20)

Sum metodu çağrıldı. Sonuç: 30
IsBetween metodu çağrıldı. 13 sayısı 10 ile 20 arasındadır
Set(int, int) metodu çağrıldı
x değeri: 3 y değeri: 4
Set(double, double) metodu çağrıldı
x değeri: 6 y değeri: 9
x değeri: 6 y değeri: 9

Görüldüğü üzere; Invoke() metodu ile çalışma sırasında dinamik olarak değer atamaları yaptık ve ardından bu metotları çağırdık. Burda dikkat etmeniz gereken bir nokta da, aşırı yüklenmiş Set metodunun ilgili versiyonunu (Set(int a, int b) ve Set(double a, double b)) bulmak için parametre tipi kontrolü yapılması gerektiği. İlgili kontroller aşağıdaki satırlarda gösterilmiştir:

if (m.Name.Equals("Set", StringComparison.Ordinal) && pi[0].ParameterType == typeof(int)) if (m.Name.Equals("Set", StringComparison.Ordinal) && pi[0].ParameterType == typeof(double))

Type ile Yapılandırıcıları (Constructor) Çağırmak


Yukardaki örneklerde reflection ile metotları nasıl elde edip nasıl kullanılacağını anlattık. Fakat bu örnekler metotları çağırmak dışında bir işe yaramamıştı. Çünkü zaten nesneyi biz yapılandırmıştık. Reflection'un esas gücü bir nesne runtime sırasında oluşturulduğunda (ilk yapılandırma) ortaya çıkar. Tabiki bunun için yapılandırıcıları da elde etmemiz gerekecektir. Bunun için, tahmin edeceğiniz üzere Type'ın GetConstructors() metodunu çağırmamız gerekiyor. İlgili constructor bulunduktan sonra, çalıştırmak için yine Invoke() metodunu kullanacağız. Constructoru çağırarak ilk değer atamaları yapıldıktan sonra ilgili metodu yine benzer yöntemle çağıracağız.

Aşağıdaki örneği inceleyelim:

using System; using System.Reflection; class MyClass2 { int x; int y; public MyClass2(int a, int b) { this.x = a; this.y = b; } public int Sum() { return x + y; } } class InvokeConstructorTest { static void Main() { Type t = typeof(MyClass2); int res; //Constructor bilgilerini al Console.WriteLine(t.Name + "'in yapılandırıcıları:"); ConstructorInfo[] ci = t.GetConstructors(); foreach (ConstructorInfo c in ci) { //Dönüş tipi ve adını gösterelim Console.Write(t.Name + "("); ParameterInfo[] pi = c.GetParameters(); for (int i = 0; i < pi.Length; i++) { Console.Write(pi[i].ParameterType.Name + " " + pi[i].Name); if (i < pi.Length - 1) Console.Write(", "); } Console.WriteLine(")"); } //Biz 2 parametreli yapılandırıcıyı arıyor olacağız int x;//bu değişkende, aradığımız constructorın indexi tutulacak for (x = 0; x < ci.Length; x++) { ParameterInfo[] pi = ci[x].GetParameters(); if (pi.Length == 2) break; } //Buraya aranan constructor bulunmadığı durumda //programdan çık gibi kontroller eklenebilir //Şimdi de nesneyi yapılandıralım object[] constParams = new object[2]; constParams[0] = 10; constParams[1] = 20; object ob = ci[x].Invoke(constParams); Console.WriteLine("Nesne, constructor(10, 20) ile yapılandırıldı..."); MethodInfo[] mi = t.GetMethods(); foreach (MethodInfo m in mi) { ParameterInfo[] pi = m.GetParameters(); if (m.Name.Equals("Sum", StringComparison.Ordinal)) { Console.WriteLine("Sum() metodu çağrılıyor..."); res = (int)m.Invoke(ob, null); Console.WriteLine("Sonuç bulundu: " + res); } } Console.Read(); } }

Bu programın çıktısı aşağıdaki gibi olacaktır:

MyClass2'in yapılandırıcıları:
MyClass2(Int32 a, Int32 b)
Nesne, constructor(10, 20) ile yapılandırıldı...
Sum() metodu çağrılıyor...
Sonuç bulundu: 30

Bu örneği incelersek; ilk önce yapılandırıcı listesini elde ettik. 2 parametreli yapılandırıcıyı bildiğimizden 2 parametreli olanı bulduk (daha karmaşık yapılandırılar için önceki örneklerde olduğu gibi parametre tipi kontrolü yapılarak ilgili yapılandırı bulunabilir). Sonra bu yapılandırıcıya runtime zamanında değer atadık ve nesneyi yapılandırdık. Sonra ilgili metodu da aynı yöntemle bularak, yapılandırıcı ile yapılandırdığımız nesnenin metodunu çağırdık.

Assembly'leri Reflection'da Kullanmak


Buraya kadar hep açık kodunu bildiğimiz yapılar için reflection kullandık. Fakat reflectionun tam gücü assembly'lerin kullanımında ortaya çıkmaktadır. Bildiğiniz üzere bir assembly, içerdiği classlar, yapılar, metotlar ve parametreler gibi birçok bilgiyi beraberinde taşır. Bu da reflection kullanımı için son derece uygundur. Bu şekilde dışardan eklenen bir assembly için sık sık tanımlama gereği duymadan reflection ile dinamik ve esnek olarak kullanabiliriz. Örneğin; bir sistemdeki tüm tipleri araştıran ve görsel olarak ortaya koyan bir "tip tarayıcısı" gibi bir programınızın olduğunu düşünelim. Bu noktada reflection kullanmak işinizi son derece kolaylaştıracaktır.

Assembly'lerin kullanımına bir örnek yapalım. Öncelikle birkaç classtan oluşan bir program yazıp derleyeceğiz. Sonra bu programın derlenmesi sonucu oluşan exe'yi runtime sırasında yükleyip kullanacağız.

Basit olması açısından bir proje oluşturup 3 tane class yerleştirip derleyelim. Derlendikten sonra dizinin altında bir exe oluşacaktır. Bu exe'nin adını belirtip ilgili tipleri elde ettikten sonra yine aynı yöntemlerle kullanacağız:

using System; using System.Reflection; public class MyClass { int x; int y; public MyClass(int a, int b) { this.x = a; this.y = b; } public int Sum() { return x + y; } } public class OtherClass { public string Show() { return "Bu da diğer class"; } } public class UseAssemblyReflection { static void Main() { int res; Assembly asm = Assembly.LoadFrom("ReflectionAssembly.exe"); Type[] types = asm.GetTypes(); Console.WriteLine("ReflectionAssembly.exe'deki tiplerin listesi"); foreach (Type temp in types) { Console.WriteLine(temp.Name); } Type t = types[0];//MyClass'ı alıyoruz Console.WriteLine("\nMyClass'ın Constructorları listeleniyor"); ConstructorInfo[] ci = t.GetConstructors(); foreach (ConstructorInfo c in ci) { //Dönüş tipi ve adını gösterelim Console.Write(t.Name + "("); ParameterInfo[] pi = c.GetParameters(); for (int i = 0; i < pi.Length; i++) { Console.Write(pi[i].ParameterType.Name + " " + pi[i].Name); if (i < pi.Length - 1) Console.Write(", "); } Console.WriteLine(")"); } //Biz 2 parametreli yapılandırıcıyı arıyor olacağız int x;//bu değişkende, aradığımız constructorın indexi tutulacak for (x = 0; x < ci.Length; x++) { ParameterInfo[] pi = ci[x].GetParameters(); if (pi.Length == 2) break; } //Buraya aranan constructor bulunmadığı durumda //programdan çık gibi kontroller eklenebilir //Şimdi de nesneyi yapılandıralım object[] constParams = new object[2]; constParams[0] = 10; constParams[1] = 20; object ob = ci[x].Invoke(constParams); Console.WriteLine("\nNesne, constructor(10, 20) ile yapılandırıldı..."); MethodInfo[] mi = t.GetMethods(); foreach (MethodInfo m in mi) { ParameterInfo[] pi = m.GetParameters(); if (m.Name.Equals("Sum", StringComparison.Ordinal)) { Console.WriteLine("Sum() metodu çağrılıyor..."); res = (int)m.Invoke(ob, null); Console.WriteLine("Sonuç bulundu: " + res); } } Console.Read(); } }

Bu programın çıktısı aşağıdaki gibi olacaktır:

ReflectionAssembly.exe'deki tiplerin listesi
MyClass
OtherClass
UseAssemblyReflection

MyClass'ın Constructorları listeleniyor
MyClass(Int32 a, Int32 b)

Nesne, constructor(10, 20) ile yapılandırıldı...
Sum() metodu çağrılıyor...
Sonuç bulundu: 30


Ben projemi oluştururken ReflectionAssembly adını vermiştim. Bu yüzden ilgili exe'yi belirtirken "ReflectionAssembly.exe" olarak belirttim. Assembly'nin yüklenme şekline dikkat ediniz:


Assembly asm = Assembly.LoadFrom("ReflectionAssembly.exe");

asm nesnesine ilgili exe'nin bilgileri yüklendikten sonra tipleri elde etmek için aşağıdaki satırı kullandık.

Type[] types = asm.GetTypes();

Bundan sonrası yine yukarda anlattığımız yöntemlerle aynı.

Not: Burda unutulmaması gerekir ki, yüklenmesi gereken assembly'nin türü illa ki exe olması gerekmez. Projenin türüne göre dll de olabilir. dll olması durumunda Add > Reference ile referans eklendikten sonra direk ilgili dll'in adı verilebilir (ObjectDeneme.dll gibi) ya da referans eklemeden direk path olarak da verilebilir (C:\users\......\ObjectDeneme.dll gibi)

Reflection hakkında bu kadar bilgi verdikten sonra diyeceksiniz ki, biz zaten özelliklerini bildiğimiz dll veya exe'leri kullandık. Zaten daha önce de belirtmiştik bir assembly beraberinde tiplerin özelliklerini de taşır. Dolayısıyla bu normal bir durumdur. Fakat hiç özelliklerini bilmediğiniz tiplerin de reflection ile özelliklerini açığa çıkarmanız mümkündür. Yukardaki kodları, hayal gücünüze bağlı olarak daha dinamik hale getirip aynı anda sistemdeki bütün tiplerin özelliklerini listeleyebilirsiniz.

Reflection konusunun sonuna geldik. Bir sonraki yazımızda Nitelikler konusunu ele alacağız.

Umarım faydalı olabilmişimdir... Hepinize iyi günler dilerim..


Paylaş:

0 yorum:

Yorum Gönder

Bu Blogda Ara