鍍金池/ 教程/ iOS/ 值對(duì)象
與四軸無人機(jī)的通訊
在沙盒中編寫腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學(xué)
NSString 與 Unicode
代碼簽名探析
測(cè)試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動(dòng)開發(fā)
Collection View 動(dòng)畫
截圖測(cè)試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個(gè)完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進(jìn)的自動(dòng)布局工具箱
動(dòng)畫
為 iOS 7 重新設(shè)計(jì) App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實(shí)例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動(dòng)畫解釋
響應(yīng)式 Android 應(yīng)用
初識(shí) TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項(xiàng)目介紹
Swift 的強(qiáng)大之處
測(cè)試并發(fā)程序
Android 通知中心
調(diào)試:案例學(xué)習(xí)
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機(jī)制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學(xué)習(xí)的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計(jì)優(yōu)雅的移動(dòng)游戲
繪制像素到屏幕上
相機(jī)與照片
音頻 API 一覽
交互式動(dòng)畫
常見的后臺(tái)實(shí)踐
糟糕的測(cè)試
避免濫用單例
數(shù)據(jù)模型和模型對(duì)象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場(chǎng)
照片框架
響應(yīng)式視圖
Square Register 中的擴(kuò)張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點(diǎn)互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計(jì)的藝術(shù)
導(dǎo)航應(yīng)用
線程安全類的設(shè)計(jì)
置換測(cè)試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機(jī)項(xiàng)目
Mach-O 可執(zhí)行文件
UI 測(cè)試
值對(duì)象
活動(dòng)追蹤
依賴注入
Swift
項(xiàng)目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測(cè)試 View Controllers
訪談
收據(jù)驗(yàn)證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場(chǎng)
游戲
調(diào)試核對(duì)清單
View Controller 容器
學(xué)無止境
XCTest 測(cè)試實(shí)戰(zhàn)
iOS 7
Layer 中自定義屬性的動(dòng)畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲(chǔ)
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴(kuò)展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫支持
Fetch 請(qǐng)求
導(dǎo)入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機(jī)捕捉
語言標(biāo)簽
同步案例學(xué)習(xí)
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識(shí)別
玩轉(zhuǎn)字符串
相機(jī)工作原理
Build 過程

值對(duì)象

在這篇文章里,我們會(huì)看看如何用 Objective-C 寫值對(duì)象 (value objects)。在編寫中,我們會(huì)接觸到 Objective-C 中的一些重要的接口和方法。所謂值對(duì)象,就是指那些能夠被判等的,持有某些數(shù)值的對(duì)象 (對(duì)它們判等時(shí)我們看重值是否相等,而對(duì)是否是同一個(gè)對(duì)象并不是那么關(guān)心)。通常來說,值對(duì)象會(huì)被用作 model 對(duì)象。比如像下面的 Person 對(duì)象就是一個(gè)簡單的例子:

@interface Person : NSObject

@property (nonatomic,copy) NSString* name;
@property (nonatomic) NSDate* birthDate;
@property (nonatomic) NSUInteger numberOfKids;

@end

創(chuàng)造這樣的對(duì)象可以說是我們?nèi)諒?fù)一日的基本工作了,雖然這些對(duì)象表面上看起來相當(dāng)簡單,但是其中暗藏玄機(jī)。

我們中有很多人會(huì)教條主義地認(rèn)為這類對(duì)象就應(yīng)該是不可變的 (immutable)。一旦你創(chuàng)建了一個(gè) Person 對(duì)象,它就不可能在做任何改變了。我們?cè)谏院髸?huì)在本話題中涉及到可變性的問題。

屬性

首先我們來看看定義一個(gè) Person 時(shí)所用到的屬性。創(chuàng)建屬性是一件機(jī)械化的工作:對(duì)于一般的屬性,你會(huì)將它們聲明為 nonatomic。默認(rèn)情況下,對(duì)象屬性是 strong 的,標(biāo)量屬性是 assign 的。但是有一個(gè)例外,就是對(duì)于具有可變副本的屬性,我們傾向于將其聲明為 copy。比如說,name 屬性的類型是 NSString,有可能有人創(chuàng)建了一個(gè) Person 對(duì)象,并且給這個(gè)屬性賦了一個(gè) NSMutableString 的名字值。然后過了一會(huì)兒,這個(gè)可變字符串被變更了。如果我們的屬性不是 copy 而是 strong 的話,隨著可變字符串的改變,我們的 Person 對(duì)象也將發(fā)生改變,這不是我們希望發(fā)生的。對(duì)于類似數(shù)組或者字典這樣的容器類來說,也是這樣的情況。

