Swift'de Protokol Yönelimli Programlamaya Giriş

Yayınlanan: 2022-03-11

Protokol, 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'de Protokol Yönelimli Programlamaya Giriş

Protokol odaklı programlama ile Swift'de esnek ve genişletilebilir kod yazın.
Cıvıldamak

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:

  1. 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.

  2. 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:

  1. 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.

  2. 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'te Collection 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.