Standart Swift değer türleri, yığın bellek kaynaklarını yönetmek için sözde kopyalama ve ARC kullanır, böylece değerlerin işlev sınırları arasında serbestçe kopyalanmasına olanak tanır. Buna karşılık, ~Copyable (kopyalanamaz) olarak tanımlanan bir yapı, tamamen sözde kopyalamayı yasaklayarak benzersiz sahipliği zorunlu kılar. Böyle bir yapı bir işleve geçirildiğinde, Swift açık sahiplik notasyonları gerektirir: consuming sahipliği kalıcı olarak çağrılana aktarırken, borrowing geçici okuma-yazma erişimi sağlar, ve inout geçici olarak özel değiştirilebilir erişim sağlar. Bu model, yalnızca taşıma kaynakları için ARC aşırılığını ortadan kaldırır ve hareket sonrası kullanım veya çift kopya hatalarına karşı derleme zamanında güvenliği garanti eder.
Yüksek frekanslı bir ticaret uygulaması geliştiriyorduk; burada 2MB’lık bir piyasa veri paketi, tutarlılık ve performans için eşsiz kalması gereken bir çekirdek alanı DMA tamponunu temsil ediyordu.
Problem: Bu tamponu işleme aşamaları (ağ alımı, doğrulama, strateji motoru) arasında temel belleği kopyalamadan veya sıcak yol üzerinde referans sayımını tetiklemeksizin geçiştirmek. Standart sınıflar, kabul edilemez ARC gecikmeleri getirdi; el ile yönetilen güvenli olmayan işaretçiler ise bellek sızıntıları ve sarkan referanslar konusunda risk oluşturuyordu.
Çözüm 1: Referans sayımlı sınıf. Tamponu bir deinit işleyicisine sahip bir sınıfa sarmayı düşündük. Artıları, tanıdık bellek yönetimi ve kolay paylaşım içeriyordu. Ancak, eksileri ağırdı: her bileşen arasında geçiş, atomik saklama/serbest bırakma işlemlerini tetikleyerek önbellek yerelliğini yok etti ve 100 mikro saniyelik gecikme gereksinimlerimizi ihlal etti.
Çözüm 2: Güvenli olmayan ham işaretçiler. UnsafeMutablePointer<UInt8> kullanarak manuel tahsis yaparak tamamen ARC'yi önledik. Artıları sıfır aşırılık ve tam kontrol sağladı. Ancak, eksileri, derleme zamanı güvenliği garantilerinin olmamasıydı; geliştiriciler tamponu iki kez serbest bırakabilir veya tahsis edilmemiş belleğe erişebilir, bu da üretimde çökmesine yol açabilirdi.
Çözüm 3: Sahiplik değiştiricileri olan kopyalanamaz yapı. İçinde işaretçiyi barındıran struct MarketDataBuffer: ~Copyable tanımladık. Tamponu alan işlevler consuming kullandı (örneğin, func process(_ buffer: consuming MarketDataBuffer)), inceleme işlevleri ise borrowing (örneğin, func validate(_ buffer: borrowing MarketDataBuffer)) kullandı. Bu, benzersiz sahiplik için derleme zamanında zorlamalar sağladı ve sıfır çalışma zamanı aşırılığı oluşturdu.
Seçilen çözüm ve sonuç: Çözüm 3’ü seçtik. Sonuç, derleyicinin kazara kopyaları ve hareket sonrası kullanım hatalarını önlediği deterministik bir veri borusuydu. Sistem, tam sıfır ARC trafiği ile paketleri işledi ve DMA tamponunun her zaman yalnızca bir mantıksal sahibi olmasını garanti etti, gecikme tutarlılığını önemli ölçüde artırdı.
Bir işlev parametresini consuming olarak işaretlemenin, işlev döndükten sonra kopyalanamaz bir değeri kullanma yeteneğini nasıl etkiler?
Bir parametre consuming olarak işaretlendiğinde, işlev değeri girişte sahiplenir. ~Copyable türü için bu, bir kopya yerine yıkıcı bir taşıma oluşturur. Çağıran, değeri bırakmak zorundadır ve işlev çağrısı tamamlandıktan sonra orijinal değişken, kullanılabilirlik dışı hale gelir. Erişim denemesi derleme zamanı hatasına yol açar. Bu, doğrusal sahipliği destekler, böylece değer yaşamı boyunca yalnızca bir sahibi olur. Kopyalanabilir türler için consuming, gereksinimi karşılamak için otomatik bir kopya tetikler, ancak kopyalanamaz türler için hiçbir çoğaltma gerçekleşmez.
Neden kopyalanamaz türler, Swift'in 6.0 sürümünden önce standart genel koleksiyonlar (Array) içinde saklanamaz?
Swift 6.0'dan önce, standart kütüphanedeki genel türler, tür parametrelerinin Copyable'a uyması gerektiğini varsayıyordu. Kopyalanamaz türler, ~Copyable kısıtlamasını kullanarak Copyable'dan açık bir şekilde vazgeçtikleri için, bu varsayılan gereksinimi ihlal ettiler ve bir Array veya Optional içinde saklanamadılar. Swift 6.0, kopyalanamaz genel türleri tanıttı, böylece konteynerler kopyalanamaz öğeleri ~Copyable kısıtlamasını yayarak koşullu olarak destekleyebildi. Ancak, append gibi işlemler consuming anlamını kullanmak zorundadır ve koleksiyon kendisi, kopyalanamaz elemanlar içeriyorsa kopyalanamaz hale gelir, bu da API sınırlarında sahiplik dikkatli bir şekilde ele almayı gerektirir.
Kopyalanamaz türler için borrowing parametre değiştirici ile geleneksel inout değiştiricisi arasındaki fark nedir?
borrowing değiştiricisi, sahipliği aktarmadan geçici, değişmez erişim sağlar. Çağıran, değeri korur ve işlev döndükten sonra kullanmaya devam edebilir, eğer işlev içinde tüketilmemişse. Buna karşılık, inout değiştirici, özel bir erişimi temsil eder: değer, işlevin süresi boyunca değişiklik yapılmasına izin vermek için geçici olarak işlev içine hareket ettirilir ve sonra geri döner. Kopyalanamaz türler için borrowing, sahipliği bırakmadan yalnızca okuma denetimi sağlamak için gereklidir, oysa inout, değerin, geçerli, potansiyel olarak değiştirilmiş bir durumda çağırana döndüğünü garanti eder. Kritik olarak, borrowing, işlevin değeri tüketmesini veya taşımasını engellerken, inout, değerin çağırana geçerli bir durumda dönmesini garanti eder.