Swift'de Protokol Yönelimli Programlamaya Giriş
Yayınlanan: 2022-03-11Protokol, Swift programlama dilinin çok güçlü bir özelliğidir.
Protokoller, "belirli bir göreve veya işlevsellik parçasına uyan yöntemler, özellikler ve diğer gereksinimlerin bir planını" tanımlamak için kullanılır.
Swift, derleme zamanında protokol uygunluğu sorunlarını kontrol ederek geliştiricilerin programı çalıştırmadan önce bile koddaki bazı önemli hataları keşfetmesine olanak tanır. Protokoller, geliştiricilerin dilin ifade gücünden ödün vermeden Swift'de esnek ve genişletilebilir kodlar yazmasına olanak tanır.
Swift, diğer birçok programlama dilini rahatsız eden arabirimlerin en yaygın tuhaflıklarından ve sınırlamalarından bazılarına geçici çözümler sağlayarak protokolleri kullanmanın rahatlığını bir adım öteye taşıyor.
Swift'in önceki sürümlerinde, birçok modern programlama dilinde olduğu gibi yalnızca sınıfları, yapıları ve enumları genişletmek mümkündü. Ancak Swift'in 2. sürümünden itibaren protokolleri genişletmek de mümkün hale geldi.
Bu makale Swift'deki protokollerin yeniden kullanılabilir ve bakımı yapılabilir kod yazmak için nasıl kullanılabileceğini ve protokol odaklı büyük bir kod tabanında yapılan değişikliklerin protokol uzantıları kullanılarak tek bir yerde nasıl birleştirilebileceğini inceliyor.
protokoller
Protokol nedir?
En basit haliyle bir protokol, bazı özellikleri ve yöntemleri tanımlayan bir arayüzdür. Bir protokole uyan herhangi bir tür, protokolde tanımlanan belirli özellikleri uygun değerlerle doldurmalı ve gerekli yöntemleri uygulamalıdır. Örneğin:
protocol Queue { var count: Int { get } mutating func push(_ element: Int) mutating func pop() -> Int }
Kuyruk protokolü, tamsayı öğeleri içeren bir kuyruğu tanımlar. Sözdizimi oldukça basittir.
Protokol bloğu içinde, bir özelliği tanımladığımızda, özelliğin yalnızca gettable { get }
veya hem gettable hem de settable { get set }
olup olmadığını belirtmeliyiz. Bizim durumumuzda, Count ( Int
türünden) değişkeni yalnızca gettable'dır.
Bir protokol, bir özelliğin alınabilir ve ayarlanabilir olmasını gerektiriyorsa, bu gereksinim sabit bir depolanmış özellik veya salt okunur bir hesaplanmış özellik tarafından karşılanamaz.
Protokol yalnızca bir özelliğin alınabilir olmasını gerektiriyorsa, gereksinim herhangi bir özellik tarafından karşılanabilir ve bu, kendi kodunuz için faydalıysa, özelliğin de ayarlanabilir olması için geçerlidir.
Bir protokolde tanımlanan işlevler için, işlevin mutating
anahtar sözcükle içeriği değiştirip değiştirmeyeceğini belirtmek önemlidir. Bunun dışında bir fonksiyonun imzası tanım olarak yeterlidir.
Bir protokole uymak için, bir tür tüm örnek özelliklerini sağlamalı ve protokolde açıklanan tüm yöntemleri uygulamalıdır. Aşağıda, örneğin, Queue
protokolümüze uyan bir yapı Container
bulunmaktadır. Yapı, itilen Int
s öğelerini özel bir dizi items
saklar.
struct Container: Queue { private var items: [Int] = [] var count: Int { return items.count } mutating func push(_ element: Int) { items.append(element) } mutating func pop() -> Int { return items.removeFirst() } }
Ancak mevcut Kuyruk protokolümüzün büyük bir dezavantajı var.
Yalnızca Int
s ile ilgilenen kapsayıcılar bu protokole uyabilir.
“İlişkili türler” özelliğini kullanarak bu sınırlamayı kaldırabiliriz. İlişkili türler jenerikler gibi çalışır. Göstermek için, ilişkili türleri kullanmak için Kuyruk protokolünü değiştirelim:
protocol Queue { associatedtype ItemType var count: Int { get } func push(_ element: ItemType) func pop() -> ItemType }
Artık Kuyruk protokolü her tür öğenin depolanmasına izin veriyor.
Container
yapısının uygulanmasında, derleyici bağlamdan ilişkili türü belirler (yani, yöntem dönüş türü ve parametre türleri). Bu yaklaşım, genel bir öğe türüyle bir Container
yapısı oluşturmamıza olanak tanır. Örneğin:
class Container<Item>: Queue { private var items: [Item] = [] var count: Int { return items.count } func push(_ element: Item) { items.append(element) } func pop() -> Item { return items.removeFirst() } }
Protokolleri kullanmak, birçok durumda kod yazmayı basitleştirir.
Örneğin, bir hatayı temsil eden herhangi bir nesne Error
(veya yerelleştirilmiş açıklamalar sağlamak istiyorsak LocalizedError
) protokolüne uyabilir.
Aynı hata işleme mantığı, kodunuz boyunca bu hata nesnelerinden herhangi birine uygulanabilir. Sonuç olarak, hataları temsil etmek için belirli bir nesne (Objective-C'deki NSError gibi) kullanmanıza gerek yoktur, Error
veya LocalizedError
protokollerine uyan herhangi bir türü kullanabilirsiniz.
Hatta String türünü LocalizedError
protokolüyle uyumlu hale getirmek ve dizeleri hata olarak atmak için genişletebilirsiniz.
extension String: LocalizedError { public var errorDescription: String? { Return NSLocalizedString(self, comment:””) } } throw “Unfortunately something went wrong” func handle(error: Error) { print(error.localizedDescription) }
Protokol Uzantıları
Protokol uzantıları, protokollerin mükemmelliği üzerine kuruludur. Şunları yapmamıza izin veriyorlar:
Protokol yöntemlerinin varsayılan uygulamasını ve protokol özelliklerinin varsayılan değerlerini sağlayın, böylece bunları "isteğe bağlı" hale getirin. Bir protokole uyan türler kendi uygulamalarını sağlayabilir veya varsayılanları kullanabilir.
Protokolde açıklanmayan ek yöntemlerin uygulanmasını ekleyin ve protokole uyan herhangi bir türü bu ek yöntemlerle “süsleyin”. Bu özellik, her bir türü ayrı ayrı değiştirmek zorunda kalmadan, halihazırda protokole uyan birden çok türe belirli yöntemler eklememize olanak tanır.
Varsayılan Yöntem Uygulaması
Bir protokol daha oluşturalım:
protocol ErrorHandler { func handle(error: Error) }
Bu protokol, bir uygulamada meydana gelen hataları işlemekten sorumlu nesneleri tanımlar. Örneğin:
struct Handler: ErrorHandler { func handle(error: Error) { print(error.localizedDescription) } }
Burada sadece hatanın yerelleştirilmiş açıklamasını yazdırıyoruz. Protokol uzantısı ile bu uygulamayı varsayılan hale getirebiliriz.
extension ErrorHandler { func handle(error: Error) { print(error.localizedDescription) } }
Bunu yapmak, varsayılan bir uygulama sağlayarak handle
yöntemini isteğe bağlı hale getirir.
Varolan bir protokolü varsayılan davranışlarla genişletme yeteneği oldukça güçlüdür ve mevcut kodun uyumluluğunu bozma konusunda endişelenmenize gerek kalmadan protokollerin büyümesine ve genişletilmesine olanak tanır.
Koşullu Uzantılar
Bu nedenle, handle
yönteminin varsayılan bir uygulamasını sağladık, ancak konsola yazdırmak son kullanıcı için pek yardımcı olmuyor.
Hata işleyicinin bir görünüm denetleyicisi olduğu durumlarda, muhtemelen onlara yerelleştirilmiş bir açıklama içeren bir tür uyarı görünümü göstermeyi tercih ederiz. Bunu yapmak için ErrorHandler
protokolünü genişletebiliriz, ancak uzantıyı yalnızca belirli durumlar için geçerli olacak şekilde sınırlayabiliriz (yani, tür bir görünüm denetleyicisi olduğunda).
Swift, where
anahtar sözcüğünü kullanarak protokol uzantılarına bu tür koşulları eklememize izin verir.
extension ErrorHandler where Self: UIViewController { func handle(error: Error) { let alert = UIAlertController(title: nil, message: error.localizedDescription, preferredStyle: .alert) let action = UIAlertAction(title: "OK", style: .cancel, handler: nil) alert.addAction(action) present(alert, animated: true, completion: nil) } }
Yukarıdaki kod parçasındaki Self (büyük harf "S" ile), türe (yapı, sınıf veya enum) atıfta bulunur. Protokolü yalnızca UIViewController
UIViewController
yöntemleri kullanabiliriz (örneğin present(viewControllerToPresnt: animated: completion)
).

Artık, ErrorHandler
protokolüne uyan tüm görünüm denetleyicileri, yerelleştirilmiş bir açıklama ile bir uyarı görünümü gösteren handle
yönteminin kendi varsayılan uygulamasına sahiptir.
Belirsiz Yöntem Uygulamaları
Her ikisi de aynı imzaya sahip bir metoda sahip iki protokol olduğunu varsayalım.
protocol P1 { func method() //some other methods } protocol P2 { func method() //some other methods }
Her iki protokolün de bu yöntemin varsayılan uygulamasına sahip bir uzantısı vardır.
extension P1 { func method() { print("Method P1") } } extension P2 { func method() { print("Method P2") } }
Şimdi her iki protokole de uyan bir tür olduğunu varsayalım.
struct S: P1, P2 { }
Bu durumda, belirsiz yöntem uygulamasıyla ilgili bir sorunumuz var. Tür, yöntemin hangi uygulamasını kullanması gerektiğini açıkça belirtmiyor. Sonuç olarak, bir derleme hatası alıyoruz. Bunu düzeltmek için, yöntemin uygulamasını türe eklemeliyiz.
struct S: P1, P2 { func method() { print("Method S") } }
Birçok nesne yönelimli programlama dili, belirsiz uzantı tanımlarının çözümünü çevreleyen sınırlamalarla boğuşmaktadır. Swift, programcının derleyicinin yetersiz kaldığı yerlerde kontrolü ele geçirmesine izin vererek protokol uzantıları aracılığıyla bunu oldukça zarif bir şekilde ele alıyor.
Yeni Yöntemler Ekleme
Queue
protokolüne bir kez daha bakalım.
protocol Queue { associatedtype ItemType var count: Int { get } func push(_ element: ItemType) func pop() -> ItemType }
Queue
protokolüne uyan her türün, saklanan öğelerin sayısını tanımlayan bir count
örneği özelliği vardır. Bu, diğer şeylerin yanı sıra, hangisinin daha büyük olduğuna karar vermek için bu tür türleri karşılaştırmamızı sağlar. Bu yöntemi protokol uzantısı ile ekleyebiliriz.
extension Queue { func compare<Q>(queue: Q) -> ComparisonResult where Q: Queue { if count < queue.count { return .orderedDescending } if count > queue.count { return .orderedAscending } return .orderedSame } }
Bu yöntem, kuyruk işleviyle ilgili olmadığı için Queue
protokolünün kendisinde açıklanmamıştır.
Bu nedenle, protokol yönteminin varsayılan bir uygulaması değildir, daha ziyade Queue
protokolüne uyan tüm türleri “süsleyen” yeni bir yöntem uygulamasıdır. Protokol uzantıları olmadan bu yöntemi her türe ayrı ayrı eklememiz gerekir.
Protokol Uzantıları ve Temel Sınıflar
Protokol uzantıları, bir temel sınıf kullanmaya oldukça benzer görünebilir, ancak protokol uzantılarını kullanmanın birkaç faydası vardır. Bunlar aşağıdakileri içerir, ancak bunlarla sınırlı değildir:
Sınıflar, yapılar ve numaralandırmalar birden fazla protokole uyabildiğinden, birden çok protokolün varsayılan uygulamasını alabilirler. Bu, kavramsal olarak diğer dillerdeki çoklu mirasa benzer.
Protokoller sınıflar, yapılar ve numaralandırmalar tarafından benimsenebilirken, temel sınıflar ve kalıtım yalnızca sınıflar için kullanılabilir.
Swift Standart Kitaplık Uzantıları
Kendi protokollerinizi genişletmenin yanı sıra, Swift standart kitaplığından protokolleri genişletebilirsiniz. Örneğin, kuyruk koleksiyonunun ortalama boyutunu bulmak istiyorsak, bunu standart Collection
protokolünü genişleterek yapabiliriz.
Swift'in standart kitaplığı tarafından sağlanan ve öğelerinde dizinlenmiş alt simge yoluyla geçiş yapılabilen ve erişilebilen dizi veri yapıları, genellikle Collection
protokolüne uygundur. Protokol uzantısı aracılığıyla, bu tür standart kitaplık veri yapılarının tümünü genişletmek veya bunlardan birkaçını seçici olarak genişletmek mümkündür.
Not: Swift 2.x'te daha önce
CollectionType
olarak bilinen protokol, Swift 3'teCollection
olarak yeniden adlandırıldı.
extension Collection where Iterator.Element: Queue { func avgSize() -> Int { let size = map { $0.count }.reduce(0, +) return Int(round(Double(size) / Double(count.toIntMax()))) } }
Artık herhangi bir kuyruk koleksiyonunun ortalama boyutunu hesaplayabiliriz ( Array
, Set
, vb.). Protokol uzantıları olmasaydı, bu yöntemi her koleksiyon türüne ayrı ayrı eklememiz gerekirdi.
Swift standart kitaplığında protokol uzantıları, örneğin map
, filter
, reduce
vb. yöntemleri uygulamak için kullanılır.
extension Collection { public func map<T>(_ transform: (Self.Iterator.Element) throws -> T) rethrows -> [T] { } }
Protokol Uzantıları ve Polimorfizm
Daha önce de söylediğim gibi, protokol uzantıları, bazı yöntemlerin varsayılan uygulamalarını eklememize ve yeni yöntem uygulamaları eklememize izin verir. Ancak bu iki özellik arasındaki fark nedir? Hata işleyiciye geri dönelim ve öğrenelim.
protocol ErrorHandler { func handle(error: Error) } extension ErrorHandler { func handle(error: Error) { print(error.localizedDescription) } } struct Handler: ErrorHandler { func handle(error: Error) { fatalError("Unexpected error occurred") } } enum ApplicationError: Error { case other } let handler: Handler = Handler() handler.handle(error: ApplicationError.other)
Sonuç ölümcül bir hatadır.
Şimdi handle(error: Error)
yöntemi bildirimini protokolden kaldırın.
protocol ErrorHandler { }
Sonuç aynı: ölümcül bir hata.
Bu, protokol yönteminin varsayılan bir uygulamasını eklemek ile protokole yeni bir yöntem uygulamasını eklemek arasında bir fark olmadığı anlamına mı geliyor?
Numara! Bir fark vardır ve handler
değişkeninin türünü Handler
ErrorHandler
değiştirerek bunu görebilirsiniz.
let handler: ErrorHandler = Handler()
Şimdi konsola çıktı: İşlem tamamlanamadı. (Uygulama Hatası hatası 0.)
Ancak, handle(error: Error) yönteminin bildirimini protokole döndürürsek, sonuç ölümcül hataya geri dönecektir.
protocol ErrorHandler { func handle(error: Error) }
Her durumda ne olduğuna bakalım.
Protokolde yöntem bildirimi olduğunda:
Protokol, handle(error: Error)
yöntemini bildirir ve varsayılan bir uygulama sağlar. Yöntem, Handler
uygulamasında geçersiz kılınır. Bu nedenle, değişkenin türünden bağımsız olarak, yöntemin doğru uygulanması çalışma zamanında çağrılır.
Protokolde yöntem bildirimi olmadığında:
Yöntem protokolde bildirilmediğinden, tür onu geçersiz kılamaz. Bu nedenle, çağrılan bir yöntemin uygulanması, değişkenin türüne bağlıdır.
Değişken Handler
, türden yöntem uygulaması çağrılır. Değişkenin ErrorHandler
türünde olması durumunda, protokol uzantısından yöntem uygulaması çağrılır.
Protokole Yönelik Kod: Güvenli ama Etkileyici
Bu makalede, Swift'deki protokol uzantılarının bazı gücünü gösterdik.
Arayüzlü diğer programlama dillerinden farklı olarak Swift, protokolleri gereksiz sınırlamalarla kısıtlamaz. Swift, geliştiricinin belirsizliği gerektiği gibi çözmesine izin vererek bu programlama dillerinin ortak tuhaflıkları üzerinde çalışır.
Swift protokolleri ve protokol uzantıları ile yazdığınız kod, çoğu dinamik programlama dili kadar anlamlı olabilir ve derleme zamanında yine de tip açısından güvenli olabilir. Bu, kodunuzun yeniden kullanılabilirliğini ve sürdürülebilirliğini sağlamanıza ve Swift uygulamanızın kod tabanında daha güvenle değişiklik yapmanıza olanak tanır.
Bu makalenin sizin için yararlı olacağını umuyoruz ve her türlü geri bildirimi veya daha fazla bilgiyi memnuniyetle karşılıyoruz.