Çoğu Swift Geliştiricisinin Yaptıklarını Bilmedikleri Hatalar

Yayınlanan: 2022-03-11

Bir Objective-C arka planından geldiğimde, başlangıçta Swift'in beni geride tuttuğunu hissettim. Swift, bazen çileden çıkaran güçlü bir şekilde yazılmış doğası nedeniyle ilerleme kaydetmeme izin vermiyordu.

Objective-C'den farklı olarak Swift, derleme zamanında birçok gereksinimi zorlar. id türü ve örtük dönüşümler gibi Objective-C'de gevşetilen şeyler Swift'de bir şey değildir. Bir Int ve Double olsa bile ve bunları eklemek istiyorsanız, bunları açıkça tek bir türe dönüştürmeniz gerekecektir.

Ayrıca, seçenekler dilin temel bir parçasıdır ve basit bir kavram olmalarına rağmen bunlara alışmak biraz zaman alır.

Başlangıçta, her şeyi paketini açmaya zorlamak isteyebilirsiniz, ancak bu sonunda çökmelere yol açacaktır. Dile alıştıkça, derleme zamanında birçok hata yakalandığı için çalışma zamanı hatalarına neredeyse hiç sahip olmadığınızı sevmeye başlıyorsunuz.

Swift programcılarının çoğu, diğer şeylerin yanı sıra, diğer dillerde aşina oldukları aynı uygulamaları kullanarak Swift kodu yazmalarına yol açabilecek olan Objective-C ile daha önce önemli deneyime sahiptir. Ve bu bazı kötü hatalara neden olabilir.

Bu makalede, Swift geliştiricilerinin yaptığı en yaygın hataları ve bunlardan kaçınmanın yollarını özetliyoruz.

Hata yapmayın - Objective-C en iyi uygulamaları, Swift'in en iyi uygulamaları değildir.
Cıvıldamak

1. Zorla Açma Opsiyonelleri

İsteğe bağlı türde bir değişken (örn. String? ) bir değer tutabilir veya tutmayabilir. Bir değer tutmadıklarında, nil eşittirler. İsteğe bağlı bir değer elde etmek için önce onları açmanız gerekir ve bu iki farklı şekilde yapılabilir.

Bunun bir yolu, bir if let veya bir guard let kullanarak isteğe bağlı bağlamadır, yani:

 var optionalString: String? //... if let s = optionalString { // if optionalString is not nil, the test evaluates to // true and s now contains the value of optionalString } else { // otherwise optionalString is nil and the if condition evaluates to false }

İkincisi, ! operatör veya örtük olarak açılmamış isteğe bağlı bir tür kullanma (örn. String! ). İsteğe bağlı nil ise, paketi açmaya zorlamak çalışma zamanı hatasına neden olur ve uygulamayı sonlandırır. Ayrıca, örtük olarak açılmamış bir isteğe bağlı değere erişmeye çalışmak da aynısına neden olacaktır.

Bazen sınıf/yapı başlatıcıda başlatamadığımız (veya istemediğimiz) değişkenlerimiz vardır. Bu nedenle, onları isteğe bağlı olarak bildirmek zorundayız. Bazı durumlarda, kodumuzun belirli bölümlerinde nil olmayacaklarını varsayıyoruz, bu yüzden onları açmaya zorluyoruz veya bunları örtük olarak açılmamış isteğe bağlı olarak ilan ediyoruz çünkü bu her zaman isteğe bağlı bağlama yapmaktan daha kolaydır. Bu dikkatle yapılmalıdır.

Bu, uçta veya film şeridinde bir nesneye başvuran değişkenler olan IBOutlet s ile çalışmaya benzer. Ana nesnenin başlatılması üzerine başlatılmazlar (genellikle bir görünüm denetleyicisi veya özel UIView ), ancak viewDidLoad (bir görünüm denetleyicisinde) veya awakeFromNib (bir görünümde) çağrıldığında nil olmayacağından emin olabiliriz, ve böylece onlara güvenli bir şekilde erişebiliriz.

Genel olarak, en iyi uygulama, sarmayı açmaya zorlamaktan ve dolaylı olarak açılmış isteğe bağlı seçenekleri kullanmaktan kaçınmaktır. Her zaman isteğe bağlı nil olabileceğini düşünün ve isteğe bağlı bağlama kullanarak veya bir sarmayı açmaya zorlamadan önce nil olup olmadığını kontrol edin veya örtük olarak açılmamış bir isteğe bağlı olması durumunda değişkene erişin.

2. Güçlü Referans Döngülerinin Tuzaklarını Bilmemek

Güçlü bir referans döngüsü, bir çift nesne birbirine güçlü bir referans tuttuğunda var olur. Bu Swift için yeni bir şey değil, çünkü Objective-C aynı sorunu yaşıyor ve deneyimli Objective-C geliştiricilerinin bunu düzgün bir şekilde yönetmesi bekleniyor. Güçlü referanslara ve neyin neyi referans aldığına dikkat etmek önemlidir. Swift belgelerinin bu konuya ayrılmış bir bölümü vardır.

Kapakları kullanırken referanslarınızı yönetmek özellikle önemlidir. Varsayılan olarak, kapaklar (veya bloklar), içlerinde referans verilen her nesneye güçlü bir referans tutun. Bu nesnelerden herhangi birinin kapağın kendisine güçlü bir referansı varsa, güçlü bir referans döngüsüne sahibiz. Referanslarınızın nasıl yakalandığını düzgün bir şekilde yönetmek için yakalama listelerinden yararlanmak gerekir.

Blok tarafından yakalanan örneğin blok çağrılmadan önce serbest bırakılma olasılığı varsa, onu zayıf bir referans olarak yakalamanız gerekir, bu nil olabileceğinden isteğe bağlı olacaktır. Şimdi, yakalanan örneğin bloğun ömrü boyunca yeniden tahsis edilmeyeceğinden eminseniz, onu sahipsiz bir referans olarak yakalayabilirsiniz. weak yerine unowned kullanmanın avantajı, referansın isteğe bağlı olmayacağı ve değeri, paketi açmanıza gerek kalmadan doğrudan kullanabilmenizdir.

Aşağıdaki örnekte, Xcode Playground'da çalıştırabilirsiniz, Container sınıfı bir diziye ve dizisi değiştiğinde çağrılan isteğe bağlı bir kapatmaya sahiptir (bunu yapmak için özellik gözlemcilerini kullanır). What sınıfının bir Container örneği vardır ve başlatıcısında Whatever öğesine bir kapatma atar ve bu kapatma self arrayDidChange , böylece Whatever örneği ile kapatma arasında güçlü bir ilişki oluşturur.

 struct Container<T> { var array: [T] = [] { didSet { arrayDidChange?(array: array) } } var arrayDidChange: ((array: [T]) -> Void)? } class Whatever { var container: Container<String> init() { container = Container<String>() container.arrayDidChange = { array in self.f(array) } } deinit { print("deinit whatever") } func f(s: [String]) { print(s) } } var w: Whatever! = Whatever() // ... w = nil

Bu örneği çalıştırırsanız, deinit whatever asla yazdırılmadığını fark edeceksiniz, bu da w örneğimizin bellekten ayrılmadığı anlamına gelir. Bunu düzeltmek için, self güçlü bir şekilde yakalamamak için bir yakalama listesi kullanmalıyız:

 struct Container<T> { var array: [T] = [] { didSet { arrayDidChange?(array: array) } } var arrayDidChange: ((array: [T]) -> Void)? } class Whatever { var container: Container<String> init() { container = Container<String>() container.arrayDidChange = { [unowned self] array in self.f(array) } } deinit { print("deinit whatever") } func f(s: [String]) { print(s) } } var w: Whatever! = Whatever() // ... w = nil

Bu durumda, unowned özelliğini kullanabiliriz çünkü self , kapanışın ömrü boyunca asla nil olmayacaktır.

Bellek sızıntılarını ve sonunda daha güvenli bir kodu azaltacak olan referans döngülerinden kaçınmak için neredeyse her zaman yakalama listelerini kullanmak iyi bir uygulamadır.

3. self Her Yerde Kullanmak

Objective-C'den farklı olarak, Swift ile, bir metot içindeki bir sınıfın veya yapının özelliklerine erişmek için self kullanmamız gerekmez. Bunu yalnızca bir kapanışta yapmamız gerekiyor çünkü onun self yakalaması gerekiyor. self gerekli olmayan yerlerde kullanmak tam olarak bir hata değildir, gayet iyi çalışır ve hiçbir hata veya uyarı olmayacaktır. Ancak, neden gereğinden fazla kod yazasınız? Ayrıca, kodunuzu tutarlı tutmak önemlidir.

4. Tiplerinizin Tipini Bilmemek

Swift, değer türlerini ve referans türlerini kullanır. Ayrıca, bir değer türünün örnekleri, referans türlerinin örneklerinin biraz farklı davranışını sergiler. Örneklerinizin her birinin hangi kategoriye uyduğunu bilmemek, kodun davranışında yanlış beklentilere neden olur.

En nesne yönelimli dillerde, bir sınıfın bir örneğini oluşturduğumuzda ve onu diğer örneklere ve yöntemlere bir argüman olarak ilettiğimizde, bu örneğin her yerde aynı olmasını bekleriz. Bu, onda yapılacak herhangi bir değişikliğin her yere yansıyacağı anlamına gelir, çünkü aslında elimizdekiler tamamen aynı verilere yapılan bir dizi referanstır. Bu davranışı sergileyen nesneler referans türleridir ve Swift'de class olarak bildirilen tüm türler referans türleridir.

Ardından, struct veya enum kullanılarak bildirilen değer türlerimiz var. Değer türleri, bir değişkene atandıklarında veya bir işleve veya yönteme argüman olarak iletildiklerinde kopyalanır. Kopyalanan örnekte bir şeyi değiştirirseniz, orijinal olan değiştirilmez. Değer türleri değişmezdir . CGPoint veya CGSize gibi bir değer türünün örneğinin özelliğine yeni bir değer atarsanız, değişikliklerle birlikte yeni bir örnek oluşturulur. Bu nedenle, değişiklikleri bize bildirmek için bir dizideki özellik gözlemcilerini (yukarıdaki Container sınıfındaki örnekte olduğu gibi) kullanabiliriz. Gerçekte olan, değişikliklerle birlikte yeni bir dizinin yaratılmasıdır; özelliğe atanır ve ardından didSet çağrılır.

Bu nedenle, uğraştığınız nesnenin bir referans veya değer türünde olduğunu bilmiyorsanız, kodunuzun ne yapacağına ilişkin beklentileriniz tamamen yanlış olabilir.

5. Enumların Tam Potansiyelini Kullanmamak

Numaralandırmalar hakkında konuştuğumuzda, genellikle, altında tamsayılar olan ilgili sabitlerin bir listesi olan temel C enumunu düşünürüz. Swift'de numaralandırmalar çok daha güçlüdür. Örneğin, her numaralandırma durumuna bir değer ekleyebilirsiniz. Numaralandırmalar ayrıca her durumu daha fazla bilgi ve ayrıntıyla zenginleştirmek için kullanılabilecek yöntemlere ve salt okunur/hesaplanmış özelliklere sahiptir.

Numaralandırmalarla ilgili resmi belgeler çok sezgiseldir ve hata işleme belgeleri, numaralandırmaların Swift'deki ekstra gücü için birkaç kullanım durumu sunar. Ayrıca, onlarla yapabileceğiniz hemen hemen her şeyi öğrenmek için Swift'deki kapsamlı numaralandırma araştırmasını takip edin.

6. İşlevsel Özelliklerin Kullanılmaması

Swift Standart Kitaplığı, işlevsel programlamada temel olan ve diğerlerinin yanı sıra harita, küçültme ve filtre gibi tek bir kod satırıyla çok şey yapmamızı sağlayan birçok yöntem sunar.

Birkaç örnek inceleyelim.

Diyelim ki, bir tablo görünümünün yüksekliğini hesaplamanız gerekiyor. Aşağıdaki gibi bir UITableViewCell alt sınıfınız olduğu göz önüne alındığında:

 class CustomCell: UITableViewCell { // Sets up the cell with the given model object (to be used in tableView:cellForRowAtIndexPath:) func configureWithModel(model: Model) // Returns the height of a cell for the given model object (to be used in tableView:heightForRowAtIndexPath:) class func heightForModel(model: Model) -> CGFloat }

Bir dizi model örneğine sahip olduğumuzu düşünün modelArray ; tablo görünümünün yüksekliğini bir kod satırı ile hesaplayabiliriz:

 let tableHeight = modelArray.map { CustomCell.heightForModel($0) }.reduce(0, combine: +)

map , her hücrenin yüksekliğini içeren bir dizi CGFloat ve reduce onları toplayacaktır.

Bir dizideki öğeleri kaldırmak istiyorsanız, aşağıdakileri yapabilirsiniz:

 var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"] func isSupercar(s: String) -> Bool { return s.characters.count > 7 } for s in supercars { if !isSupercar(s), let i = supercars.indexOf(s) { supercars.removeAtIndex(i) } }

Bu örnek, her öğe için indexOf çağırdığımız için zarif veya çok verimli görünmüyor. Aşağıdaki örneği göz önünde bulundurun:

 var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"] func isSupercar(s: String) -> Bool { return s.characters.count > 7 } for (i, s) in supercars.enumerate().reverse() { // reverse to remove from end to beginning if !isSupercar(s) { supercars.removeAtIndex(i) } }

Şimdi, kod daha verimli, ancak filter kullanılarak daha da geliştirilebilir:

 var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"] func isSupercar(s: String) -> Bool { return s.characters.count > 7 } supercars = supercars.filter(isSupercar)

Sonraki örnek, belirli bir dikdörtgenle kesişen çerçeve gibi belirli kriterleri karşılayan bir UIView tüm alt görünümlerini nasıl kaldırabileceğinizi gösterir. Şunun gibi bir şey kullanabilirsiniz:

 for v in view.subviews { if CGRectIntersectsRect(v.frame, rect) { v.removeFromSuperview() } } ``` We can do that in one line using `filter` ``` view.subviews.filter { CGRectIntersectsRect($0.frame, rect) }.forEach { $0.removeFromSuperview() }

Yine de dikkatli olmalıyız, çünkü bir satır okunamayan spagetti koduyla sonuçlanabilecek süslü filtreleme ve dönüştürme oluşturmak için bu yöntemlere birkaç çağrıyı zincirlemeye cazip gelebilirsiniz.

7. Konfor Alanında Kalmak ve Protokol Odaklı Programlamayı Denememek

Swift oturumunda WWDC Protokol Odaklı Programlama'da belirtildiği gibi, Swift'in ilk protokol yönelimli programlama dili olduğu iddia ediliyor. Temel olarak bu, programlarımızı protokoller etrafında modelleyebileceğimiz ve sadece protokollere uyarak ve onları genişleterek türlere davranış ekleyebileceğimiz anlamına gelir. Örneğin, bir Shape protokolümüz varsa, CollectionType ( Array , Set , Dictionary gibi türlerle uyumlu olan) genişletebilir ve buna kesişimler için toplam alanı hesaplayan bir yöntem ekleyebiliriz.

 protocol Shape { var area: Float { get } func intersect(shape: Shape) -> Shape? } extension CollectionType where Generator.Element: Shape { func totalArea() -> Float { let area = self.reduce(0) { (a: Float, e: Shape) -> Float in return a + e.area } return area - intersectionArea() } func intersectionArea() -> Float { /*___*/ } }

where Generator.Element: Shape , uzantıdaki yöntemleri belirten bir kısıtlama olduğu ifadesi, yalnızca Shape ile uyumlu türlerin öğelerini içeren CollectionType ile uyumlu tür örneklerinde kullanılabilir. Örneğin, bu yöntemler Array<Shape> örneğinde çağrılabilir, ancak Array<String> örneğinde kullanılamaz. Shape protokolüne uyan bir Polygon sınıfımız varsa, bu yöntemler Array<Polygon> örneği için de kullanılabilir olacaktır.

Protokol uzantılarıyla, protokolde belirtilen yöntemlere varsayılan bir uygulama verebilirsiniz; bu, daha sonra bu türlerde (sınıflar, yapılar veya numaralandırmalar) herhangi bir değişiklik yapmak zorunda kalmadan o protokole uyan tüm türlerde kullanılabilir olacaktır. Bu, Swift Standart Kitaplığı boyunca kapsamlı bir şekilde yapılır; örneğin, map ve reduce , CollectionType öğesinin bir uzantısında tanımlanır ve bu aynı uygulama, herhangi bir ekstra kod olmadan Array ve Dictionary gibi türler tarafından paylaşılır.

Bu davranış, Ruby veya Python gibi diğer dillerdeki karışımlara benzer. Yalnızca varsayılan yöntem uygulamalarına sahip bir protokole uyarak, türünüze işlevsellik eklersiniz.

Protokol odaklı programlama ilk bakışta oldukça garip görünebilir ve çok kullanışlı olmayabilir, bu da onu görmezden gelmenize ve bir şans bile vermemenize neden olabilir. Bu gönderi, gerçek uygulamalarda protokol odaklı programlamayı iyi bir şekilde kavrar.

Öğrendiğimiz Gibi Swift Oyuncak Bir Dil Değildir

Swift başlangıçta çok fazla şüpheyle karşılandı; insanlar, Apple'ın Objective-C'yi çocuklar için bir oyuncak diliyle veya programcı olmayanlar için bir şeyle değiştireceğini düşünüyor gibiydi. Ancak Swift, programlamayı çok keyifli kılan ciddi ve güçlü bir dil olduğunu kanıtladı. Güçlü bir şekilde yazıldığı için hata yapmak zordur ve bu nedenle dil ile yapabileceğiniz hataları listelemek zordur.

Swift'e alışıp Objective-C'ye döndüğünüzde farkı göreceksiniz. Swift'in sunduğu güzel özellikleri kaçıracaksınız ve aynı etkiyi elde etmek için Objective-C'de sıkıcı kodlar yazmanız gerekecek. Diğer zamanlarda, derleme sırasında Swift'in yakalayabileceği çalışma zamanı hatalarıyla karşılaşırsınız. Apple programcıları için harika bir yükseltmedir ve dil olgunlaştıkça daha yapılacak çok şey var.

İlgili: Bir iOS Geliştirici Kılavuzu: Objective-C'den Learning Swift'e