Swift 4 Operatör Fonksiyonları (Operator Functions) #24

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

--

Operatör fonksiyonları konusu Java’da yoktur. C#’a C++’tan basitleştirilerek aktarılmıştır. Swift’te de operatör fonksiyonları dahil edilmiştir. Bilindiği gibi operator fonbksiyonları dile ekstra bir işlevsellik katmamaktadır. Operatör fonksiyonlarının yalnızca okunabilirliği artırıcı bir işlevi vardır. Bu sayede biz kendi sınıf ya da yapılarımız türünden iki değişkeni sanki onlar temel türlenmiş gibi toplama, çıkartma vs. işlemlerine sokabiliriz. Bu durumda aslında arka planda bizim belirlediğimiz ismine “operatör fonksiyonu” denilen fonksiyon çağrılmaktadır.

Swift’te opetaör fonksiyonları global düzeyde bildirilmek zorundadır. Operatör fonksiyonları sınıfların ya da yapıların içerisinde bildirilemezler. Bildirimleri sırasında operatör fonksiyonlarına operatör sembolüyle isimlendirilir. Örneğin:

func +(z1: Complex, z2: Complex) -> Complex

{

//…

}

Burada fonksiyonun isminin + operatör sembolünden oluştuğuna dikkat ediniz. OPeratör fonksiyonları isimlendirme biçiminin dışında tamamen normal fonksiyonlar gibidir.

Operatör fonksiyonlarının parametreleri ve geri dönüş değerleri herhangi bir türden olabilir. Ancak eğer operatör metodu tek operandlı bir operatöre ilişkinse operatör fonksiyonunun bir parametresi, iki operandlı bir fonksiyona ilişkinse iki parametresi bulunmak zorundadır.

Örneğin:

import Foundation

class Complex {

var real: Double

var imag: Double

init(real: Double, imag: Double)

{

self.real = real

self.imag = imag

}

var description: String {

return self.real.description + “ + “ + self.imag.description + “i”

}

}

func +(z1: Complex, z2: Complex) -> Complex

{

let result = Complex(real: z1.real + z2.real, imag: z1.imag + z2.imag)

return result

}

let z1 = Complex(real: 2, imag: 3)

let z2 = Complex(real: 3, imag: 4)

var result: Complex

result = z1 + z2

print(result.description)

Swift derleyicisi bir operatörle karşılaştığında önce operandların türlerine bakar. Eğer operand’lar temel türlerdense işlemi gerçekleştirir. Fakat operand’lardan en az biri bir sınıf ya da yapı türündense bu işlemi yapabilecek bir operatör fonksiyonu araştırır. Eğer böyle bir fonksiyonu bulursa operand’ları o fonksiyona argüman olarak göderir ve fonksiyonu çağırır. Fonksiyonun geri dönüş değeri işlem sonucu olarak elde edilir.

Operatör fonksiyonları kombine edilebilir. Yani birinin sonucu diğerine girdi yapılabilir. Örneğin:

result = z1 + z2 + z3

Burada z1 ve z2 toplanarak bir değer elde edilmiş, elde edilen değer de z3 ile toplanmıştır. Sonuç da result değişkenine atanmıştır.

Eğer önek bir operatöre ilişkin operatör yazmak istiyorsak operatör bildiriminin başına prefix anahtar sözcüğü, sonek bir operatöre ilişkin operatör metodu yazmak istiyorsak postfix anahtar sözcüğü getirilmelidir. infix operatörler için bildirimin başına hiçbir şey getirilmez. (infix anahtar sözcüğü de getirilmez). Örneğin:

import Foundation

struct Number {

var val: Int

init()

{

val = 0

}

init(val: Int)

{

self.val = val

}

var description: String {

return val.description

}

}

func +(a: Number, b: Number) -> Number

{

var result = Number()

result.val = a.val + b.val

return result

}

func -(a: Number, b: Number) -> Number

{

var result = Number()

result.val = a.val — b.val

return result

}

func *(a: Number, b: Number) -> Number

{

var result = Number()

result.val = a.val * b.val

return result

}

func /(a: Number, b: Number) -> Number

{

var result = Number()

result.val = a.val / b.val

return result

}

prefix func +(a: Number) -> Number

{

return a

}

prefix func -(a: Number) -> Number

{

return Number(val: -a.val)

}

let a = Number(val: 10)

let b = Number(val: 10)

let c = Number(val: 40)

let result = -a * +b — c

print(result.description)

++ ve — operatörlerinin önek ve sonek biçimleri yazılırken bu operatör fonksiyonlarının tek bir parametresi bulunmak zorundadır. Geri dönüş değerleri de aynı türdne olacak biçimde oluşturulmalıdır. Operatörlerin hem önek hem de sonek biçimlerinde artırma ya da eksiltme parametre üzerinde yapılmalıdır. Önek biçimde parametrenin kendisine, sonek biçimde de artırılmadan öncek kopyaya geri dönülebilir:

prefix func -(a: Number) -> Number

{

return Number(val: -a.val)

}

prefix func ++(a: Number) -> Number

{

++a.val

return a

}

Swift’te diğer dillerin aksine operatör olmayan atom kümesinden de operatör fonksiyonu oluşturulabilir. Ancak operatör sembollerinin belli karakterlerden seçilmesi zorunlu tutulmuştur. Bu karakterleri UNICODE tablodaki listesi dilin kendi referans kitabında belirtilmiştir. Bunların görüntüsel karşılığı için https://gist.github.com/wxs/d773cb2a2e9891dbfd63 adresine başvurulabilir.

