Swift 4 Çokbiçimlilik (Polymorphism) #21

Özcan Akkoca
Etiya
Published in
9 min readJun 29, 2017

--

Çokbiçimlilik biyolojiden aktarılmış bir terimdir. Biyolojide çokbiçimlilik canlıların çeşitli doku ve organlarının temel işlevi aynı kalmak üzere farklılık göstermesidir. Örneğin kulak pek çokk canlıda vardır. Temel işlevi duymaktır. Fakat her canlı aynı biçimde duymaz.

Yazılımda çok biçimlilik üç biçimde tanımlanabilmektedir:

1) Biyolojik Tanım: Çokbiçimlilik taban sınıfn belli bir metodunun türemiş sınıflar tarafından onlara özgü bir biçimde yeniden tanımlanmasıdır.

2) Yazılım Mühendisliği Tanımı: Çokbiçimlilik türden bağımsız kod parçalarının yazılması için bir tekniktir.

3) Aşağı Seviyeli Tanım: Çokbiçimlilik önceden yazılmış kodların sonradan yazılacak kodları çağırabilmesi özelliğidir. (Normalde bunun tam tersi olması beklenir)

Swift’te çokbiçimlilik tıpkı Java’da olduğu gibi default bir durumdur. Yani sınıfın statik olmayan her metodu başına final anahtar sözcüğü getirilmediyse sanal metot gibi davranmaktadır.

Swift’te taban sınıftaki static olmayan bir metodun türemiş sınıfta aynı isimle, aynı geri dönüş değeri türüyle ve aynı parametrik yapıyla (yani aynı parametre türleri ve dışsal isimlerle) türemiş sınıfta bildirilmesine “taban sınıftaki metodun türemiş sınıfta override edilmesi” denilmektedir. Override işleminde ayrıca override anahtar sözcüğünün de türemiş sınıf metot bildiriminin başında bulundurulması gerekir. Örneğin:

import Foundation

class A {

var a: Int

init(a: Int)

{

self.a = a

}

func foo(val: Int) -> Int

{

print(“A.foo”)

return val + a

}

}

class B : A {

var b: Int

init(a: Int, b: Int)

{

self.b = b

super.init(a: a)

}

override func foo(val: Int) -> Int

{

print(“B.foo”)

return val + b

}

}

