讀古今文學網 > iOS編程基礎:Swift、Xcode和Cocoa入門指南 > 10.5 訪問器、屬性與鍵值編碼 >

10.5 訪問器、屬性與鍵值編碼

從結構上來說,Objective-C實例變量類似於Swift實例屬性:它是一個伴隨著類的每個實例的變量,其生命週期與值都關聯到這個特定的實例。不過,Objective-C實例變量通常是私有的,這意味著其他類的實例是看不到它的(Swift看不到)。如果實例變量是公共的,那麼Objective-C類通常都會實現訪問器方法:getter方法與setter方法(如果外界可以改寫這個實例變量)。這種情況很常見,因此有如下命名約定:

getter方法

getter應該與實例變量有相同的名字(如果實例變量前有下劃線,那麼getter是沒有這個下劃線的)。這樣,如果實例變量是myVar(或_myVar),那麼getter方法應該命名為myVar。

setter方法

setter方法名應該以set開頭,後跟大寫的實例變量名(如果實例變量前有下劃線,那麼setter是沒有這個下劃線的)。setter應該接收一個參數,即準備賦給實例變量的新值。這樣,如果實例變量是myVar(或_myVar),那麼setter應該命名為setMyVar:。

這種模式(一個getter方法,可能還有一個命名適當的setter方法)非常常見,它還有一種簡寫形式:Objective-C類可以通過關鍵字@property與一個名字來聲明屬性。比如,下面這行代碼來自於UIView類的聲明:


@property(nonatomic) CGRect frame;
  

(請忽略掉圓括號中的內容。)在Objective-C中,這種聲明構成了一種承諾,它會提供一個getter訪問器方法frame並返回一個CGRect,同時還會提供一個setter訪問器方法setFrame:並接收一個CGRect參數。

如果Objective-C以這種形式聲明@property,那麼Swift就會將其看作Swift屬性。這樣,UIView的frame屬性聲明就會被直接轉換為Swift中類型為CGRect的實例屬性frame:


var frame: CGRect
  

Objective-C的屬性名只不過是個語法糖而已。在設置UIView的frame屬性時,實際會調用其setFrame:setter方法;在獲取UIView的frame屬性時,實際會調用其frame getter方法。在Objective-C中,屬性的使用是可選的;Objective-C可以並且經常會直接調用setFrame:與frame方法。不過在Swift中卻不行。如果Objective-C類有正式的@property聲明,那麼其訪問器方法會對Swift隱藏。

Objective-C屬性聲明可以在圓括號中包含單詞readonly。這表示只有getter但卻沒有setter。比如(請忽略掉圓括號中的其他內容):


@property(nonatomic,readonly,strong) CALayer *layer;
  

Swift會在聲明後通過{get}來反映出這種限制,就好像這是個計算只讀屬性一樣;編譯器不允許為這樣的屬性賦值:


var layer: CALayer { get }
  

Objective-C屬性及相應的訪問器方法都有自己的生命週期,獨立於任何底層的實例變量。雖然訪問器方法可用於訪問不可見的實例變量,但不一定總是這樣的。在設置UIView的frame屬性並且setFrame:訪問器方法得到調用時,你是不知道該方法到底做了哪些事情:它可能會設置一個名為frame或_frame的實例變量,但誰知道呢?從這個意義上來說,訪問器與屬性就是門面,隱藏了底層的實現。這與Swift中設置變量但又不知道或不關心它是個存儲變量還是個計算變量類似;對於設置變量的代碼來說,變量真正所設置的東西是不重要的(可能也是不知道的)。

10.5.1  Swift訪問器

就像Objective-C屬性實際上只是訪問器方法的簡寫一樣,Objective-C將Swift屬性也看作訪問器方法的簡寫形式,即便沒有這樣的方法亦如此。如果在Swift中聲明的類有一個名為prop的屬性,Objective-C就會調用prop方法獲取其值,調用setProp:方法設置其值,即便沒有實現這樣的方法亦如此。這些調用會通過隱式的訪問器方法路由到屬性。

