Tarihçe: Nesne güvenliği kavramı, erken Rust döneminde, özelliği nesnelerinin (dyn Trait) dinamik dispatch'i destekleyebilmesi için hafıza güvenliğinden ödün vermeden veya sonsuz derleme zamanı kodu üretimi gerektirmeden sağlanmak üzere ortaya çıktı. Sanal dispatch tanıtıldığında, dil tasarımcıları, her bir genel tür için derleme zamanında belirli makine kodu oluşturma - monomorfizasyon ile çalışma zamanı çok biçimliliği için sabit boyutlu vtable gereksinimi arasında temel bir çatışma ile karşılaştılar. Bu, teorik olarak sınırsız sayıda vtable girişi gerektiren genel yöntemler içeren özelliklerin doğrudan özellik nesnelerine dönüştürülemeyeceğini belirleyen bir kısıtlamaya yol açtı.
Sorun: fn process<T>(&self, input: T) gibi bir genel yöntem monomorfizasyonuna dayanır; burada derleyici, çağrı yerlerinde her somut tür T için belirgin bir işlev gövdesi oluşturur. Ancak, bir özellik nesnesi somut türü silerek sadece sabit işlev imzalarını içeren bir vtable'a gösterim sunar. Vtable'ın, derleme zamanında belirlenen sınırlı, sabit bir boyuta sahip olması gerektiğinden, her olası tür T için sonsuz bir potansiyel kurulum setini barındıramaz. Ayrıca, tür parametreleri derleme zamanı yapılarıdır, ancak özellik nesnesi dispatch'i çalışma zamanı gerçekleştiğinden, çağrıcının vtable üzerinden yöntemi çağırırken gerekli tür parametrelerini sağlama olanağı yoktur.
Çözüm: TypeId deseni, somut türü özellik imzasından silerek tür tanımlamasını çalışma zamanına erteleyerek bunu çözer. Genel bir parametre kabul etmek yerine, özellik yöntemi Box<dyn Any> veya &dyn Any kabul eder. Uygulama, her tür için derleyici tarafından üretilen benzersiz bir tanımlayıcı olan TypeId'i kullanarak, çalışma zamanı merdiveniyle somut türü doğrular. Bu yaklaşım nesne güvenliğini geri getirir çünkü özellik metodunun kendisi sabit bir imzaya sahiptir, oysa tür özel mantık uygulama içinde, Any özelliği temelinde kontrol edilen dönüşümlerle kapsüllenmiştir.
use std::any::{Any, TypeId}; // Bu özellik genel yöntem nedeniyle nesne güvenli değildir trait GenericProcessor { fn process<T: Any>(&self, input: T); } // Bu özellik tür silme aracılığıyla nesne güvenlidir trait ObjectSafeProcessor { fn process_any(&self, input: Box<dyn Any>); } struct Logger; impl ObjectSafeProcessor for Logger { fn process_any(&self, input: Box<dyn Any>) { if let Ok(s) = input.downcast::<String>() { println!("Logging String: {}", s); } else if let Ok(n) = input.downcast::<i32>() { println!("Logging i32: {}", n); } else { println!("Logging unknown type"); } } } fn main() { let processor: Box<dyn ObjectSafeProcessor> = Box::new(Logger); processor.process_any(Box::new("hello".to_string())); processor.process_any(Box::new(42i32)); }
Bağlam: Modüler bir oyun motoru, sistemlerin diğer sistemlerin somut türleri hakkında derleme zamanı bilgisi olmadan olaylara abone olmasını sağlayan bir EventBus mimarisi gerektiriyordu. İlk tasarım, farklı olay türleri için sıfır maliyetli soyutlamaları kullanmak üzere genel bir on_event<E: Event>(&mut self, event: E) yöntemi ile bir System niteliği tanımladı.
Sorun: Bu tasarım, System nesnesinin nesne güvenli olmadığı için heterojen sistemleri bir Vec<Box<dyn System>> içinde depolamayı engelledi. Motorun, olay türlerinin derleme zamanı bilinmediği dinamik olarak yüklenmiş DLL eklentilerini desteklemesi gerekiyor, bu da merkezi kayıt için statik dispatch'i uygulanamaz hale getiriyor.
Çözüm 1: Kapalı Enum Dispatch. Tüm olası olayları içeren kapsamlı bir GameEvent enum'ı tanımlayın. Artılar: Sıfır çalışma zamanı üzerindeki maliyet, tahsis edilmez ve derleme zamanında kapsamlı desen eşleştirme. Eksiler: Açık/kapalı ilkesini ihlal eder; eklentilerden yeni olaylar eklemek, temel enum'ı değiştirmeyi ve motoru yeniden derlemeyi gerektirir, bu da ikili uyumluluğu bozar.
Çözüm 2: Any ile Tür Silme. Özelliği on_event(&mut self, event: Box<dyn Any>) olarak yeniden yapılandırın ve dahili yönlendirme için TypeId'yi kullanın. Artılar: Bilinmeyen olay türleri ile dinamik eklentileri tamamen destekler, nesne güvenliğini korur ve kaydın Box<dyn System>'yi depolamasına olanak tanır. Eksiler: Aşağıdan çıkarma için çalışma zamanı üzerindeki maliyet, tür uyuşmazlıkları olursa potansiyel panik ve olay işleme için derleme zamanı kapsamlılığı kaybı.
Çözüm 3: Ziyaretçi Deseni. Olayların belirli sistem arayüzlerini ziyaret etme yeteneğine sahip olduğu çift dispatch'i uygulayın. Artılar: Aşağıdan çıkarma olmadan tür güvenli, çalışma zamanı tür kontrolü üzerindeki maliyet yok. Eksiler: Olaylar ve sistemler arasında sıkı bağlılık, önemli miktarda boilerplate kod ve mevcut olay tanımlarını değiştirmeden yeni sistemlerle genişletme zorluğu.
Seçilen: Çözüm 2 (Tür Silme), eklenti mimarisinin açık olay türleri setini gerektirdiği için seçildi. EventBus, TypeId'den işleyici geri çağırmalarına eşlemeler depolar ve sistemler, kaydettikleri ilgi türlerine dönüştürdükleri Box<dyn Any> alır. Sonuç, eklentilerin gelen olayları tanımlamasına ve sistemleri motoru yeniden derlemeden tanımlamasına olanak tanıyan esnek bir mimariydi; olay sınırlarında, aşağıdan çıkarma için küçük çalışma zamanı maliyetini modülerlik için değerli bir değişim olarak kabul etti.
Neden Box<dyn Any> downcast_ref<T>() çağrısına izin verir, oysa T bir genel parametreyken, genel yöntemler genellikle nesne güvenliğini engeller?
downcast_ref yöntemi, Any niteliği içinde tanımlanmaz, aksine impl dyn Any aracılığıyla boyutsuz tür üzerindeki birinherent yöntem olarak tanımlanır. Any niteliği yalnızca fn type_id(&self) -> TypeId gerektirir, bu da nesne güvenlidir. Genel downcast_ref ayrı olarak uygulanır ve standart kütüphanenin uygulama kodunda bulunan somut type_id işlevi işaretçisini kullanarak, saklanan türün tanımlayıcısını istenen türün TypeId ile karşılaştırmak için type_id()'yi içten içe çağırır. Bu, genel mantığın vtable girişinde değil, standart kütüphanenin uygulama kodunda yer alması nedeniyle vtable kısıtlamasını aşar.
Genel yöntemlerdeki örtük Sized sınırı, nesne güvenliği ile nasıl etkileşir ve neden where Self: Sized ifadesinin açıkça eklenmesi bunu geri getirir?
Varsayılan olarak, genel yöntemler, monomorfizasyonun işlev gövdesini oluşturmak için derleme zamanında türün boyutunu bilmesini gerektirdiğinden, Self: Sized gerektiren katmanlar içerir. Özellik nesneleri (dyn Trait) boyutsuzdur (!Sized), bu da onları böyle metodlarla uyumsuz hale getirir. Açıkça where Self: Sized eklemek, aslında, vtable gereksinimlerinden hariç tutar (yöntem tür nesneleri aracılığıyla çağrılabilir olmaktan çıkar), böylece özelliğin nesne güvenliğini geri kazandırır. Adaylar genellikle bunu yöntemin kullanılamaz hale geldiği şeklinde yanlış anlar; ancak hâlâ somut türlerde ve genel bağlamlarda çağrılabilir, sadece nesne üzerinden dinamik dispatch ile değil.
Bir niteliğin ilişkili türleri, genel yöntemlere benzer nesne güvenliği sorunlarına neden olabilir mi ve genel yöntemlerden nasıl farklıdır?
İlişkili türlerin, self'in değer ile tüketildiği veya Self döndüren yöntemlerde görünmesi durumunda nesne güvenliği sorunlarına yol açabilir; çünkü özellik nesnesi somut türü siler, bu da ilişkili türü çağrı yerinde belirsiz hale getirir. Ancak, genel yöntemlerin aksine, ilişkili türler, özellik nesnesi türünü oluştururken belirtilebilir (örneğin, Box<dyn Iterator<Item=u32>>), bu da belirli ilişkili tür kurulum için vtable'ı monomorfize eder. Bu, hem genel yöntemler hem de ilişkili türlerin, özellik nesnesinin oluşturulma noktasında sayılamayan bir açık tür setini temsil etmesi açısından temel olarak farklıdır.