Eğer biz operatör olmayan bir sembolden operatör fonksiyonu yazacaksak operator bildirimi ile onun önek mi, araek mi, sonek mi olduğunu, öncelik derecesini ve soldan-sağa, sağdan-solalık durumunu (associativity) belirtmeliyiz. Bu işlemin genel biçimi şöyledir:

<durumu(infix, prefix, postfix)> operator <operatör sembolü>

{

precedence <öncelik derecesi>

associativity <left, right, none>

}

Aslında bizim standart operatör dediğimiz operatörler için de bu bildirimler önceden yapılmış durumdadır. Yani örneğin +, -, * / operatörleri için zaten yukarıdaki bildirimler yapılmış durumdadır. Pekiyi operatörün önceliği nasıl tespit edilmektedir? İşte operatör önceliği olarak bir sayı verilir. Standart operatörler için aşağıdaki öncelik değerleri belirlenmiştir:

<<

Bitwise left shift

None

Exponentiative, 160

>>

Bitwise right shift

None

Exponentiative, 160

*

Multiply

Left associative

Multiplicative, 150

/

Divide

Left associative

Multiplicative, 150

%

Remainder

Left associative

Multiplicative, 150

&*

Multiply, ignoring overflow

Left associative

Multiplicative, 150

&

Bitwise AND

Left associative

Multiplicative, 150

+

Add

Left associative

Additive, 140

-

Subtract

Left associative

Additive, 140

&+

Add with overflow

Left associative

Additive, 140

&-

Subtract with overflow

Left associative

Additive, 140

|

Bitwise OR

Left associative

Additive, 140

^

Bitwise XOR

Left associative

Additive, 140

..<

Half-open range

None

Range, 135

Closed range

None

Range, 135

is

Type check

Left associative

Cast, 132

as, as?, and as!

Type cast

Left associative

Cast, 132

??

Nil Coalescing

Right associative

Nil Coalescing, 131

<

Less than

None

Comparative, 130

<=

Less than or equal

None

Comparative, 130

>

Greater than

None

Comparative, 130

>=

Greater than or equal

None

Comparative, 130

==

Equal

None

Comparative, 130

!=

Not equal

None

Comparative, 130

===

Identical

None

Comparative, 130

!==

Not identical

None

Comparative, 130

~=

Pattern match

None

Comparative, 130

&&

Logical AND

Left associative

Conjunctive, 120

||

Logical OR

Left associative

Disjunctive, 110

?:

Ternary Conditional

Right associative

Ternary Conditional, 100

=

Assign

Right associative

Assignment, 90

*=

Multiply and assign

Right associative

Assignment, 90

/=

Divide and assign

Right associative

Assignment, 90

%=

Remainder and assign

Right associative

Assignment, 90

+=

Add and assign

Right associative

Assignment, 90

-=

Subtract and assign

Right associative

Assignment, 90

<<=

Left bit shift and assign

Right associative

Assignment, 90

>>=

Right bit shift and assign

Right associative

Assignment, 90

&=

Bitwise AND and assign

Right associative

Assignment, 90

|=

Bitwise OR and assign

Right associative

Assignment, 90

^=

Bitwise XOR and assign

Right associative

Assignment, 90

&&=

Logical AND and assign

Right associative

Assignment, 90

||=

Logical OR and assign

Right associative

Assignment, 90

Öncelik dereceleri [0, 255] aralığında bir sayı olmak zorundadır. Biz kendi uydurduğumuz sembole yukarıdaki değerleri de dikkate alarak istediğimiz bir değeri verebiliriz. Örneğin biz ∑ sembolünün önceliğine 145 verirsek bunun önceliği +, — operatörlerinden daha fazla fakat * ve / operatörlerinden daha düşük olur.

Swift’te tek operandlı operatörler için öncelik derecesi ve associativity belirtilememektedir. Bunların öncelik derecesi her zaman 140 olarak değerlendirilir. Eğer “associativity” belirtilmezse default durum “none” kabul edilir. “None” associativity o operatörün başka bir operatörle kombine edilemeyeceğini belirtmektedir. Bu durumda önek bir operatör için bildirimn şöyle yapılabilir:

prefix operator ∑ { }

Örneğin:

prefix operator ∑ {}

prefix func ∑(a: [Int]) -> Int

{

var total = 0

for x in a {

total += x

}

return total

}

let result = ∑[1, 2, 3, 4, 5] + 10

print(result)

infix operatör için de şöyle bir örnek düzenlenebilir:

infix operator ∑+ {

precedence 140

associativity left

}

func ∑+(a: [Int], b: [Int]) -> [Int]

{

let minCount = a.count < b.count ? a.count : b.count

var result = [Int]()

for var i = 0; i < minCount; ++i {

result.append(a[i] + b[i])

}

return result

}

var result = [1, 2, 3] ∑+ [5, 7, 9, 10]

print(result)

Swift’te biz zaten tanımlı olan operatörler için de yeniden operatör fonksiyonları yazabiliriz. (C++’ta ve C#’ta bu mümkün değildir) Örneğin:

func +(a: Int, b: Int) -> Int

{

return a * b

}

let x = 10 + 20 + 30

print(x)

Sınıflarda ve Yapılarda Erişim Belirleyici Anahtar Sözcükler

Bilindiği gibi Swift’te veri elemanlarına “stored property” denilmektedir. Aslında “stored property”ler derleyicinin kendisinin yazdığı get ve set metotları yoluyla erişimi yapmaktadır. Yani başka bir deyişle biz stored property’yi kullandığımızda aslında o değişkene zaten doğrudan erişmemekteyiz. O değişkene biz bir metot yoluyla erişmekteyiz. Örneğin:

class Sample {

var no: Int

init()

{

no = 0

}

//…

}

var s = Sample()

s.no = 100

Burada no isimli “stored property”ye erişim aslında arka planda “computed” bir property ile yapılmaktadır. Yani aslında yukarıdaki kodun derleyici tarafından derlenmiş biçimi şöyledir:

class Sample {

var compilerGeneratedName: Int

var no: Int {

get {

return compilerGeneratedName

}

set {

compilerGeneratedName = newValue

}

}

init()

{

compilerGeneratedName = 0

}

//…

}

var s = Sample()

s.no = 100

Görüldüğü gibi aslında C++, Java ve C#’ta olduğu gibi bizim veri elemanlarını private bölüme yerleştirerek onlara public metotlara erişmemiz işlemi Swift’te otomatik yapılmaktadır. Pekiyi sınıfın “stored property”lerinin isimleri ya da türleri değişirse onu kullanan kodların değiştirilmesi nasıl engellenecektir? İşte eğer biz sınıf ya da yapımızdaki “stored property’leri” değiştirirsek onlar için gerçekten bizim eski türü dikkate alarak “computed” property yazmamız gerekir. Örneğin:

class Sample {

var otherNo: Double

var no: Int {

get {

return Int(otherNo)

}

set {

otherNo = Double(newValue)

}

}

init()

{

otherNo = 0

}

//…

}

var s = Sample()

s.no = 100

Fakat yine de sınıfa eklenen isimlerin algılamayı kolaştırmak ve kapsülleme sağlamak için gizlenmesi gerekebilmektedir. İşte bu nedenle Swift’te 2.0 versiyonu ile private, public ve internal erişim belirleyicileri getirilmiştir.

internal default erişim belirleyicisidir. Yani erişim belirleyici anahtar sözcüklerden hiçbiri kullanılmamışsa internal belirleyicisi kullanılmış gibi etki oluşur. internal bir sınıf ya da yapı elemanına aynı modülden (başka bir kaynak dosya da dahil olmak üzere) erişilebilir. Ancak başka bir modülde bulunan bir sınıf ya da yapının internal elemanlarına erişilemez. Yani internal belirleyicisi ile gizleme modül temelinde etki göstermektedir. private belirleyicisi yalnızca ilgili elemana aynı kaynak dosyadan erişilebileceğini belirtmektedir. Yani örneğin bir modül içerisinde üç kaynak dosya bulunuyor olsun. Eğer eleman private ise yalnızca aynı modül söz konusu olsa bile aynı kaynak dosyadan o elemana erişilebilir. Eğer eleman public erişim belirleyicisi ile bildirilmişse elemana her modülden yani her yerden erşilebilir. Swift’te C++, Java ve C#’ta olduğu gibi protected belirleyicisi yoktur. Örneğin:

public class Sample {

private var a: Int

public init()

{

a = 0

}

public func foo()

{

//…

}

private func bar()

{

//…

}

//…

}

var s: Sample

s = Sample()

s.a = 10

Ayrıca Swift’te türlerin başına da erişim belirleyici anahtar sözcükler getirilebilmektedir. Burada da default durum internal’dır. internal bir sınıf yalnızca kendi modülünden kullanılabilir. Biz bir modül yazarken ilgili sınıfın başka bir modül tarafından kullanılmasını istiyorsak sınıf bildiriminin başına public belirleyicisini getirmeliyiz. Örneğin:

public class Sample {

//…

}

Tabii public olmayan bir sınıfın public elemana sahip olması anlamlı değildir. Swift derleyici bu durumda uyarı mesajı vermektedir.

guard Deyimi

guard deyimi yalnızca else bölümü bulunan if deyimine benzetilebilir. Genel biçimi şöyledir:

guard <Bool türden ifade> else {

//…

}

guard deyiminin yalnızca else kısmı vardır. Bu kısım kontrol ifadesi false ise çalıştırılır. guard deyimi bir çeşit assert etkisi yaratmak için dile sokulmuştur. Yani bu deyimde programcı sağlanması gereken koşulu belirtir. Bu koşul sağlanmıyorsa deyimin else kısmı çalışır. guard deyiminin else kısmından normal akışsal çıkış yasaklanmıştır. Programcının else bloğu içerisinde akışı başka bir yere yöneltmesi zorunludur. Bu da tipik olarak return gibi, break, continue gibi deyimlerle ya da exception fırlatan throw işlemiyle yapılabilir. Örneğin:

func foo(a: Int)

{

guard a >= 0 else {

print(“parameter cannot be negative”)

return

}

print(“ok”)

}

foo(10)

foo(-20)

Görüldüğü gibi burada guard deyiminin else kısmında akışık else bloğunu bitirmesine izin verilmemiştir. Yani guard deyiminden sonraki deyimler ancak koşul doğruysa çalıştırılmaktadır.

guard deyimi daha çok seçeneksel türler için tercih edilmektedir. Tıpkı if deyiminde olduğu gibi guard deyiminde de otomatik unwrap özelliği ile nil karşılaştırması vardır. Örneğin:

func foo(a: Int?)

{

guard let b = a else {

print(“parameter cannot be nil”)

return

}

print(“\(b)”) // geçerli

}

foo(10)

foo(nil)

guard deyiminin seçeneksel biçiminin if deyiminin seçeneksel biçiminde önemli bir farkı vardır. guard deyiminin seçeneksel biçiminde bildirilen değişken guard deyimi dışında da kullanılabilmektedir. Ancak if deyiminin seçeneksel biçiminde bildirilen değişken if deyiminin dışında kullanılamaz. Örneğin:

func foo(a: Int?)

{

if let b = a { }

else {

print(“parameter cannot be nil”)

return

}

print(“\(b)”) // error

}

Burada if deyiminin dışında b’ye erişilemediğine dikkat ediniz. Örneğin:

var dict: [String: Int] = [“Ali”: 123, “Veli”: 234, “Selami”: 543]

guard let val = dict[“Veli”] else {

print(“value cannot find”)

return

}

defer Deyimi

defer deyiminin genel biçimi şöyledir:

defer {

//…

}

defer deyimi bir bloğun içerisindeyse o bloktan çıkıldığında çalıştırılır, akış defer deyimine geldiğinde çalıştırılmaz. Aynı blok içerisinde birden fazla defer varsa bunlar bloktan çıkış sırasında ters sırada çalıştırılır. Örneğin:

func foo(a: Int?)

{

print(“one”)

defer {

print(“defer 1”)

}

print(“two”)

defer {

print(“defer 2”)

}

print(“three”)

}

foo(10)

Burada ekrana şunlar basılacaktır:

one

two

three

defer 2

defer 1

defer bloktan nasıl çıkılırsa çıkılsın çalıştırılır. Yani normal çıkış, return ile çıkış, break ve continue ile çıkışlarda da defer deyimleri çalıştırılır. Örneğin:

func foo(a: Int)

{

defer {

print(“defer 1”)

}

defer {

print(“defer 2”)

}

if a < 0 {

return

}

print(“one”)

print(“two”)

print(“three”)

}

foo(-10)

Pekiyi defer deyiminin kullanım nedeni nedir? İşte blok içerisinde birtakım kaynaklar tahsis edilmiş olabilir. Daha sonra bir exception oluşabilir. Bu durumda defer deyiminde tahsisatlar geri bırakılabilir. Örneğin bir kaynağı tipik kullanımı aşağıdaki gibi olsun:

func foo()

{

<kaynak tahsis et>

<kaynakla işlem yap>

<kaynağı bırak>

}

Buradaki problem kaynak tahsis edildikten sonra o kaynak kullanılırken oluşan exception ile akışın başka bir yere atlayabilmesidir. İşte exception oluştuğunda kaynağın otomatik boşaltımı defer sayesinde şöyle yapılabilir:

func foo()

{

<kaynak tahsis et>

defer {

<kaynağı bırak>

}

<kaynakla işlem yap>

}

Bu tür işlemler Java ve C#’ta try-finally bloklarıyla ya da C#’taki using deyimleriyle gerçekleştirilebilmektedir. C++’ta “stack unwinding” mekanizması olduğu için zaten bu tür sorunlar sınıfların destructor metotlarıyla çözülmektedir.

defer deyiminin blok çıkışında çalıştırılabilmesi için akışın defer üzerinden geçmiş olması gerekir. Örneğin:

func foo(a: Int)

{

defer {

print(“defer 1”)

}

if a < 0 {

return

}

defer {

print(“defer 2”)

}

}

Burada a eğer sıfırdan küçükse ikinci defer bloktan çıkıldığında çalıştırılmayacaktır. Ancak o ana kadar akışın ulaştığı defer’ler bloktan çıkılırken çalıştırılır.

Exception İşlemleri

Exception konusu Swift’e 2.0 versiyonuyla sokulmuştur. Swift’in exception mekanizması C++, Java ve C#’tan biraz farklıdır. Swidt’te bir türün exception amacıyla fırlatılabilmesi için onun ErrorType isimli boş bir protokolü destekliyor olması gerekir. Örneğin:

class MyException : ErrorType {

//….

}

enum YourException : ErrorType {

//…

}

Swift’te enum’lar exception mekanizmasında çok daha yaygın olarak kullanılmaktadır. Çünkü enum’ların her case bölümü bir exception cinsini belirtebilmektedir. Örneğin:

enum MyException : ErrorType {

case NotFound

case IncorrectArgument

case OutOfRange

}

Exception’ın throw edilmesinde yine throw deyimi kullanılır. Örneğin:

throw MyException.NotFound

Swift’te fonksiyon ya da metotlarda “exception belirlemesi” vardır. Eğer bir fonksiyon ya da metot dışına bir exception throw edilmişse bu durum fonksiyon ya da metodun bildiriminde parametre parantezinden sonra throws anahtar sözcüğüyle belirtilmek zorundadır. Örneğin:

func foo() throws

{

//…

}

Fakat Swift’te Java’daki gibi fonksiyonun ya da metodun hangi tür ile throw ettiği belirtilmemektedir. Eğer fonksiyon ya da metodun geri dönüş değeri varsa -> atomu throws anahtar sözcüğünden sonraya yerleştirilir. Örneğin:

func foo(a: Int) throws -> Int

{

//…

return 0

}

Swift’te C++, Java ve C#’ta olduğu gibi try bloğu yoktur. try değimi yanına bir ifade almaktadır. Örneğin:

try foo()

gibi. Exception’ı yakalama do-catch bloklarıyla yapılır.

do {

//…

}

catch MyException.NotFound {

//…

}

catch MyException.IncorrectArgument {

//…

}

catch MyException.OutOfRange {

//…

}

do bloğu içerisinde try deyimi yanına ifade yazılarak kullanılır. Bu durumda eğer o ifadede bir exception oluşursa akış do bloğunun aynı türden catch bloğuna aktarılır. O catch bloğu çalıştırıldıktan sonra diğer catch blokları atlanır ve akış catch bloklarının sonundan devam eder. Örneğin:

enum MyException : ErrorType {

case NotFound

case IncorrectArgument

case OutOfRange

}

func foo(a: Int) throws -> Int

{

if a < 0 {

throw MyException.IncorrectArgument

}

print(“Ok”)

return 0

}

do {

let x = try foo(-20)

print(x)

}

catch MyException.IncorrectArgument {

print(“argument must not be negative!”)

}

Akış do bloğuna girdikten sonra hiç exception oluşmazsa catch blokları atlanır ve akış catch bloklarının sonundan devam eder.

Eğer bir fonksiyon throws ile exception fırlatacağını belirtmişse onun kesinlikle try ile çağrılması zorunludur. Fakat try deyiminin do bloğu içerisinde çağrılması zorunlu tutulmamıştır. Ancak yakalanamayan exception’lar yine Java ve C#’ta olduğu gibi programın çökmesine yol açarlar.

Örneğin:

import Foundation

enum MyException : ErrorType {

case NotFound

case IncorrectArgument

case OutOfRange

}

func foo(name: String) throws -> Int

{

var dict: [String: Int] = [“Ali”: 123, “Veli”: 234, “Selami”: 543]

guard let val = dict[name] else {

throw MyException.NotFound

}

return val

}

do {

var no = try foo(“Alice”)

print(no)

}

catch MyException.NotFound {

print(“Value not found”)

}

catch cümlesinin genel biçimi şöyledir:

catch [tür] [(bildirim)] [kalıp] {

//…

}

Eğer catch anahtar sözcüğünün yanı boş bırakılırsa bu durum her türden exception’ın yakalanacağı anlamına gelir.

throw işlemi yapar bazı bilgiler de exception’ı yakalayacak catch cümlesine gönderilebilir. Bu örneğin enum elemanlarına tür bilgisi atayarak gerçekleştirilebilir. Örneğin:

enum MyException : ErrorType {

case NotFound

case IncorrectArgument

case OutOfRange(String)

}

func foo(a: Double) throws -> Double

{

guard a >= 0 else {

throw MyException.OutOfRange(“value must be positive or zero!”)

}

return sqrt(a)

}

do {

let result = try foo(-20)

print(result)

}

catch MyException.OutOfRange(var msg) {

print(“Error: \(msg)”)

}

Burada catch sentaksına dikkat ediniz: Enum’un parametresinde tür belirtilmemektedir.Yani bildirim aşağıdaki gibi yapılmamalıdır:

catch MyException.OutOfRange(var msg: String) { // error!

print(“Error: \(msg)”)

}

Tabii enum elemanı bir sınıf ya da yapı türünden de olabilir. Örneğin:

enum MyException : ErrorType {

case NotFound

case IncorrectArgument(IncorrectArgumentException)

case OutOfRange

}

class IncorrectArgumentException {

var msg: String

init(msg: String)

{

self.msg = msg;

}

var description : String {

return msg

}

}

func foo(a: Double) throws -> Double

{

guard a >= 0 else {

throw MyException.IncorrectArgument(IncorrectArgumentException(msg: “value must be positive or zero!”))

}

return sqrt(a)

}

do {

let result = try foo(-20)

print(result)

}

catch MyException.IncorrectArgument(var e) {

print(“Error: \(e.description)”)

}

Aslında enum’ların dışında sınıflar ve yapılarla da throw işlemi yapılabilir. Fakat bunların yakalanması için catch cümlesinde where kalıbının kullanılması gerekir. Şöyle ki: Aslında catch cümlesinden biz ErrorType referansı elde ederiz. Bu referansın dinamik türüne ilişkin bir kalıp oluşturarak catch düzenlemesini yapabiliriz. Örneğin:

class IncorrectArgumentException : ErrorType {

var msg: String

init(msg: String)

{

self.msg = msg;

}

var description : String {

return msg

}

}

func foo(a: Double) throws -> Double

{

guard a >= 0 else {

throw IncorrectArgumentException(msg: “value must be positive or zero!”)

}

return sqrt(a)

}

do {

let result = try foo(-20)

print(result)

}

catch var e where e is IncorrectArgumentException {

var iae = e as! IncorrectArgumentException

print(“\(iae.description)”)

}

Tabii yukarıda da belirtildiği gibi Swift’in exception mekanizması temelde enum’larla kullanılmak üzere tasarlanmıştır. Yukarıdaki gibi doğrudan bir sınıf ile throw etmek nadir rastlanabiecek bir tekniktir.

try operatörünün yanı sıra exception kontrolü için try? ve try! operatörleri de vardır. try? ile exception kontrolü uygulandığında eğer try? operatörünün yanındaki ifadede exception oluşursa akış catch bloğuna aktarılmamaktadır. Bunun yerine bu operatör nil değerini üretmektedir. Tabii bu durumda try? operatörünün do bloğu içerisinde kullanılmasının da bir anlamı yoktur. Örneğin:

enum MyException : ErrorType {

case IncorrectArgument(String)

}

func foo(a: Double) throws -> Double

{

guard a >= 0 else {

throw MyException.IncorrectArgument(“value must be positive or zero!”)

}

return sqrt(a)

}

let result: Double? = try? foo(20)

if let r = result {

print(“\(r)”)

}

else {

print(“exception occurred!”)

}

Tabii try? operatörünün kullanılabilmesi için bu operatörün sağındaki ifadenin bir değer veriyor olması gerekir. Yani örneğin foo fonksiyonu bir değer geri döndürmeseydi biz try? operatörünü kullanamazdık.

try! operatöründe eğer exception oluşursa programın çalışma zamanı sırasında program çöker. Exception oluşmazsa akış normal biçimde devam eder. try! operatörünün de do bloğunun içerisinde kullanılmasının bir anlamı yoktur. Örneğin:

enum MyException : ErrorType {

case IncorrectArgument(String)

}

func foo(a: Double) throws -> Double

{

guard a >= 0 else {

throw MyException.IncorrectArgument(“value must be positive or zero!”)

}

return sqrt(a)

}

let result: Double = try! foo(-20)

print(result)

Fonksiyonların inout Parametreleri

Normal olarak fonksiyonlar çağrılırken argümanlardan parametre değişkenlerine karşılıklı bir atama yapılır. Eğer fonksiyonun parametre değişkeni inout belirleyicisi ile bildirilmişse bu durumda ilgili argümanın adresi fonksiyona geçirilir. Artık fonksiyon içerisinde o parametre değişkenine atama yapıldığında parametreye karşılık gelen argümana atama yapılmış gibi bir etki oluşur. inout parametreli bir fonksiyon aynı türden bir değişkenle çağrılmak zorundadır. Ayrıca argümanın & operatörü ile niteliklendirilmesi gerekir. Örneğin:

func foo(inout a: Int)

{

a = 20 // buradaki a aslında çağrılma ifadesindeki argüman olan x

}

var x: Int = 10

print(x) // 10

foo(&x)

print(x) // 20

inout parametresinin C ve C++’taki göstericiye C#’taki ref parametresine karşılık geldiğine dikkat ediniz. Cocoa kütüphanesinde bazı fonksiyonlar ve metotlar adresiyle aldıkları değişkenlere değer aktarabilmektedir. Dolayısıyla bu fonksiyonlar Swift’te ilgili parametreye karşılık gelen argümanın & operatörü ile niteliklendirilmesiyle çağrılırlar. Örneğin:

func swap(inout a: Int, inout _ b: Int)

{

let temp = a

a = b

b = temp

}

var a = 10, b = 20

swap(&a, &b)

print(“a = \(a), b = \(b)”)

inout parametresi ile bildirilmiş olan bir fonksiyon çağrılırken ona karşı gelen argümana değer atanmış olması zorunludur. Fonksiyonun bu inout parametreye değer ataması ise zorunlu değildir. inout belirleyici ile var ve let belirleyicileri birarada kullanılamamaktadır.

Generic’ler

Bazen farklı türler için aynı işi yapan birden fazla fonksiyonun ya da metodun yazılması gerekebilmektedir. Örneğin:

func getMax(a: [Int]) -> Int

{

var max = a[0]

for var i = 1; i < a.count; ++i {

if max < a[i] {

max = a[i]

}

}

return max

}

Yukarıdaki getMax fonksiyonu Int bir dizinin en büyük elemanına geri dönmektedir. Ancak biz Double bir dizinin en büyük elemanını elde etmek istersek içi tamamen yukarıdaki gibi olan fakat parametre ve geri dönüş değerinde Double kullanılan yeni bir getMax fonksiyonunu yazmak zorundayız:

func getMax(a: [Double]) -> Double

{

var max = a[0]

for var i = 1; i < a.count; ++i {

if max < a[i] {

max = a[i]

}

}

return max

}

İşte generic özelliği yukarıdaki gibi içi aynı olan fakat farklı türler için tekrar tekrar yazılması gereken fonksiyonlar ve sınıfların daha pratik oluşturulması için düşünülmüştür. Böylece generic fonksiyonlar, sınıflar ve yapılar bir ya da birden fazla türe dayalı olarak bir şablon biçiminde bildirilirler. Derleyici de o şablona bakarak ilgili türden fonksiyonu, sınıfı ya da yapıyı bizim için oluşturur.

Swift’teki generic’lerin bildirimi ve kullanımı C# ve Java’daki generic’lere oldukça benzemektedir.

Generic Fonksiyonlar ve Metotlar

Generic bir fonksiyonun bildirimi tıpkı C# ve Java’da olduğu gibi fonksiyon ya da metot isminden sonra açısal parantezlerle tür parametrelerinin bildirilmesiyle yapılır. Örneğin:

func foo<T, K>(a: T, b: K) -> T

{

//…

}

Burada generic fonksiyonun tür parametreleri T ve K’dır. Bu T ve K herhangi iki türü temsil etmektedir. Tür parametreleri geleneksel olarak pascal yazım tarzıyla (yani ilk harfi büyük diğerleri küçük olacak biçimde) harflendirilmektedir. T ve K gibi tek harfli isimler de çok tercih edilmektedir. Örneğin swap fonksiyonu generic olarak şöyle yazılabilir:

func swap<T>(inout a: T, inout b: T)

{

let temp = a

a = b

b = temp

}

Buradaki T türü generic fonksiyon kullanılırken derleyici tarafından argümanlara bakılarak otomatik tespit edilir. Örneğin:

var x: Int = 10, y: Int = 20

swap(&x, &y)

print(“x = \(x), y = \(y)”)

Burada swap iki Int argümanla çağrıldığı için derleyici T türünün Int olduğunu tespit eder. Swift’te C++ ve C#’ta olduğu gibi generic türlerin açıkça belirtilmesi özelliği yoktur. Bu nedenle Swift’te tüm generic tür parametrelerinin fonksiyon ya da metodun imzası içerisinde kullanılıyor olması zorunludur. Yani örneğin aşağıdaki generic bildirim C++ ya da C# karşılığı dikkate alındığında geçerli olduğu halde Swift’te geçerli değildir:

func foo<T>() // error

{

//…

}

Çünkü böyle bir bildirimde T tür parametresi, parametre ya da geri dönüş değeri bildiriminde yer almadığı için derleyicinin onun gerçek türünü tespit etmesi mümkün değildir. Halbuki C++ ve C#’ta fonksiyon çağrılırken tür parametresi açıkça belirtilebilmektedir:

foo<Int>() // C++ ve C#’ta bu sentaks var fakat Swift’te yok

Generic Sınıflar, Yapılar ve Enum’lar

Nasıl bir fonksiyon ya da metot generic olabiliyorsa bir sınıfın, yapının ya da enum’un tamamı da generic olabilir. Ancak Swift’te protokoller generic olamamaktadır. (Java ve C#’ta arayüzlerin generic olabileciğini anımsayınız). Bir sınıf, yapı ya da enum türünü generic yapabilmek için sınıf, yapı ya da enum isminden sonra açısal parantezler içerisinde generic tür parametrelerinin belirtilmesi gerekir. Örneğin:

class LinkedList<T> {

//…

}

Bu biçimde bildirilen tür parametreleri tür belirten bir isim olarak sınıf, yapı ve enum bildirimlerinin içerisinde (tabii onların metotlarının da içerisinde) kullanılabilir. Örneğin:

import Foundation

struct Stack<T> {

private var stack: [T]

init()

{

stack = [T]()

}

mutating func push(val: T)

{

stack.append(val)

}

mutating func pop() -> T

{

return stack.removeLast()

}

var isEmpty: Bool {

return stack.isEmpty

}

var count : Int {

return stack.count

}

}

Generic bir tür kullanılırken tür isminden sonra açısal parantezler içerisinde kesinlikle tür argümanlarının belirtilmesi gerekir. Örneğin:

var stack = Stack<Int>()

for var i = 0; i < 10; ++i {

stack.push(i)

}

while !stack.isEmpty {

var elem = stack.pop()

print(“\(elem) “, terminator:””)

}

print(“”)

Swift’te (henüz) generic türlerin overload edilmesi özelliği yoktur. Yani aynı isimli biri generic diğeri normal olan iki sınıf, yapı ya da enum birlikte bulunamaz. Benzer biçimde farklı sayıda generic parametresi olan aynı isimli türler de birarada bulunamazlar. Halbuki C++ buna izin vermektedir. C#’ta da overload işlemi kısmen yapılabilmektedir.

Generic’lerde Tür Kısıtları (Type Constraints)

Bir generic fonksiyon ya da sınıf generic tür parametreleri hangi türden açılırsa açılsın anlamlı olmak zorundadır. Aksi halde derleme aşamasında error oluşur. Örneğin:

func findIndex<T>(a: [T], val: T) -> Int?

{

for var i = 0; i < a.count; ++i {

if a[i] == val { // error!

return i

}

}

return nil

}

Burada her T türünün (örneğin her sınıfın ya da yapının) == operatör fonksiyonu olmak zorunda değildir. İşte bu tür durumlarda biz generic parametrelerine bazı kısıtları sağlama zorunluluğu getirebiliriz. Tür parametrelerine kısıt getirme işleminin genel biçimi şöyledir:

<tür parametresi> [: <protokol listesi>]

Genel biçimden de görüldüğü gibi tür parametresini kısıtlama işlemi tür parametresinden sonra ‘:’ atomu ve sonra da protokol listesi getirilerek yapılmaktadır. Örneğin:

func findIndex<T: Equatable>(a: [T], val: T) -> Int?

{

for var i = 0; i < a.count; ++i {

if a[i] == val { // geçerli, T’nin == operatör metodu olmak zorunda

return i

}

}

return nil

}

Burada derleyiciye T türünün Equatable protokolünü destekleyen bir türle açılacağı garantisi verilmektedir. Artık biz findIndex fonksiyonunu Equatable protokolünü desteklemeyen bir türle çağırmaya çalışırsak derleme aşamasında error oluşur. Örneğin:

struct Number {

var val: Int = 0

init(_ val: Int)

{

self.val = val

}

}

var a = [Number(10), Number(20), Number(30), Number(40), Number(50)]

var index = findIndex(a, val: Number(30)) // error!

if index != nil {

print(index!)

}

Equtable protokolü == operatör fonksiyonuna sahiptir. Yani Equatable protokolünü destekleyen bir sınıf ya da yapı == operatör fonksiyonuna sahip olmak zorundadır. Örneğin:

struct Number : Equatable {

var val: Int = 0

init(_ val: Int)

{

self.val = val

}

}

func ==(a: Number, b: Number) -> Bool

{

return a.val == b.val

}

var a = [Number(10), Number(20), Number(30), Number(40), Number(50)] // geçerli

var index = findIndex(a, val: Number(30))

if index != nil {

print(index!)

}

Ancak biz Equatable protokolünü destekleyen bir türü != operatörü ile de kullanabiliriz. Çünkü standart kütüphanede aşağıdaki gibi yazılmış generic bir != operatörü vardır:

func !=<T : Equatable>(_ left: T, _ right: T) -> Bool

{

return !(left == right)

}

Başka bir deyişle biz Equatable protokolünü destekleyen bir sınıf, yapı ya da enum türünü == operatörünün yanı sıra != operatörüyle de kullanabiliriz.

Comparable isim protokol Equatable protokolünden türetilmiştir. Bu protokolde yalnızca < operatör fonksiyonu vardır. Yani Comparable protokolünü destekleyen bir tür hem == hem de < operatör fonksiyonlarını bulundurmak zorundadır. Bu iki operatör fonksiyonu bulunduğunda kütüphanedeki !=, <=, >= ve > generic operatör metotları devreye girerek != <=, >= ve > işlemlerini == ve < operatörlerini kullanarak yapmaktadır. Örneğin:

func getMax<T: Comparable>(a: [T]) -> T

{

var max = a[0]

for i in 1..<a.count {

if max < a[i] {

max = a[i]

}

}

return max

}

Burada getMax generic fonksiyonunun T tür parametresi Comparable protokolünü desteklemektedir. Dolayısıyla biz bu fonksiyon içerisinde ==, !=, <, >, <= ve >= operatörlerini kullanabiliriz. Swift’in temel türlerinin hepsi Comparable protokolünü desteklemektedir. Örneğin:

var a = [23, 45, 28, 54, 98, 12]

var max = getMax(a)

print(max)

Örneğin:

struct Number : Comparable {

var val: Int = 0

init(_ val: Int)

{

self.val = val

}

}

func ==(a: Number, b: Number) -> Bool

{

return a.val == b.val

}

func <(a: Number, b: Number) -> Bool

{

return a.val < b.val

}

func getMax<T: Comparable>(a: [T]) -> T

{

var max = a[0]

for i in 1..<a.count {

if max < a[i] {

max = a[i]

}

}

return max

}

var a = [Number(23), Number(45), Number(28), Number(54), Number(98), Number(12)]

var max = getMax(a)

print(max.val)

Swift’te generic protokol kaavramı yoktur. Ancak generic protokoller dolaylı bir biçimde belli düzeyde oluşturulabilmektedir. Bir prokolde typealias anahtar sözcüğü ile bir tür ismi uyduruklursa protokolün elemanları ona dayalı olarak oluşturulabilmektedir. Örneğin:

protocol P {

typealias T

func foo(a: T) -> T

}

Buradaki typealias ile oluşturulmuş olan tür ismine protokolün ilişkin olduğu tür (associated type) denilmektedir. Eğer böyle bir protokolü bir tür destekleyecekse T türü ne olursa olsun yalnızca bir tane protokoldeki kalıba uygun foo metodunu bulundurmak zorundadır. Örneğin:

class Sample : P {

func foo(a: Double) -> Double

{

return 0

}

//…

}

Yukarıdaki örnekte artık Sample sınıfı aynı kalıba uygun başka bir foo metodunu içermez. Örneğin:

class Sample : P {

func foo(a: Double) -> Double

{

return 0

}

func foo(a: Int) -> Int // error!

{

return 0

}

}

Swift’in standard kütüphanesinde bir grup XXXLiteralConvertible isimli protokol vardır. Bu protokolleri destekleyen türlere XXX kategorisinden sabitler doğrudan atanabilir. (Swift’te sabitlerin türlerinin olmadığını yalnızca kategorilerinin olduğunu anımsayınız.) Örneğin IntegerLiteralConvertible protokolünü destekleyen bir türe biz nokta içermeyen bir sayıyı atayabiliriz. Örneğin:

class Sample : IntegerLiteralConvertible {

//…

}

var s: Sample

s = 123 // geçerli

XXXLiteralConvertible protokolleri bir init metodu içermektedir. Bu init metodu XXX kategorisinden sabit değerleri alabilmektedir. Örneğin IntegerLiteralConvertible arayüzü aşağıdaki gibi oluşturulmuştur:

protocol IntegerLiteralConvertible {

typealias IntegerLiteralValue

init(integerLiteral value: IntegerLiteralType)

}

Örneğin:

class Sample : IntegerLiteralConvertible {

var val: Int

required init(integerLiteral val: Int)

{

self.val = val

}

}

var s: Sample

s = 123 // geçerli, Sample(123) ile eşdeğer

Biz burada IntegerLiteralConvertible protokolünü destekleyen bir Sample sınıfı yazdık. Böylece artık nokta içermeyen tamsayısal sabitleri Sample türüne doğrudan atayabiliriz. Bu atama işlemi sırasında aslında derleyici Sample sınıfı türünden protokoldeki init metodunu kullanarak nesne yaratmaktadır. Yani bu atama işlemi aslında yalnızca kısa bir yazım oluşturmaktadır. C++ ve C#’ta bu işlemin tür dönüştürme operatör fonksiyonlarıyla yapıldığını anımsayınız.

--

--