要注意的是這里的 copy 是淺拷貝;容器里還是會(huì)包含可變對(duì)象。比如,如果你有一個(gè) NSMutableArray* a,其中有一些 NSMutableDictionary 的元素,那么 [a copy] 將返回一個(gè)不可變的數(shù)組,但是里面的元素依然是同樣的 NSMutableDictionary 對(duì)象。我們稍后會(huì)看到,對(duì)于不可變對(duì)象的 copy 是沒有成本的,只會(huì)增加引用計(jì)數(shù)而已。

因?yàn)閷傩允窍鄬?duì)最近才加入到 Objective-C 的,所以在較老的代碼中,你有可能不會(huì)見到屬性。取而代之,可能會(huì)有自定義的 getter 和 setter,或者直接是實(shí)例變量。對(duì)于最近的代碼,看起來大家都贊同還是使用屬性比較好,這也正是我們所推薦的。

擴(kuò)展閱讀

NSString: copy 還是 retain

初始化方法 (Initializers)

如果我們需要的是不可變對(duì)象,那么我們要確保它在被創(chuàng)建后就不能再被更改。我們可以通過使用初始化方法并且在接口中將我們的屬性聲明為 readonly 來實(shí)現(xiàn)這一點(diǎn)。我們的接口看起來是這樣的:

@interface Person : NSObject

@property (nonatomic,readonly) NSString* name;
@property (nonatomic,readonly) NSDate* birthDate;
@property (nonatomic,readonly) NSUInteger numberOfKids;

- (instancetype)initWithName:(NSString*)name
                   birthDate:(NSDate*)birthDate
                numberOfKids:(NSUInteger)numberOfKids;

@end

在初始化方法的實(shí)現(xiàn)中,我們必須使用實(shí)例變量,而不是屬性。

編者注 在初始化方法或者是 dealloc 中最好不要使用屬性,因?yàn)槟銦o法確定 `self` 到底是不是確實(shí)調(diào)用的是你想要的實(shí)例

@implementation Person

- (instancetype)initWithName:(NSString*)name
                   birthDate:(NSDate*)birthDate
                numberOfKids:(NSUInteger)numberOfKids
{
    self = [super init];
    if (self) {
        _name = [name copy];
        _birthDate = birthDate;
        _numberOfKids = numberOfKids;
    }
    return self;
}

@end

現(xiàn)在我們就可以構(gòu)建新的 Person 對(duì)象,并且不能再對(duì)它們做改變了。這一點(diǎn)很有幫助,在寫和 Person 對(duì)象一起工作的其他類的時(shí)候,我們知道這些值是不會(huì)發(fā)生改變的。注意這里 copy 不再是接口的一部分了,現(xiàn)在它只和實(shí)現(xiàn)的細(xì)節(jié)相關(guān)。

判等

要比較相等,我們需要實(shí)現(xiàn) isEqual: 方法。我們希望 isEqual: 方法僅在所有屬性都相等的時(shí)候返回真。Mike Ash 的 Implement Equality and Hashing 和 NSHipster 的 Equality 為我們很好地闡述了如何實(shí)現(xiàn)。首先,我們需要寫一個(gè) isEqual: 方法:

- (BOOL)isEqual:(id)obj
{
    if(![obj isKindOfClass:[Person class]]) return NO;

    Person* other = (Person*)obj;

    BOOL nameIsEqual = self.name == other.name || [self.name isEqual:other.name];
    BOOL birthDateIsEqual = self.birthDate == other.birthDate || [self.birthDate isEqual:other.birthDate];
    BOOL numberOfKidsIsEqual = self.numberOfKids == other.numberOfKids;
    return nameIsEqual && birthDateIsEqual && numberOfKidsIsEqual;
}

如上,我們先檢查輸入和自身是否是同樣的類。如果不是的話,那肯定就不相等了。然后對(duì)每一個(gè)對(duì)象屬性,判斷其指針是否相等。|| 操作符的操作看起來好像是不必要的,但是如果我們需要處理兩個(gè)屬性都是 nil 的情形的話,它能夠正確地返回 YES。比較像 NSUInteger 這樣的標(biāo)量是否相等時(shí),則只需要使用 == 就可以了。

還有一件事情值得一提:這里我們將不同的屬性比較的結(jié)果分開存儲(chǔ)到了它們自己的 BOOL 中。在實(shí)踐中,可能將它們放到一個(gè)大的判斷語句中會(huì)更好,因?yàn)槿绻@么做的話你就可以避免一些不必要的取值和比較了。比如在上面的例子中,如果 name 已經(jīng)不相等了的話,我們就沒有必要再檢查其他的屬性了。將所有判斷合并到一個(gè) if 語句中我們可以自動(dòng)地得到這樣的優(yōu)化。

