小肤iOS开发代码规范_v1.0

For Objective-C , 2018.8.2

Ⅰ.前言
Ⅱ.命名规范
Ⅲ.代码注释规范
Ⅳ.代码格式化规范
Ⅴ.编码规范
Ⅵ.参考资料

Ⅰ.前言


1.需求是暂时的,只有变化才是永恒的,面向变化编程,而不是面向需求编程。
2.不要过分追求技巧,降低程序的可读性。
3.简洁的代码可以让bug无处藏身。要写出明显没有bug的代码,而不是没有明显bug的代码。
4.先把眼前的问题解决掉,解决好,再考虑将来的扩展问题。



Ⅱ.命名规范


1、统一要求
含义清楚,尽量做到不需要注释也能了解其作用,若做不到,就加注释,使用全称,不使用缩写。
2、类名
大驼峰式命名:每个单词的首字母都采用大写字母。
示例:XFHomePageViewController
3、property变量
小驼峰式命名:第一个单词以小写字母开始,后面的单词的首字母全部大写,
属性的关键字推荐按照 原子性,读写,内存管理 的顺序排列,
BlockNSString属性应该使用copy关键字,
禁止使用synthesize关键词,
可以不暴露在.h文件中的property变量,尽量写在.m文件中,保证.h文件的简洁。
示例:

typedef void (^ErrorCodeBlock) (id errorCode, NSString *message);
@property (nonatomic, readwrite, strong) UIView *headerView;    //注释
@property (nonatomic, readwrite, copy) ErrorCodeBlock errorBlock;   //将block拷贝到堆中
@property (nonatomic, readwrite, copy) NSString *userName;

4、私有变量
私有变量放在.m 文件中声明,以_开头,第一个单词首字母小写,后面的单词的首字母全部大写,
示例:NSString *_somePrivateVariable
ps:一般的,也可以使用property变量代替私有变量,这样可能更便于编码时使用点语法 / 懒加载等;
5.一般变量和对象的命名
给一个对象命名时建议采用 修饰 + 类型 的方式,如果只用修饰命名会引起歧义,比如title (这个到底是个NSString还是UILabel?),同样的,如果只用类型来命名则会缺失作用信息,比如label (ok,我知道你是个UILabel,但是用途呢?),So,正确的命名方式为:
titleLabel //表示标题的label, 是UILabel类型
confirmButton //表示确认的button,是UIButton类型
对于BOOL类型,应加上is前缀,比如:
- (BOOL)isEqualToString:(NSString *)aString 这样会更加清晰,
如果某方法返回非属性的BOOL值,那么应根据其功能, 选用hasis当前缀,比如:- (BOOL)hasPrefix:(NSString *)aString
ps:如果某个命名已经很明确了, 为了简洁可以省去类型名. 比如scores, 很明显是个array了, 就不必命名成scoreArray了,
6、宏和常量命名
1.对于宏定义的常量
#define预处理定义的常量全部大写,单词间用_分隔
重要:宏定义中如果包含表达式或变量,表达式或变量必须用小括号括起来
2.对于类型常量
对于局限于某编译单元(实现文件)的常量,以字符k开头,
例如kAnimationDuration,且需要以static const修饰,防止被修改,
对于定义于类头文件的常量,外部可见,则以定义该常量所在类的类名开头,例如XFViewClassAnimationDuration, 仿照苹果风格,在头文件中进行extern声明,在实现文件中定义其值,示例:

//宏定义的常量(个别的,如果宏定义个数较多,可以考虑手动左对齐整理)
#define ANIMATION_DURATION   0.3
#define MY_MIN(A, B)         ((A)>(B)?(B):(A))

//局部类型常量
static const NSTimeInterval kAnimationDuration = 0.3;

//外部可见类型常量
//XFViewClass.h
extern const NSTimeInterval XFViewClassAnimationDuration;
extern NSString *const      XFViewClassStringConstant;  //字符串类型

//XFViewClass.m
const NSTimeInterval XFViewClassAnimationDuration = 0.3;
NSString *const      XFViewClassStringConstant = @"XFStringConstant";

7、枚举的命名 Enum
Enum类型的命名与类的命名规则一致,
Enum中枚举内容的命名需要以该Enum类型名称开头,
NS_ENUM定义通用枚举,NS_OPTIONS定义位移枚举,示例:

//推荐,通用枚举
typedef NS_ENUM(NSInteger, UIViewAnimationTransition) {
    UIViewAnimationTransitionNone,
    UIViewAnimationTransitionFlipFromLeft,
    UIViewAnimationTransitionFlipFromRight,
    UIViewAnimationTransitionCurlUp,
    UIViewAnimationTransitionCurlDown,
};

//推荐,位移枚举
typedef NS_OPTIONS(NSUInteger, UIControlState) {
    UIControlStateNormal       = 0,
    UIControlStateHighlighted  = 1 << 0,
    UIControlStateDisabled     = 1 << 1,
};

//不推荐用 C 的方式来定义枚举类型
typedef enum : {
    CameraModeFront,
    CameraModeLeft,
    CameraModeRight,
} CameraMode;

8、Delegate
delegate做后缀,如<UIScrollViewDelegate>
optional修饰可以不实现的方法,用required修饰必须实现的方法
当你的委托的方法过多, 可以拆分 数据部分其他逻辑 部分, 数据部分用dataSource做后缀. 如<UITableViewDataSource>
使用didwill通知Delegate已经发生的变化或将要发生的变化。
重要:类的实例必须为回调方法的参数之一
回调方法的参数只有类自己的情况,方法名要符合实际含义
回调方法存在两个以上参数的情况,以类的名字开头,以表明此方法是属于哪个类的,示例:

@protocol UITableViewDataSource<NSObject>
@required
//回调方法存在两个以上参数
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;

@optional
//回调方法的参数只有类自己
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView;  // Default is 1 if not implemented
@protocol UITableViewDelegate<NSObject, UIScrollViewDelegate>
@optional
//使用`did`和`will`通知`Delegate`
- (nullable NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath;
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;

9、方法
1.方法名用小驼峰式命名,
2.方法名不要使用new作为前缀,
3.不要使用and来连接属性参数,如果方法描述两种独立的行为,使用and来串接它们,
4.方法实现时,如果参数过长,则令每个参数占用一行,以冒号对齐,
5.一般方法不使用前缀命名,私有方法可以使用统一的前缀来分组和辨识,
6.方法名要与对应的参数名保持高度一致,
7.表示对象行为的方法、执行性的方法应该以动词开头,
8.返回性的方法应该以返回的内容开头,但之前不要加get,除非是间接返回一个或多个值,
9.可以使用情态动词(动词前面canshouldwill等)进一步说明属性意思,但不要使用dodoes,因为这些助动词没什么实际意义,也不要在动词前使用副词或形容词修饰,
10.(+开头的)工厂方法 / 类方法,首字母要大写,
11.创建Category分类方法时,除了满足上述要求外,还应注意考虑尽量避免可能在未来发生的方法名冲突的问题,示例:

//不要使用 and 来连接属性参数
- (int)runModalForDirectory:(NSString *)path file:(NSString *)name types:(NSArray *)fileTypes;    //推荐
- (int)runModalForDirectory:(NSString *)path andFile:(NSString *)name andTypes:(NSArray *)fileTypes;    //反对

//表示对象行为的方法、执行性的方法
- (void)insertModel:(id)model atIndex:(NSUInteger)atIndex;
- (void)selectTabViewItem:(NSTableViewItem *)tableViewItem

//返回性的方法
- (instancetype)arrayWithArray:(NSArray *)array;

//参数过长的情况
- (void)longMethodWith:(NSString *)theFoo
                  rect:(CGRect)theRect
              interval:(CGFloat)theInterval
{
   //Implementation
}

//不要加get
- (NSSize)cellSize;  //推荐
- (NSSize)getCellSize;  //反对

//使用情态动词,不要使用do或does
- (BOOL)canHide;  //推荐
- (BOOL)shouldCloseDocument;  //推荐
- (BOOL)doesAcceptGlyphInfo;  //反对

//工厂方法 / 类方法 首字母大写
+ (XFControlManager *)SharedControlManager;

/**
分类方法(Category),比如创建UIImage的分类方法,两个 
1.创建一张纯色图片
2.获取图片中特定点的颜色值
*/
+ (UIImage *)XF_ImageWithColor:(UIColor *)color size:(CGSize)size;
- (UIColor *)xf_ColorAtPixelPoint:(CGPoint)point Alpha:(CGFloat)alpha;



Ⅲ.代码注释规范


优秀的代码大部分是可以自描述的,我们完全可以用代码本身来表达它到底在干什么,而不需要注释的辅助。
但并不是说一定不能写注释,有以下三种情况比较适合写注释:
公共接口(注释要告诉阅读代码的人,当前类能实现什么功能)。
涉及到比较深层专业知识的代码(注释要体现出实现原理和思想)。
容易产生歧义的代码(但是严格来说,容易让人产生歧义的代码是不允许存在的)。
除了上述这三种情况,如果别人只能依靠注释才能读懂你的代码的时候,就要反思代码出现了什么问题。
最后,对于注释的内容,相对于“做了什么”,更应该说明“为什么这么做”。

1、import注释
如果有一个以上的import语句,就对这些语句进行分组,每个分组的注释是可选的。示例:

// Frameworks
#import <QuartzCore>;

// Models
#import "XFUser.h"

// Views
#import "XFButton.h"
#import "XFUserView.h"

2、属性注释
写在属性之后,用两个空格隔开,示例:

@property (nonatomic, readwrite, strong) UIView *headerView;  //注释   

3、方法声明注释
一个函数(方法)必须有一个字符串文档来解释,除非它:
1.非公开,私有函数;2.很短;3.显而易见;
而其余的,包括公开接口,重要的方法,分类,以及协议,都应该伴随文档(注释):
以/开始
第二行是总结性的语句
第三行永远是空行
在与第二行开头对齐的位置写剩下的注释,建议这样写:

/This comment serves to demonstrate the format of a doc string.

Note that the summary line is always at most one line long, and after the opening block comment,
and each line of text is preceded by a single space.
*/

方法的注释使用Xcode自带注释快捷键:Commond+option+/示例:

/**
 <#Description#>
//也可以手动删去空行以减少代码行数...
 @param tableView <#tableView description#>
 @param section <#section description#>
 @return <#return value description#>
 */
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
    //...
}