Burada taban sınıftaki foo türemiş sınıfta override edilmiştir. Türemiş sınıftaki override anahtar sözcüğü okunabilirliği artırmak amacıyla zorunlu tutulmuştur. (Java’da bunun gerekmediğine, C#’ta ise sanallığın virtual anahtar sözcüğü ile başlatıldığını anımsayınız. Eğer türemiş sınıfta override edilmek istenen metot taban sınıfta yoksa bu durum derleme aşamasında error oluşturur.

Tıpkı C++’ta olduğu gibi (Java ve C#’ta böyle değil) metot türemiş sınıfta başka bir erişim belirleyici anahtar sözcükle override edilebilir. (Örneğin taban sınıftaki public bir metot türemiş sınıfta private olarak override edilebilir.) Erişim belirleyici anahtar sözcükler ilerideki bölümlerde ele alınmaktadır.

Türemiş sınıfta override edilen bir metot ondan türetilecek sınıfta yeniden override edilebilir. Böylece override işlemi devam ettirilebilir. Örneğin:

class A {

var a: Int

init(a: Int)

{

self.a = a

}

func foo(val: Int) -> Int

{

print(“A.foo”)

return val + a

}

//…

}

class B : A {

var b: Int

init(a: Int, b: Int)

{

self.b = b

super.init(a: a)

}

override func foo(val: Int) -> Int

{

print(“A.foo”)

return val + b

}

}

class C : B {

var c: Int

init(a: Int, b: Int, c: Int)

{

self.c = c

super.init(a: a, b: b)

}

override func foo(val: Int) -> Int

{

print(“C.foo”)

return val / c

}

}

Taban sınıftaki metot türemiş sınıfta override edilmek zorunda değildir. Örneğin istenirse override işlemi daha alt sınıflarda da yapılabilir.

Burada taban A sınıfındaki foo metodu A’dan türetilmiş B’de override edilmemiştir fakat C’de override edilmiştir:

class A {

var a: Int

init(a: Int)

{

self.a = a

}

func foo(val: Int) -> Int

{

print(“A.foo”)

return val + a

}

}

class B : A {

var b: Int

init(a: Int, b: Int)

{

self.b = b

super.init(a: a)

}

}

class C : B {

var c: Int

init(a: Int, b: Int, c: Int)

{

self.c = c

super.init(a: a, b: b)

}

override func foo(val: Int) -> Int

{

print(“C.foo”)

return val / a

}

}

Çokbiçimli Mekanizma

Bir sınıf referansıyla bir metot çağrıldığında metot referansın static türüne ilişkin sınıfın faaliyet alanında aranır (yani önce o sınıfta sonra o sınıfın taban sınıflarında arama yapılır). Metot bulunursa bulunan metodun dinamik türüne ilişkin override edilmiş metot çağrılır. Örneğin:

class A {

var a: Int

init(a: Int)

{

self.a = a

}

func foo(val: Int) -> Int

{

print(“A.foo”)

return val + a

}

}

class B : A {

var b: Int

init(a: Int, b: Int)

{

self.b = b

super.init(a: a)

}

override func foo(val: Int) -> Int

{

print(“B.foo”)

return val + b

}

}

class C : B {

var c: Int

init(a: Int, b: Int, c: Int)

{

self.c = c

super.init(a: a, b: b)

}

override func foo(val: Int) -> Int

{

print(“C.foo”)

return val / c

}

}

var a: A = C(a: 10, b:20, c: 30)

var result: Int

result = a.foo(90) // C’nin

print(result) // 3

Burada a referansının static türü A, dinamik türü C’dir. Dolayısıyla a.foo çağrısında dinamik türe ilişkin C sınıfının foo metodu çağrılmıştır. Örneğin:

import Foundation

class A {

var a: Int

init(a: Int)

{

self.a = a

}

func foo(val: Int) -> Int

{

print(“A.foo”)

return val + a

}

}

class B : A {

var b: Int

init(a: Int, b: Int)

{

self.b = b

super.init(a: a)

}

override func foo(val: Int) -> Int

{

print(“B.foo”)

return val * b

}

}

class C : B {

var c: Int

init(a: Int, b: Int, c: Int)

{

self.c = c

super.init(a: a, b: b)

}

override func foo(val: Int) -> Int

{

print(“C.foo”)

return val / c

}

}

func test(a: A, _ b: Int)

{

let result = a.foo(b)

print(result)

}

var a = A(a: 10)

var b = B(a: 10, b: 20)

var c = C(a: 10, b: 20, c: 30)

test(a, 100)

test(b, 100)

test(c, 100)

Burada test fonksiyonun her çağrıldığında parametre değişkeni olan a referansının dinamik türü farklı olmaktadır.

Eğer referansın dinamik türüne ilişkin sınıfta ilgili metot override edilmemişse yukarıya doğru o metodun override edildiği ilk taban sınıfın metodu çağrılır.

Swift’te init metotları da override edilebilir. (Halbuki örneğin C++, Java ve C#’ta başlangıç metotları (constructors) override edilememektedir.) Örneğin:

class A {

var a: Int

init()

{

a = 0

}

//…

}

class B : A {

var b: Int

init() // error!

{

b = 0

super.init()

}

//…

}

Burada türemiş sınıfta taban sınıf ile aynı parametrik yapıya sahip init metodu kullanıldığı halde metodun başına override anahtar sözcüğü getirilmemiştir. Bildirim şöyle olması gerekirdi:

class A {

var a: Int

init()

{

a = 0

}

//…

}

class B : A {

var b: Int

override init() // geçerli

{

b = 0

super.init()

}

//…

}

Swift’te init metot metotlarının önüne required anahtar sözcüğü getirilirse o sınıftan türetilen türemiş sınıflar o init metodunu override etmek zorundadır. Ayrıca bu durumda türemiş sınıfın override bildiriminde de required anahtar sözcüğünün bulundurulması gerekir. Örneğin:

class A {

required init(a: Int)

{

//…

}

//…

}

class B : A {

init()

{

super.init(a: 0)

//…

}

required init(a: Int)

{

//..

super.init(a: 0)

}

//…

}

required init metotları türemiş sınıfta yazılırken artık override anahtar sözcüğünün kullanılmasına gerek kalmamaktadır. Zaten required anahtar sözcüğü override anlamını da kendisi vermektedir. (Swift derleyicinde türemiş sınıfta hem required hem override belirleyicileri bulundurulursa “warning” oluşmaktadır.)

Yapılar türetmeye kapalı olduğu için yapıların init metotlarında required belirleyicisi kullanılamaz.

Swift’te “stored” ve “computed” property’ler override edilebilirler. (C#’ta da property’lerin override edilebildiğini anımsaynız.). Örneğin:

import Foundation

class A {

var a: Double

init(_ a: Double)

{

self.a = a

}

var val: Double {

get {

return a * a

}

set {

self.a = sqrt(newValue)

}

}

}

class B : A {

var b: Double

init(_ a: Double, _ b: Double)

{

self.b = b

super.init(a)

}

override var val: Double {

get {

return b * b * b

}

set {

self.b = pow(newValue, 1.0 / 3.0)

}

}

}

let a: A = B(2.0, 3.0)

var result = a.val

print(result) // 27.0

a.val = 64.0

result = a.val

print(result) // 64

Burada val computed property’si override edilmiştir. Bu nedenle a.val ifadesinde a’nın dinamik türü B olduğu için B sınıfının val property’sinin get bölümü çalıştırılmıştır. Benzer durum set bölümü için de geçerlidir.

Swift’te read/write (yani mutable) bir property read-only olarak override edilemez. Yani başka bir deyişle property’nin hem get hem de set bölümü varsa biz onu override ederken gem get hem de set bölümünü bulundurmak zorundayız. Ancak read-only bir property read/write olarak override edilebilir. Başka bir deyişle yalnızca get bölümüne sahip bir property hem get hem de set bölümüne sahip olacak biçimde override edilebilmektedir.

Taban sınıftaki stored bir property (yani veri elemanı) türemiş sınıfta computed property olarak override edilebilir. Örneğin:

import Foundation

class A {

var val: Double

init(_ val: Double)

{

self.val = val

}

}

class B : A {

var b: Double

init(_ a: Double, _ b: Double)

{

self.b = b

super.init(a)

}

override var val: Double {

get {

return b * b * b

}

set {

self.b = pow(newValue, 1.0 / 3.0)

}

}

}

let a: A = B(2.0, 3.0)

var result = a.val

print(result) // 27.0

a.val = 64.0

result = a.val

print(result) // 64

Ancak taban sınıfın stored property’si let ile bildirilmişse (yani read-only ise) biz bunu türemiş sınıfta override edememekteyiz. Her ne kadar bunun mantıksal bir gerekçesi yoksa da mevcut Swift standardı buna izin vermemektedir.

Property gözlemcileri de override edilebilmektedir. Örneğin:

import Foundation

class A {

var x: Double = 0 {

willSet (newX) {

print(“A.x willSet(x): \(newX), \(x)”)

}

didSet (oldX) {

print(“A.x didset(x): \(oldX), \(x)”)

}

}

}

class B : A {

override var x: Double {

willSet (newX) {

print(“B.x willSet(x): \(newX), \(x)”)

}

didSet (oldX) {

print(“B.x didset(x): \(oldX), \(x)”)

}

}

}

var a: A = B()

a.x = 10

print(a.x)

Override İşleminin Engellenmesi ve final Anahtar Sözcüğü

Daha önceden de belirtildiği gibi Swift’te metotlar tıpkı Java’da olduğu gibi default durumda override edilebilecek biçimdedir (yani sanal biçimdedir). Default sanallık performans konusunda küçük bir dezavantaj oluşturabilmektedir. Biz bir metodun türemiş sınıf tarafından override edilemeyeceğini final anahtar sözcüğü ile belirtebiliriz. Bu işlem Java’da da aynı biçimde yapılmaktadır. Örneğin:

class A {

//…

final func foo()

{

//…

}

}

class B : A { // B sınıfında artık foo override edilemez

//…

}

Bir sınıfın tamamı final yapılabilir (Java’da da böyle). Bu durumda o sınıftan türetme yapılamaz (yani C#’taki selaed sınıflar gibi). Türetmenin final ile engellenmesi o sınıf türünden nesneler yaratıldığında çokbiçimli mekanizmanın ortadan kalkmasından dolayı daha etkin kod üretilmesini sağlayabilmektedir. Aynı zamanda final sınıflar okunabilirliği de artırırlar.

Swift’te abstract Metotlar ve Sınıflar

Swift’te Java, C# ve C++’ta (C++’ta ismi saf sanal metotlardır) olduğu gibi resmi bir abstract metot kavramı yoktur. Diğer dillerdeki abstract sınıflar Swift’te dolaylı olarak oluşturulabilir. Bu dolaylı oluşturma yöntemi eklenti metotlar (extension methods) ve protokoller konusunda ele alınacaktır.

Any ve AnyObject Türleri

Anımsanacağı gibi Java ve C#’ta tüm sınıflar (C#’ta aynı zamanda yapılar) Object isimli bir taban sınıftan türetilmiş durumdadır. O dillerde biz bir sınıfı hiçbir sınıftan türetmesek bile onun Object sınıfından türetildiği varsayalmaktadır. Böylece o dillerde Object her türlü referansı atabileceğimiz bir tür işlevi görmektedir. Ancak Swift tıpkı C++ gibi tepede bir sınıftan türetilmiş sınıflara sahip bir dil değildir. Fakat Swift’te de her türden referansı atayabileceğimiz Any isimli bir tür bulunmaktadır. Any türünden nesne yaratılamaz ancak referanslar bildirilebilir ve her türden referans any türünden referansa atanabilir. Örneğin:

import Foundation

class Sample {

var a: Int

init()

{

a = 0

}

init(a: Int)

{

self.a = a

}

//…

}

var s: Sample = Sample(a: 10)

var any: Any

any = s // geçerli

Any türünden değişkene yalnızca referans türlerini değil yapı değişkenlerini de atayabiliriz. (Swift’te Int, String gibi türlerin yapı belirttiğini anımsayınız). Örneğin:

var any: Any // geçerli

var a = 123

any = a // geçerli

Örneğin bir fonksiyonun parametresi Any türünden olabilir. Bu durumda biz fonksiyonu herhangi bir türden değişkenle çağırabiliriz:

func foo(a: Any)

{

//…

}

var str = “ankara”

foo(str)

foo(123)

Swift’te Any türü Java ve C#’taki gibi bir taban sınıf değildir. Her türden değişkenin atanabildiği genel bir türdür.

Any türünden bir değişken bir işleme sokulamaz. Ancak as! ya da as? operatörleriyle orijinal türe dönüştürülüp işleme sokulmalıdır. Bu bakımdan Any türü Java ve C#’taki Object türüyle işlevsel olarak eşdeğer değildir.

var s = Sample(a: 10)

var a: Int = 20

var any: Any

var k: Sample

var b: Int

any = s // geçerli

k = any as! Sample

print(k.a)

any = a

b = any as! Int

print(b)

Any türünden diziler de bildirilebilir. Bu dizler örneğin for deyimiyle dolaşılabilir:

var anys: [Any] = [10, 20, 30, “ali”, “veli”, “selami”]

for var any in anys {

if any is Int {

var val = any as! Int

print(val)

}

else if any is String {

var s = any as! String

print(s)

}

}

Pekiyi any bir referans mıdır? Yanıt evet any bir referanstır. O halde biz any türünden bir referansa bir yapıyı atadığımızda any neyi tutmaktadır? İşte anımsanacağı gibi bu duruma C# dünyasında “kutulama dönüştürmesi (boxing conversion)” denilmektedir. Swift’te de bir yapıyı Any türünden bir referansa atadığımızda C#’taki kutulama dönüştürmesi gibi bir olay gerçekleşir. Yani o yapı değişkeninin heap’te bir kopyası çıkarılır. Any referansı o kopyayı gösterir hale gelir. Any türünden referansa atama sonrasında biz asıl değişkeni değiştirirsek any referansının gösterdiği yerdeki kopya değişmeyecektir. Örneğin:

var a = 100

var any:Any

any = a // Burada any a nesnesini göstermiyor, a’nın kopyasını gösteriyor

a = 200

print(any as! Int) // 100

Any türünden referanslara fonksiyon türlerini de atayabiliriz. Örneğin:

var any: Any = {() -> () in print(“Ok”)}

var f = any as! () -> ()

f()

AnyObject türü Any türü gibidir. AnyObject ile Any arasındaki fark AnyObject türüne yalnızca kategori olarak referans türlerine ilişkin değişkenler atanabilirken Any türüne yapıların ve enum’ların da atanabilmesidir. Örneğin:

var any: AnyObject = Sample(a: 10)

var s = any as! Sample

print(s.a)

Anahtar Notlar: Her ne kadar Swift’in orijinal dokümanlarında AnyObject türünden bir referansa yalnızca kategori olarak referans türlerine ilişkin değişkenlerin atanabildiği belirtilmişse de mevcut Swift derleyicilerinde AnyObject türünden referansa yapı ve enum türünden değişkenlerin de atanabildiği görülmektedir. Tabii biz kursumuzda derleyciyi de değil orijinal resmi dokümanları dikkate almaktayız.

AnyObject türünün kullanımıyla özellikle Cocoa ortamında sıkça karşılaşacağız.

--

--