接下來,按照文檔所說,我們還需要實(shí)現(xiàn)一個(gè) hash 函數(shù)。蘋果如是說:

如果兩個(gè)對(duì)象是相等的,那么它們必須有同樣的 hash 值。如果你在一個(gè)子類里定義了 isEqual: 方法,并且打算將這個(gè)子類的實(shí)例放到集合類中的話,那么你一定要確保你也在你的子類里定義了 hash 方法,這是非常重要的。

首先,我們來看看如果不實(shí)現(xiàn) hash 方法的話,下面的代碼會(huì)發(fā)生什么;

Person* p1 = [[Person alloc] initWithName:name birthDate:start numberOfKids:0];
Person* p2 = [[Person alloc] initWithName:name birthDate:start numberOfKids:0];
NSDictionary* dict = @{p1: @"one", p2: @"two"};
NSLog(@"%@", dict);

第一次運(yùn)行上面的代碼是,一切都很正常,字典中有兩個(gè)條目。但是第二次運(yùn)行的時(shí)候卻只剩一個(gè)了。事情變得不可預(yù)測(cè),所以我們還是按照文檔說的來做吧。

可能你還記得你在計(jì)算機(jī)科學(xué)課程中學(xué)到過,編寫一個(gè)好的 hash 函數(shù)是一件不太容易的事情。好的 hash 函數(shù)需要兼?zhèn)?em>確定性和均布性。確定性需要保證對(duì)于同樣的輸入總是能生成同樣的 hash 值。均布性需要保證輸出的結(jié)果要在輸出范圍內(nèi)均勻地對(duì)應(yīng)輸入。你的輸出分布越均勻,就意味著當(dāng)你將這些對(duì)象用在集合中時(shí),性能會(huì)越好。

首先我們得搞清楚到底發(fā)生了什么。讓我們來看看沒有實(shí)現(xiàn) hash 函數(shù)時(shí)候的情況下,使用 Person 對(duì)象作為字典的鍵時(shí)的情況:

NSMutableDictionary* dictionary = [NSMutableDictionary dictionary];

NSDate* start = [NSDate date];
for (int i = 0; i < 50000; i++) {
    NSString* name = randomString();
    Person* p = [[Person alloc] initWithName:name birthDate:[NSDate date] numberOfKids:i++];
    [dictionary setObject:@"value" forKey:p];
}
NSLog(@"%f", [[NSDate date] timeIntervalSinceDate:start]);

這在我的機(jī)子上花了 29 秒時(shí)間來執(zhí)行。作為對(duì)比,當(dāng)我們實(shí)現(xiàn)一個(gè)基本的 hash 方法的時(shí)候,同樣的代碼只花了 0.4 秒。這并不是精確的性能測(cè)試,但是卻足以告訴我們實(shí)現(xiàn)一個(gè)正確的 hash 函數(shù)的重要性。對(duì)于 Person 這個(gè)類來說,我們可以從這樣一個(gè) hash 函數(shù)開始:

- (NSUInteger)hash
{
    return self.name.hash ^ self.birthDate.hash ^ self.numberOfKids;
}

這將從我們的屬性中取出三個(gè) hash 值,然后將它們做 XOR (異或) 操作。在這里,這個(gè)方法對(duì)我們的目標(biāo)來說已經(jīng)足夠好了,因?yàn)閷?duì)于短字符串 (以前這個(gè)上限是 96 個(gè)字符,不過現(xiàn)在不是這樣了,參見 CFString.c 中 hash 的部分) 來說,NSString 的 hash 函數(shù)表現(xiàn)很好。對(duì)于更正式的 hash 算法,hash 函數(shù)應(yīng)該依賴于你所擁有的數(shù)據(jù)。這在 Mike Ash 的文章其他一些地方有所涉及。

hash 文檔中,有下面這樣一段話:

如果一個(gè)被插入集合類的可變對(duì)象是依據(jù)其 hash 值來決定其在集合中的位置的話,這個(gè)對(duì)象的 hash 函數(shù)所返回的值在該對(duì)象存在于集合中時(shí)是不允許改變的。因此,要么使用一個(gè)和對(duì)象內(nèi)部 狀態(tài)無關(guān)的 hash 函數(shù),要么確保在對(duì)象處于集合中時(shí)其內(nèi)部狀態(tài)不發(fā)生改變。比如說,一個(gè)可 變字典可以被放到一個(gè) hash table 中,但是只要這個(gè)字典還在 hash table 中時(shí),你就不能 更改它。(注意,要知道一個(gè)給定對(duì)象是不是存在于某個(gè)集合中是一件很困難的事情。)

