Swift 4 Enum Türleri ve Sabitleri #17
Enum türleri pek çok dilde benzer biçimde bulunmaktadır. Enum türlerinden amaç kısıtlı sayıda seçeneğe sahip olan olguları hem sayısal düzeyde hem isimlerle nitelemektir. Örneğin haftanın günleri, yönler, renkler gibi olgular tipik olarak enum türleriyle temsil edilebilmektedir.
Swift’in enum türleri semantik bakımdan C, C++ ve C#’tan ziyade Java’ya benzemektedir. Bir enum bildirimi sıfır ya da daha fazla enum sabiti içerebilir. Swift’te enum türleri protokoleri destekleyebilir. Ancak yapılarda olduğu gibi enum türleri de türetmeye kapalıdır. Yani bir enum’dan türetme yapılamaz bir enum da başka bir türden türetilemez. Enum türleri kategori olarak değer türlerine (value types) ilişkindir. Yani enum türünden bir değişken bir adres değil değerin kendisini tutar. Enum bildiriminin genel biçimi şöyledir:
enum <isim> [ : <protokol istesi>] {
[enum sabit listesi]
}
enum sabit bildiriminin ise genel biçimi şöyledir:
case <isim listesi> [(<tür)]
Örneğin:
enum Day {
case Sunday
case Monday
case Tuesday
case Wednesday
case Thursday
case Friday
case Saturday
}
Aynı bldirim şöyle de yapılabilirdi:
enum Day {
case Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday
}
Ancak aynı satıra birden fazla case yerleştirilemez.
Swift’te her enum ayrı bir tür belirtmektedir. Enum sabitlerine enum ismi ve nokta operatörüyle erişilir. Enum sabitlerinin her biri bildirildiği enum türündendir. Aynı türden iki enum birbirlerine atanabilir. Böylece biz bir enum türünden değişkene aynı enum türünün bir enum sabitini doğrudan atayabiliriz. C#, Java gibi dillerde olduğu gibi enum türleriyle tamsayı türleri arasında ve farklı iki enum türü arasında otomatik dönüştürme yoktur. Örneğin:
var d: Day;
d = Day.Friday // geçerli
print(d) // Friday
d = 1 // error!
Bir enum türünü print fonksiyonuyla yazdırdığınızda enum’un sabit isminin yazdırıldığına dikkat ediniz.
Örneğin:
enum Day {
case Sunday
case Monday
case Tuesday
case Wednesday
case Thursday
case Friday
case Saturday
}
func foo(day: Day)
{
print(day)
}
foo(Day.Thursday)
foo(Day.Saturday)
Eğer enum sabitleri aynı türden bir enum’a atanacaksa ya da == ve != operatörleriyle aynı türden bir enum ile karşılaştırılacaksa bu durumda enum sabiti enum ismi hiç belirtilmeden nokta operatörüyle kullanılabilir. Örneğin:
var d: Day
d = .Sunday // geçerli
print(d)
d = .Thursday // geçerli
print(d)
Fakat örneğin:
var d = .Sunday // error!
enum türleri switch işlemine sokulabilir. Örneğin:
enum Day {
case Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday
}
var day = Day.Wednesday
switch day {
case .Sunday:
print(“Pazar”)
case .Monday:
print(“Pazartesi”)
case .Tuesday:
print(“Salı”)
case .Wednesday:
print(“Çarşamba”)
case .Thursday:
print(“Perşembe”)
case .Friday:
print(“Cuma”)
case .Saturday:
print(“Cumartesi”)
}
Burada switch içerisinde bütün seçeneklerin ele alındığına dikkat ediniz. Dolayısıyla buradaki switch deyimi deault kısma gereksinim duymamaktadır.
Swift’te de enum türlerinin ilişkin olduğu tamsayı türleri vardır. Buna Swift terminolojisinde “temel değer türü (raw value type)” denilmektedir. Enum türünün temel değer türü tıpkı C#’ta olduğu gibi enum isminden sonra ‘:’ atomu ile belirtilir. Enum türünün temel değer türleri tamsayı türlerinden biri, gerçek sayı türlerinden biri, String ya da Character türü olabilir. Bir türün enum türünün temel değer türü olabilmesi için onun Equatable protokolünü desteklemesi gerekir. (Bunun bazı ayrıntıları vardır. Protokoller konusunda ele alınacaktır.) Sınıflar ya da yapılar enum türlerinin temel değer türleri olamazlar.
Örneğin:
enum Color : Int { // Color enum türünün temel değer türü Int
case Red, Green, Blue
}
Eğer enum türüne temel değer türü iliştirilmişse artık biz enum sabitlerine istediğimz değerleri atayabiliriz. Diğer dillerde olduğu gibi enum sabitlerinin birine değer verilirse diğerleri onu izler.
Örneğin:
enum Color : Int {
case Red = 1, Green, Blue
}
Burada Color.Red = 1, Color.Green = 2, Color.Blue = 3 olacaktır. Ancak diğer değerlerin önceki değerleri izlemesi için listede sola doğru ilk kez değer verilen enum sabitine tamsayı bir değerin verilmiş olması gerekir. Örneğin:
enum Direction : Double {
case North = 1.5, East, South = 4, West = 3 // error 1.5 tamsayı değil
}
Fakat örneğin:
enum Direction : Double {
case North = 1, East, South = 4.5, West = 3 // geçerli
}
Enum değişkeni içerisindeki değer rawValue property’si ile elde edilebilir. Enum’un temel değer türü hangi türse rawValue property’si de bize o türden bir değer verir.
enum Color : Int {
case Red = 1, Green, Blue
}
var c = Color.Blue
print(c.rawValue) // 3
print(Color.Blue.rawValue) // 3
Eğer enum türünün temel değer türü belirtilmemişse biz enum sabitlerine değer atayamayız ve enum türünün rawValue property’sini de kullanamayız. Örneğin:
enum Color {
case Red = 1, Green, Blue // error! Color enum’unun rawValue property’si yok!
}
Ya da örneğin:
enum Color {
case Red, Green, Blue
}
var c = Color.Blue
print(c.rawValue) // error! Color enum’unun rawValue property’si yok!
Switf’teki enum türleri metot da içerebilir. Enuım türlerinin başlangıç metotları (init metotları) söz konusu olabilir. Enum metotları içerisinde self anahtar sözcüğü yine enum değişkeninin kendisini temsil eder. Örneğin:
enum Direction {
case North, East , South, West
init()
{
self = .South
}
init(d: Direction)
{
self = d
}
func disp()
{
print(self)
}
}
var d: Direction = Direction()
d.disp()
Swift’te enum elemanlarına ayrı birer tür de atanabilir. Bu işlem enum isminden sonra parantezler içerisinde tür belirtilerek yapılır. Örneğin:
enum Fruit {
case Apple(Int)
case Banana(Double)
case Cherry
}
Burada Fruit enum türünün Apple elemanı Int türüyle, Banana elemanı Double türüyle ilişkilidir. Cherry elemanı da hiçbir türle ilişkili değildir. Örneğin:
var a: Fruit = Fruit.Apple(10)
var b: Fruit = Fruit.Banana(12.4)
var c: Fruit = Fruit.Cherry
Burada bir noktaya dikkat çekmek istiyoruz: Enum türünün temel değer türü belirtildiğinde bu değer türü her elemana ilişkin kabul edilir. Oysa biz elemanlara ayrı ayrı değer türleri atayabilmekteyiz. Örneğin:
enum Fruit {
case Apple(Int)
case Banana(Int)
case Cherry(Int)
}
bildirimi ile aşağıdaki bildiriminin işlevsel olarak birbirine yakındır:
enum Fruit : Int {
case Apple
case Banana
case Cherry
}
Görüldüğü gibi Swift’te enum sabitlerinin her biri farklı türlerden değerleri tutabilir. Pekiyi bu durumda enum türünden bir değişken bellekte kaç byte yer kaplayacaktır?
Örneğin:
enum Info {
case Name(String)
case IdentityNo(Int)
}
var info: Info // info ne kadar yer kaplayacak
İşte enum türlerini C/C++’taki birliklere (unions) benzetebiliriz. Tipik olarak Swift derleyicisi enum türünden bir değişken için o enum’un en uzun eleman türü kadar yer ayırmaktadır. Enum elemanlarının temel değer türleri ne olursa olsun bu elemanların ilgili enum türünden olduğuna dikkat ediniz. Örneğin:
var info: Info // info ne kadar yer kaplayacak
info = Info.Name(“test”) // geçerli
print(info)
info = Info.IdentityNo(123) // geçerli
print(info)
Eğer enum elemanlarına tür bilgisi atadıysak artık rawValue property’si tanımlı değildir, biz rawValue property’sini kullanamayız. Yani rawValue property’si yalnızca enum’un kendisi tür bilgisiyle ilişkilendirilmişse anlamlıdır.
Hem enum’un kendisine hem de elemanlarına tür bilgisi atayamayız. Örneğin aşağıdaki enum bildirimi geçerli değildir:
enum Fruit : Int { // error!
case Apple(Int)
case Banana(Double)
case Cherry
}
Pekiyi bir enum sabitine tür bilgisi atayalım ve ona aşağıdaki gibi bir değer vermiş olalım:
var f: Fruit = .Banana(12.4)
Burada f’in içerisinde Banana vardır ve ona 12.4 değeri iliştirilmiştir. Swift tasarımına göre programcı içinb asıl önemli olan f’te ne olduğudur. O da örneğimizde Banana’dır. Maalesef Banana’ya atadığımız bu 12.4 değerini reflection dışında elde etmenin pratik bir yolu henüz yoktur. Bu değer Swift’te switch-case içerisinde elde edilip kullanılabilmektedir.
Örneğin:
enum Fruit {
case Apple(Int)
case Banana(Double)
case Cherry
}
var f: Fruit = .Banana(12.4)
switch f {
case .Apple(let a):
print(“Apple: \(a)”)
case .Banana(let b):
print(“Banana: \(b)”)
case .Cherry:
print(“Cherry”)
}
Buradaki case sentaksına dikkat ediniz. Enum elemanlarına iliştirilen türler birer tuple da olabilir. Örneğin:
enum Fruit {
case Apple(Int, Int, Int)
case Banana(Double)
case Cherry
}
var f: Fruit = .Apple(10, 20, 30)
switch f {
case .Apple(let a, let b, let c):
print(“Apple: (\(a), \(b), \©)”)
case .Banana(let b):
print(“Banana: \(b)”)
case .Cherry:
print(“Cherry”)
}
Fonksiyonlar Türünden Değişkenlerin Bildirilmesi
Fonksiyonlar da bellekte ardışıl byte toplulukları biçiminde bulunmaktadır. Onların da adresleri vardır. Örneğin aşağıda iki parametresinin toplamına geri dönen add isimli bir fonksiyonun IA32’deki sembolik makine dili karşılığını görüyorsunuz:
add:
push ebp
mov ebp, esp
mov eax, [ebp + 8]
add eax, [ebp + 12]
ret
Bir fonksiyonun yalnızca ismi (parantezler olmadan) onun bellekteki adresi anlamına gelir. Örneğin:
func add(a: Int, _ b: Int) -> Int
{
return a + b
}
Burada add ismi bu fonksiyonun bellekteki başlangıç adresi anlamına gelir. Fonksiyonu çağırırken kullandığımız parantezler ise “o adresteki fonksiyonu çağır” anlamına gelmektedir. Örneğin:
result = add(10, 20)
Burada add adresinden başlayan fonksiyon çağrılmış ve onun geri dönüş değeri result değişkenine atanmıştır.
Swift’te bir fonksiyonu tutabilecek bir değişken bildirilebilir. Bu açıdan fonksiyon türünden değişkenler de birinci sınıf vatandaş (first class citizen) statüsündedir. Fonksiyonu tutabilecek değişkenlerin türleri aşağıdaki gibi oluşturulmaktadır:
([tür listesi]) -> <geri dönüş değerinin türü>
Geri dönüş değeri olmayan fonksiyonların türleri ise şöyle belirtilir:
([tür listesi) -> ()
ya da:
([tür listesi) -> Void
Aslında Void sözcüğü zaten aşağıdaki gibi typealias yapılmıştır:
typealias Void = ()
Yani Void demekle () demek tamamen aynı anlamdadır (typealias’lar sonraki konularda ele alınmaktadır).
Fonksiyon türünden bir değişkene ancak parametre türleri ve geri dönüş değeri aynı olan fonksiyonların ya da metotların adresleri atanabilir.
Örneğin:
func foo(a: Int) -> Int
{
return a * a
}
var f: (Int) -> Int
var result: Int
f = foo // geçerli, foo’nun parametresi ve geri dönüşi değeri Int
result = f(10) // foo çağrılır, geri dönüş değeri elde edilir
print(result)
f bir fonksiyonu tutan değişiken olsun. f değişkeninin tuttuğu fonksiyon yine f(…) ifadesi ile çağrılmaktadır. Örneğin:
result = f(10)
print(result) // 100
Swift’te fonksiyon türleri kategori olarak referans türlerine ilişkindir. Yani bir fonksiyon türünden değişken bir fonksiyonun adresini tutar. Yukarıdaki örnekte f foo fonksiyonunun adresini tutmaktadır. Bundan sonra “bir değişkenin bir fonksiyonu tuttuğundan” söz edildiğinde onun adresini tuttuğu anlaşılmalıdır.
Aynı türden iki fonksiyon değişkeni birbirine atandığında aslında bunların içerisindeki adresler birbirlerine atanmaktadır.
func foo(a: Int) -> Int
{
return a * a
}
var f: (Int) -> Int
var g: (Int) -> Int
var result: Int
f = foo // geçerli
result = f(10)
print(result) // 100
g = f // geçerli
result = g(10)
print(result) // 100
Böylesi işlemlerin C ve C++’ta fonksiyon göstericileri (pointer to functions) ile C#’ta da delegeler (delegates) ile yapıldığını anımsayınız.
Bir fonkisyon değişkeni yapı ya da sınıfların içerisindeki metotların da adreslerini tutabilir. Bu durmda bizim ilgili değişkene sınıf ya da yapı değişkeni ile metot ismini nokta operatörü ile birleştirerek vermemiz gerekir. (Bu kısmın C#’taki delegelere çok benzediğine dikkat ediniz). Örneğin:
struct Sample {
var a:Int
init(a: Int)
{
self.a = a
}
func disp()
{
print(a)
}
}
var t: Sample = Sample(a: 10)
var f: () -> Void = t.disp
f() // t.disp() ile aynı anlamda
Örneğin:
struct Sample {
var a:Int
init(a: Int)
{
self.a = a
}
func disp(str: String)
{
print(“\(str): \(a)”)
}
}
var t: Sample = Sample(a: 10)
var f: (String) -> Void = t.disp
f(“Value”) // t.disp(“Value”) ile aynı anlamda
Benzer biçimde statik metotlar da fonksiyon değişkenlerine sınıf ismi belirtilerek atanmalıdır:
struct Sample {
static func foo()
{
print(“foo”)
}
}
var f: () -> Void = Sample.foo
f() // Sample.foo() ile eşdeğer
Bir fonksiyonun parametre değişkeni bir fonksiyon türünden olabilir. Örneğin:
func foo(a: Int, f: (Int) -> Int)
{
var result: Int
result = f(a)
print(result)
}
func square(a: Int) -> Int
{
return a * a
}
foo(10, f: square)
Örneğin:
func forEachString(strs: [String], _ f: (String) -> ())
{
for str in strs {
f(str)
}
}
func disp(str: String)
{
print(str)
}
let names = [“Ali”, “Veli”, “Selami”, “Ayşe”, “Fatma”]
forEachString(names, disp)
Array yapısının sort isimli metodu bizden bir karşılaştırma fonksiyonu alır. Sıraya dizme sırasında dizinin iki elemanını bu karşılaştırma fonksiyonuna sokarak duruma göre yer değiştirme uygular. Karşılaştırma fonkiyonunun parametrik yapısı şöyledir:
cmp(a: T, b: T) -> Bool
Burada T dizinin türünü belirtiyor. Eğer biz diziyi küçükten büyüğe sıraya dizecekseksek ilk parametre ikinci parametreden küçükse true değerine, değilse false değerine geri dönmeliyiz. Örneğin:
let names = [“Ali”, “Veli”, “Selami”, “Ayşe”, “Fatma”]
func cmp(a: String, _ b: String) -> Bool
{
return a < b
}
var result = names.sort(cmp)
print(result)
func cmp1(a: Person, _ b: Person) -> Bool
{
return a.name < b.name
}
func cmp2(a: Person, _ b: Person) -> Bool
{
return a.no < b.no
}
var result = persons.sort(cmp1)
print(result)
print(“ — — — — “)
result = persons.sort(cmp2)
print(result)
Array yapısının sort metodu MutableCollectionType arayüzünden gelmektedir. Böyle bir sort fonksiyonunu “kabarcık sıralaması (bubble sort)” algoritmasıyla aşağıdaki gibi yazabiliriz:
let names = [“Ali”, “Veli”, “Selami”, “Ayşe”, “Fatma”]
let numbers = [5, 7, 4, 2, 10]
func cmp(a: String, _ b: String) -> Bool
{
return a < b
}
func cmp(a: Int, _ b: Int) -> Bool
{
return a > b
}
func mysort<T>(array: [T], _ cmp: (T, T) -> Bool) -> [T]
{
var sortedArray = array
for var i = 0; i < sortedArray.count — 1; ++i {
for var k = 0; k < sortedArray.count — i — 1; ++k {
if !cmp(sortedArray[k], sortedArray[k + 1]) {
let temp = sortedArray[k]
sortedArray[k] = sortedArray[k + 1]
sortedArray[k + 1] = temp
}
}
}
return sortedArray
}
var result1 = mysort(names, cmp)
print(result1)
var result2 = mysort(numbers, cmp)
print(result2)
Bir fonksiyonunun geri dönüş değeri de bir fonksiyon türünden olabilir. Yani fonksiyon bize geri dönüş değeri olarak bir fonksiyon verebilir. Örneğin:
func inc(a: Int) -> Int
{
return a + 1
}
func dec(a: Int) -> Int
{
return a — 1
}
func which(a: Int) -> (Int) -> Int
{
return a > 0 ? inc : dec
}
var f: (Int) -> Int
f = which(10)
print(f(100)) // 101