Swift 4 Türetme İşlemleri #20
Anımsanacağı gibi Swift’te değer türleri (yani struct, enum türleri) türetmeye kapalıdır. Yani bir yapı ya da enum türünden biz bir sınıf, yapı ya da enum türü, bir sınıf türünden de bir yapı ya da enum türü türetemeyiz. Ancak sınıflardan sınıflar türetilebilir.
Swift’te türetme işleminin genel biçimi şöyledir:
class <türemiş sınıf ismi> : <taban sınıf ismi> {
//…
}
Swift’in türetme sentaksının C++ ve C#’takine benzediğine dikkat ediniz.
Diğer dillerde de olduğu gibi türetme durumunda taban sınıfın elemanları sanki türemiş sınıfın elemanlarıymış gibi işlem görmektedir. Yani biz türemiş sınıf türünden bir referansla hem türemiş sınıfın hem de taban sınıfın elemanlarını kullanabiliriz. Örneğin:
class A {
var x: Int = 0
func foo()
{
print(“A.foo”)
}
}
class B : A {
var y: Int = 0
func bar()
{
print(“B.bar”)
}
}
var b = B();
b.foo() // geçerli
b.bar() // geçerli
print(b.x) // geçerli
print(b.y) // geçerli
Yine Swift’te de türemiş sınıf türünden bir nesne hem taban sınıf property’lerini i(yani veri alamanlarını) hem de türemiş sınıfın kendi property’lerini (yani veri elemanlarını) içerir. sınıfların ve yapıların metotları nesne içerisinde değildir. Programın kod alanı içerisindedir. Örneğin:
Yine Swift’te de türemiş sınıf nesnesinin taban sınıf kısmı ile türemiş sınıf kısmı ardışıl bir blok oluşturur. Nesnenin taban sınıf kısmı bellekte düşük adreste bulunmaktadır.
Programların Kod, Stack, Data ve Heap Alanları
Her uygulamanın bellek alanı kabaca dört kısma ayrılmaktadır: Kod, stack, data ve heap. Bir programdaki bütün global fonksiyonlar ve metotlar (yani yapıların ve sınıfların fonksiyonları) biraraya getirilerek kod bölümünde toplanmaktadır. Yani yapıların ya da sınıfların metotları nesnenin içerisinde değildir. Onlar uygulamanın adres alanının kod bölümündedir. Metotların sınıflarla ilişkisi mantıksal düzeydedir.
Fonksiyonların ve metotların parametre değişkenleri ve onların blokları içerisinde bildirilmiş olan yerel değişkenler stack’te yaratılmaktadır. Stack doldur-boşalt tarzı bir alandır. Bir değişken programın akışı fonksiyon ya da metotta o değişkenin bildirildiği yere geldiğinde stack’te yaratılır. Fonksiyon ya da metot bittiğinde stack’ten yok edilir. Örneğin:
Görüldüğü gibi bir fonksiyon ya da metot çağrılmadığı sürece onların içerisindeki yerel değişkenler zaten bellekte yer kaplıyor durumda değildir. Stack’te değişkenlerin yaratılması ve yok edilmesi çok hızlı bir biçimde yapılmaktadır.
Swift’te fonksiyonların ve sınıfların dışında bildirilen değişkenler uygulamanın data bölümünde yaratılırlar. Data bölümü programın yaşamı boyunca (yani run time boyunca) bellekte kalmaktadır.
Sınıf nesneleri heap’te yaratılır. Sınıf nesnelkeri ile onların adreslerinin tutulduğu değişkenleri (referansları) birbirine karıştırmayınız. Örneğin:
Görüldüğü gibi aşağıdaki gibi bir işlemde a (eğer fonksiyonun içerisindeyse) stack’te yratılır. Ancak A nesnesi heap’te yaratılmaktadır:
var a = A()
Heap’te yaratılan sınıf nesneleri onu hiçbir referans göstermediği zaman sistem tarafından otomatik silinmektedir. Heap’teki nesnelerin yok edilmesi stack’e göre çok dsaha yavaş yapılmaktadır. Belli bir anda bir sınıf nesnesini n tane referans gösteriyor durumdadır. Buna nesnenin referans sayacı denir. Nesnenin referans sayacı sıfıra düştüğünde artık nesne çöp durumuna gelir. Swift’teki ARC (ya da C# ve Java’daki garbage collector) nesneyi heap’ten siler.
Türetme Durumunda init Metotlarının Çağrılması
Bilindiği gibi C++’ C# ve Java gibi dillerde türemiş sınıf türünden bir nesne yaratıldığında türemiş sınıfın başlangıç metotları (constructors) çağrılmaktadır. Bu dillerde türemiş sınıfın başlangıç metotları taban sınıfın başlangıç metotlarını çağırmaktadır. Böylece nesnenin taban sınıf kısmına taban sınıfın başlangıç metotları ilkdeğerlerini atamaktadır. Halbuki Swift’te diğer nesne yönelimli dillerin çoğunda olduğu gibi türemiş sınıf başlangıç metodu (yani init metodu) otomatik olarak taban sınıfın başlangıç metodunu (yani init metodunu) çağırmaz. Örneğin:
class A {
var x: Int
init(x: Int)
{
self.x = x
}
}
class B : A {
var y: Int = 20
init(y: Int) // error taban sınıfın x property’si değer almamış
{
self.y = y
}
}
var b = B(y: 100);
Swift’te türemiş sınıfın taban sınıf kısmındaki property’lere ilkdeğerlerinin verilmesi için başvurulan normal yol türemiş sınıfın taban sınıfın init metodunu super.init(…) ifadesi ile çağırmasıdır. Bu bakımdan Swift’te türemiş sınıflar için iki farklı init metodu tanımlanmıştır: “designated init metodu (designated initializer)” ve “convenience init metodu (convenience initializer)”. Eğer türemiş sınıfın init metodunda convenience anahtar sözcüğü belirtilmemişse bu init metodu “designated” init metodu, convenience anahtar sözcüğü belirtilmişse bu init metodu da “convenience” init metodudur. Örneğin:
class A {
//…
}
class B : A {
init(a: Int) // designated init metodu
{
//…
}
convenience init() // convenience init metodu
{
//…
}
//…
}
Swift’te sınıfın bir init metodu kendi sınıfının init metodunu self anahtar sözcüğüyle, taban sınıfın init metodunu ise super anahtar sözcüğüyle çağırmak zorundadır.
Türemiş sınıf ve taban sınıf init metotlarının yazımı sırasında şu kurallara uyulmak zorundadır:
1) Türemiş sınıfın “designated init” metodu taban sınıfın “designated init” metotlarından birini çağırmak zorundadır. Türemiş sınıfın “designated init” metotları kendi sınıflarının init metotlarını çağıramaz.
2) Türemiş sınıfın “convenience init” metodu aynı sınıfın başka bir init metodunu çağırmak zorundadır. Taban sınıfın herhangi bir init metodunu çağıramaz.
3) Türemiş sınıfın “convenience init” metodu sınıfın başka bir “convenience init” metodunu çağırıyor olabilir. Ancak bu zincirin en sonunda türemiş sınıfın “convenience init” metodunun türemiş sınıfın “designated init” metodunu çağırıyor olması gerekir.
Örneğin:
class A {
var x: Int
init(x: Int)
{
self.x = x
}
}
class B : A {
var y: Int
init(x: Int, y: Int)
{
self.y = y
super.init(x: x)
}
convenience init()
{
self.init(x: 10, y: 20)
}
}
class C : B {
var z: Int
init(x: Int, y: Int, z: Int)
{
self.z = z
super.init(x: x, y: y)
}
}
let c = C(x:10, y: 20, z: 30)
4) Türemiş sınıfın “designated init metodu” ancak kendi sınıfının tüm property elemanlarına değer atandıktan sonra taban sınıfın “designated init” metodunu çağırabilir. Yani taban sınıfın init metodu çağrılmadan önce türemiş sınıfın tüm property elemanlarına değer atanmış olmak zorundadır. Örneğin:
class A {
var x: Int
init(x: Int)
{
self.x = x
}
}
class B : A {
var y: Int
init(x: Int, y: Int)
{
super.init(x: x) // error! henüz y’ye değer atanmamış
self.y = y
}
}
Burada B sınıfının y elemanına henüz değer atanmadan A sınıfının init metodu çağrılmıştır. Bu durum error oluşturur. Bu kural C++, Java ve C# gibi dillerdeki olağan akışla çelişkili gibi görünebilir. Gerçekten de o dillerde her zaman önce nesnenin taban sınıf kısmı değerlenmektedir. Swift’te init metotları da override edildiği için böyle bir kuarla gereksinim duyulmuştur.
5) Türemiş sınıfın “designated init” metodu taban sınıfın designated init metodunu çağırmadan önce taban sınıfın property elemanlarını ya da metotlarını kullanamaz. Kullanırsa error oluşur. Örneğin:
class A {
var x: Int
init(x: Int)
{
self.x = x
}
func foo()
{
//…
}
}
class B : A {
var y: Int
init(x: Int, y: Int)
{
self.y = y
super.x = 50 // error! taban sınıfın init metodundan önce property’si kullanılmış
super.init(x: x)
}
}
6) Türemiş sınıfın “convenience init” metodu kendi sınıfının başka bir init metodunu çağırmadan önce kendi sınıfının ya da taban sınıfın bir property elemanını ya da metodunu kullanamaz. Örneğin:
class A {
var x: Int
init(x: Int)
{
self.x = x
}
}
class B : A {
var y: Int
init(x: Int, y: Int)
{
self.y = y
super.init(x: x)
}
convenience init()
{
self.y = 50 // error!
self.init(y: 10)
}
convenience init(y: Int)
{
self.init(x: 100, y: y)
}
}
7) Türemiş sınıf için hiçbir init metodu yazılmamışsa taban sınıfın init metotları sanki türemiş sınıfın init metotlarıymış gibi kullanılır. Örneğin:
class A {
var x: Int
init(x: Int)
{
self.x = x
}
}
class B : A {
var y: Int = 20
}
var b: B = B(x: 10) // geçerli
8) Eğer türemiş sınıf taban sınıfın tüm designated init metotlarını override etmişse taban sınıfın tüm convenience init metotları sanki türemiş sınıfın convenience init metotlarıymış gibi kullanılabilir. Örneğin:
class A {
var x: Int
init(x: Int)
{
self.x = x
}
convenience init()
{
self.init(x: 10)
}
}
class B : A {
var y: Int = 20
override init(x: Int)
{
super.init(x: 10)
}
}
var b: B = B() // geçerli
Swift’te init metotları da override edilebilir. Bu durum override işleminin ele alındığı kısımda açıklanmaktadır.
Pekiyi convenience ve designated init metotlarının ayrı ayrı olmasının anlamı nedir? Aslında şu durum sağlanmaya çalışılmıştır: Sınıfın bir init metodu diğer bir init metodunu çağırabilsin, fakat taban sınıfın init metodu toplamda yalnızca bir kez çağrılsın. İşte bunun derleme zamanında garanti alınması için bunlar uydurulmuştur. Aslında örneğin aynı sorun C#’ta benzer biçimde çözülmüştür. Şöyle ki: C#’ta başlangıç metotlarının kapanış parantezinden sonra ya this(…) ifadesi ya da base(…) ifadesi getirilir. Ancak iki ifade bir arada kullanılamaz. İşte aslında C#’ın bu anlamdaki this(…) sentaksı Swift’teki “convenience init” metoduna, base(…) sentaksı da “designated init” metoduna karşılık gelmektedir. Tabii burada belli bir “Objective-C” uyumu da aynı zamanda korunmaya çalışılmıştır.
Başarısız Olabilen init Metotları (Failable Initializers)
Bilindiği gibi nesne yönelimli pek çok dilde başlangıç metotlarının geri dönüş değerleri yoktur. Bu nedenle başlangıç metotlarında başarısızlıkla karşılaşıldığında genel olarak exception fırlatılmaktadır. Oysa Swift’te başarısız olabilen (failable) init metotları da vardır. Böyle init metotları init? ismiyle bildirilir.
class Sample {
var msg: String = “”
init?(msg: String)
{
if msg.isEmpty {
return nil
}
self.msg = msg
}
}
var s : Sample? = Sample(msg: “”)
if let ss = s {
print(ss)
}
else {
print(“nil”)
}
Görüldüğü gibi başarısız olabilen init metotları ilgili sınıf T türündense aslında bize T? biçiminde seçenksel değer vermektedir. Örneğimizde init metoduyla yaratılan nesne referansının Sample? türüne atandığına dikkat ediniz.
Başarısız olabilen convenience ya da designated init metotları kendi sınıflarının ya da taban sınıflarının normal init metotlarını (non-failable init methods) çağırabilir. Ancak normal init metotları kendi sınıflarının ya da taban sınıflarının başarısız olabilen init metotlarını çağıramaz.
Türemiş Sınıftan Taban Sınıfa Referans Dönüştürmeleri
Nense yönelimli dillerin hepsinde türemiş sınıf türünden referans (ya da adres) taban sınıf türünden referansa doğrudan atanabilmektedir. Bunun nedeni türemiş sınıf nesnesinin tabanm sınıf elemanlarını da içeriyor olmasındandı. Örneğin:
Türemiş sınıf referansını taban sınıf referansına atadıktan sonra artı bu taban sınıf referansı bağımsız bir taban sınıf nesnesini gösteriyor durumda değildir. Türemiş sınıf nesnesinin taban sınıf kısmını gösteriyor durumdadır. Örneğin:
class A {
var x: Int
init(_ x: Int)
{
self.x = x
}
}
class B : A {
var y: Int
init(_ x: Int, _ y: Int)
{
self.y = y
super.init(x)
}
}
var b: B = B(10, 20)
var a: A
a = b
print(a.x) // 10
a.x = 100
print(b.x) // 100
Türemis sınıf referansının taban sınıf referansına atanabilmesinin önemli sonuçları vardır. Bu sayede bir türetme şeması üzerinde genel işlemler yapan fonksiyonlar ya da metotlar yazılabilir. Örneğin:
Biz burada displayInfo fonksiyonunu Employee türünden argümanla çağırmak zorunda değiliz. Worker, Manaher, Executive, SalesPerson, … türünden argümanlarla da çağırabiliriz. Böylece displayInfo genel işlem yapan bir fonksiyon durumuna gelmiştir. Tüm çalışan sınıflarının bir Employee kısmının olduğuna dikkat ediniz. Tabii burada biz displayInfo fonksiyonunu örneğin Executive argümanı ile çağırsak displayInfo Executive’in ancak Employee bilgilerini yazdırabilir. Taban sınıf referansıyla türemiş sınıfın elemanlarına erişmek (orada onlar olsa bile) mümkün değildir.
Türemiş sınıf referansının taban sınıf referansına atanabilmesi çokbiçimlilik (polymorphism) için de gerekmektedir.
Yukarıdaki işlemin tersi yani taban sınıf referansının türemiş sınıf referansına atanması geçerli değildir. Eğer böyle bir atama yapılabilseydi bu durumda olmayan veri elemanlarına erişme gibi bir durum oluşurdu. Örneğin:
Referansların Statik ve Dinamik Türleri
Referansların statik ve dinamik türleri vardır. (Ancak değer türlerinin bu biçimde iki türü yoktur) Bir referansın statik türü bildirimde belirtilen türüdür. Dinamik türü ise referans bir nesneyi gösteriyorsa gösterdiği nesnenin bütünün türüdür. Örneğin:
var x: A
var y: B = B()
x = y
Burada x referansının static türü A’dır, dinamik türü B’dir. Çünkü x’in gösterdiği yerde bütünü B olan bir nesne vardır. x onun A parçasını göstermektedir.
Burada y referansının statik türü de dibnamik türü de B’dir. Örneğin:
Burada x’in statik türü A’dır. Fakat dinamik türü C’dir. Çünkü x’in gösterdiği yerdeki nesnenin bütünün türü (yani en geniş halinin türü) C’dir. Benzer biçimde y’nin statik türü B’dir fakat dinamik türü C’dir. z’in ise hem statik hem de dinamik türü C’dir.
Referansın statik türü hiç değişmez. Fakat dinamik türü onun nereyi gösterdiğine bağlı olarak değişebilir. Örneğin:
Burada ok ile gösterilen üç yerde de x’in statik türü A’dır. Ancak dinamik türü sırasıyla A, B ve C olacak biçimde değişmektedir. Örneğin:
Burada foo’nun her çağrımında parametre değişkeni olan a’nın dinamik türü değişmektedir.
Taban Sınıftan Türemiş Sınıfa Referans Dönüştürmeleri (Downcating)
Bazen taban sınıftan türemiş sınıfa dönüştürme de yapılmak istenebilir. Örneğin bir fonksiyon ya da metot bazı gerekçelerden dolayı türmeiş sınıf nesnesinin adresini bize taban sınıf referansıymış gibi veriyor olabilir. Bizim orijinal nesneyi tam olarak kullanabilmemiz için onu türemiş sınıfa dönüştürmemiz gerekir.
Taban sınıftan türemiş sınıfa yapılan dönüştürmeler haklı ya da haksız olabilir. Eğer dönüştürülmek istenen referansın dinamik türü dönüştürülmek istenen sınıfı kapsıyorsa dönüştürme haklıdır, kapsamıyorsa haksızdır. Örneğin:
aşağıdan yukarıya doğru dönüştürmelerin (upcasts) haksız olma olasılığı yoktur. O nedenle biz türemiş sınıf referansını doğrudan taban sınıf referansına atayabiliriz.
Swift’te aşağıya doğru dönüştürmeler as! ve as? operatörleriyle yapılmaktadır. Bu iki operatör de iki operandlı araek operatörlerdir. Bu operatörlerle aşağıya doğru dönüştürme yapıldığında her zaman derleme aşamasından başarıyla geçilir. Ancak as! operatörü programın çalışma zamanı sırasında da kontrol uygulamaktadır. as! ve as? operatörlerinin sol tarafındaki operand bir sınıf türünden referans belirtmek zorundadır. Bunların sağ tarafındaki operand bir sınıf ismi olmak zorundadır. Örneğin:
class A {
var x: Int
init(_ x: Int)
{
self.x = x
}
}
class B : A {
var y: Int
init(_ x: Int, _ y: Int)
{
self.y = y
super.init(x)
}
}
var x: A
var y: B = B(10, 2)
var z: B
// Burada x referansının dinamik türü B’dir
x = y // geçerli (upcast)
z = x // error: downcat as! ya da as? operatörü ile yapılmalı
z = x as! B // geçerli
as! operatörü eğer dönüştürme haksızsa dönüştürmenin yapıldığı yerde exception oluşturur. Yani program çöker.
as? operatörü as! operatöyle aynı biçimde kullanılır. Ancak bu operatör dönüştürme haksızsa exception fırlatmaz nil referans üretir. Swift’te biz Java ve C#’ta olduğu gibi referanslara nil (onlarda nul) yerleştiremeyiz. Referanslara dahi Swift’te nil değerin yerleştirilebilmesi için o türün seçeneksel (optional) olması gerekir. Dolayısıyla as? operatörü de sağ tarafındaki tür (yani dönüştürülmek istenen tür) T ise bize T? türünden bir değer üretmektedir. Örneğin:
Örneğin:
import Foundation
class A {
var x: Int
init(_ x: Int)
{
self.x = x
}
}
class B : A {
var y: Int
init(_ x: Int, _ y: Int)
{
self.y = y
super.init(x)
}
}
class C : B {
var z: Int
init(_ x: Int, _ y: Int, _ z: Int)
{
self.z = z
super.init(x, y)
}
}
var x: A = B(10, 20)
var y: B
var z: C?
z = x as? C
if z != nil {
print(“\(z!.x), \(z!.y), \(z!.z)”)
}
else {
print(“haksız dönüştürme!”)
}
Swift’te (tıpkı Java ve C#’ta olduğu gibi) aralarında türetme ilişkisi olmayan iki sınıf arasında as! ya da as! operatörü ile dönüştürme yapılamaz. Örneğin:
Burada biz B’den C’ye dönüştürme yapamayız. Yapmaya çalışırsak derleme zamanında error oluşur. Çünkü aslında hiçbir zaman örneğin B türünden bir referans C’yi gösterir duruma gelmemektedir. Tabii eğer böyle birşey B’den A’ya sonra A’dan C’ye dönüştürme yoluyla yapılamaya çalışılabilir. Ancak bu dönüştürme hiçbir zaman zaten haklı olamayacaktır.
is Operatörü
is operatörü iki operandlı araek bir operatördür. Bu opeeratörün sol tarafındaki operand bir referans, sağ tarafındaki opereand bir sınıf ismi olmak zorundadır. Operatör sol taraftaki referansın dinamik türünün sağ taraftaki türü içerip içermediğine bakar. Eğer sol taraftaki referansın dinamik türü sağ taraftaki türü içeriyor sa true değeri, içermiyorsa false değeri üretir. Bu operatörün ürettiği değer Bool türündendir. Örneğin:
import Foundation
class A {
var x: Int
init(_ x: Int)
{
self.x = x
}
}
class B : A {
var y: Int
init(_ x: Int, _ y: Int)
{
self.y = y
super.init(x)
}
}
var x: A = B(10, 20)
if x is B {
var y = x as! B
print(“\(y.x), \(y.y)”)
}
else {
print(“x’in dinamik türü B’yi içermiyor”)
}