Swift, C++'un sıfır maliyetli soyutlamaları ile Objective-C'nin dinamik esnekliği arasındaki farkı kapatmak için tasarlandı. İlk sürümler sınıf mirasına ve sanal yöntem tablolarına büyük ölçüde dayanıyordu, ancak Swift 2.0'da protokol odaklı programlamanın tanıtılması daha ince bir dağıtım modeli gerektiriyordu. Derleyici ekibi, protokol gereksinimlerinin (protokol gövdesinde ilan edilen yöntemler) çalışma zamanı çok biçimliliği için tanıklık tablolarını kullanırken, yalnızca uzantılarda tanımlanan yöntemlerin statik olarak çözümlenmesi için bir hibrit yaklaşım seçti. Bu tasarım kararı, statik dağıtımın performans özelliklerinden ödün vermeden geriye dönük modelleme ve değer türlerini destekleme ihtiyacına dayanmaktadır.
Geliştiriciler sıklıkla, bir protokol uzantısında yöntem uygulaması sağlamanın uyumlu türlerin polimorfik olarak geçersiz kılabileceği "varsayılan" bir davranış oluşturduğunu varsayarlar. Ancak, Swift uzantı yöntemlerini referansın derleme zamanı türüne göre statik olarak dağıtır, örneğin, örneğin çalışma zamanı türüne göre değil. Varlık kutuları (any Protocol) kullanıldığında, derleme zamanı türü varlık konteynerinin kendisidir ve bu da çağrıların uzantının uygulamasına çözülmesine neden olur. Bu, alt sınıflardaki veya yapıların özel uygulamalarının heterojen koleksiyonlarda sessizce atlattığı kötü niyetli hatalar yaratır.
Gerçek dinamik polimorfizm sağlamak için, yöntem protokol beyanında bir protokol gereksinimi olarak tanımlanmalıdır. Bu, derleyiciyi yöntem için bir tanıklık tablosu girişi ayırmaya zorlar ve çalışma zamanında doğru gerçekleştirimi bulmasına olanak tanır. Polimorfizmin gerekli olmadığı performans kritik algoritmalar için yöntemler uzantılarda kalmalıdır, böylece derleyici bunları iç içe geçirebilir veya diğer statik optimizasyonları gerçekleştirebilir. Swift 5.6+, varlık türü silmeyi daha görünür hale getirmek için any anahtar kelimesi sözdizimini tanıttı ve tür bilgisi kaybolduğunu hatırlatarak statik dağıtımın uzantıya varsayılan olarak döndüğünü hatırlatıyor.
protocol Drawable { func draw() // Gereksinim: tanıklık tablosu aracılığıyla dinamik dağıtım } extension Drawable { func draw() { print("Varsayılan") } func render() { print("Statik render") } // Uzantı: yalnızca statik dağıtım } struct Circle: Drawable { func draw() { print("Daire") } func render() { print("Daire render") } } let shape: any Drawable = Circle() shape.draw() // "Daire" yazdırır (dinamik dağıtım) shape.render() // "Statik render" yazdırır (statik dağıtım - Daire'nin sürümünü yok sayar!)
Bir vektör grafik motoru geliştiriyorduk; çeşitli şekiller bir RenderCommand protokolüne uyuyordu. Tüm şekiller için varsayılan bir rasterize edilmiş küçük resim sağlamak amacıyla generatePreview() yöntemini yalnızca bir protokol uzantısında ekledik. Somut türler, keskin görüntüleme için kendi optimized generatePreview() yöntemlerini tanımladı. Bu şekilleri bir [any RenderCommand] dizisinde sakladığımızda, her bir öğeye generatePreview() çağrısının aynı bulanık varsayılan resmi ürettiğini keşfettik, özel yüksek kaliteli ön izleme yerine.
Üç ayrı çözüm düşündük. Öncelikle, generatePreview() yöntemini RenderCommand protokol beyanına resmi bir gereksinim olarak taşıyabilirdik. Bu yaklaşım, tanıklık tablosu aracılığıyla dinamik dağıtımı garanti eder ve çalışma zamanında doğru yöntem çözümlemesini sağlar. Ancak, bu, her şekil türünün uyumda yöntemi açıkça tanımlamasını zorunlu kılardı, ancak varsayılan uygulamayı uzantıda bırakarak özelleştirme gerekmeyen türler için boilerplate'i azaltabilirdik.
İkinci olarak, işlem hattımızı generiklerle yeniden yapılandırabilirdik, örneğin, func process<T: RenderCommand>(commands: [T]) gibi bir işlev imzası yerine [any RenderCommand] kullanmak. Bu, doğru uygulamaya statik dağıtımı koruyacaktı çünkü Swift, derleme zamanında generikleri monomorfize ederek tür bilgilerini korur. Dezavantaj, heterojen şekil türlerini (mixing BezierCurve ve Polygon) tek bir dizi içinde saklayamayacağımızdı; bu, tür silme sarmalayıcısını uygulamadan önemli ölçüde kod karmaşıklığını artırırdı.
Üçüncüsü, her yöntem çağrısını uygun somut türe yönlendirmek için Ziyaretçi desenini uygulayabilirdik. Bu, protokol tanımını tamamen değiştirmeden polimorfik davranış elde etmemizi sağlardı. Ancak, bu çözüm önemli tutamaç kodları ekledi ve sisteme yeni şekil türleri eklenirken bakım yükü oluşturdu.
Sonunda, protokolü modülümüzün dahili bir parçası olarak tutmamız ve polimorfik davranışın netliğinin render motorunun doğruluğu için temel olduğuna karar verdik. Gereksinimi eklemenin ikili boyutumuza önemsiz bir etkisi vardı ve tanıklık tablosu yönlendirmesinin hafif aşırı yükü, görüntüleme hesaplamalarıyla karşılaştırıldığında algılanamazdı. Bu değişikliği uyguladıktan sonra, ön izleme üretimi her şeklin optimizasyonunu doğru bir şekilde kullandı ve UI'daki görsel hataları ortadan kaldırdı.
Neden bir alt sınıf yalnızca bir protokol uzantısında tanımlanan bir yöntemi geçersiz kılamaz?
Bir yöntem yalnızca bir protokol uzantısında tanımlandığında ve protokolde ilan edilmediğinde, Swift, onun için bir tanıklık tablosu girişi ayırmaz. Dağıtım, referans türüne göre derleme zamanında statik olarak çözümlenir. Eğer bir sınıf protokole uyuyorsa ve aynı imzaya sahip bir yöntemi tanımlıyorsa, bu yeni, ilgisiz bir yöntem oluşturur, uzantı yöntemini geçersiz kılmak yerine gölgeler. Bu, bir protokol varlığı aracılığıyla (any Protocol) erişildiğinde, her zaman protokol uzantısının uygulanmasının çağrılacağı anlamına gelir ve sınıfın sürümünü göz ardı eder. Polimorfik davranış elde etmek için, yöntemin protokol beyanında dinamik dağıtım gerektiren bir gereksinim olarak tanımlanması gerekir.
some (opak sonuç türleri) yerine any kullanmanın protokol uzantı yöntemleri üzerindeki dağıtımı nasıl etkilediği?
some Drawable ile, somut tür derleme zamanında bilinir, çünkü Swift generikleri monomorfize eder. Opak bir türde bir uzantı yöntemini çağırırken, derleyici somut türün uygulamasına statik olarak dağıtım yapabilir çünkü tür bilgisi, sahne arkasında gizlenmiş olsa bile korunur. Buna karşın, any Drawable, somut türü silen varlık kutusudur ve derleyicinin, gereklilik olmayan yöntemler için protokol uzantısının varsayılan uygulamasını kullanmasını zorlar. Anahtar fark, some'ın statik polimorfizmi korumasıdır; derleyici için doğru yönteme karşı iç içe geçirme veya doğrudan bağlanma sağlarken, any yalnızca gereklilikler için bir çalışma zamanı vtable arama zorlar ve bunun dışındaki her şey uzantıya varsayılan olarak döner.
Bir uzantı yönteminin bir protokol gereksinimine dönüştürülmesinin ikili boyut ve performans etkisi nedir?
Bir uzantı yöntemini bir protokol gereksinimine dönüştürmek, protokolün tanıklık tablosuna bir giriş ekler, bu da 64 bit mimarilerde her uyum için ikili boyutu yaklaşık 8 byte artırır. Her uyumlu tür artık bu kaydı tanıklık tablosuna doldurmak zorundadır ve tür başına küçük bir bellek aşırı yükü eklenir. Performans açısından, gereksinimler, tanıklık tablosu aracılığıyla dolaylı bir çağrı aşırı yükü içerir (bir ek işaretçi derecelendirmesi ve atlama gerekir), oysa uzantı yöntemleri iç içe geçebilir veya doğrudan sıfır aşırı yük ile çağrılabilir. Ancak, gereksinimler için iç içe geçirme kaybı genellikle CPU'nun dalga belirleyicisi ile telafi edilir ve doğru polimorfik davranışın sağlanmasının avantajı çoğu uygulama kodundaki dolaylı çağrının nanosecond düzeyindeki maliyetinden genellikle daha ağır basar.