4、代码块注释
单行的用// + 空格 开头,多行的采用/* */注释
5、TODO
使用//TODO:说明,标记一些未完成的或完成的不尽如人意的地方,示例:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    //TODO:增加初始化
    return YES;
}



Ⅳ.代码格式化规范


1、指针*位置
定义一个对象时,指针*靠近变量,示例:
NSString *userName;
2、方法的声明和定义
-+返回值之间留一个空格,方法名和第一个参数之间不留空格,示例:
- (void)insertSubview:(UIView *)view atIndex:(NSInteger)index;
3、代码缩进
不要在工程里使用 Tab 键,使用空格来进行缩进。在 Xcode > Preferences > Text Editing 将 Tab 和自动缩进都设置为 4 个空格
MethodMethod之间空一行
一元运算符与变量之间没有空格、二元运算符与变量之间必须有空格,示例:

!bValue
fLength = fWidth * 2;

- (void)sampleMethod1;

- (void)sampleMethod2;

4、大括号写法
对于类的method:左括号另起一行写(遵循苹果官方文档),
对于其他使用场景(if,for,while,switch等): 左括号跟在第一行后边,示例:

- (void)sampleMethod
{
    BOOL someCondition = YES;
    if (someCondition) {
        // do something here
    }
}

5、对method进行分组(推荐,但不做要求)
使用#pragma mark -对method进行分组,视代码量以及具体情形而定,唯一目的都是增加代码的可读性,示例:

#pragma mark - Life Cycle Methods
- (instancetype)init
- (void)dealloc

- (void)viewWillAppear:(BOOL)animated
- (void)viewDidAppear:(BOOL)animated
- (void)viewWillDisappear:(BOOL)animated
- (void)viewDidDisappear:(BOOL)animated

#pragma mark - Override Methods

#pragma mark - Intial Methods

#pragma mark - Network Methods

#pragma mark - Target Methods

#pragma mark - Public Methods

#pragma mark - Private Methods

#pragma mark - UITableViewDataSource  
#pragma mark - UITableViewDelegate  

#pragma mark - Lazy Loads

#pragma mark - NSCopying  

#pragma mark - NSObject  Methods



Ⅴ.编码规范


编码规范简单来说就是为了保证写出来的代码具备三个原则:可复用易维护可扩展,这其实也是面向对象的基本原则; 可复用,简单来说就是不要写重复的代码,有重复的部分要尽量封装起来重用,否则修改文件的时候得满地找相同逻辑的地方;易维护,就是不要把代码复杂化,不要去写巨复杂逻辑的代码,而是把复杂的逻辑代码拆分开一个个小的模块,这也是Do one thing的概念,每个模块(或者函数)职责要单一,这样的代码会易于维护,也不容易出错;可扩展,则是要求写代码时要考虑后面的扩展需求,这个涉及到架构层面了,利用对应的设计模式来保证,不再此处赘述;

1.赋值

//Preferred
result = object ? : [self createObject];   //条件赋值
BOOL isAdult = age > 18;   //BOOL赋值

//Not preferred
result = object ? object : [self createObject];

BOOL isAdult;
if (age > 18)
{
    isAdult = YES;
}
else
{
    isAdult = NO;
}

2.if条件判断

1.一般的,须列出所有分支(穷举所有的情况),而且每个分支都须给出明确的结果

//Preferred
NSString *hintStr;
if (count < 3) {
  hintStr = "Good";
} else {
  hintStr = "";
}

//Not preferred
NSString *hintStr;
if (count < 3) {
  hintStr = "Good";
}

2.复杂的,条件过多,过长的时候应该换行。条件表达式如果很长,则需要将他们提取出来赋给一个BOOL值,或者抽取出一个方法

//Preferred
if ([self canDeleteJob:job]) { ... }        
 
- (BOOL)canDeleteJob:(Job *)job
{
    BOOL invalidJobState = job.JobState == JobState.New
                          || job.JobState == JobState.Submitted
                          || job.JobState == JobState.Expired;
    BOOL invalidJob = job.JobTitle && job.JobTitle.length;
 
    return invalidJobState || invalidJob;
}

//Not preferred
if (job.JobState == JobState.New
    || job.JobState == JobState.Submitted
    || job.JobState == JobState.Expired
    || (job.JobTitle && job.JobTitle.length))
{
    //....
}

3.条件语句的判断应该是变量在右,常量在左

//Preferred
if (6 == count) { ... }
if (nil == someObject) { ... }
if (someObject) { ... } 
if (!someObject) { ... } 

//Not preferred
if (count == 6) { ... }
if (someObject == nil) { ... }
if (someObject == YES) { ... } 
if (someObject != nil) { ... } 

4.特别地,嵌套判断,不要使用过多的分支,要善于使用return来提前返回错误的情况,把最正确的情况放到最后返回,

//Preferred
if (!user.UserName) return NO;
if (!user.Password) return NO;
if (!user.Email) return NO;
return YES;

//Not preferred
BOOL isValid = NO;
if (user.UserName)
{
    if (user.Password)
    {
        if (user.Email) isValid = YES;
    }
}
return isValid;

5.每个分支的实现代码都须被大括号包围

//Preferred:
if (!error) {
  return success;
}
//或者
if (!error) return success;

//Not preferred
if (!error)
    return success;

3.for语句

1.不可在for循环内修改循环变量,防止for循环失去控制

for (int index = 0; index < 10; index++){
   ...
   logicToChange(index);
}

2.避免使用continuebreak
continuebreak所描述的是“什么时候不做什么”,所以为了读懂二者所在的代码,我们需要在头脑里将他们取反。
其实最好不要让这两个东西出现,因为我们的代码只要体现出“什么时候做什么”就好了,而且通过适当的方法,是可以将这两个东西消灭掉的;

情形1:如果出现了continue:只需要把continue的条件取反即可,

NSMutableArray *filteredProducts = [NSMutableArray array];
for (NSString *level in products) {
    if ([level hasPrefix:@"bad"]) {
       continue;
    }
    [filteredProducts addObject:level];
}

/**
我们可以看到,通过判断字符串里是否含有“bad”这个prefix来过滤掉一些值,
其实我们是可以通过取反,来避免使用continue的,
*/
NSMutableArray *filteredProducts = [NSMutableArray array];
for (NSString *level in products) {
    if (![level hasPrefix:@"bad"]) {
       [filteredProducts addObject:level];
    }
}

情形2:消除while里的break:将break的条件取反,并合并到主循环里

/**
while (condition1) {
  ...
  if (condition2) {
    break;
  }
}

在while里的break其实就相当于“不存在”,
既然是不存在的东西就完全可以在最开始的条件语句中将其排除,
取反并合并到主条件:
*/
while (condition1 && !condition2) {
  ...
}

