iOS開發---圖解KVC

什么是KVC?

KVC(Key-value coding)鍵值編碼,單看這個名字可能不太好理解。其實是指iOS的開發中,可以允許開發者通過Key名直接訪問對象的屬性,或者給對象的屬性賦值。這樣就可以在運行時動態地訪問和修改對象的屬性。而不是在編譯時確定,很多高級的iOS開發技巧都是基于KVC實現的。目前網上關于KVC的文章在非常多,有的只是簡單地說了下用法,我會運用圖解的方式寫下這遍文章就是為了讓大家更好的理解。

KVC方法全覽

KVC提供了一種間接訪問其屬性方法或成員變量的機制,可以通過字符串來訪問對應的屬性方法或成員變量。

KVC方法全覽

KVC基礎操作

KVC取值

取值方法
  1. 通過key
- (nullable id)valueForKey:(NSString *)key;                          //直接通過Key來取值
  1. 通過keyPath
- (nullable id)valueForKeyPath:(NSString *)keyPath;                  //通過KeyPath來取值
基于getter取值底層實現

當調用valueForKey的代碼時,其搜索方式如下:

你需要先看一下這張流程圖,大致知道如何運轉的,之后再看文字描述,仔細了解其機制

基于getter取值
  1. 通過getter方法搜索實例,按照get<Key>, <key>, is<Key>, _<key>的順序查找getter`方法。如果發現符合的方法,就調用對應的方法并拿著結果跳轉到第五步。否則,就繼續到下一步。

  2. 如果沒有找到簡單的getter方法,則搜索其匹配模式的方法countOf<Key>objectIn<Key>AtIndex:<key>AtIndexes:

    如果找到其中的第一個和其他兩個中的一個,則就會返回一個可以響應NSArray所有方法的代理集合(它是NSKeyValueArray,是NSArray的子類)。或者說給這個代理集合發送屬于NSArray的方法,就會以countOf<Key>,objectIn<Key>AtIndex<Key>AtIndexes這幾個方法組合的形式調用。否則,繼續到第三步。

    代理對象隨后將NSArray接收到的countOf<Key>objectIn<Key>AtIndex:<key>AtIndexes:的消息給符合KVC規則的調用方。

    當代理對象和KVC調用方通過上面方法一起工作時,就會允許其行為類似于NSArray一樣。

  3. 如果沒有找到NSArray簡單存取方法,或者NSArray存取方法組。那么會同時查找countOf<Key>enumeratorOf<Key>memberOf<Key>:命名的方法。

    如果找到三個方法,則創建一個集合代理對象,該對象響應所有NSSet方法并返回。否則,繼續執行第四步。

    給這個代理對象發NSSet的消息,就會以countOf<Key>enumeratorOf<Key>,memberOf<Key>組合的形式調用。

  4. 如果沒有發現簡單getter方法,或集合存取方法組,以及接收類方法accessInstanceVariablesDirectly是返回YES的。搜索一個名為_<key>_is<Key><key>is<Key>的實例,根據他們的順序。

    如果發現對應的實例,則立刻獲得實例可用的值并跳轉到第五步,如果重寫了類方法+ (BOOL)accessInstanceVariablesDirectly返回NO的話,那么會直接調用valueForUndefinedKey:

  5. 如果取回的是一個對象指針,則直接返回這個結果。
    如果取回的是一個基礎數據類型,但是這個基礎數據類型是被NSNumber支持的,則存儲為NSNumber并返回。
    如果取回的是一個不支持NSNumber的基礎數據類型,則通過NSValue進行存儲并返回。

  6. 如果所有情況都失敗,則調用valueForUndefinedKey:方法并拋出異常,這是默認行為。但是子類可以重寫此方法。

KVC設值

賦值方法
  1. 通過key
  • 直接將屬性名當做key,并設置value,即可對屬性進行賦值。

    - (void)setValue:(nullable id)value forKey:(NSString *)key;          //通過Key來設值
    
  1. 通過keyPath
  • 除了對當前對象的屬性進行賦值外,還可以對其更“深層”的對象進行賦值。KVC進行多級訪問時,直接類似于屬性調用一樣用點語法進行訪問即可。例如Person屬性中有name屬性,我就可以通過Person.name進行賦值

    - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  //通過KeyPath來設值
    
基于setter賦值底層實現

這是setValue:forKey:的默認實現,給定輸入參數valuekey。試圖在接收調用對象的內部,設置屬性名為keyvalue,通過下面的步驟

你需要先看一下這張流程圖,大致知道如何運轉的,之后再看文字描述,仔細了解其機制

基于setter搜素
  1. 查找set<Key>:_set<Key>命名的setter,按照這個順序,如果找到的話,代碼通過setter方法完成設置。
  2. 如果沒有找到setter方法,KVC機制會檢查+ (BOOL)accessInstanceVariablesDirectly的返回值,如果accessInstanceVariablesDirectly類屬性返回YES,則查找一個命名規則為_<key>_is<Key><key>is<Key>的實例變量。根據這個順序,如果發現則將value賦值給實例變量,如果返回值為NO,KVC會執行setValue:forUndefinedKey:方法。
  3. 如果沒有發現setter或實例變量,則調用setValue:forUndefinedKey:方法,并默認提出一個異常,但是一個NSObject的子類可以提出合適的行為。

KVC批量操作

  • 在對象調用setValuesForKeysWithDictionary:方法時,可以傳入一個包含keyvalue的字典進去,KVC可以將所有數據按照屬性名和字典的key進行匹配,并將valueUser對象的屬性賦值。

    //創建一個model模型,里面的字符串名稱必須和key的名稱對應,不然該方法會崩潰
    @interface PersonModel : NSObject
    @property (nonatomic, copy) NSString *key1;
    @property (nonatomic, copy) NSString *key2;
    @property (nonatomic, copy) NSString *id;
    @property (nonatomic, copy) NSString *key3;
    @property (nonatomic, copy) NSString *other;
    @end
      
    PersonModel *person = [[PersonModel alloc] init];
    //1.這是直接賦值,數據量小會很簡單,但是數據量一多就很麻煩,就像我們進行網絡請求時
    person.key1 = dictionary[@"key1"];
    person.key2 = dictionary[@"key2"];
    person.key3 = dictionary[@"key3"];
    
    //2.通過下面該方法可以批量賦值
    //2.1如果model里面的string不存在于dictionary中,輸出結果為null;
    [person setValuesForKeysWithDictionary:dictionary];
    NSLog(@"\n%@\n%@\n%@\n%@\n", person.key1,person.key2,person.key3,person.other);
    
    //輸出結果
    test1
    test2
    test3
    (null)
    
    //2.2如果dictionary中有的元素,moedl中沒有運行會直接出錯,那么我們應該怎么解決?
    //我們需要實現setValue:forUndefinedKey:這個方法能過濾掉不存在的鍵值
    -(void)setValue:(id)value forUndefinedKey:(NSString *)key{
      //這里我們不需要寫任何內容
    }
    person.key1 = dictionary[@"key1"];
    person.key2 = dictionary[@"key2"];
    person.key3 = dictionary[@"key3"];
    [person setValuesForKeysWithDictionary:dictionary];
    NSLog(@"\n%@\n%@\n%@\n", person.key1,person.key2,person.key3);
    
    //輸出結果
    test1
    test2
    test3
    
    //2.3如果dictionar中的key與model中的變量名字不同,怎么賦值?
    //還是從setValue:forUndefinedKey:這個方法入手
    -(void)setValue:(id)value forUndefinedKey:(NSString *)key{
      if ([key isEqualToString:@"key2"]) {
        self.id = value;
    }
    person.key1 = dictionary[@"key1"];
    person.id = dictionary[@"key2"];
    person.key3 = dictionary[@"key3"];
    [person setValuesForKeysWithDictionary:dictionary];
    NSLog(@"\n%@\n%@\n%@\n", person.key1,person.id,person.key3);
      
    //輸出結果
    test1
    test2
    test3
    

KVC集合屬性操作

KVC提供的valueForKeyPath:方法非常強大,可以通過該方法對集合對象進行“深入”操作,在其keyPath中嵌套集合運算符,例如求一個數組中對象某個屬性的count。(集合對象主要指NSArrayNSSet,但不包括NSDictionary)

集合運算符格式

上面表達式主要分為三部分,left部分是要操作的集合對象,如果調用KVC的對象本來就是集合對象,則left可以為空。中間部分是表達式,表達式一般以@符號開頭。后面是進行運算的屬性。

  • 為了驗證操作符,我們需要先建立一個Model類
@interface Transaction : NSObject
@property (nonatomic, strong) NSString *payee;
@property (nonatomic, strong) NSNumber *amount;
@property (nonatomic, strong) NSDate *date;
@end

@interface BankAccount : NSObject
@property (nonatomic, strong) NSArray *transcationArray;
@end

集合操作符

處理集合包含的對象,并根據操作符的不同返回不同的類型,返回值以NSNumber為主。

//@avg用來計算集合中right keyPath指定的屬性的平均值
NSNumber *transactionAverage = [bankAccount.transcationArray valueForKeyPath:@"@avg.amount"];
NSLog(@"@avg = %@", transactionAverage);

//@count用來計算集合的總數
NSNumber *numberOfTransactions = [bankAccount.transcationArray valueForKeyPath:@"@count"];
NSLog(@"@count = %@", numberOfTransactions);
//備注:@count操作符比較特殊,它不需要寫right keyPath,即使寫了也會被忽略。

//@sum用來計算集合中right keyPath指定的屬性的總和。
NSNumber *amountSum = [bankAccount.transcationArray valueForKeyPath:@"@sum.amount"];
NSLog(@"@sum = %@", amountSum);

//@max用來查找集合中right keyPath指定的屬性的最大值
NSNumber *amountMax = [bankAccount.transcationArray valueForKeyPath:@"@max.amount"];
NSLog(@"@max = %@", amountMax);

//@min用來查找集合中right keyPath指定的屬性的最小值。
NSNumber *amountMin = [bankAccount.transcationArray valueForKeyPath:@"@min.amount"];
NSLog(@"@min = %@", amountMin);

數組操作符

根據操作符的條件,將符合條件的對象包含在數組中返回。

//@unionOfObjects將集合對象中,所有payee對象放在一個數組中并返回
NSArray *payees = [bankAccount.transcationArray valueForKeyPath:@"@unionOfObjects.payee"];
NSLog(@"@unionOfObjects = %@", payees);

//@distinctUnionOfObjects將集合對象中,所有payee對象放在一個數組中,并將數組進行去重后返回。
NSArray *distinctPayees = [bankAccount.transcationArray valueForKeyPath:@"@distinctUnionOfObjects.payee"];
NSLog(@"@distinctUnionOfObjects = %@", distinctPayees);
//注意:以上兩個方法中,如果操作的屬性為nil,在添加到數組中時會導致Crash。

嵌套操作符

處理集合對象中嵌套其他集合對象的情況,返回結果也是一個集合對象。

//@distinctUnionOfArrays是用來操作集合內部的集合對象,將所有right keyPath對應的對象放在一個數組中,并進行排重。
NSArray *collectedPayees = [allArray valueForKeyPath:@"@unionOfArrays.payee"];
NSLog(@"@unionOfArrays = %@", collectedPayees);

//@distinctUnionOfSets是用來操作集合內部的集合對象,將所有right keyPath對應的對象放在一個set中,并進行排重。
NSArray *collectedDistinctPayees = [allArray valueForKeyPath:@"@distinctUnionOfArrays.payee"];
NSLog(@"@distinctUnionOfArrays = %@", collectedDistinctPayees);

KVC與容器類

對象的屬性可以是一對一的,也可以是一對多的。一對多的屬性要么是有序的(數組),要么是無序的(集合)。

??:根據KVO的實現原理,是在運行時生成新的子類并重寫其setter方法,在其內容發生改變時發送消息。但這只是對屬性直接進行賦值會觸發,如果屬性是容器對象,對容器對象進行addremove操作,則不會調用KVO的方法。可以通過KVC對應的API來配合使用,使容器對象內部發生改變時也能觸發KVO

在進行容器對象操作時,先通過key或者keyPath獲取集合對象,然后再對容器對象進行addremove等操作時,就會觸發KVO的消息通知了。

KVC與有序容器(NSMutableArray)

取值方法
  1. 通過key
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
//該方法返回一個可變有序數組
  1. 通過keyPath
- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
//該方法返回一個可變有序數組
NSMutableArray取值底層實現

當調用mutableArrayValueForKey的代碼時,其搜索方式如下:

你需要先看一下這張流程圖,大致知道如何運轉的,之后再看文字描述,仔細了解其機制

NSMutableArray取值底層實現
  1. 搜索insertObject:in<Key>AtIndex: , removeObjectFrom<Key>AtIndex: 或者 insert<Key>AdIndexes , remove<Key>AtIndexes 格式的方法
    如果至少找到一個insert方法和一個remove方法,那么同樣返回一個可以響應NSMutableArray所有方法代理集合(類名是NSKeyValueFastMutableArray),那么給這個代理集合發送NSMutableArray的方法,以insertObject:in<Key>AtIndex: , removeObjectFrom<Key>AtIndex: 或者 insert<Key>AdIndexes , remove<Key>AtIndexes組合的形式調用。

    當對象接收一個mutableArrayValueForKey:消息并實現可選替換方法,例如replaceObjectIn<Key>AtIndex:withObject:replace<Key>AtIndexes:with<Key>:方法,代理對象會在適當的情況下使用它們,以獲得最佳性能。

  2. 如果上步的方法沒有找到,則搜索set<Key>: 格式的方法,如果找到,那么發送給代理集合的NSMutableArray最終都會調用set<Key>:方法。

    也就是說,mutableArrayValueForKey:取出的代理集合修改后,用set<Key>: 重新賦值回去去。這樣做效率會低很多。所以推薦實現上面的方法。

  3. 如果上一步的方法還還沒有找到,再檢查類方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默認行為),會按_<key>,<key>,的順序搜索成員變量名,如果找到,那么發送的NSMutableArray消息方法直接交給這個成員變量處理。

  4. 如果還是找不到,則調用valueForUndefinedKey:

KVC與無序容器(NSMutableSet)

取值方法
  1. 通過key
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
//方法返回一個可變的無序數組
  1. 通過keyPath
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;
//方法返回一個可變的無序數組
NSMutableSet取值底層實現

當調用NSMutableSet的代碼時,其搜索方式如下:

你需要先看一下這張流程圖,大致知道如何運轉的,之后再看文字描述,仔細了解其機制

NSMutableSet取值底層實現
  1. 搜索addObject<Key>Object: , remove<Key>Object: 或者 add<Key> , remove<Key> 格式的方法
    如果至少找到一個insert方法和一個remove方法,那么同樣返回一個可以響應NSMutableSet所有方法代理集合(類名是NSKeyValueFastMutableSet2),那么給這個代理集合發送NSMutableSet的方法,以addObject<Key>Object: , remove<Key>Object: 或者 add<Key> , remove<Key>組合的形式調用。還有兩個可選實現的接口:intersect<Key> , set<Key>:
  2. 如果receiverManagedObject,那么就不會繼續搜索。
  3. 如果上一步的方法沒有找到,則搜索set<Key>: 格式的方法,如果找到,那么發送給代理集合的NSMutableSet最終都會調用set<Key>:方法。 也就是說,mutableSetValueForKey取出的代理集合修改后,用set<Key>: 重新賦值回去去。這樣做效率會低很多。所以推薦實現上面的方法。
  4. 如果上一步的方法還沒有找到,再檢查類方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默認行為),會按_<key>,<key>的順序搜索成員變量名,如果找到,那么發送的NSMutableSet消息方法直接交給這個成員變量處理。
  5. 如果還是找不到,調用valueForUndefinedKey:
    可見,除了檢查receiverManagedObject以外,其搜索順序和mutableArrayValueForKey基本一至

KVC異常處理

  1. key或者keyPath發生錯誤

當根據KVC搜索規則,沒有搜索到對應的key或者keyPath,則會調用對應的異常方法。異常方法的默認實現,在異常發生時會拋出一個NSUndefinedKeyException的異常,并且應用程序Crash

我們可以重寫下面兩個方法:

- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
  1. 傳參為nil

通常情況下,KVC不允許你要在調用setValue:屬性值 forKey:(或者keyPath)時對非對象傳遞一個nil的值。因為值類型是不能為nil的。如果你不小心傳了,KVC會調用setNilValueForKey:方法。這個方法默認是拋出異常,所以一般而言最好還是重寫這個方法。

我們可以重寫這個方法:

-(void)setNilValueForKey:(NSString *)key{
    NSLog(@"不能將%@設成nil",key);
}

KVC處理非對象

KVC是支持基礎數據類型和結構體的,可以在settergetter的時候,通過NSValueNSNumber來轉換為OC對象。該方法valueForKey:總是返回一個id對象,如果原本的變量類型是值類型或者結構體,返回值會封裝成NSNumber或者NSValue對象。這兩個類會處理從數字,布爾值到指針和結構體任何類型。然后開發者需要手動轉換成原來的類型。盡管valueForKey:會自動將值類型封裝成對象,但是setValue:forKey:卻不行。你必須手動將值類型轉換成NSNumber或者NSValue類型,才能傳遞過去。

  • 可以調用initWithBool:方法對基礎數據類型進行包裝
@property (nonatomic, assign, readonly) BOOL boolValue;
- (NSNumber *)initWithBool:(BOOL)value 

KVC屬性驗證

KVC提供了屬性值,用來驗證key對應的Value是否可用的方法

  • 在調用KVC時可以先進行驗證,驗證通過下面兩個方法進行,支持keykeyPath兩種方式。驗證方法默認實現返回YES,可以通過重寫對應的方法修改驗證邏輯。

    驗證方法需要我們手動調用,并不會在進行KVC的過程中自動調用。

- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKeyPath:(NSString *)inKeyPath error:(out NSError **)outError;

這個方法的默認實現是去探索類里面是否有一個這樣的方法:-(BOOL)validate<Key>:error:如果有這個方法,就調用這個方法來返回,沒有的話就直接返回YES

@implementation Address
-(BOOL)validateCountry:(id *)value error:(out NSError * _Nullable __autoreleasing *)outError{  //在implementation里面加這個方法,它會驗證是否設了非法的value
    NSString* country = *value;
    country = country.capitalizedString;
    if ([country isEqualToString:@"Japan"]) {
        return NO;                                                                             //如果國家是日本,就返回NO,這里省略了錯誤提示,
    }
    return YES;
}
@end
NSError* error;
id value = @"japan";
NSString* key = @"country";
BOOL result = [add validateValue:&value forKey:key error:&error]; //如果沒有重寫-(BOOL)-validate<Key>:error:,默認返回Yes
if (result) {
    NSLog(@"鍵值匹配");
    [add setValue:value forKey:key];
}
else{
    NSLog(@"鍵值不匹配"); //不能設為日本,其他國家都行
}
NSString* country = [add valueForKey:@"country"];
NSLog(@"country:%@",country);
//打印結果 
KVCDemo[867:58871] 鍵值不匹配
KVCDemo[867:58871] country:China

KVC適用場景

動態的取值和設值

利用KVC動態的取值和設值是最基本的用途了。相信每一個iOS開發者都能熟練掌握

Model和字典轉換

在上面KVC批量操作已闡述

用KVC來訪問和修改私有變量

根據上面的實現原理我們知道,KVC本質上是操作方法列表以及在內存中查找實例變量。我們可以利用這個特性訪問類的私有變量,例如下面在.m中定義的私有成員變量和屬性,都可以通過KVC的方式訪問。

這個操作對readonly的屬性,@protected的成員變量,都可以正常訪問。如果不想讓外界訪問類的成員變量,則可以將accessInstanceVariablesDirectly屬性賦值為NO

修改一些控件的內部屬性

這也是iOS開發中必不可少的小技巧。眾所周知很多UI控件都由很多內部UI控件組合而成的,但是Apple度沒有提供這訪問這些控件的API,這樣我們就無法正常地訪問和修改這些控件的樣式。而KVC在大多數情況可下可以解決這個問題。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,563評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,694評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,672評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,965評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,690評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,019評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,013評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,188評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,718評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,438評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,667評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,149評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,845評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,252評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,590評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,384評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380