這也是你需要確保對(duì)象的不可變性的另一個(gè)重要原因。只要確保了這一點(diǎn),你就不必再擔(dān)心這個(gè)問題了。

擴(kuò)展閱讀

NSCopying

為了讓我們的對(duì)象更有用,我們最好實(shí)現(xiàn)一下 NSCopying 接口。這能夠使我們能在容器類中使用它們。對(duì)于我們的類的一個(gè)可變的變體,可以這么實(shí)現(xiàn) NSCopying

- (id)copyWithZone:(NSZone *)zone
{
    Person* p = [[Person allocWithZone:zone] initWithName:self.name
                                                birthDate:self.birthDate
                                             numberOfKids:self.numberOfKids];
    return p;
}

然而,在接口的文檔中,他們提到了另一種實(shí)現(xiàn) NSCopying 的方式:

對(duì)于不可變的類和其內(nèi)容來說,NSCopying 的實(shí)現(xiàn)應(yīng)該保持原來的對(duì)象,而不是創(chuàng)建一份新的拷貝。

所以,對(duì)于我們的不可變版本,我們只需要這樣就夠了:

- (id)copyWithZone:(NSZone *)zone
{
    return self;
}

NSCoding

如果我們想要序列化對(duì)象,我們可以實(shí)現(xiàn) NSCoding。這個(gè)接口中有兩個(gè) required 的方法:

- (id)initWithCoder:(NSCoder *)decoder
- (void)encodeWithCoder:(NSCoder *)encoder

實(shí)現(xiàn)這個(gè)和實(shí)現(xiàn)判等方法同樣直接,也同樣機(jī)械化:

- (id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super init];
    if (self) {
        _name = [aDecoder decodeObjectForKey:@"name"];
        _birthDate = [aDecoder decodeObjectForKey:@"birthDate"];
        _numberOfKids = [aDecoder decodeIntegerForKey:@"numberOfKids"];
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder
{
    [aCoder encodeObject:self.name forKey:@"name"];
    [aCoder encodeObject:self.birthDate forKey:@"birthDate"];
    [aCoder encodeInteger:self.numberOfKids forKey:@"numberOfKids"];
}

可以在 NSHipsterMike Ash 的博客上了解這方面的更多內(nèi)容。順帶一提,在處理比如來自網(wǎng)絡(luò)的數(shù)據(jù)這樣不信任的來源的數(shù)據(jù)時(shí),不要使用 NSCoding,因?yàn)閿?shù)據(jù)可能被篡改過。通過修改歸檔的數(shù)據(jù),很容易實(shí)施遠(yuǎn)程代碼運(yùn)行攻擊。在處理這樣的數(shù)據(jù)時(shí),應(yīng)該使用 NSSecureCoding 或者像 JSON 這樣的自定義格式

Mantle

現(xiàn)在,我們還有一個(gè)問題:這些能自動(dòng)化么?答案是能。一種方式是1代碼生成,但是幸運(yùn)的是有一種更好的替代:Mantle。Mantle 使用自舉 (introspection) 的方法生成 isEqual:hash。另外,它還提供了一些幫助你創(chuàng)建字典的方法,它們可以被用來讀寫 JSON。當(dāng)然,一般來說在運(yùn)行時(shí)做這些不如你自己寫起來高效,但是另一方面,自動(dòng)處理這個(gè)流程的話犯錯(cuò)的可能性要小得多。

可變性

在 C 中可變值是默認(rèn)的,其實(shí)在 Objective-C 中也是這樣的。一方面,這非常方便,因?yàn)槟憧梢栽谌魏螘r(shí)候改變它。在構(gòu)建相對(duì)小的系統(tǒng)外,這一般不成問題。但是正如我們中很多人的經(jīng)驗(yàn)一樣,在構(gòu)建較大的系統(tǒng)時(shí),使用不可變的對(duì)象會(huì)容易得多。在 Objective-C 中,我們一直是使用不可變對(duì)象的,現(xiàn)在其他的語言也逐漸開始添加不可變對(duì)象了。

我們來看看使用可變對(duì)象的兩個(gè)問題。其中一個(gè)是它們有可能在你不希望的時(shí)候發(fā)生改變,另一個(gè)是在多線程中使用可變對(duì)象。

不希望的改變

假設(shè)我們有一個(gè) table view controller,其中有一個(gè) people 屬性:

@interface ViewController : UITableViewController

@property (nonatomic) NSArray* people;

@end

在實(shí)現(xiàn)中,我們僅僅把數(shù)組中的每個(gè)元素映射到一個(gè) cell 中:

 - (NSInteger)numberOfSectionsInTableView:(UITableView*)tableView
 {
     return 1;
 }

 - (NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section
 {
     return self.people.count;
 }

現(xiàn)在,在設(shè)定上面的 view controller 中,我們的代碼可能是這樣的:

self.items = [NSMutableArray array];
[self loadItems]; // Add 100 items to the array
tableVC.people = self.items;
[self.navigationController pushViewController:tableVC animated:YES];

table view 將開始執(zhí)行 tableView:numberOfRowsInSection: 之類的方法,一開始,一切都 OK。但是假設(shè)在某個(gè)時(shí)候,我們進(jìn)行了這樣的操作:

[self.items removeObjectAtIndex:1];

這改變了 items 數(shù)組,但是它同時(shí)也改變了我們的 table view controller 中的 people 數(shù)組。如果我們沒有進(jìn)一步地同 table view controller 進(jìn)行通訊的話,table view 還會(huì)認(rèn)為有 100 個(gè)元素需要顯示,然而我們的數(shù)組卻只包括 99 個(gè)元素。你大概知道我們會(huì)面臨怎樣的窘境了。在這里,我們應(yīng)該做的是將屬性聲明為 copy

 @interface ViewController : UITableViewController

 @property (nonatomic, copy) NSArray* items;

 @end

現(xiàn)在,我們?cè)趯⒖勺償?shù)組設(shè)置給 items 的時(shí)候,會(huì)生成一個(gè)不可變的 copy。如果我們?cè)O(shè)定的是一個(gè)通常 (不可變) 的數(shù)組,那么 copy 操作是沒有開銷的,它僅僅只是增加了引用計(jì)數(shù)。

多線程

假設(shè)我們有一個(gè)用來表示銀行賬號(hào)的可變對(duì)象 Account,其有一個(gè) transfer:to: 方法:

- (void)transfer:(double)amount to:(Account*)otherAccount
{
    self.balance = self.balance - amount;
    otherAccount.balance = otherAccount.balance + amount;
}

多線程的代碼可能會(huì)在以很多方式掛掉。比如線程 A 要讀取 self.balance,線程 B 有可能在 A 繼續(xù)之前就修改了這個(gè)值。對(duì)于這其中可能造成的各種風(fēng)險(xiǎn),請(qǐng)參看我們的話題二。

如果我們使用的是不可變對(duì)象的話,事情就簡單多了。我們不能改變它們,這個(gè)規(guī)則迫使我們?cè)谝粋€(gè)完全不一樣的層級(jí)上來提供可變性,這將使代碼簡單得多。

緩存

不可變性還能在緩存數(shù)值方面幫助我們。比如,假設(shè)你已經(jīng)將一個(gè) markdown 文檔解析成一個(gè)帶有表示各種不同元素的結(jié)點(diǎn)的樹結(jié)構(gòu)了。在你想從這個(gè)結(jié)構(gòu)中生成 HTML 的時(shí)候,因?yàn)槟阒肋@些元素都不會(huì)再改變,所以可以該將這些值都緩存下來。如果你的對(duì)象是可變的,你可能就需要每次都從頭開始生成 HTML,或者是為每一個(gè)對(duì)象做構(gòu)建優(yōu)化和觀察操作。如果是不可變的話,你就不必?fù)?dān)心緩存會(huì)失效了。當(dāng)然,這可能會(huì)帶來性能的下降,但是在絕大多數(shù)情況下,簡單帶來的好處相比于那一點(diǎn)輕微的性能下降是值得的。

其他語言的不可變性

不可變對(duì)象是從像 Haskell 這樣的函數(shù)式編程語言中借鑒過來的概念。在 Haskell 中,值默認(rèn)都是不可變的。Haskell 程序一般都有一個(gè)單純函數(shù)式 (purely functional) 作為核心,在其中沒有可變對(duì)象,沒有狀態(tài),也沒有像 I/O 這樣的副作用。

在 Objective-C 程序中我們可以借鑒這些。在任何可能的地方使用不可變的對(duì)象,我們的程序會(huì)變得容易測(cè)試得多。Gary Bernhardt 做了一個(gè)很棒的演講,向我們展示了使用不可變對(duì)象如何幫助我們開發(fā)更好的軟件。在演講中他用的是 Ruby,但是在 Objective-C 中,概念其實(shí)是相通的。

擴(kuò)展閱讀