情形3:在有返回值的方法里消除break:将break转换为return立即返回

- (BOOL)hasBadProductIn:(NSArray<NSString *> *)products {
    BOOL result = false;
    for (NSString *level in products) {
        if ([level hasPrefix:@"bad"]) {
            result = true;
            break;
        }
    }
    return result;
}

/**
有人喜欢这样做:在有返回值的方法里break之后,再返回某个值,
其实完全可以在break的那一行直接返回,
遇到错误条件直接返回,
*/
- (BOOL)hasBadProductIn:(NSArray<NSString *> *)products {
    for (NSString *level in products) {
        if ([level hasPrefix:@"bad"]) {
            return true;
        }
    }
    return false;
}

4.Switch语句

1.每个分支都必须用大括号括起来

switch (integer) {  
  case 1:  {
    // ...  
    break;   
  } 
  case 2: {  
    // ...  
    break;  
  }  
  default:{
    // ...  
    break; 
  }
}

2.使用枚举类型时,不能有default分支, 除了使用枚举类型以外,都必须有default分支
在Switch语句使用枚举类型的时候,如果使用了default分支,在将来就无法通过编译器来检查新增的枚举类型了

XFLeftMenuTopItemType menuType = XFLeftMenuTopItemMain;  
switch (menuType) {  
  case XFLeftMenuTopItemMain: {
    // ...  
    break; 
   }
  case XFLeftMenuTopItemShows: {
    // ...  
    break; 
  }
  case XFLeftMenuTopItemSchedule: {
    // ...  
    break; 
  }
}

5.函数

1.一个函数只做一件事(单一原则)
每个函数的职责都应该划分的很明确(就像类一样)

//Preferred
[self dataConfiguration];
[self viewConfiguration];

//Not preferred
- (void)dataConfiguration
{   
   ...
   [self viewConfiguration];
}

2.对于有返回值的函数(方法),每一个分支都必须有返回值

//Preferred
- (int)function
{
    if (condition1) {
        return count1;
    } else if (condition2) {
        return count2;
    } else {
       return defaultCount;
    } 
}

//Not preferred
- (int)function
{
    if (condition1) {
        return count1;
    } else if (condition2) {
        return count2;
    }
}

3.对输入参数的正确性和有效性进行检查,参数错误立即返回

- (void)functionParam1:(id)param1 Param2:(id)param2
{
      if (param1 is unavailable){
           return;
      }

      if (param2 is unavailable){
           return;
      }

     //Do some right thing
}

4.如果在不同的函数内部有相同的功能,应该把相同的功能抽取出来单独作为另一个函数

//Preferred:将a,b函数抽取出来作为单独的函数
- (void)basicConfig {
  [self a];
  [self b];
}

- (void)logic1 {
  [self basicConfig];
  [self c];
}

- (void)logic2 {
  [self basicConfig];
  [self d];
}

//Not preferred
- (void)logic {
  [self a];
  [self b];
  if ([logic1 condition]) {
    [self c];
  } else {
    [self d];
  }
}

5.将函数内部比较复杂的逻辑提取出来作为单独的函数
一个函数内的不清晰(逻辑判断比较多,行数较多)的那片代码,往往可以被提取出去,构成一个新的函数,然后在原来的地方调用它这样你就可以使用有意义的函数名来代替注释,增加程序的可读性,举一个发送邮件的例子:

//Preferred
- (void)sendEmail
{
    [self openEmailSite];
    [ self login];

   [self writeEmailTitle:title Content:content 
                Receiver:receiver Attachment:attachment];

    [self send];
}

- (void)writeEmailTitle:(id)title Content:(ID)content 
               Receiver:(id)receiver Attachment:(id)attachment
{
    [self writeTitle:title];
    [self writeContent:content];
    [self writeReceiver:receiver];
    [self addAttachment:attachment];
}

//Not preferred
- (void)sendEmail
{
    [self openEmailSite];
    [ self login];

    [self writeTitle:title];
    [self writeContent:content];
    [self writeReceiver:receiver];
    [self addAttachment:attachment];

    [self send];
}



Ⅵ.参考资料


iOS 代码规范
iOS开发总结之代码规范
iOS开发代码规范(通用)
Objective-C开发编码规范
【iOS】命名规范
Ios Code Specification
Apple Coding Guidelines for Cocoa
Google Objective-C Style Guide
iOS团队编程规范

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容