Derinlemesine yazılım eğitimleri için kanalımı takip edebilirsiniz...

UnsafeAccessorAttribute İle Yeni Bir Reflection Yaklaşımı

Merhaba,

Bu içeriğimizde, .NET 8 ile sunulan UnsafeAccessorAttribute‘u ile reflection davranışına dair yeni bir yaklaşımı değerlendirecek ve bir yandan da klasik yaklaşımla mukayese ederek, örnek kullanım senaryoları üzerinden getirisi olan avantajları ve riskleri tartışıyor olacağız.

Bilindiği üzere C#’ta, türler üzerinde runtime incelemelerini ve dinamik etkileşimleri reflection davranışı ile gerçekleştirmekteyiz. Özellikle bu davranış bizler açısından, ORM gibi yaklaşımlarda yahut Dependency Injection gibi pattern’larda vazgeçilmez çözümler sağlamaktadır diyebiliriz. Ancak reflection tabanlı çözümlerin performans ve maliyet açısından önemli bedeller doğurduğunu biliyoruz. Haliyle bizler bu içeriğimizde, .NET 8 ile gelmiş olan UnsafeAccessorAttribute ile birlikte, geleneksel reflection modelini tamamlayıcı nitelikte olan ve düşük seviyeli fakat oldukça yüksek performanslı olarak erişim davranışını sağlayan yeni bir mekanizmayı değerlendirecek ve mevzu bahis olan bu maliyet ve bedelleri yerine göre nasıl minimize edebildiğimizi inceleyeceğiz.

Reflection ile ilgili mutlak bilgi için Derinlemesine Reflection Davranışı başlıklı eğitimime göz atabilirsiniz.

Klasik reflection’da ne sorun vardı?

Klasik reflection yaklaşımı, runtime’da meta veri keşfi sağlayarak esneklik sunmakla birlikte, performans maliyeti ve Native AOT (Ahead-of-Time) senaryolarındaki kısıtları nedeniyle sıklıkla eleştirilmektedir.

Şöyle ki;

public class Person
{
    public Person() { }
    public Person(string fullName)
        => _fullName = fullName;

    public string FirstName { get; set; } = string.Empty;
    public string MiddleName { get; set; } = string.Empty;
    public string LastName { get; set; } = string.Empty;

    public string _fullName;

    public string GetFullName()
        => $"{FirstName} {LastName}".Trim();

    public void Rename(string first, string? middle, string last)
    {
        FirstName = first ?? string.Empty;
        MiddleName = middle ?? string.Empty;
        LastName = last ?? string.Empty;
    }
}

Misal olarak elimizde yukarıdaki gibi bir sınıfın olduğunu varsayalım… Bu sınıfın FirstName property’sine reflection ile erişmeye çalıştığımızda aşağıdaki gibi bir yaklaşım sergilememiz gerekmektedir;

var firstNameProperty = typeof(Person)
    .GetProperty(nameof(Person.FirstName));

Bu yaklaşımda;

  • Metadata’lar runtime’da okunmaktadır.
  • Veriler Boxing / Unboxing işlemlerine tabi tutulmaktadır.
  • AOT / trimming senaryolarında çoğu zaman sıkıntı çıkma ihtimali taşımaktadır.
  • JIT için optimize işlemi zorludur, bu yüzden yavaş işlevsellik söz konusu olabilmektedir.

Bunca bedele karşın, UnsafeAccessorAttribute ise derleme zamanında static binding imkanı sağlayarak, private member’larda dahil olmak üzere tür içi öğelere neredeyse doğrudan çağrı ile erişim sunmaktadır.

UnsafeAccessorAttribute İle Yeni Yaklaşım

System.Runtime.CompilerServices namespace’i altında bulunan UnsafeAccessorAttribute, aşağıdaki fikir üzerine inşa edilmiştir.

Erişim yapacağım member tam olarak belli; ismi ve imzası derleme zamanında aşikar. O halde reflection ile çalışma zamanında meta veri arama maliyetine katlanmadan, doğrudan bu member’a bağlanmak istiyorum.

Bu attribute’u kullanabilmek için gövdesi olmayan (extern) static metotlar tanımlamamız gerekmektedir. Bu metotların imzası, erişilmek istenen member’ın imzası ile eşleştirilir ve CLR bu eşleştirmeyi kullanarak derleme zamanında uygun çağrı noktasını üretir.

Şöyle ki;

Eski yol:

person.GetType()
    .GetField("_fullName")!
    .SetValue(person, "Gençay YILDIZ");

Yeni yol:

[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_fullName")]
static extern ref string SetPersonFullName(Person person);

SetPersonFullName(person) = "Gençay YILDIZ";

Bakın… Bu örnekte görüldüğü üzere UnsafeAccessorAttribute ile static extern olan bir metodu işaretleyerek, UnsafeAccessorKind.Field parametresiyle de bu metodun esasında bir field’a bind olacağını ifade etmiş bulunuyoruz. CLR, compile time’da bu bağlantıyı çözerek doğrudan o member’a çağrı üretecektir ve böylece reflection olmaksızın mış gibi bir davranışla bu işlem daha az maliyetle gerçekleştirilmiş olacaktır.

Ayrıca buradaki kullanıma odaklanırsak eğer SetPersonFullName(person) = "Gençay YILDIZ" ifadesinin pek alışılagelmiş bir kullanım olmadığını söyleyebiliriz. Ne de olsa sanki bir metot çağrımının üzerine bir değer atıyormuş gibi zahiren bir görüntü mevzu bahis 🙂 Evet, bu görüntü saçma bir mantık yansıtıyor olabilir ama esasında izahı oldukça kolay. Şöyle ki; ilgili static extern olan metodun yapısına bakarsanız eğer ref string ifadesini göreceksiniz. Bu ifade ile bu metodun bir string değer döndürmesinden öte, bir string türde referans döndürdüğünü anlıyoruz. Yani bu imza bir değer döndüren metottan ziyade geriye bir referans döndüren yapıya sahiptir. Haliyle buradaki SetPersonFullName(person) = "Gençay YILDIZ" ifadesi C# compiler açısından metodun döndürdüğü referansa ‘Gençay YILDIZ’ değerini atamaktadır 🙂 İşte esasında tüm mesele bundan ibarettir…

Aslında C#’ın bu Ref Returns özelliğine aşina olanlar için yukarıdaki bu yapının esasında aşağıdakinden bir farkının olmadığı rahatlıkla anlaşılabilecektir.

[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_fullName")]
static extern ref string SetPersonFullName(Person person);

ref string fullName = ref SetPersonFullName(person);
fullName = "Gençay YILDIZ";

Evet, ne de olsa SetPersonFullName metodundan gelen referansı yine ref ile işaretlenmiş(yani bir değer değil de bir referansı işaretleyecek olan) bir değişkenle karşılayarak o değişken aracılığıyla gelen referans üzerinde gerekli dokunuşları gerçekleştirebilmekteyiz. Bu iki kullanımda özünde aynı davranışı sergilediği için aralarında hiçbir fark söz konusu değildir.

Tabi UnsafeAccessorAttribute‘u yalnızca field’lar da kullanmakla sınırlı değiliz. İhtiyaç doğrultusunda metotlarda ve hatta constructor’larda da bu attribute’u tercih edebilmekteyiz.

Metotlarda kullanım

[UnsafeAccessor(UnsafeAccessorKind.Method, Name = nameof(Person.GetFullName))]
static extern string GetPersonFullName(Person person);

var fullName = GetPersonFullName(person);

Evet, görüldüğü üzere compile time’da UnsafeAccessorAttribute‘u ile bir metodu işaretleyebilmek için static extern metodunun imzasını birebir o metotla aynı yapmamız yeterli olacaktır. Bakın bu sefer metodun geri dönüş değerini ref ile işaretlemediğimize dikkatinizi çekerim. Haliyle bu şekilde bir imzayla salt bir metodu ifade ettiğimiz alenen ortada olmaktadır. Dolayısıyla kullanırken de direkt metot çağrımı davranışıyla kullanıyoruz.

Constructor’larda kullanım
İlginçtir ki UnsafeAccessorAttribute‘unu constructor’larda bile kullanabilmekteyiz.

[UnsafeAccessor(UnsafeAccessorKind.Constructor)]
static extern Person PersonInitialize(string fullName);
Person person = PersonInitialize("Gençay YILDIZ");

Burada görüldüğü üzere metot imzasında belirtilen ‘Person’ tipi sayesinde bir nesne oluşturulacağı ifade edilmektedir ve compiler açısından hangi constructor’ın kullanılacağına da implicit olarak klasik parametre sayısı ve tür seçilimine göre eşleştirme yapılarak karar verilmektedir.

Evet, bu açıdan bakıldığında UnsafeAccessorAttribute‘unun oldukça etkin yeteneklere sahip olduğunu söyleyebiliriz👏

Güvenlik, Bakım ve Kısıtlar

Her ne kadar sunulan model, performans ve AOT bakımından belirgin avantajlar sunuyor olsa da, adındaki ‘unsafe’ nitelemesinin tesadüf olmadığını vurgulamak gerekmektedir. Şöyle ki; erişilen member’ın adı değiştirildiğinde (örneğin _fullName yerine _name şeklinde bir değişiklik yapıldığında) burada compile time’da değil runtime’da hata söz konusu olacaktır. Ee bu durumda da ilgili attribute’un implementasyon detayında güçlü bir bağımlılık durumunun söz konusu olduğunu söyleyebiliriz. Bu tarz durumları Kırılgan Sözleşme (Fragile Contract) olarak nitelendirmekteyiz. Haliyle burada ciddi bir güvensiz durum söz konusudur diyebiliriz.

Nihai olarak;
UnsafeAccessorAttribute, reflection davranışını tamamlayan yeni bir yapı taşı ortaya koymakta ve çalışma zamanlı yapılan keşif süreçleriyle, derleme zamanlı static binding arasında yeni bir denge noktası sunmaktadır. Bu attribute, özellikle doğru bağlamda kullanıldığı taktirde performans ve maliyet açısından oldukça kayda değer kazanımlar sağlayabilmektedir. Ancak söz konusu kazançlar, elbetteki yukarıdaki satırlarda ifade etmeye çalıştığımız gibi belli başlı ihlaller, bakım zorlukları ve sınırlı dinamiklikler gibi bedellerle birlikte gelmektedir. Bundan kaynaklı UnsafeAccessorAttribute kullanımının, yalnızca gerekçesi açıkça temellendirilmiş, iyi izole edilmiş ve uzun vadeli etkileri doğru değerlendirilmiş senaryolarla sınırlandırılmasının sağlıklı bir tasarım olacağı kanaatindeyim.

İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…

Not : Örnek çalışmaya aşağıdaki GitHub adresinden erişebilirsiniz.
https://github.com/gncyyldz/UnsafeAccessorAttribute.Example

Bunlar da hoşunuza gidebilir...

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir