12.Gün - Swift Sınıflar (Class) ve Kalıtım (Inheritance)
Table of Contents
Sınıflar, ilk bakışta Struct’a oldukça benzemektedir. Sınıflarla da kendisine ait property ve methodları olan yeni veri türleri oluşturabiliriz. Fakat bir fark ile Sınıflar bize kalıtım(inheritance) özelliğini de sunar. Kalıtım ile bir sınıfı diğer bir sınıfın temelleri üzerine inşa edebiliriz.
Struct’lar SwiftUI’de kullanıcı arayüz tasarımı yaparken yoğun olarak kullanılırlar. Veriler için de Sınıflar yoğun olarak kullanılmaktadır.
Sınıf (Class) Nasıl Oluşturulur? #
Struct ile kendi özel veri tipimizi oluşturmayı görmüştük. Kendi özel veri tipimizi oluşturmanın bir diğer yolu da Sınıflardır. Struct ile birçok ortak noktası olduğu gibi, çok önemli farkları da bulunmaktadır.
Sınıf ile Struct’ın Ortak Noktaları #
- Her ikisini de kendimiz oluşturabilir ve isimlendiririz.
- Property observer ve access control dahil olmak üzere her ikisine de property ve method ekleyebiliriz.
- Her ikisinde de custom initializer oluşturabiliriz.
Sınıf ile Struct’ın Farklılıkları #
- Bir sınıfı, başka bir sınıfın özellikleri üzerine inşa edebilir, tüm property ve methodlarını başlangıç noktası olarak alabiliriz. İstersek bazı methodları seçerek geçersiz de kılabiliriz.
- İlk madde dolayısıyla, Sınıflar otomatik olarak memberwise initializer oluşturmazlar. Bu sebeple Sınıflar için custom initializer oluşturmamız ya da tüm property’ler için varsayılan değer atamamız gerekmektedir.
- Memberwise initializer oluşturulamamasının sebebi kalıtımdır (inheritence). Kalıtım yoluyla bir sınıf oluşturduğumuzu düşünelim, ardından ana sınıfımda bazı property’lerde değişiklik yaptığımda, kalıtımı miras alan sınıfın initializer’ı bozulabilirdi.
- Sınıfın bir instance’ını kopyaladığımızda her iki kopya da aynı verileri paylaşır, yani bir kopyayı değiştirirsek diğer kopya da değişir.
- Bir Sınıfın instance’nın son örneği yok edildiğinde, Swift isteğe bağlı olarak deinitializer adı verilen özel bir fonksiyon çağırabilir.
- Bir sınıfı sabit (constant) yapsak bile, değişken (
var
) oldukları sürece property’lerini değiştirebiliriz.
Sınıf (Class) Tanımı #
class Game {
var score = 0 {
didSet {
print("Score is now \(score)")
}
}
}
var newGame = Game()
newGame.score += 10
Yukarıdaki class
tanımı, struct
tanımına oldukça benzemektedir. Fakat arka plandaki beş fark gerçekten çok önemlidir.
Kalıtım Yoluyla Miras Alma (Inheritance) #
Swift kalıtım (inheritance) yoluyla mevcut bir sınıfı temel alarak yeni bir sınıf oluşturmamızı sağlar. Bir sınıf, başka bir sınıftan (parent class veya super class) miras aldığında, Swift yeni sınıfa (child class veya subclass) parent sınıfın property ve methodlarına erişim vererek, child sınıfın özelliklerine ekleme ve değişiklikler yapmamıza olanak tanır.
Bir sınıfın diğerinden miras almasını sağlamak için, child olacak sınıfta :
ile parent sınıfın adı yazılır. Örneğimizi inceleyelim;
//PARENT CLASS
class Employee {
let hours: Int
init(hours: Int) {
self.hours = hours
}
}
Employee
sınıfından iki alt sınıf oluşturabiliriz. İki alt sınıfın her biri hours
property’sine ve initializer’ına sahip olacaktır.
//CHILD CLASSES
class Developer: Employee {
func work() {
print("I'm writing code for \(hours) hours.")
}
}
class Manager: Employee {
func work() {
print("I'm going to meetings for \(hours) hours.")
}
}
Bu iki child sınıfın doğrudan hours
property’sini kullanabildiğine dikkat edelim.
Bu child sınıflar, Employee
’den miras alır, ardından her bir child kendi özelleştirmelerini ekler. Dolayısıyla her birinin bir instance’ını oluşturup work()
methodunu çağırırsak farklı bir sonuç elde ederiz.
let robert = Developer(hours: 8)
let joseph = Manager(hours: 10)
robert.work()
joseph.work()
//ÇIKTI:
//----------------------------------------
//I'm writing code for 8 hours.
//I'm going to meetings for 10 hours.
Child sınıf tarafından property’ler miras alınabildiği gibi, methodlar da miras alınabilir. Örneğin Employee
sınıfına aşağıdaki methodu ekleyelim.
func printSummary() {
print("I work \(hours) hours a day.")
}
Developer
Employee
’den miras aldığı için, Developer
instance’larının hepsinde printSummary()
methodunu çağırmaya başlayabiliriz.
let novall = Developer(hours: 8)
novall.printSummary()
//ÇIKTI:
//----------------------------------------
//I work 8 hours a day.
Miras aldığımız bir methodu değiştirmek istediğimizde ise işler biraz karmaşık hale gelebilir. Örneğin, Employee
sınıfına printSummary()
methodunu koyduk ancak belki de child sınıflardan biri daha farklı davranmasını istiyor olabilir.
Bu noktada Swift basit bir kural ortaya koyar: Eğer bir child sınıf, parent sınıftaki bir methodu değiştirmek istiyorsa, child sınıfta override
kullanılması gerekir. Bu iki şey yapar;
override
kullanmadan bir methodu değiştirmeye çalışırsak, Swift kodu oluşturmayı reddeder. Bu sayede bir methodu yanlışlıkla geçersiz kılmamış oluruz.override
kullanıyorsak, ancak methodumuz aslında üst sınıftan bir şeyi geçersiz kılmıyorsa, Swift yine kodumuzu oluşturmayacaktır. Çünkü muhtemel hata yapmışızdır.
Dolayısıyla Developer
sınıfının benzersiz bir printSummary()
methoduna sahip olmasını istiyorsak, aşağıdaki kodu Developer
sınıfına ekleriz.
override func printSummary() {
print("I'm a developer who will sometimes work \(hours) hours a day, but other times spend hours arguing about whether code should be indented using tabs or spaces.")
}
override
yapılırken bir ayrım da söz konusudur. Parent sınıfımızın parametre almayan bir work()
methodu varsa, ancak alt sınıfın da String
kabul eden bir work()
methodu varsa bu durumda parent methodu değiştirmediğimiz için override
kullanmamız gerekmez.
Sınıfımızın kalıtımı (inheritance) desteklememesi gerektiğinden eminsek, onu
final
olarak işaretleyebiliriz. Bu durumda, sınıfın kendisi başka sınıflardan miras alabilir fakat miras almak için kullanılamayacağı anlamına gelir. Hiçbir child sınıffinal
bir sınıfı parent olarak alamaz.
Sınıflar için Initializer Ekleme #
Sınıf initializer’ları struct initializer’larındna biraz daha karmaşıktır. Bir child sınıfın custom initializer’ı varsa kendi property’lerini ayarladıktan sonra mutlaka parent sınıfın initializer’ını çağırmalıdır.
Bir sınıf kalıtım yoluyla miras alsın veya almasın, otomatik olarak memberwise initializer Sınıflarda kullanılamaz. Dolayısıyla ya kendi custom initializer’ımızı yazmamız gerekir ya da sınıfın tüm property’lerine varsayılan değer ataması yapmamız gerekir.
Bir sınıf tanımlayarak başlayalım;
class Vehicle {
let isElectric: Bool
init(isElectric: Bool) {
self.isElectric = isElectric
}
}
Bu sınıfın tek bir Boolean property’si ve bu property’yi ayarlamak için bir initializer’ı var.
Şimdi Vehicle
sınfından miras alarak bir Car
sınıfı yapmak istedik;
//DİKKAT GEÇERSİZ KOD.
class Car: Vehicle {
let isConvertible: Bool
init(isConvertible: Bool) {
self.isConvertible = isConvertible
}
}
Ancak Swift yukarıdaki kodu reddedecektir. Vehicle
sınıfının isElectric
adında bir property’si var fakat bunun için bir değer sağlamadık.
Swift’in bizden istediği Car
sınıfına isElectric
ve isConvertible
property’lerini içeren bir initializer sağlamamızdır. Fakat isElectric
’i kendimiz depolamak yerine onu aktarmamız gerekir. Yani parent sınıftan (Vehicle
) kendi initializer’ını çalıştırmasını istememiz gerekir.
class Car: Vehicle {
let isConvertible: Bool
init(isElectric: Bool, isConvertible: Bool) {
self.isConvertible = isConvertible
super.init(isElectric: isElectric)
}
}
super
, Swift’in self
’e benzer şekilde bizim için otomatik olarak sağladığı değerlerden biridir. initializer gibi parent sınıfımıza ait methodları çağırmamızı sağlar. İstersek super
’i diğer methodlar için de kullanabiliriz, sadece init ile sınırlı değildir.
Artık her iki sınıfımızda da geçerli bir initalizer’a sahip olduğumuza göre, Car
’ın bir instance’ını şu şekilde oluşturabiliriz.
let teslaX = Car(isElectric: true, isConvertible: false)
Bir child sınıfın kendi intializer’ı yoksa, otomatik olarak parent sınıfın initializer’ını devralır.
Sınıflar Nasıl Kopyalanır? #
Swift’te sınıfın bir instance’ının tüm kopyaları aynı veriyi paylaşır yani, bir kopyada yaptığımız herhangi bir değişiklik otomatik olarak diğer kopyaları da değiştirir. Bunun nedeni Swift’te Sınıfların reference type olmasıdır.
Bunu uygulamada görelim;
class User {
var username = "Anonymous"
}
User
sınıfının sadece bir tane property’si vardır. Fakat bir Sınıfın içinde olduğundan tüm kopyalarda paylaşılacaktır.
User
sınıfından bir instance oluşturalım;
var user1 = User()
Ardından user1
’in bir kopyasını alalım ve kopya üzerinde username
’in değerini değiştirelim;
var user2 = user1
user2.username = "Taylor"
Şimdi de her iki instance’ın username’ini yazdıralım;
print(user1.username)
print(user2.username)
//ÇIKTI:
//----------------------------------------
//Taylor
//Taylor
Instance’ın yalnızca birini değiştirsek de diğeri de değişti.
Bu bir hata gibi görülebilir fakat aslında bu önemli bir özelliktir. Bu sayede uygulamamızın genelinde ortak verileri kolayca paylaşabiliriz.
Sınıfların aksine Struct’larda instance kopyalarında veriler paylaşılmaz. Yani kodumuzda User
sınıfını User
Struct olarak değiştirirsek farklı bir sonuç elde ederiz. Bu durumda ekrana önce Anonymous ardından Taylor yazacaktır.
Deep Copy #
Bir sınıf instance’ının benzersiz bir kopyasını (unique copy) oluşturma işlemine deep copy denilmektedir. Deep copy yapılırken, yeni bir instance oluşturulur ve kopyalama işlemi bu şekilde yapılabilir.
class User {
var username = "Anonymous"
func copy() -> User {
let user = User()
user.username = username
return user
}
}
Yukarıdaki copy()
methodu ile aynı başlangıç değerine sahip instance oluşturabiliriz. Bu sayede gelecekte yapılacak herhangi bir değişiklik orijinali etkilemeyecektir.
Referance Type ve Value Type #
Struct’lar, value type’dır. Yani verileri kendileri tutarlar, kaç tane property veya methodunun olduğu önemsizdir, yine de sabit bir değer gibi kabul edilirler. Diğer taraftan, Sınıflar ise reference type ‘dır. Yani başka bir yerde bulunan veriye atıfta bulunurlar.
var message = "Welcome"
var greeting = message
greeting = "Hello"
Açıkçası value type anlamak sezgisel olarak oldukça kolaydır. Yukarıdaki kod çalıştığında message
hala “Welcome” olarak kalacak, sadece greeting
”Hello” olacaktır. Struct’lar değerleri tamamen değişkenlerinin içinde bulunur ve diğer değerlerle paylaşılmaz. Bu tüm verilerinin doğrudan depolandığı anlamına gelir, bu sebeple kopyalandıklarında tüm verilerin deep copy’sini almış oluruz.
Buna karşın, reference type’ı işaret tabelası gibi düşünebiliriz. Bir sınıfın instance’ını oluşturduğumuzda, instance’ı depolayan değişken aslında nesnenin kendisini değil, nesnenin varolduğu belleği işaret eder. Nesnenin bir kopyasını alırsak, yeni bir işaret tabelası elde ederiz ancak bu tabela hala orijinal nesnenin bulunduğu belleği işaret eder. Bir sınıfın bir örneğini değiştirmenin, tüm kopyaları değiştirmesinin nedeni budur. Nesnenin tüm kopyaları aynı bellek parçasına işaret eden tabelalardır sadece.
Sınıflarda Deinitializer #
Swift Sınıflarda isteğe bağlı olarak deinitializer oluşturulabilir. Deinitializer, nesne oluşturulduğunda değil, yok edildiğinde çağrılır.
Deinitializer özellikleri;
- Tıpkı initializer’larda olduğu gibi, deinitializer’larda da
func
kullanılmaz. - Deinitializer’lar parametre almaz veya geri değer döndürmezler,
()
parantezler bile yazılmaz. - Bir sınıfın instance’ının son kopyası yok edildiğinde deinitializer otomatik olarak çağırılır.
- Hiçbir zaman deinitializer’ı doğrudan çağıramayız; bunlar sistem tarafından otomatik olarak çağrılır.
- Struct’lar kopyalanamadıklarından, deinitializer’ları yoktur.
Deinitializer’ların tam olarak ne zaman çağrılacağı, ne yaptığımıza ve kapsamına (scope) bağlıdır. Kapsam (scope), bilginin mevcut olduğu bağlam (context) anlamına gelir. Kapsamı (scope) şu şekilde örneklendirebiliriz;
- Bir fonksiyon içinde bir değişken oluşturursak, ona fonksiyonun dışından erişemeyiz.
- Bir
if
koşulunun içinde bir değişken oluşturursak, bu değişken koşulun dışında kullanılamaz. - Döngü değişkeni (loop variable) da dahil olmak üzere, bir
for
döngüsü içinde bir değişken oluşturursak, bu değişkeni döngü dışında kullanamayız.
Bir değer kapsamdan(scope) çıktığında, içinde oluşturulduğu bağlamın (context) ortadan kalktığı anlamına gelir. Struct söz konusu olduğunda bu, verilerin yok edildiği anlamına gelir, ancak sınıflarda verilerin yalnızca bir kopyası yok edildiği anlamına gelir yani başka yerlerde hala başka kopyalar olabilir. Ancak son kopya yok olduğunda temel veri de yok edilir ve kullandığı bellek sisteme geri verilir.
Bunu göstermek bir örnek yapalım;
class User {
let id: Int
init(id: Int) {
self.id = id
print("User \(id): I'm alive!")
}
deinit {
print("User \(id): I'm dead!")
}
}
Döngü kullanarak, bu Sınıfın instance’larını hızlı bir şekilde oluşturup yok edebiliriz. Döngü içinde bir User
instance oluşturursak, döngü yinelemesi (iteration) sona erdiğinde temel veri yok edilecektir.
for i in 1...3 {
let user = User(id: i)
print("User \(user.id): I'm in control!")
}
print("Loop is over!")
//ÇIKTI:
//----------------------------------------
//User 1: I'm alive!
//User 1: I'm in control!
//User 1: I'm dead!
//User 2: I'm alive!
//User 2: I'm in control!
//User 2: I'm dead!
//User 3: I'm alive!
//User 3: I'm in control!
//User 3: I'm dead!
//Loop is over!
Bu kod çalıştırıldığında, her bir user
’ın ayrı ayrı oluşturulduğu ve yok edildiğini göreceğiz. Yenisi oluşturulmadan önce diğeri tamamen yok edilecektir.
Deinitializer yalnızca bir sınıfın instance’ına kalan son referans yok edildiğinde çağrılır. Bu sakladığımız bir değişken, sabit ya da Array olabilir.
Örneğin; User
instance’larını oluşturdukça ekliyor olsaydık, bunlar yalnızca dizi temizlendiğinde yok edilirdi.
var users = [User]()
for i in 1...3 {
let user = User(id: i)
print("User \(user.id): I'm in control!")
users.append(user)
}
print("Loop is finished!")
users.removeAll()
print("Array is clear!")
//ÇIKTI:
//----------------------------------------
//User 1: I'm alive!
//User 1: I'm in control!
//User 2: I'm alive!
//User 2: I'm in control!
//User 3: I'm alive!
//User 3: I'm in control!
//Loop is finished!
//User 1: I'm dead!
//User 2: I'm dead!
//User 3: I'm dead!
//Array is clear!
Sınıfların İçindeki Değişkenlerle Çalışma (Mutability) #
Swift’in sınıfları işaret levhaları gibi çalışır: sahip olduğumuz bir sınıf instance’ının her kopyası aslında temel veri parçasına işaret eden bir işaret levhasıdır.
class User {
var name = "Paul"
}
let user = User()
user.name = "Taylor"
print(user.name)
//ÇIKTI:
//----------------------------------------
//Taylor
Yukarıdaki kod, sabit(constant) User
instance’ı oluşturur, ancak daha sonra onu değiştirir. Biraz garip görünüyor mi görünüyor?
Ancak sabit değeri hiç değiştirmez.Evet sınıfın içindeki veriler değişti, ancak sınıf instance’ının kendisi (oluşturduğumuz nesne) değişmedi ve aslında değiştirilemez çünkü onu sabit yaptık.
Şöyle düşünelim; bir user
’ı işaret eden, sabit (constant) bir tabela oluşturduk, ancak bu user
’ın isim etiketini sildik ve farklı bir isim yazdık. Söz konusu user
değişmedi -hala var- ancak dahili verilerinin bir kısmı değişti.
Eğer name
property’sini sabit(constant) yapsaydık, o zaman değiştirilemezdi.
Buna karşın, hem user
instance’ını hem de name
property’sini değişken (var) yapsak ne olur? Property’yi değiştirebiliriz, ayrıca istersek tamamen yeni bir User
instance’ına da geçebiliriz. Tabela benzetmesine devam edecek olursak, bu tabelayı tamamen farklı bir kişiyi gösterecek şekilde çevirmek gibi olacaktır.
class User {
var name = "Paul"
}
var user = User()
user.name = "Taylor"
user = User()
print(user.name)
//ÇIKTI:
//----------------------------------------
//Paul
Yukarıdaki kod sonuç olarak “Paul” yazdıracaktır, çünkü name
’i “Taylor” olarak değiştirsek de tüm user
nesnesinin üzerine yeni bir tane yazarak onu “Paul” olarak resetledik.
Son varyasyonumuz, değişken bir instance ve sabit bir property’ye sahip olmaktır. Bu istersek yeni bir User
oluşturabileceğimiz ancak property’sini yalnızca bir kez atayabileceğimiz manasına gelmektedir.
Elimizdeki dört olasılığı da değerlendirelim;
- Sabit (
let
) instance, sabit (let
) property : her zaman aynıuser
’a işaret eden, her zaman aynıname
’ e sahip tabela. - Sabit (
let
) instance, değişken (var
) property : her zaman aynıuser
’ı işaret eden bir tabela, ancakname
değişebilir. - Değişken (
var
) instance, sabit (let
) property : farklıuser
’ları işaret edebilen tabela ancakname
asla değişmez. - Değişken (
var
) instance, değişken (var
) property : farklıuser
işaret edebilen bir tabela ve buuser
’larınname
’leri de değişebilir.
Diyelim ki bize bir User
instance verildi. Instance sabittir (let
) ancak içindeki property değişken (var
) olarak bildirilmiştir. Bu durum bize sadece istediğimiz zaman bu property’yi değiştirebileceğimizi değil aynı zamanda bu property’nin başka bir yerde değiştirilme olasılığı olduğunu da söyler : sahip olduğumuz sınıf başka bir yerden kopyalanmış olabilir ve property değişken (var
) olduğu için kodun başka bir kısmı bunu sürpriz bir şekilde değiştirebilir.
Sabit (let
) property’ler gördüğümüzde, ne mevcut kodumuzun ne de programın herhangi bir yerinin bu property’yi değiştirilemeyeceğinden emin olabiliriz. Ancak değişken (var
) property’ler ile uğraştığımızda instance’ın sabit (let
) olup olmadığına bakılmaksızın, verilerin değişebileceği olasılığı ortaya çıkar.
Bu Struct’lardan farklıdır, çünkü sabit struct’ların property’leri değişken yapılsa bile değiştirilemez. Çünkü struct’larda işaret levhası yoktur verilerini doğrudan tutar. Bu struct içindeki bir değeri değiştirmeye çalıştığımızda dolaylı olarak struct’ın kendisini de değiştirmiş olacağımız anlamına gelir ki bu da sabit olduğu için mümkün değildir.
İşte bu sebeplerle Sınıflarda mutating
keywordünü kullanmaya gerek yoktur. Fakat struct’larda mutating
oldukça önemlidir. Struct’larda mutating
olarak işaretlenmiş methodlar ile property’lerinin değişebildiğini görmüştük, fakat struct’ın instance’ı sabit(let
) olarak tanımlandıysa property’yi yine de değiştiremeyiz.
//AŞAĞIDAKİ KOD ÇALIŞMAYACAKTIR
struct Test {
var name = "Anon"
mutating func change(name2:String) {
self.name = name2
}
}
//BURADA let OLARAK TANIMLAMA YAPILDIĞINDAN DEĞİŞEMEZ.
let instance = Test()
instance.change(name2:"görkem")
print(instance.name)
Sınıflarda ise instance’ın nasıl oluşturulduğunun önemi yoktur (sabit veya değişken). Bir property’nin değiştirilip değiştirilemeyeceğini belirleyen tek şey, property’nin kendisinin bir sabit olarak oluşturulup oluşturulmadığıdır. Swift property’nin nasıl oluşturulduğuna bakarak değişip değişmeyeceğini anlayabilir özel olarak işaretlemeye gerek yoktur.
100 Days of SwiftUI Checkpoint - 7 #
Bu yazıyı İngilizce olarak da okuyabilirsiniz.
You can also read this article in English.