在Swift中,你不應該為屬性編寫顯式的訪問器方法!編譯器不允許你這麼做。如果需要顯式實現訪問器方法,那麼請使用計算屬性。比如,我向UIViewController子類添加了一個名為color的計算屬性並提供getter與setter:


class ViewController: UIViewController {
    var color : UIColor {
        get {
            print(\"someone called the getter\")
            return UIColor.redColor
        }
        set {
            print(\"someone called the setter\")
        }
    }
}
  

Objective-C代碼現在可以顯式調用隱式的setColor:與color訪問器方法,當調用時,你會看到計算屬性的setter與getter方法實際上會被調用:


ViewController* vc = [ViewController new];
[vc setColor:[UIColor redColor]]; // \"someone called the setter\"
UIColor* c = [vc color]; // \"someone called the getter\"
  

這證明了在Objective-C中,你已經提供了setColor:與color訪問器方法。你甚至可以修改這些訪問器方法的Objective-C名字!要想做到這一點,請添加一個@objc(...)特性,並在圓括號中放入其Objective-C名字。可以將其添加到計算屬性的setter與getter方法中,或添加到屬性自身當中:


@objc(hue) var color : UIColor?

Objective-C代碼現在可以直接調用hue與setHue:訪問器方法了。

如果只是想向setter添加功能,那麼可以使用setter觀察者。比如,要想向UIView子類中的Objective-C setFrame:方法添加功能,那麼可以覆寫frame屬性並添加一個didSet觀察者:


class MyView: UIView {
    override var frame : CGRect {
        didSet {
            print(\"the frame setter was called: (super.frame)\")
        }
    }
}
  

10.5.2 鍵值編碼

Cocoa可以通過運行期指定的字符串名動態調用訪問器(這樣就可以訪問Swift屬性了),這種機制叫作鍵值編碼(KVC),類似於通過respondsToSelector:使用選擇器名進行內省的能力。字符串名是鍵;傳遞給訪問器或從訪問器返回的是值。鍵值編碼的基礎是NSKeyValueCoding協議,這是個非正式協議;它實際上是個注入NSObject中的類別。因此,要想使用鍵值編碼,Swfit類必須要繼承自NSObject。

基本的鍵值編碼方法是valueForKey:與setValue:forKey:。當在對像上調用其中一個方法時,該對象就會被內省。簡而言之,首先會尋找恰當的選擇器;如果不存在,那就會直接訪問實例變量。另外一對有用的方法是dictionaryWithValuesForKeys:與setValuesForKeysWithDictionary:,你可以通過它們僅使用一個命令就能以NSDictionary的方式獲取與設置多個鍵值對。

鍵值編碼中的值必須是個Objective-C對象,其Swift類型是AnyObject。在調用valueForKey:時,你會接收到一個包裝了AnyObject的Optional,需要將其向下類型轉換為真實的類型。

對於給定的鍵來說,如果一個類提供了訪問器方法或擁有實例變量來對其進行訪問,那麼我們會說這個類針對於該鍵是鍵值編碼兼容的(或KVC兼容的)。對於給定的鍵來說,如果一個類不是對其鍵值編碼兼容的,那麼訪問該鍵會導致運行期異常。當出現這種崩潰時,如果熟悉拋出的消息將是大有裨益的,下面就來故意製造崩潰的結果:


let obj = NSObject
obj.setValue(\"hello\", forKey:\"keyName\") // crash
  

控制台會打印出消息「This class is not key value coding-compliant for the key keyName.」。雖然缺少引號,但這條錯誤消息的最後一個單詞是導致崩潰的鍵字符串。

如何才能讓上述方法調用不崩潰呢?接收方法調用的對象所屬的類需要有一個setKeyName:setter方法(或keyName及_keyName實例變量)。如10.5.1節所述,在Swift中,實例屬性表示存在訪問器方法。這樣,我們可以在擁有所聲明的屬性的任何NSObject子類實例上使用鍵值編碼,前提是鍵字符串是該屬性的字符串名。下面就來試一下!類聲明如以下代碼所示:


class Dog : NSObject {
    var name : String = \"\"
}
  

下面是測試代碼:


let d = Dog
d.setValue(\"Fido\", forKey:\"name\") // no crash!
print(d.name) // \"Fido\" - it worked!
  

10.5.3 鍵值編碼的使用

實際上,你可以通過鍵值編碼在運行期根據字符串來決定調用哪個訪問器。最簡單的一種情況就是,你可以通過字符串訪問動態指定的屬性。這在Objective-C代碼中是非常有價值的;不過,這種自由的內省能力與Swift的精神恰恰相反,在將我自己編寫的Objective-C代碼轉換為Swift時,我發現可以通過其他方式達到相同的目的。

下面是個示例。在flashcard應用中有個名為Term的類,代表一個拉丁語單詞。它聲明了很多屬性。每個卡片會顯示一個單詞,其各種屬性會顯示在不同的文本域中。如果用戶輕拍了3個文本域之一,那麼我希望界面上顯示的單詞換成下一個,並且這個單詞要不同於上一個。這樣,代碼對於三個文本域來說都是一樣的;唯一的差別是在尋找下一個顯示的單詞時,我們應該考慮哪個屬性。在Objective-C中,到目前為止最簡單的方式就是使用鍵值編碼:


NSInteger tag = g.view.tag; // the tag tells us which text field was tapped
NSString* key = nil;
switch (tag) {
    case 1: key = @\"lesson\"; break;
    case 2: key = @\"lessonSection\"; break;
    case 3: key = @\"lessonSectionPartFirstWord\"; break;
}
// get current value of corresponding instance variable
NSString* curValue = [[self currentCardController].term valueForKey: key];
  

不過在Swift中,可以通過匿名函數數組來完成相同的功能:


let tag = g.view!.tag - 1
let arr : [(Term) -> String] = [
    {$0.lesson}, {$0.lessonSection}, {$0.lessonSectionPartFirstWord}
]
let f = arr[tag]
let curValue = f(self.currentCardController.term)
  

不過,鍵值編碼在iOS編程中也是頗具價值的,特別是因為很多內建的Cocoa類都允許以特殊的方式使用鍵值編碼。比如:

·如果向NSArray發送valueForKey:,那麼它會向數組中的每個元素發送valueForKey:,並返回一個包含了結果的新數組,這是一種優雅的簡寫方式,NSSet亦如此。

·NSDictionary實現了valueForKey:以作為objectForKey:的替代方案(這對於字典構成的NSArray來說特別有用)。與之類似,NSMutableDictionary會將setValue:forKey:作為setObject:forKey:的同義;只不過在調用removeObject:forKey:時value:可以為nil。

·NSSortDescriptor通過向NSArray中的每個元素發送valueForKey:來對NSArray排序。這樣,根據特定的字典鍵值對字典數組排序,以及根據特定的屬性值對對像數組排序就變得很容易了。

·NSManagedObject(與Core Data配合使用)用於確保對在實體模型中所配置的特性做到鍵值編碼兼容。這樣,我們就可以通過valueForKey:與setValue:forKey:訪問這些特性了。

·可以通過CALayer與CAAnimation使用鍵值編碼來定義並獲取任意鍵的值,就好像它們是字典一樣;它們實際上對於每個鍵都是鍵值編碼兼容的。這對於向這些類的實例附加識別與配置信息是非常有用的。實際上,這是我在Swift中使用鍵值編碼最常用的方式。

10.5.4  KVC與插座變量

鍵值編碼是插座變量連接能夠正常運作的關鍵所在(參見第7章)。Nib中的插座變量名是個字符串,鍵值編碼會在nib加載時將該字符串轉換為所要尋找的屬性。

假設有一個Dog類,它有一個@IBOutlet屬性master,你繪製了一個從nib中的這個類到Person nib對像上的\"master\"插座變量。當nib加載時,插座變量名\"master\"會通過鍵值編碼轉換為訪問器方法名setMaster:,Dog實例的setMaster:隱式訪問器方法在調用時會將Person實例作為其參數,這樣會將Dog實例的master屬性值設為Person實例(如圖7-9所示)。

如果nib中的插座變量名與類中的屬性名之間不匹配,那麼在運行期,當nib加載時,Cocoa會使用鍵值編碼根據插座變量名來設置對像中的值,但卻會失敗,並拋出異常,錯誤消息表示該類針對於這個鍵(插座變量名)並不是鍵值編碼兼容的;也就是說,應用會在nib加載時崩潰。另一種類似的情況是插座變量是正確的,但後面卻修改或刪除了類中的屬性名(參見7.3.4節)。

10.5.5 鍵路徑

可以通過鍵路徑在一個表達式中將鍵串聯起來。如果一個對像針對某個鍵是鍵值編碼兼容的,並且該鍵所對應的值又針對另一個鍵是鍵值編碼兼容的,那就可以通過調用valueForKeyPath:與setValue:forKeyPath:將這兩個鍵串聯起來。鍵路徑字符串看起來像是使用點符號鏈接起來的一連串鍵名。比如,valueForKeyPath(\"key1.key2\")會調用消息接收者的valueForKey:,並且將\"key1\"作為鍵,然後獲得調用所返回的對象,並對該對像調用valueForKey:,並且將\"key2\"作為鍵。

為了演示這種簡寫方式,假設對像myObject有一個實例屬性theData,它是個字典數組,這樣每個字典都有一個name鍵和一個description鍵:


var theData = [
    [
        \"description\" : \"The one with glasses.\",
        \"name\" : \"Manny\"
    ],
    [
        \"description\" : \"Looks a little like Governor Dewey.\",
        \"name\" : \"Moe\"
    ],
    [
        \"description\" : \"The one without a mustache.\",
        \"name\" : \"Jack\"
    ]
]
  

我們可以通過鍵值編碼與鍵路徑鑽取到該字典數組中:


let arr = myObject.valueForKeyPath(\"theData.name\") as! [String]
  

結果是個包含了字符串\"Manny\"\"Moe\"與\"Jack\"的數組。如果不清楚原因,那麼請回顧一下之前介紹的NSArray與NSDictionary是如何實現valueForKey:的吧。

再回顧一下第7章介紹的自定義運行時特性。該特性就使用了鍵值編碼!當在對象的身份查看器中定義了運行時特性時,你在第1列中所輸入的字符串就是個鍵路徑。

10.5.6 數組訪問器

鍵值編碼是一項強大的技術,同時擁有很多衍生品(參見Apple的Key-Value Coding Programming Guide瞭解詳情)。這裡只介紹其中一種。如果鍵的值看起來像是個數組或是集合(即便實際情況並非如此),那麼借助於鍵值編碼,對象可以將鍵合成起來。你需要實現特別命名的訪問器方法;在使用相應的鍵時,鍵值編碼會看到它們。

為了說明這一點,向對像myObject所屬的類添加如下方法:


func countOfPepBoys -> Int {
    return self.theData.count
}
func objectInPepBoysAtIndex(ix:Int) -> AnyObject {
    return self.theData[ix]
}
  

通過實現countOf...與objectIn...AtIndex:,我告訴鍵值編碼系統認為給定的鍵\"pepBoys\"存在並且是個數組。通過鍵值編碼方式獲取鍵\"pepBoys\"的值的操作可以成功,並且返回的對象可以看作NSArray,但實際上它是個代理對像(NSKeyValueArray)。現在可以這樣做:


let arr : AnyObject = myObject.valueForKey(\"pepBoys\")!
let arr2 : AnyObject = myObject.valueForKeyPath(\"pepBoys.name\")!
  

在上述代碼中,arr是個數組代理;arr2是由3個男孩的名字構成的相同的數組。這個示例看起來毫無意義:底層實現已經是個數組了,使用\"pepBoys\"與之前所用的\"theData\"有何區別呢?表面上看沒什麼不同,但實際情況卻並非如此。假設並沒有數組存在,countOfPepBoys與objectInPepBoysAtIndex:的結果是通過完全不同的操作得到的。實際上,我們創建了一個類似於NSArray的鍵;並且將一些實現細節隱藏在其後。