Sorunun tarihi:
Rust 1.26'da, dil geliştiricilerin karmaşık somut türleri—iç içe geçmiş iterator adaptörleri veya closure tarafından üretilen yapılar gibi—gizlemelerini sağlamak için dönüş konumuna dayalı impl Trait (RPIT) kavramını tanıttı. Bu özellik, derleyicinin derleme sırasında tek bir somut uygulamaya çözümlediği opak bir varoluşsal tür oluşturur ve uygulama detaylarının API sınırları boyunca sızmasını önler. Genel tanımlamalardan farklı olarak, çağıran tarafından seçilen değil, fonksiyon uygulaması tarafından seçilen RPIT, ancak derleyicinin doğru makine kodunu oluşturmak için somut türün tam boyutunu ve düzenini bilmesini gerektirir.
Sorun:
Bir fonksiyon impl Trait dönerken kendisini yeniden çağırmaya çalıştığında, somut dönüş türü mantıksal olarak kendisini içermek zorundadır; bu da sonsuz boyutta öz-tanımlı bir tür tanımı oluşturur. Rust, yığıt üzerine yerleştirilen tüm dönüş değerlerinin Sized trait'ini uygulamasını katı bir şekilde gerektirir ve derleyicinin derleme zamanında sabit bir bellek düzeni ve hizalamasını hesaplayabilmesini zorunlu kılar. Çünkü bir özyinelemeli impl Trait dönüşü, her çağrı çerçevesinin öncekini iç içe geçirdiği sınırsız bir tür genişlemesini ima eder; bu nedenle derleyici depolama boyutunu veya yığıt çerçevesi ofsetlerini belirleyemez ve bu da tür kontrolü sırasında bir döngü tespit hatasına yol açar.
Çözüm:
Çözüm, özyinelemeli döngüyü açıkça kırmak için yığın tahsisi aracılığıyla dolaylılık getirmeyi gerektirir ve bu, sonsuz boyutta özyinelemeli yapının sabit boyutlu bir işaretçiye dönüştürülmesini sağlar. Box<dyn Trait> döndürerek, fonksiyon yerine bir işaretçi içeren bir yağ tablosu işaretçisi ve bir veri işaretçisi içeren bir yağ tabanlı işaretçi döndürür; bunlar, arka planda özyineleme derinliği ne olursa olsun Sized'ır. Alternatif olarak, türlerin yığında veya diğer işaretçi türlerinde açıkça kendilerini saran bir özyinelemeli enum tanımlayarak, geri dönüş türünün kendisinin somut, Sized bir yapı olarak kalacağını açıkça belirtir.
trait Expr { fn val(&self) -> i32; } struct Literal(i32); impl Expr for Literal { fn val(&self) -> i32 { self.0 } } struct Sum(Box<dyn Expr>, Box<dyn Expr>); impl Expr for Sum { fn val(&self) -> i32 { self.0.val() + self.1.val() } } // HATA: özyinelemeli opak tür // fn eval(n: u32) -> impl Expr { // if n == 0 { Literal(0) } // else { Sum(Box::new(eval(n-1)), Box::new(Literal(1))) } // } // BAŞARILI: Box<dyn Trait> ile açık dolaylılık fn eval(n: u32) -> Box<dyn Expr> { if n == 0 { Box::new(Literal(0)) } else { Box::new(Sum(eval(n-1), Box::new(Literal(1)))) } }
Yüksek performanslı bir ağ proxy'si için bir yapılandırma ayrıştırıcı tasarlarken, iç içe geçmiş politika ifadeleri için özyinelemeli bir iniş ayrıştırıcısı implemente etmem gerekti. İlk tasarım, iç AND/OR/NOT mantık düğümlerinin iç temsilini aşağıdaki tüketicilerden gizlemek için Policy adında bir trait içermesi gereken bir parse fonksiyonu belirtilmişti.
İlk yaklaşım doğrudan özyineleme içeriyordu: parse fonksiyonu token'lara göre sıraya giriyordu ve AND(policy1, policy2) gibi bileşik ifadeler için, alt politikaları ayrıştırmak üzere kendisini özyinelemeli olarak çağırıyor ve bunları doğrudan impl Policy olarak döndürüyor. Bu strateji, monomorfizasyon aracılığıyla sıfır maliyetli soyutlamalar ve yığın tahsisi yükünden kaçınma avantajı sunuyordu. Ancak, Rust bir döngü hatası bildirerek özyinelemeli çağrının sonsuz tür boyutunu ima ettiğini belirtti, çünkü somut dönüş türü kendisinin iç içe örneklerini sınırsız olarak içermek zorundaydı.
İkinci düşünülen çözüm, dönüş türünü Box<dyn Policy> olarak değiştirmekti; bu, her özyinelemeli alt politikayı yığın üzerinde tahsis etti ve yalnızca bir trait nesnesi yağ tabanlı işaretçiyi döndürdü. Bu yaklaşım, geri dönüş türü artık iç içe geçiş derinliğinden bağımsız olarak sabit boyutlu bir işaretçi olduğu için başarıyla derlendi. Ancak, her düğüm için yığın tahsisi üzerinde fazladan yük oluşturdu ve politika değerlendirmesi sırasında dinamik yönlendirme gerekirdi.
Üçüncü alternatif, Literal, And, Or ve Not için açıkça Box<PolicyNode> saran özyinelemeli bir PolicyNode enum'u tanımlamayı araştırdı; bu, dyn yönlendirmesinden kaçınarak kapalı bir dizi düğüm türü gerektiriyordu ve derleme zamanında biliniyordu. Bu statik yönlendirme yaklaşımı, trait nesneleri ile ilişkili yapı tabloları ve tahsilleri ortadan kaldırırken, üçüncü tarafların yeni düğüm türleri tanımlamasını gerektirdi ve kapalı bir enum'u kullanılamaz hale getirdi.
Box<dyn Policy> yaklaşımını seçtik çünkü politika motoru üçüncü taraf genişlemesine yeni politika türleri tanımlaması için çalışma zamanında eklenti kaydı gerektiriyordu ve bu nedenle kapalı bir enum uygun değildi. Sonuç olarak, yığın alanını yığın kararlılığı ve dinamik esneklik karşılığında bozan işlevsel bir özyinelemeli ayrıştırıcı elde ettik; daha sonra, yüksek verimli senaryolarda tahsis baskısını azaltmak için kuyruk özyinelemeli ayrıştırma desenlerini yinelemeli döngülere dönüştürerek sıcak yolları optimize ettik.
async fn şeklinin özyineleme kısıtlamalarıyla nasıl etkileştiği, çünkü bu da opak bir impl Future döner?
async fn, Future'ı uygulayan bir durum makinesine dönüştürülür; her .await noktası, üretilen enum'da depolama gerektiren farklı bir askıya alma durumunu temsil eder. Özyinelemeli async fn çağrıları, oluşturulan geleceğin kendisini bir varyant olarak içermesi gerektirdiği için aynı sonsuz tür hatasını tetikler ve Sized kuralını ihlal eder. Adaylar genellikle derleyicinin otomatik olarak async özyineleme ile başa çıktığını varsayıyor; ancak çözüm, geleceği manuel olarak Box::pin ile sarmak ya da Pin<Box<dyn Future<Output = T>>>> döndürmekten geçer; bu, özyinelemeli geleceği sabit bir bellek konumuna sabitler ve yığın tahsisini zorunlu kılar. Bu, ayrıca döndürülen geleceğin 'static veya düzgün bir şekilde sınırlı olmasının sağlanması ve async fn özyinelemesinin standart impl Trait dönüşleriyle aynı Sized kurallarını takip ettiğini anlamak için ek bir karmaşıklık getirir.
Argument pozisyonunda (APIT) impl Trait kullanımının neden aynı özyineleme kısıtlamalarını tetiklemediği?
Argument pozisyonundaki impl Trait, belirtilen trait ile sınırlı anonim bir genel tür parametresi için sözdizimsel şekerdir. Bir fonksiyon APIT ile kendisini özyinelemeli olarak çağırdığında, derleyici her çağrı yerinde geçirilen her somut tür için ayrı bir monomorfize örneği oluşturur, yani özyinelemeli fonksiyon kendisinin opak bir versiyonunu döndürmez, her yığın seviyesinde farklı somut türler kabul eder. Adaylar sıklıkla RPIT ve APIT'yi karıştırırlar ve APIT'nin çağrı grafiyle monomorfizasyon işlemine katıldığını ve dönüş türünün (somut duruma göre biliniyorsa) bilindiğini, oysa RPIT'nin kendisi için çözümü olmayan tek bir opak tür tanımladığını kaçırırlar; bu, doğrudan yanıtlarla kendine referans veren sonsuz bir yapı oluşturmaz.
impl Trait bir yapı içinde sarmalanarak, Wrapper<T>(T) gibi kullanılabilir mi; burada T: Trait?
Bir alan olarak impl Trait içeren bir yapı doğrudan adlandırılamaz çünkü impl Trait yalnızca işlev dönüş pozisyonlarında veya argüman pozisyonlarında geçerlidir; tür tanımlarında veya yapı alanlarında geçerli değildir. Adaylar sıklıkla Box<impl Trait> yazmaya çalışır; bu, işlev dönüş bağlamları dışında geçersiz bir sözdizimidir ve opak tür taklidi ile somut bir tür oluşturucusu arasında bir karışıklık yaşar. Bu yanlış anlama, impl Trait'i genel pozisyonlarda kullanılabilecek birinci sınıf bir tür olarak görmekten kaynaklanır ve yalnızca belirli fonksiyon imzalarında mevcut olan ve derleyici tarafından oluşturulan bir varoluşsal tür olarak anlamaktan doğar. Doğru yaklaşım, tür silme için Box<dyn Trait> veya somut türün açıkça tanımlandığı bir özyinelemeli enum çözümü gerektirir; bu, tür sisteminin çalışma zamanından önce Sized kuralını hesaplayabilmesini sağlar.