首页 > 娱乐前沿 > 热点
编程的智慧
佚名 2015-11-24 11:50:27

  编程是一件创造性的工作,是一门艺术。精通任何一门艺术,都需要很多的练习和领悟,所以这里提出的“智慧”,并不是号称三天瘦二十斤的减肥药,它并不能代替你自己的勤奋。然而我希望它能给迷惑中的人们指出一些正确的方向,让他们少走一些弯路,基本做到一分耕耘一分收获。

  反复推敲代码

  既然“天才是百分之一的灵感,百分之九十九的汗水”,那我先来谈谈这汗水的部分吧。有人问我,提高编程水平最有效的办法是什么?我想了很久,终于发现最有效的办法,其实是反反复复地修改和推敲代码。

  在IU的时候,由于Dan Friedman的严格教导,我们以写出冗长复杂的代码为耻。如果你代码多写了几行,这老顽童就会大笑,说:“当年我解决这个问题,只写了5行代码,你再回去想想吧……” 当然,有时候他只是夸张一下,故意刺激你的,其实没有人能只用5行代码完成。然而这种提炼代码,减少冗余的习惯,却由此深入了我的骨髓。

  有些人喜欢炫耀自己写了多少多少万行的代码,仿佛代码的数量是衡量编程水平的标准。然而,如果你总是匆匆写出代码,却从来不回头去推敲,修改和提炼,其实是不可能提高编程水平的。你会制造出越来越多平庸甚至糟糕的代码。在这种意义上,很多人所谓的“工作经验”,跟他代码的质量,其实不一定成正比。如果有几十年的工作经验,却从来不回头去提炼和反思自己的代码,那么他也许还不如一个只有一两年经验,却喜欢反复推敲,仔细领悟的人。

  有位文豪说得好:“看一个作家的水平,不是看他发表了多少文字,而要看他的废纸篓里扔掉了多少。” 我觉得同样的理论适用于编程。好的程序员,他们删掉的代码,比留下来的还要多很多。如果你看见一个人写了很多代码,却没有删掉多少,那他的代码一定有很多垃圾。

  就像文学作品一样,代码是不可能一蹴而就的。灵感似乎总是零零星星,陆陆续续到来的。任何人都不可能一笔呵成,就算再厉害的程序员,也需要经过一段时间,才能发现最简单优雅的写法。有时候你反复提炼一段代码,觉得到了顶峰,没法再改进了,可是过了几个月再回头来看,又发现好多可以改进和简化的地方。这跟写文章一模一样,回头看几个月或者几年前写的东西,你总能发现一些改进。

  所以如果反复提炼代码已经不再有进展,那么你可以暂时把它放下。过几个星期或者几个月再回头来看,也许就有焕然一新的灵感。这样反反复复很多次之后,你就积累起了灵感和智慧,从而能够在遇到新问题的时候直接朝正确,或者接近正确的方向前进。

  写优雅的代码

  人们都讨厌“面条代码”(spaghetti code),因为它就像面条一样绕来绕去,没法理清头绪。那么优雅的代码一般是什么形状的呢?经过多年的观察,我发现优雅的代码,在形状上有一些明显的特征。

  如果我们忽略具体的内容,从大体结构上来看,优雅的代码看起来就像是一些整整齐齐,套在一起的盒子。如果跟整理房间做一个类比,就很容易理解。如果你把所有物品都丢在一个很大的抽屉里,那么它们就会全都混在一起。你就很难整理,很难迅速的找到需要的东西。但是如果你在抽屉里再放几个小盒子,把物品分门别类放进去,那么它们就不会到处乱跑,你就可以比较容易的找到和管理它们。

  优雅的代码的另一个特征是,它的逻辑大体上看起来,是枝丫分明的树状结构(tree)。这是因为程序所做的几乎一切事情,都是信息的传递和分支。你可以把代码看成是一个电路,电流经过导线,分流或者汇合。如果你是这样思考的,你的代码里就会比较少出现只有一个分支的if语句,它看起来就会像这个样子:

if (...) {
  if (...) {
    ...
  } else {
    ...
  }
} else if (...) {
  ...
} else {
  ...
}

  注意到了吗?在我的代码里面,if语句几乎总是有两个分支。它们有可能嵌套,有多层的缩进,而且else分支里面有可能出现少量重复的代码。然而这样的结构,逻辑却非常严密和清晰。在后面我会告诉你为什么if语句最好有两个分支。

  写模块化的代码

  有些人吵着闹着要让程序“模块化”,结果他们的做法是把代码分部到多个文件和目录里面,然后把这些目录或者文件叫做“module”。他们甚至把这些目录分放在不同的VCS repo里面。结果这样的作法并没有带来合作的流畅,而是带来了许多的麻烦。这是因为他们其实并不理解什么叫做“模块”,肤浅的把代码切割开来,分放在不同的位置,其实非但不能达到模块化的目的,而且制造了不必要的麻烦。

  真正的模块化,并不是文本意义上的,而是逻辑意义上的。一个模块应该像一个电路芯片,它有定义良好的输入和输出。实际上一种很好的模块化方法早已经存在,它的名字叫做“函数”。每一个函数都有明确的输入(参数)和输出(返回值),同一个文件里可以包含多个函数,所以你其实根本不需要把代码分开在多个文件或者目录里面,同样可以完成代码的模块化。我可以把代码全都写在同一个文件里,却仍然是非常模块化的代码。

  想要达到很好的模块化,你需要做到以下几点:

  写可读的代码

  有些人以为写很多注释就可以让代码更加可读,然而却发现事与愿违。注释不但没能让代码变得可读,反而由于大量的注释充斥在代码中间,让程序变得障眼难读。而且代码的逻辑一旦修改,就会有很多的注释变得过时,需要更新。修改注释是相当大的负担,所以大量的注释,反而成为了妨碍改进代码的绊脚石。

  实际上,真正优雅可读的代码,是几乎不需要注释的。如果你发现需要写很多注释,那么你的代码肯定是含混晦涩,逻辑不清晰的。其实,程序语言的逻辑表达能力,是远远高于自然语言的。使用大量的自然语言去解释程序的细节,是本末倒置的。

  有人受到了Donald Knuth提出的所谓“文学编程”(Literate Programming)的误导,认为程序里面注释应该是主要的部分,而代码其次,其实并不是这样的。很多人(包括Knuth自己)使用文学编程,其实并没有写出容易理解的代码。Knuth认为人与人之间交流,必须使用自然语言,而其实如果使用得当,程序语言能够更加清晰精确地在人类之间传递信息。

  之所以说“如果使用得当”,是因为如果没能合理利用程序语言提供的优势,你会发现程序还是很难懂,以至于需要写注释。所以我现在告诉你一些要点,也许可以帮助你大大减少写注释的必要:

  1. 使用有意义的函数和变量名字。如果你的函数和变量的名字,能够切实的描述它们的逻辑,那么你就不需要写注释来解释它在干什么。比如:

    // put elephant elephant1 into fridge fridge2
    putElephantIntoFridge(elephant1, fridge2);

    由于我的函数名putElephantIntoFridge已经说明了它要干什么(把大象放进冰箱),所以上面那句注释完全没有必要。

  2. 把复杂的逻辑提取出去,做成“帮助函数”。有些人写的函数很长,以至于看不清楚里面的语句在干什么,所以他们误以为需要写注释。如果你仔细观察这些代码,就会发现不清晰的那片代码,往往可以被提取出去,做成一个函数,然后在原来的地方调用。由于函数有一个名字,这样你就可以使用有意义的函数名来代替注释。举一个例子:

    ... ...
    ... ...
    // put elephant elephant1 into fridge fridge2
    openDoor(fridge2);
    if (driveElephantIntoFridge(elephan1, fridge2)) {
       feedElephant(new Treat(), elephant1);
    } else {
       putBananaIntoFridge(new Banana(), fridge2);
       waitForElephantEnter(elephant1, fridge2);
    }
    closeDoor(fridge2);
    ... ...
    ... ...

    如果你把这片代码提出去定义成一个函数:

    function putElephantIntoFridge(elephant, fridge) {  openDoor(fridge2);  if (driveElephantIntoFridge(elephan1, fridge2)) {    feedElephant(new Treat(), elephant1);  } else {    putBananaIntoFridge(new Banana(), fridge2);    waitForElephantEnter(elephant1, fridge2);  }  closeDoor(fridge2);}

    然后原来的代码就可以改成:

    ... ...... ...putElephantIntoFridge(elephant1, fridge2);... ...... ...

    注释就没必要了。

  程序语言相比自然语言,是非常强大而严谨的,它其实已经具有自然语言的主要元素:主语,谓语,宾语,名词,动词,如果,因为,所以,否则,是,不是,…… 所以如果你充分利用了程序语言的表达能力,你完全可以用程序本身来表达它到底在干什么,而不需要自然语言的辅助。

  有少数的时候,你也许会为了绕过其他一些代码的设计问题,采用一种违反直觉的作法。这时候你就可以使用很短的一条注释,说明为什么要写成那奇怪的样子。这样的情况应该很少出现,否则这意味着整个代码的设计都有问题。

  写简单的代码

  现在我提出一些我自己正在使用的代码规范,稍微解释一下为什么它们能让代码更加简单,从而提高代码的质量。

  写直观的代码

  我写代码有一条重要的原则:如果有更加直接,更加清晰的写法,就选择它,即使它看起来更长,更笨,也一样选择它。比如,Unix命令行有一种“巧妙”的写法是这样:

command1 && command2 && command3

  由于Shell语言的逻辑操作a && b具有“短路”的特性,如果a等于false,那么b就没必要执行了。这就是为什么当command1成功,才会执行command2,当command2成功,才会执行command3。同样,

command1 || command2 || command3

  操作符||也有类似的特性。上面这个命令行,如果command1成功,那么command2和command3都不会被执行。如果command1失败,command2成功,那么command3就不会被执行。

  这比起用if语句来判断失败,似乎更加巧妙和简洁,所以有人就借鉴了这种方式,在程序的代码里也使用这种方式。比如他们可能会写这样的代码:

if (action1() || action2() && action3()) {
  ...
}

  你看得出来这代码是想干什么吗?action2和action3什么条件下执行,什么条件下不执行?也许稍微想一下,你知道它在干什么:“如果action1失败了,执行action2,如果action2成功了,执行action3”。然而那种语义,并不是直接的“映射”在这代码上面的。比如“失败”这个词,对应了代码里的哪一个字呢?你找不出来,因为它包含在了||的语义里面,你需要知道||的短路特性,以及逻辑或的语义才能知道这里面在说“如果action1失败……”。每一次看到这行代码,你都需要思考一下,这样积累起来的负荷,就会让人很累。

  其实,这种写法是滥用了逻辑操作&&和||的短路特性。这两个操作符可能不执行右边的表达式,原因是为了机器的执行效率,而不是为了给人提供这种“巧妙”的用法。这两个操作符的本意,只是作为逻辑操作,它们并不是拿来给你代替if语句的。也就是说,它们只是碰巧可以达到某些if语句的效果,但你不应该因此就用它来代替if语句。如果你这样做了,就会让代码晦涩难懂。

  上面的代码写成笨一点的办法,就会清晰很多:

if (!action1()) {
  if (action2()) {
    action3();
  }
}

  这里我很明显的看出这代码在说什么,想都不用想:如果action1()失败了,那么执行action2(),如果action2()成功了,执行action3()。你发现这里面的一一对应关系吗?if=如果,!=失败,…… 你不需要利用逻辑学知识,就知道它在说什么。

  写无懈可击的代码

  在之前一节里,我提到了自己写的代码里面很少出现只有一个分支的if语句。我写出的if语句,大部分都有两个分支,所以我的代码很多看起来是这个样子:

if (...) {
  if (...) {
    ...
    return false;
  } else {
    return true;
  }
} else if (...) {
  ...
  return false;
} else {
  return true;
}

  使用这种方式,其实是为了无懈可击的处理所有可能出现的情况,避免漏掉corner case。每个if语句都有两个分支的理由是:如果if的条件成立,你做某件事情;但是如果if的条件不成立,你应该知道要做什么另外的事情。不管你的if有没有else,你终究是逃不掉,必须得思考这个问题的。

  很多人写if语句喜欢省略else的分支,因为他们觉得有些else分支的代码重复了。比如我的代码里,两个else分支都是return true。为了避免重复,他们省略掉那两个else分支,只在最后使用一个return true。这样,缺了else分支的if语句,控制流自动“掉下去”,到达最后的return true。他们的代码看起来像这个样子:

if (...) {
  if (...) {
    ...
    return false;
  } 
} else if (...) {
  ...
  return false;
} 
return true;

  这种写法看似更加简洁,避免了重复,然而却很容易出现疏忽和漏洞。嵌套的if语句省略了一些else,依靠语句的“控制流”来处理else的情况,是很难正确的分析和推理的。如果你的if条件里使用了&&和||之类的逻辑运算,就更难看出是否涵盖了所有的情况。

  由于疏忽而漏掉的分支,全都会自动“掉下去”,最后返回意想不到的结果。即使你看一遍之后确信是正确的,每次读这段代码,你都不能确信它照顾了所有的情况,又得重新推理一遍。这简洁的写法,带来的是反复的,沉重的头脑开销。这就是所谓“面条代码”,因为程序的逻辑分支,不是像一棵枝叶分明的树,而是像面条一样绕来绕去。

  正确处理错误

  使用有两个分支的if语句,只是我的代码可以达到无懈可击的其中一个原因。这样写if语句的思路,其实包含了使代码可靠的一种通用思想:穷举所有的情况,不漏掉任何一个。

  程序的绝大部分功能,是进行信息处理。从一堆纷繁复杂,模棱两可的信息中,排除掉绝大部分“干扰信息”,找到自己需要的那一个。正确地对所有的“可能性”进行推理,就是写出无懈可击代码的核心思想。这一节我来讲一讲,如何把这种思想用在错误处理上。

  错误处理是一个古老的问题,可是经过了几十年,还是很多人没搞明白。Unix的系统API手册,一般都会告诉你可能出现的返回值和错误信息。比如,Linux的read系统调用手册里面有如下内容:

RETURN VALUE
On success, the number of bytes read is returned...
On error, -1 is returned, and errno is set appropriately.

  ERRORS EAGAIN, EBADF, EFAULT, EINTR, EINVAL, …

  很多初学者,都会忘记检查read的返回值是否为-1,觉得每次调用read都得检查返回值真繁琐,不检查貌似也相安无事。这种想法其实是很危险的。如果函数的返回值告诉你,要么返回一个正数,表示读到的数据长度,要么返回-1,那么你就必须要对这个-1作出相应的,有意义的处理。千万不要以为你可以忽视这个特殊的返回值,因为它是一种“可能性”。代码漏掉任何一种可能出现的情况,都可能产生意想不到的灾难性结果。

  对于Java来说,这相对方便一些。Java的函数如果出现问题,一般通过异常(exception)来表示。你可以把异常加上函数本来的返回值,看成是一个“union类型”。比如:

String foo() throws MyException {
  ...
}

  这里MyException是一个错误返回。你可以认为这个函数返回一个union类型:{String, MyException}。任何调用foo的代码,必须对MyException作出合理的处理,才有可能确保程序的正确运行。Union类型是一种相当先进的类型,目前只有极少数语言(比如Typed Racket)具有这种类型,我在这里提到它,只是为了方便解释概念。掌握了概念之后,你其实可以在头脑里实现一个union类型系统,这样使用普通的语言也能写出可靠的代码。

  由于Java的类型系统强制要求函数在类型里面声明可能出现的异常,而且强制调用者处理可能出现的异常,所以基本上不可能出现由于疏忽而漏掉的情况。但有些Java程序员有一种恶习,使得这种安全机制几乎完全失效。每当编译器报错,说“你没有catch这个foo函数可能出现的异常”时,有些人想都不想,直接把代码改成这样:

try {
  foo();
} catch (Exception e) {}

  或者最多在里面放个log,或者干脆把自己的函数类型上加上throws Exception,这样编译器就不再抱怨。这些做法貌似很省事,然而都是错误的,你终究会为此付出代价。

  如果你把异常catch了,忽略掉,那么你就不知道foo其实失败了。这就像开车时看到路口写着“前方施工,道路关闭”,还继续往前开。这当然迟早会出问题,因为你根本不知道自己在干什么。

  catch异常的时候,你不应该使用Exception这么宽泛的类型。你应该正好catch可能发生的那种异常A。使用宽泛的异常类型有很大的问题,因为它会不经意的catch住另外的异常(比如B)。你的代码逻辑是基于判断A是否出现,可你却catch所有的异常(Exception类),所以当其它的异常B出现的时候,你的代码就会出现莫名其妙的问题,因为你以为A出现了,而其实它没有。这种bug,有时候甚至使用debugger都难以发现。

  如果你在自己函数的类型加上throws Exception,那么你就不可避免的需要在调用它的地方处理这个异常,如果调用它的函数也写着throws Exception,这毛病就传得更远。我的经验是,尽量在异常出现的当时就作出处理。否则如果你把它返回给你的调用者,它也许根本不知道该怎么办了。

  另外,try { … } catch里面,应该包含尽量少的代码。比如,如果foo和bar都可能产生异常A,你的代码应该尽可能写成:

try {
  foo();
} catch (A e) {...}
try {
  bar();
} catch (A e) {...}

  而不是

try {
  foo();
  bar();
} catch (A e) {...}

  第一种写法能明确的分辨是哪一个函数出了问题,而第二种写法全都混在一起。明确的分辨是哪一个函数出了问题,有很多的好处。比如,如果你的catch代码里面包含log,它可以提供给你更加精确的错误信息,这样会大大地加速你的调试过程。

  正确处理null指针

  穷举的思想是如此的有用,依据这个原理,我们可以推出一些基本原则,它们可以让你无懈可击的处理null指针。

  首先你应该知道,许多语言(C,C++,Java,C#,……)的类型系统对于null的处理,其实是完全错误的。这个错误源自于Tony Hoare最早的设计,Hoare把这个错误称为自己的“billion dollar mistake”,因为由于它所产生的财产和人力损失,远远超过十亿美元!

  这些语言的类型系统允许null出现在任何对象(指针)类型可以出现的地方,然而null其实根本不是一个合法的对象。它不是一个String,不是一个Integer,也不是一个自定义的类。null的类型本来应该是NULL,也就是null自己。根据这个基本观点,我们推导出以下原则:

  扩展话题:关于Optional类型和Union类型

  有些语言,比如Java 8和Swift,提供了一种叫“Optional类型”的东西。比如在Java 8里面,你可以使用Optional<String>来表示“可能是String,可能没有”。很多人以为有了Optional类型,就可以完美的解决null指针的问题,然而它并不是想象的那样完美。

  因为你看到的类型是Optional<String>,而不是String,所以类型系统不允许你直接把它当String来用。这多出来的一层关卡,可以防止你不问三七二一就取它的值,你总要想一下。然而这并不能从根本上解决问题。Optional并不能完全阻止你产生跟NullPointerException等价的运行时错误。因为你仍然可以写这样的代码:

Optional<String> x = Optional.empty();
String y = x.get();

  没有检查x.isPresent()就使用x.get(),结果出现NoSuchElementException。这其实等价于没有检查null就在dereference它。只不过现在出现的不是NullPointerException,而是NoSuchElementException。两个都是运行时错误,换汤不换药,程序照样崩溃。所以你看到了,Optional只是一种善意的“提示”,它使你不会在完全不知情的情况下犯错误。可是如果你忽略这种提示,照样可以犯一样的错误。Optional并没有任何强制性的力量。

  Swift的Optional类型跟Java的是一样的问题,Swift的手册里指出:“Using the ! operator to unwrap an optional that has a value of nil results in a runtime error.” 所以,Swift并不能静态地阻止你对一个值为nil的Optional进行!操作。如果你做了,就会产生“运行时错误”。

  另外,Optional类型会导致程序变得复杂。Optional和null指针,在结构上有一个很大的差别。Optional比null指针多了一层数据结构。Optional把需要的值放在了另外一个对象里面。你必须用x.get()来得到里面这个值,这跟使用null的时候很不一样。当你判断了一个String不可能是null,你不需要再做一次get把内容给取出来。比如:

String found = find();
if (found != null) {
  total += found.length();
}

  判断found不是null之后,我们可以直接用found.length()得到它的长度,而不需要先使用found.get()。这个例子貌似小事,然而如果Optional类型被放进另外的结构或者容器里面,或者包含了另外类型,你就知道它的繁琐和痛苦了。Optional的这个问题,跟Haskell的Maybe类型的问题一样,经常导致类型嵌套层数太多,太烦。

  相比之下,union类型系统可以完全静态地防止NullPointerException,而不导致类型的过度嵌套。Union类型可以完全的涵盖Optional类型的功能,非常的简单,而且有很多其它的好处。这种类型系统已经存在于Typed Racket语言(一个Scheme的后代),还没有面世的Yin语言也实现了union类型。PySonar的类型推导系统里面也具有union类型。Union类型系统非常强大,它不但可以完全静态地消灭NullPointerException,而且可以取代Java等语言的exception机制。它让错误处理变得非常严密,却又非常方便。

  不过需要注意的是,就算你有了union类型系统,完全静态地防止了NullPointerException,上面提到的几条对待null的原则仍然是有用的。在有union类型的语言里面,一个容易犯的错误是不假思索的扩展union类型,把什么可能性都加进去,结果最后得到很大的union类型。这导致很多变量和参数具有union类型,每个变量都有可能是好多种东西,以至于你需要做好几个判断才能通过类型检查。这种现象跟null指针的泛滥的问题并没有本质的区别,因为你没能有效地控制住“可能性”。这个“可能性爆炸”的问题,程序语言也许不能给你很好的帮助。只有靠自己,遵循上面的原则,尽早排除union类型或者减少其中的可能性,你才能避免这种混乱。

  防止过度工程

  人的脑子真是奇妙的东西。虽然大家都知道过度工程(over-engineering)不好,在实际的工程中却经常不由自主的出现过度工程。所以我觉得必须分析一下过度工程出现的信号和兆头,在初期的时候就避免它。

  过度工程即将出现的一个重要信号,就是当你过度的思考“将来”,考虑一些还没有发生的事情,还没有出现的需求。比如,“如果我们将来有了上百万行代码,有了几千号人,这样的工具就支持不了了”,“将来我可能需要这个功能,所以我现在就把代码写来放在那里”,“将来很多人要扩充这片代码,所以现在我们就让它变得可重用”……

  这就是为什么很多软件项目如此复杂。实际上没做多少事情,却为了所谓的“将来”,加入了很多不必要的复杂性。眼前的问题还没解决呢,就被“将来”给拖垮了。人们都不喜欢目光短浅的人,然而在现实的工程中,有时候你就是得看近一点,把手头的问题先搞定了,再谈以后扩展的问题。

  另外一种过度工程的来源,是过度的关心“代码重用”。很多人“可用”的代码还没写出来呢,就在关心“重用”。为了让代码可以重用,最后被自己搞出来的各种框架捆住手脚,最后连可用的代码就没写好。如果可用的代码都写不好,又何谈重用呢?很多一开头就考虑太多重用的工程,到后来被人完全抛弃,没人用了,因为别人发现这些代码太难懂了,自己从头开始写一个,反而省好多事。

  过度地关心“测试”,也会引起过度工程。有些人为了测试,把本来很简单的代码改成“方便测试”的形式,结果引入很多复杂性,以至于本来一下就能写对的代码,最后复杂不堪,出现很多bug。

  世界上有两种“没有bug”的代码。一种是“没有明显的bug的代码”,另一种是“明显没有bug的代码”。第一种情况,由于代码复杂不堪,加上很多测试,各种coverage,貌似测试都通过了,所以就认为代码是正确的。第二种情况,由于代码简单直接,就算没写很多测试,你一眼看去就知道它不可能有bug。你喜欢哪一种“没有bug”的代码呢?

  根据这些,我总结出来的防止过度工程的原则如下:

  1. 先把眼前的问题解决掉,解决好,再考虑将来的扩展问题。
  2. 先写出可用的代码,反复推敲,再考虑是否需要重用的问题。
  3. 先写出可用,简单,明显没有bug的代码,再考虑测试的问题。

上一篇  下一篇

I 相关 / Other

遭提前资遣 勤益员工至台北公司抗议

勤益电子遭到提前资遣的员工24日北上至公司抗议差别待遇。(杨兆元摄) 勤益电子遭到提前资遣的员工24日

慈禧向全世界宣战,加速清朝灭亡

“冲动是魔鬼”这句话说得十分有水准,魔鬼附体的大清帝国独裁者在冲动下做出了空前绝后的旷世之举,居然向

陈宝存:环京地区楼市之固安价值分析

陈宝存:环京地区楼市之固安价值分析1、北京人口外溢需求为主,包括北京中轴线以西全部地区、大兴亦庄房山长

付费2015/11/23复盘分析

【行情回顾】今日大盘冲高回落、走出A型反转,沪指收于3610(-0.56%)、深指跌-0.91%、创业板指跌收2770.6(

付费基于五层次分析的潜伏模式

正确的潜伏前提有三个:1、明确的个股预期2、清晰的五层次分析3、清晰的交易计划和严格的执行。

I 热点 / Hot