Swift 4 Operatör Fonksiyonları (Operator Functions) #24
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.