代码可读性


这是一篇结合项目代码与《编写可读艺术的代码》一书结合的读书笔记

代码应当易于理解

《编写可读艺术的代码》这本书告诉我们代码应该写的容易理解,我更喜欢作者的另一个说法是使别人用最短的时间理解你的代码

不知道大家有没有想过,什么样的代码是好的,当我们忙于业务开发的时候可以停下来思考一下这个问题?当我看到这个问题的时候,脑海中的第一个想法就是肯定是将代码写的越少越好

Part 1 表面层次的改进

把信息装到名字里

选择直接的名字,写简单明了的注释,把代码写的整洁,格式更好,说白了就是让他人仅通过类名,方法名以及变量名,就可以知道他们的作用,或者说大概能了解它们的作用,这要求我们起的名字的含义要足够清晰明确,不能太过于含糊。

一、使用专业,明确,具体的名字,避免使用模糊,抽象,具有二义性的名字

通常我们会定义 getXXX() 方法名,表示表获取数据,get 这个名字并没有表达出更多的信息,你并不知道是从网络上获取或者是数据库上,又或者是从缓存中获取数据,可以考虑用 fetchdownloadfindsearch 等含义更明确,更形象的单词替代。

1、getXxx() --> findXxx()

我个人对于 get 的理解是得到一个数据,这个数据应该是一个类的简单属性,不用经过复杂的计算,例如 JavaBean 的 get 操作。

Picasso.with(this).load(info.getIvArrearsReasonImageRes()).into(imgArrear);

//通过该方法获取一个图片资源,然后交由 Picasso 帮我们加载
info.getIvArrearsReasonImageRes();

初看这个方法我会以为这个图片资源是 info 对象的一个属性,以下是这个方法的全部内容:

public int getIvArrearsReasonImageRes() {
    if (type == 1) {
        return R.drawable.debt_1;
    } else if (type == 2) {
        return R.drawable.debt_2;
    } else if (type == 3) {
        return R.drawable.debt_3;
    } else if (type == 5) {
        return R.drawable.debt_5;
    } else if (type == 7) {
        return R.drawable.debt_7;
    } else if (type == 9) {
        return R.drawable.debt_9;
    } else if (type == 20) {
        return R.drawable.debt_20;
    } else {
        return R.drawable.debt_other;
    }
}

这个方法会根据 info 对象中的 int 类型的属性 type 值 来判断应该返回哪个图片资源文件,所以我将方法名更改为 findArrearsReasonImageRes(),这样你可能在看到这个方法时候会有一个预期。(内心PS:我只要知道 get 一个图片资源就好了,谁管你内部是怎么返回给我。

再来看看下面一个方法

private int getCurSmsId() {
    if (smsList != null && !ListUtils.isEmpty(smsList.getSmsList())) {
        for (SmsTemplateItem item : smsList.getSmsList()) {
            if (item.isSelected) {
                return item.id;
            }
        }
    }
    return -1;
}

这个方法是用来获取当前选中短信模板的ID,也是很普通的循环查找,我更愿意把它命名为 findSelectedId(),之所以没有加上模板这个单词是因为这个方法本身就是被短信模板业务对象所调用。

2、orders() --> parseOrders() 或者 parseOrderArray()

toDetailPage(assign.getTask_Id(), assign.orders(), isInRefresh ? POSITION_REFRESH : POSITION_MY_TASK);

这个方法接收三个参数,其中第二个参数表示从 assign 对象中获取 Order 类型的数组,以下是该方法的全部内容:

 public List<Order> orders() {
    if (!TextUtils.isEmpty(orders)) {
        return Json.fromJsons(orders, Order.class);
    } else {
        return null;
    }
}

orders 是一串原始的的 Json 字符串,当它不为空的时候,我们会将它解析成对应类型的数组并返回,最初在不知道这个内部逻辑的情况下,我以为 ordresassign 中的属性,并且在 assign 的整个生命周期中,我们会对它内部持有的 Order 数组(我以为 Orders 是数组)进行修改,由于我们只是将 Json 字符串解析成对应的对象类型而已,无论我们对解析后的对象如何操作,都不会对原始 Json 字符串产生任何影响,这个方法给我们造成了不小的麻烦,后来我们将它的名字修改为 parseOrders()/parseOrderArray()

3、distanceBetweenUs() --> computeDistanceBetweenUs()

这个例子也很直白 distanceBetweenUs(),返回我们之间的距离,但是实际上代码的内部是通过地图 SDK 进行耗时的计算得到的。我想将它改为 computeDistanceBetweenUs() 应该会合适一些,至少看到这个名字,你会思考它会不会耗时。

4、start() 和 stop() --> create() 和 destroy()

public interface Presenter {
    void start();
    void stop();
    //...
}

这个接口表示 MVP 模式中的 P层(Presenter),由于 Presenter 需要和我们的生命周期进行绑定,所以我们会在 P 层中提供对应的生命周期方法,然而上述代码中的 start() 和 stop() 两个方法是在 onCreate() 和 onDestroy() 方法中被调用的,这很可能会产生误解,因此我将它们改成了 onCreate()onDestroy()

二、避免空泛的名字

1、temp

private static SpannableString getRMBSpannableString(double rmb, String color, String typefaceSpan) {
    if (rmb <= 0)
        return null;
    String temp = "¥" + Strings.priceRoundFloor(rmb);
    SpannableString spannableString = new SpannableString(temp);
    spannableString.setSpan(new ForegroundColorSpan(Color.parseColor(color)), 0, temp.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
// new TypefaceSpan("monospace") 保证 ¥ 符号显示 2横
    spannableString.setSpan(new TypefaceSpan(typefaceSpan), 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    return spannableString;
}

这个方法是用来获取一个格式化的人民币字符串,在代码中间出现了两个变量 tempspannableString 没有具体的含义,我选择这两个变量名的原因是因为我的懒惰,完全没有考虑到可读性

2、retval

private int getTotalPackageNum() {
    int retval = 0;
    for (CarloadLuggageItem item : packageListItems) {
        retval += item.getPackage_num();
    }
    return retval;
}

retval 意思是返回值 return value,这段代码的中心是求和操作,retval 代表包裹的总数量,我们可以选择命名为 totalPackageNum 可能会体现出更多的信息

3、循环迭代器

for (int i = 0; clubs.length; i++) {
    for (int j = 0; clubs.[i].members.lenght; j++) {
        for (int k = 0; users.length; k++) {
            if (clubs[i].members[k] == users[j]){
                do something...
            }
        }
    }
}

你可能很难发现 clubs[i].members[k] == users[j] 这段代码,我们标错了数组的下标,如果不使用 i j k 作为数组的下标而是结合变量名如 if (clubs[ci].members[mj] == users[uk]) 的方式,会更容易发现问题

4、给变量名加上重要的细节和特殊的格式

  • 超时时间单位: int timeOut -- int timeOutSecs
  • 创建时间单位: long createTime -- long createTimeMills
  • 原始的 Json 字符串: String orders -- String rawJsonOrders
  • 缓存单位: long cacheSize -- cacheMb
  • ...

5、抽象的名字

  • SmartSerialExecutor: 智能的串行线程池,实际并没有很智能,也没有能够知道这个线程池有什么特色
  • BitmapCrop:Bitmap 裁剪,实际上内部代码仅仅是提供一个圆形 Bitmap
  • ...

6、作用域大的变量名字更长

  • RefreshListOrderItemView.java Method: addOrderItems
  • ActivityTaskUnFinished.java Filed: order

三、代码格式

  • 使用一致的布局,让读者很快就习惯这种风格
  • 让相似的代码看上去相似
  • 把相关的代码分组,形成代码块

1、有意义的顺序

我们都使用过 Dialog,它大概由,标题,副标题,内容,确认按钮,取消按钮这几部分组成,我们在自定义 Dialog 的时候可以按照 Dialog 展示的顺序来定义属性:

 public static class CustomDialog {
       private String title;
       private String subTitle;
       private String content;
       private String negativeButton;
       private String positiveButton;
}

如果我们随意的,无序的定义这些属性,在属性很多的情况下使用起来可能会有一些混乱,如果属性定义的顺序和设计稿的顺序序一致,在使用起来就很舒畅

2、代码分段

public void loadPic(ScreenAd ad) {
        final String linkUrl = ad.getLink_url();
        final int countDown = ad.getCount_down();

        String displayUrl = ad.getDisplay_url();
        if (TextUtils.isEmpty(displayUrl)) {
            return;
        }

        int screenAdWidth = ad.getWidth();
        int screenAdHeight = ad.getHeight();
        if (screenAdWidth <= 0 || screenAdHeight <= 0) {
            return;
        }
        
        initAdUI(screenAdWidth, screenAdHeight);
        Picasso.with(getActivity()).load(displayUrl).fit().into(ivA);
    }

通过空格对代码进行分段,这样每一段都有各自的逻辑处理,如果没有空格进行分段的话,代码就会变得不够清晰。

3、团队风格

团队风格很重要,当团队人数增加后,如果每个人都按照自己喜爱的风格来编程,整个工程对于他人来说将变得更难理解。比如我们的初始工程中大部分的 Activity 命名方式都是 ActivityBusiness,尽管我习惯于业务在前的命名方式,但我仍然遵循项目中之前的命名方式。

Part 2 简化循环和逻辑

一、使控制流易读

把条件,循环以及其他对控制流的改变做的越“自然”越好。运用一种方式使读者不用停下来重读你的代码

控制流语句指代码中的条件判断,循环等语句。大量的复杂的 if,switch, for 循环等语句会使代码产生大量的分支,缩进,嵌套,变得混乱。我们的目标是让这些控制流语句变得容易阅读,容易理解。

二、条件语句中参数的顺序

按照我们平时口语的表达习惯,一般你会说 “我的身高高于 180 cm” 而不是 “180 cm 没有我的身高高”,我们习惯将变量放在左侧,而将常量放在右侧。

final int id = AwsomeDaemonService.getId();
if (Transporter.LOGIN_INFORMATION_LOSS == id) {
    Toasts.shortToast(R.string.login_information_loss_prompt);
    return;
}

这段代码的意思,判断当前用户的信息是否丢失,如果丢失的话,则 Toast 提示用户。我们比较语句写的是 Transporter.LOGIN_INFORMATION_LOSS == id 中文直译过来就是用户信息丢失状态是当前用户的状态,我们在脑海中还需要将它转换为用户当前的状态是丢失状态。

将比较语句改为 if (id == Transporter.LOGIN_INFORMATION_LOSS) 可读性会更好些。

1、if else 语句块的顺序

  • 先处理正逻辑,用 if(debug) 而不是 if(!debug)
  • 先处理简单的情况
  • 先处理危险的情况

以上是书中给出的关于 if/else 书写顺序的建议,这些建议之间会有冲突,需要具体情况具体对待。

先处理正逻辑

if (!TextUtils.equals(response.getContent(), "0")) {
    vRedPoint.setVisibility(View.VISIBLE);
} else {
    vRedPoint.setVisibility(View.GONE);
}

我在项目中找到一些代码,对于 if/else 代码块处理的代码行数相差无几,上面的例子用来控制一个 View 是否需要隐藏,即使先写负逻辑,也没有觉得有什么问题。

if (Transporter.get().isSleep()) {
    ivEmpty.setImageResource(R.drawable.close_assign_gray);
    tipTV.setText("休息一下,劳逸结合");
    vGoHotMap.setVisibility(View.GONE);
} else {
    ivEmpty.setImageResource(R.drawable.empty_picking_up_order);
    tipTV.setText("暂无订单\n打开地图,前往订单多的区域");
    vGoHotMap.setVisibility(View.VISIBLE);
}

根据骑士的开工收工状态,来切换一些文案内容的显示,这段代码是正逻辑,但是即使我将写成负逻辑,我想也没有什么关系。

从中文表达逻辑上来说,我们可能更习惯先处理正逻辑在处理负逻辑,但是在它们的代码块行数没有明显区别的时候,我觉得处理顺序没有什么影响

先处理简单的情况

先处理 if/else 语句块中简单的那部分

if (order.supplierDistanceBetweenYou() <= 0) {
        tvDistanceBetweenYou.setText("计算中");
        order.supplierDistanceBetweenYou(new AddressUtil.WalkDistanceListener() {
            @Override
            public void onWalkDistanceSearched(int distance) {
                if (isDetached())
                    return;
                long orderId = (Long) tvDistanceBetweenYou.getTag();
                if (orderId == order.getId()) {
                    order.setDistanceBetweenYouAndSupplier(distance);
                    tvDistanceBetweenYou.setText(Strings.formatDistanceWithMax(distance));
                    mapPresenter.addDistanceTip(distance);
                }
            }

            @Override
            public void onSearchFailed() {
                if (isDetached() || tvDistanceBetweenYou == null)
                    return;
                long orderId = (Long) tvDistanceBetweenYou.getTag();
                if (orderId == order.getId()) {
                    float[] results = new float[1];
                    Location.distanceBetween(PhoneInfo.lat, PhoneInfo.lng, order.getSupplier_lat(), order.getSupplier_lng(), results);
                    float distance = results[0];
                    order.setDistanceBetweenYouAndSupplier(distance);
                    tvDistanceBetweenYou.setText(distance == 0 ? "..." : Strings.formatDistanceWithMax(distance));
                    mapPresenter.addDistanceTip(distance);
                    }
                }
            });
} else {
    mapPresenter.addDistanceTip(order.supplierDistanceBetweenYou());
}

上面代码会从 order 信息中获取一个距离信息,如果距离 <= 0 的话就需要一系列复杂的计算,然而 else 语句块中的代码只有一行,很有可能这个时候,我们满屏都是 if 语句块中的代码,很容易忽略掉 else 语句块的处理,这个时候我们不妨将先处理距离 > 0 的情况,这样 if/else 的代码块都可以在一屏之间看见,处理了简单的逻辑之后,可以更专注在复杂的逻辑中。

先处理有趣的情况

if (subscribedTypes != null) {
    for (Class<?> eventType : subscribedTypes) {
        unsubscribeByEventType(subscriber, eventType);
    }
    typesBySubscriber.remove(subscriber);
} else {
    Log.w(TAG, "Subscriber to unregister was not registered before: " + subscriber.getClass());
}

这是 EventBus 解除注册中的一段代码,相比较于 else 语句中的打印日志,明显我们更专注 if 语句中的逻辑。

2、三目运算符

记得在我最开始写代码的时候,特别喜欢用三目运算符,只用一行代码就可以替代原有的 if/else 结构

if/else 结构:

if (conditions){
    return XXX;
}else {
    return YYY;
}

三目运算符结构:

conditions ? XXX : YYY;

如果实际情况有这么简单,当然可以写成三目运算符的情况,然而你很有可能面临这样的情况:

height = (height <= availableHeight) ? (width * rawPicHeight / rawPicWidth) : rootHeight - iconHeight;

这段代码根据某个条件来计算 height 的值,这里三目运算符已经不是从简单的两个值中做出选择,而是为了将所有的代码挤进一行里,导致可读性变差。

使用 if/else 格式来改写上述代码:

if (height <= availableHeight) {
    height = (width * rawPicHeight / rawPicWidth);
} else {
    height = rootHeight - iconHeight;
}

相对于追求最小化代码行数,一个更好的度量方法是最小化理解它所需要的时间

三、最小化嵌套

我们的项目中由于各种各样的原因,肯定会有一些嵌套很深的代码,多层嵌套的代码难以理解,会加深我们的思维栈,每当多处理一层嵌套,我们脑海中的思维栈就加深了一层,因此我们要尽量减少代码中的嵌套。

if (Transporter.isLogin()) {
    //是否需要展示实地培训的绿色按钮
    if (transporter.canApplyOfflineTraining()) {
        ViewUtils.visible(vFieldTraining);
    }
            
    if (transporter.isMissionCompleted()) {
        ViewUtils.gone(vTiro);
        TiroHelper.getInstance().destroyBottomDialog();
    } else{
        if (isNeedCheckTiroNew) {
            //检测新手体验单是否展示
            TiroHelper.getInstance().checkNewTiroDialogIsShowing(this);
            getTransporterDetails();
        }
    }
} else {
    //如果未登陆,直接隐藏收工按钮
    ViewUtils.gone(vAssign);
    ViewUtils.gone(vFieldTraining);
    ViewUtils.gone(vTiro);
    isNeedCheckTiroNew = false;
    TiroHelper.getInstance().destroyBottomDialog();
}

这是我从 ActivityMain.java 中抽出并简化的一段代码,在最初我们可能只有接单的 if/else 结构

if (Transporter.isLogin()) {
    //do something 
} else {
    //do something
}

后来需求变更要求我们在用户登录的情况下,新增一些判断,我们自然而然的就写成了多嵌套的形式,我们可以通过提前返回来减少嵌套

if (!Transporter.isLogin()) {
     //如果未登陆,直接隐藏收工按钮
    ViewUtils.gone(vAssign);
    ViewUtils.gone(vFieldTraining);
    ViewUtils.gone(vTiro);
    isNeedCheckTiroNew = false;
    TiroHelper.getInstance().destroyBottomDialog();
    return;
}

//是否需要展示实地培训的绿色按钮
if (transporter.canApplyOfflineTraining()) {
    ViewUtils.visible(vFieldTraining);
}
          
if (transporter.isMissionCompleted()) {
    ViewUtils.gone(vTiro);
    TiroHelper.getInstance().destroyBottomDialog();
    return;
} 
if (isNeedCheckTiroNew) {
    //检测新手体验单是否展示
    TiroHelper.getInstance().checkNewTiroDialogIsShowing(this);
    getTransporterDetails();
}

减少循环内的嵌套

for (CarloadLuggageItem checkItem : result.getDetails()) {
    if (!checkItem.isAvailable()) {
        for (int i = 0; i < packageListItems.size(); i++) {
            CarloadLuggageItem item = packageListItems.get(i);
                if (TextUtils.equals(item.getJd_order_no(), checkItem.getJd_order_no())) {
                    item.setIs_passed(CarloadLuggageItem.UNPASS);
                    bindFailList.add(new CheckFailItem(item.getJd_order_no(), item.getPackage_num()));
                break;
            }
        }
    }
}

这也是之前项目中的代码,用来更改本地数据的成功或者失败的状态,我们依然可以通过提前返回来减少一层嵌套:

for (CarloadLuggageItem checkItem : result.getDetails()) {
    if (checkItem.isAvailable())
        continue;
    for (int i = 0; i < packageListItems.size(); i++) {
        CarloadLuggageItem item = packageListItems.get(i);
        if (TextUtils.equals(item.getJd_order_no(), checkItem.getJd_order_no())) {
            item.setIs_passed(CarloadLuggageItem.UNPASS);
            bindFailList.add(new CheckFailItem(item.getJd_order_no(),item.getPackage_num()));
            break;
        }
    }

}
  • 在写一个比较的时候,把变量写在左侧,把常量写在右侧更好一些
  • 重新排列 if/else 语句中的语句块,通常来讲,先处理正确的/简单的/有趣的情况
  • 三目运算符有可能导致代码的可读性变差,可以用更整洁的方式替代它
  • 嵌套的代码块需要更加集中注意力去理解,尽量将它改写成线性的代码
  • 可以通过提早返回来减少代码嵌套,让代码变得更整洁

四、拆分超长的表达式

大多数人脑只能同时思考 3-4 件事情,如果代码,表达式太长,就会超出大脑思考的并发数,这样的代码就会变得难以理解,因此我们需要将超长的表达式拆分容易理解的小代码块

1、解释变量

我们可以通过额外引入一个变量-解释变量,让它来表示一个小一点的子表达式。

if (ListUtils.isEmpty(order.getDisplay_tags())
                //标签不为空
                && TextUtils.isEmpty(order.getOrigin_mark())
                && TextUtils.isEmpty(order.getOrigin_mark_icon())
                && TextUtils.isEmpty(order.getOrigin_mark_no())) {
    targetView.setVisibility(View.GONE);
} else {
    targetView.setVisibility(View.VISIBLE);
}

if 语句中的条件表达式很长,但是它其实就要表达一个意思,需不需要显示 Tag,我们可以定义一个解释变量,用来解释这个条件表达式

boolean isNeedShowTag = order.getDisplay_tags())
                        && TextUtils.isEmpty(order.getOrigin_mark())
                        && TextUtils.isEmpty(order.getOrigin_mark_icon())
                        && TextUtils.isEmpty(order.getOrigin_mark_no())
                        
if (isNeedShowTag) {
    targetView.setVisibility(View.VISIBLE);
} else {
    targetView.setVisibility(View.GONE);
}

2、总结变量,重复代码

即使一个表达式很简单,你可以直接看出它的含义,也可以把它装入一个新的变量中-总结变量,用一个很短的名字,来代替一大段代码,易于理解和思考

if (order.getFetchType() == Order.FETCH_TYPE_FROM_PACKAGE) {
    //同城速递集包取件
    getDadaApiV1().fetchCityExpressList(getActivity(), order, id);
} 
... 省略

if (order.getFetchType() != Order.FETCH_TYPE_FROM_PACKAGE) {
    EventBus.getDefault().post(new ForcePickUpEvent());
}

代码组要是来判断该笔订单是否是同城速递订单,然后针对不同的情况进行处理。我们可以通过增加一个总结变量来表达的更清楚。

boolean isSameCityExpress = order.getFetchType() == Order.FETCH_TYPE_FROM_PACKAGE;
if (isSameCityExpress){
    do something
}

if (!isSameCityExpress){
    do something
}

有的时候,我们的代码中会有一些重复的代码,我们也可以将这些重复代码提取出来,用一个变量来表示

if (order.getOrder_status() == Order.ORDER_STATUS_PICKUP) {
    OrderOperation.getInstance().dispatching(getActivity(),order);
} else if (order.getOrder_status() == Order.ORDER_STATUS_DISPATCHING) {
    OrderOperation.getInstance().finish(getActivity(), true, order, null,finishCode);
} else if (order.getOrder_status() == Order.ORDER_STATUE_FINISHED){
    ... 
}

order.getOrder_status() 会重复出现多次,它表示获取订单的状态,我们可以定义一个变量来表示它 int orderStatus = order.getOrder_status()

3、短路操作

有的时候我们可以使用短路行为来使代码变得更简洁。
例如我们 ListUtils 工具类中的方法。

public static <V> boolean isEmpty(List<V> sourceList) {
    return (sourceList == null || sourceList.size() == 0);
}

public static <V> boolean isNotEmpty(List<V> sourceList) {
    return (sourceList != null && sourceList.size() > 0);
}

很好的利用短路逻辑,不然你可能需要写多个 if 语句

public static <V> boolean isEmpty(List<V> sourceList) {
    if (sourceList == null) {
        return true;
    }
    
    if (sourceList.size() == 0) {
        return true;
    }
    
    return false;
}

五、变量与可读性

为什么说对变量的草率运用会让程序变得更难理解?

  1. 变量越多,就会越难追踪它们的动向
  2. 变量的作用域越大,就需要追踪它的动向越久

1、减少变量

没有价值的临时变量,多余的中间变量,我们都可以通过一些方式来消除它。

没有价值的临时变量
没有起到解释或者总结的作用,并且只用到了一次,一般来说,在定义临时变量的时候,预计是在之后会使用到它,但实际上没有使用到它,也没有将它删除。

/**
* 获取订单卡片的小费
*/
public static double getOrderTips(OrderTaskInfo item) {
    Order order = item.getFirstOrder();
    double displayOrderTips = order.getTips();
    ...
}

可以看到我们定义了一个 order 对象,这个 order 对象只用到了一次,没有什么特殊的价值,我们完全可以移除这个对象 double displayOrderTips = item.getFirstOrder().getTips();

减少中间结果

int selectPosition
for (int i = 0; i < adapter.size(); i++) {
    if (adapter.get(i).isSelected()) {
        selectPosition = i;
        break;
    }
}
return adapter.get(selectPosition);

这段代码是找出数据源中的选中项,并且返回它,我们可以通过在循环体中直接返回选中项,从而消除 selectPosition 这个变量

for (int i = 0; i < adapter.size(); i++) {
    Object object = adapter.get(i);
    if (object.isSelected()) {
        return object; 
    }
}

2、减小每个变量的作用域

变量的作用域越小越好,尽量将变量移动到一个对其他代码可见性低的地方。

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

推荐阅读更多精彩内容

  • “任何一个傻瓜都能写出计算机可以理解的代码,唯有写出人类容易理解的代码,才是优秀的程序员。”,这句话出自《重构》这...
    HJDaryl阅读 2,563评论 3 2
  • 第一部分 打好基础 Laying the Foundation 第一章 欢迎进入软件构建的世界 Welcome t...
    白桦叶阅读 4,623评论 0 17
  • Globalidx全球指数,30年股指期货投资平台,8大指数交易,低点差无交易手续费。 联系QQ:29205986...
    GI推荐阅读 713评论 0 0
  • 在鲁迅的这篇文章里,看到了《少有人走的路》关于父母对子女的爱的正确表达,《少有人走的路》更像是鲁迅此文的扩充版,增...
    依布茶卡阅读 2,360评论 0 0
  • 尊敬的各位领导,亲爱的朋友们,下午好,我是来自四川省成都的XXX,在舞台上有一点紧张,希望大家给我点鼓励(微笑)。...
    可爱的苗苗阅读 220评论 0 1