C语言有哪些不错的第三方库 比如处理c 数据库链接字符串串的,正则表达式的,计算的等等

写点代码相关的写点代码相关的编程相关,web开发相关关注专栏李引证 的文章{&debug&:false,&apiRoot&:&&,&paySDK&:&https:\u002F\u002Fpay.zhihu.com\u002Fapi\u002Fjs&,&wechatConfigAPI&:&\u002Fapi\u002Fwechat\u002Fjssdkconfig&,&name&:&production&,&instance&:&column&,&tokens&:{&X-XSRF-TOKEN&:null,&X-UDID&:null,&Authorization&:&oauth c3cef7c66aa9e6a1e3160e20&}}{&database&:{&Post&:{&&:{&title&:&JS下的正则表达式&,&author&:&browsnet&,&content&:&\u003Cp\u003E今天在群里看到有不少人说对正则表达式很苦手,我把自己的一些正则经验说一下。\u003C\u002Fp\u003E\u003Cp\u003E正则表达式这个玩意历史悠久,而且是始于数学,进入计算机世界是从编辑器开始的,不过真正流行开来是随着perl的内置强大正则引擎而开始的,在之后,正则表达式被大规模的使用,现在各种语言都基本上内置正则引擎,但各语言的正则表达式表现各异,甚至相差十万八千里。\u003C\u002Fp\u003E\u003Cp\u003E粗略的分类的话,正则引擎有DFA和NFA之分,DFA那一派有古老历史,虽然速度较快,但功能较为简单,目前大多数人使用的语言,比如C系列,大多数脚本语言,都是NFA引擎。\u003C\u002Fp\u003E\u003Cp\u003ENFA引擎是以表达式为主导的,所以一个好的表达式是非常有用的,不好的表达式甚至能造成很大的性能问题(DFA由于其构造,所以表达式不重要,匹配的内容决定了其性能)\u003C\u002Fp\u003E\u003Cp\u003E但是NFA引擎在各大语言中也表现出很多不一样的地方,甚至说可以是五花八门吧,我主要说说JS下的。\u003C\u002Fp\u003E\u003Cp\u003EJS支持\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E大多数元字符\u003C\u002Fli\u003E\u003Cli\u003E字符组\u003C\u002Fli\u003E\u003Cli\u003E段落起始和终结,以及伪逻辑行\u003C\u002Fli\u003E\u003Cli\u003E匹配优先量词 .* 和 忽略优先量词 .*?\u003C\u002Fli\u003E\u003Cli\u003E正向环视 ?=\u003C\u002Fli\u003E\u003Cli\u003E反向引用 \\1 \\2\u003C\u002Fli\u003E\u003Cli\u003E非捕获的分组 ?:\u003C\u002Fli\u003E\u003Cli\u003E常用修饰符 \u002Figx\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cp\u003EJS 不支持的有\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E逆向环视(逆向断言)?&=\u003C\u002Fli\u003E\u003Cli\u003E命名分组 ?=p\u003C\u002Fli\u003E\u003Cli\u003E分组内部修饰符 (?=i)\u003C\u002Fli\u003E\u003Cli\u003E固化分组 ?&\u003C\u002Fli\u003E\u003Cli\u003E占位量词 .*+\u003C\u002Fli\u003E\u003Cli\u003E还有可能我一时没想到的\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cp\u003E顺便说一下,上面这几个,php都支持~~\u003C\u002Fp\u003E\u003Cp\u003E好了,知道了JS的NFA支持度就能对它能够实现的表达式有个大致的了解了,下面简单说一下NFA引擎的回溯机制。\u003C\u002Fp\u003E\u003Cp\u003E正则表达式的回溯机制很简单,就是从左到右不停的和待匹配的文本进行对比,如果只是纯字符很好办\u002Fhello\u002F,正则表达式对比完整个文本就可以知道有没有这个了,但加入量词和多选分组以后,意义就不同了。\u003C\u002Fp\u003E\u003Cp\u003E举例来说, \u002Fa(.*)z\u002F 用来匹配 fadsazsfajlsdajfladsb,那么它首先会对比第一个字符,发现不对后,它从文本的第一个字符开始,这样a对应上了,这个时候就开始下面的.的匹配,大家都知道点号.可以匹配除了换行符以外的任何字符,而*代表这个字符可能重复多次也可能一次都不出现,此时对比下一个字符 d ,它是可以匹配的,所以继续下去,一直结尾(因为它是匹配优先的,所以.*会直接到行结尾),这个时候匹配出现问题了,因为后面还有个z需要被匹配到,此时回溯机制开始发挥作用,它会沿着上次的选择来吐出之前匹配的一个字符,不停的重复动作,直到z字符,找到以后,整个表达式就匹配成功了,这样匹配结束了,可喜可贺。\u003C\u002Fp\u003E\u003Cp\u003E但是如果用它来匹配 adddfsf呢? a先被匹配到,然后下一个d也可以被匹配到,一直到结尾,这个时候它也会照着老方法,吐出字符,然后返回找z,但是一直吐出到第一个匹配的a的时候,它还是没有找到最后一个z,所以它匹配是失败的,返回的结果也是失败的。\u003C\u002Fp\u003E\u003Cp\u003E回溯就是这么一个机制,它在正则表达式面临多选的情况下(比如这里的量词*就是一个多选,它会想着是不是要匹配),会记录下自己还没有尝试的选择,作为一个存档点,在挂了(匹配失败)以后返回存档点,然后尝试另一条路。\u003C\u002Fp\u003E\u003Cp\u003E知道了回溯机制,就明白了为什么不好的表达式会导致性能问题,你可能会造成大量的回溯(毕竟正则引擎要记住待选状态)\u003C\u002Fp\u003E\u003Cp\u003E下面简单举两个例子帮助大家理解一下。\u003C\u002Fp\u003E\u003Cp\u003E第一个是很常见的例子,把一串连续数字变成三位分割的数字,比如1234变成1,234,这种在现实生活中很常见。\u003C\u002Fp\u003E\u003Cp\u003E首先分析一下需求,要把1234变成1,234,区别就在于,逗号前面需要一个数字,比如234这个是不用改的,然后逗号后面要三个数字,你不可能用1,23,4这样。\u003C\u002Fp\u003E\u003Cp\u003E理解这个就好办了,直接写一个\u003C\u002Fp\u003E\u003Ccode lang=\&js\&\u003E \u002F(\\d)(\\d\\d\\d)+\u002F\u003C\u002Fcode\u003E\u003Cp\u003E 这个前面的\\d匹配了一个数字,后面的三个\\d代表着把后面的数字分成三个三个一组,这样就可以了么?事实证明想法太简单了,因为它会把12345变成1,234,5,它从遇到的第一个数字\\d就开始匹配了,然后每隔三个加一个逗号,所以会变成这样。\u003C\u002Fp\u003E\u003Cp\u003E那么我们思考一下,(\\d\\d\\d)这个必须要保证至少有一组或好几组的情况下,才能让前面的数字\\d能够匹配,这样来的结果才有意义,所以必须要用到正序断言,改进如下 \u002F(\\d)(?=(\\d\\d\\d)+)\u002F ,这样保证最后面的必然是一个三位连续的数字\u003C\u002Fp\u003E\u003Cp\u003E但是这样又带来一个问题,就是前面的数字会被断成一个一个的,类似234567,它只能保证后面的必然是567,但之前的每一个的后缀都能匹配到三位数字,所以它的结果会变成2,3,4,567,这显然不是我要的结果。\u003C\u002Fp\u003E\u003Cp\u003E之所以被断掉,是因为它只考虑到\\d\\d\\d出现了,但没有想到它后面的数字是什么样的,我们改进一下,加一条否定断言 \u002F(\\d)(?=(\\d\\d\\d)+(?!\\d))\u002F 这个否定断言是的意思是分三个数一组不断重复之后,最后面一定没有数字。 也就是说 123456,它先匹配1,然后找后面的234,这个匹配到了,但是后面56不符合三位一组的要求,根据否定断言的判断,它还是一个数字,所以不能就这样结束了,这样整个数字没有被匹配到,它于是会认为失败,然后回溯到第一个数字1,进一位,到2,这个时候依然是失败的,于是到3,这个时候成功了,因为3后面的数字是456,而456的后面没有任何数字了,被否定断言认同了,所以它是成功的。\u003C\u002Fp\u003E\u003Cp\u003E不过这里有个缺点在于括号,括号分组会让内存记录下来匹配结果,也就是捕获结果,我们并不需要其中的结果,可以用非捕获分组来去掉它\u003C\u002Fp\u003E\u003Cp\u003E综合上诉,表达式就清楚了 \&7\&.replace(\u002F(\\d)(?=(?:\\d\\d\\d)+(?!\\d))\u002Fg,\&$1,\&) 大家可以试验一下\u003C\u002Fp\u003E\u003Cp\u003E第二个例子也很常用,比如一篇文章里,出现了美元100$和100¥这两种货币,我想用一句正则表达式把美元换成人民币,把人民币转换成美元。\u003C\u002Fp\u003E\u003Cp\u003E分析一下需求,就明白需要找到的无非是数字和币种,然后用函数转换var content='……'; content.replace(\u002F\u002Fg,function(m){……})\u003C\u002Fp\u003E\u003Cp\u003E所以我们来找到数字和币种,首先想到的就是 \u002F(\\d+)([$¥])\u002F,前一个匹配一连串数字,然后字符组匹配$或者¥,它可以匹配100$和100¥这样的字符串,不过我们遇到的数字的格式未必是这样的,比如说文章里有1,234.5$的数字,我们也想匹配到,这个时候就不能直接使用\\d+了。\u003C\u002Fp\u003E\u003Cp\u003E让我们修改一下,\u002F(\\d[,\\d.]*)([$¥])\u002F ,这个时候它可以匹配到1,234.5$了,但是它也同样可以匹配到1,2.........3,4.5$这种明显不符合规定的数字\u003C\u002Fp\u003E\u003Cp\u003E再修改一下\u002F\\d(?:,(?=\\d\\d\\d)|\\d)*(?:\\.\\d+)?[$¥]\u002F,这里大概的意识是,先至少匹配一个数字,然后查看是否是逗号接着三个数字,然后再查看是否有小数点,所以它能够匹配1,234.50$这样的了,而且它不会匹配错误的数字比如 上一天分类hao123,3000$,这个时候它只会匹配后面的3000$,因为123,3000不是一个数字,当然如果是123,300就没办法了。\u003C\u002Fp\u003E\u003Cp\u003E这个时候我们已经可以用它来替换了var content='……'; content.replace(\u002F(\\d(?:,(?=\\d\\d\\d)|\\d)*(?:\\.\\d+)?)([$¥])\u002Fg,function(m,s1,s2){\u002F\u002Fs2为币种,s1为数字})\u003C\u002Fp\u003E\u003Cp\u003E但上面的方法是否恰当?答案是否定的,因为事实上,在这里用一个很复杂的正则表达式来进行精确的数字匹配,不是最优解,完全可以使用\u002F(\\d[,\\d.]*)([$¥])\u002F来进行匹配一系列疑似的数字,然后再用JS的字符串对数字进行分析,找出需要的数字进行转换。\u003C\u002Fp\u003E\u003Cp\u003E所以正则不是万能的,在能用其它办法进行处理的时候,可以适当降低一下正则的复杂度,这对于性能来说也是很有帮助的。\u003C\u002Fp\u003E\u003Cp\u003E下面再说一下固定分组和占位量词,它们在JS的NFA中并不受到支持,但之所以提出来,是因为他们对于优化回溯很有帮助,它们可以放弃分组内的存档点,这样的话可以让你不用尝试所有路线而加速匹配失败,这对于复杂正则表达式的性能很有用。\u003C\u002Fp\u003E\u003Cp\u003EJS虽然不支持上面两个,但可以简单使用正序断言加上反向引用来进行模拟 ,比如我可以使用\u002F(?=(\\d+))\\1:\u002F这个正则表达式来匹配 : 这种数字。\u003C\u002Fp\u003E\u003Cp\u003E为什么不用\u002F\\d+:\u002F而用上面的那个呢?原因其实很简单,因为面对最后没有冒号的情况时,回溯这个时候开始起作用,它会不停的向前一个存档点移,而且是一个数字一个数字退,这个时候虽然我们人一眼就判断出,如果后面没有冒号,那必然是匹配失败,但是NFA引擎却没有这种能力,它只能回溯,而这是徒劳的。\u003C\u002Fp\u003E\u003Cp\u003E使用\u002F(?=(\\d+))\\1:\u002F的时候,前面一个正序断言会匹配一堆数字,但是当它退出这个括号的时候,会直接放弃掉所有内部保存的存档点,反向应用\\1会把之前正序断言内部的捕获接收掉,所以不需要再次匹配了,而后面的冒号如果没有匹配到,它这个时候由于没有存档点,会立马知晓自己已经匹配失败,返回失败的速度大大提高。\u003C\u002Fp\u003E\u003Cp\u003E当然这比php等语言内置的固定分组和占位量词要慢些,但报告失败的速度比其它的要快。\u003C\u002Fp\u003E\u003Cp\u003E在实际做JS的正则的时候,可以结合需要匹配的文本来模拟匹配,注意匹配永远都是从左到右,回溯永远都是从右到左,回溯的多并不可怕,NFA本身有一套优化方案,但切记不要使用(.+)*这种错误的方法匹配文本,它的回溯是n的n+1次方,甚至有可能会导致堆栈溢出。\u003C\u002Fp\u003E&,&updated&:new Date(&T13:26:13.000Z&),&canComment&:false,&commentPermission&:&anyone&,&commentCount&:1,&likeCount&:6,&state&:&published&,&isLiked&:false,&slug&:&&,&isTitleImageFullScreen&:false,&rating&:&none&,&sourceUrl&:&&,&publishedTime&:&T21:26:13+08:00&,&links&:{&comments&:&\u002Fapi\u002Fposts\u002F2Fcomments&},&url&:&\u002Fp\u002F&,&titleImage&:&https:\u002F\u002Fpic3.zhimg.com\u002Fbf38ff44eba86be412d79e657e608355_r.jpg&,&summary&:&&,&href&:&\u002Fapi\u002Fposts\u002F&,&meta&:{&previous&:null,&next&:null},&snapshotUrl&:&&,&commentsCount&:1,&likesCount&:6},&&:{&title&:&【科普向】JavaScript的四则符和比较符&,&author&:&browsnet&,&content&:&\u003Cp\u003E看一下这个\u003C\u002Fp\u003E\u003Cbr\u003E\u003Ccode lang=\&js\&\u003Evar a=10;\nvar b=1;\nconsole.log(a+b); \u002F\u002F11\nconsole.log(a-b);
\u002F\u002F9\nconsole.log(a&b);
\u002F\u002Ftrue\nconsole.log(a&b);
\u002F\u002Ffalse\n\u003C\u002Fcode\u003E\u003Cbr\u003E\u003Cbr\u003E\u003Cp\u003E相信大家都能一眼看出结果,符合常理,不过如果计算的时候不是数字呢?\u003C\u002Fp\u003E\u003Cbr\u003E\u003Ccode lang=\&js\&\u003Evar a=\&10\&;\nvar b=1;\nconsole.log(a+b); \u002F\u002F \&101\&\nconsole.log(a-b);
\u002F\u002F9\nconsole.log(a&b);
\u002F\u002Ftrue\nconsole.log(a&b);
\u002F\u002Ffalse\n\u003C\u002Fcode\u003E\u003Cbr\u003E\u003Cbr\u003E\u003Cp\u003E可以看到当a的类型变成字符串的时候,a+b的结果已经不一样了,是一个字符串的”101”,这就是加号(+)的一个二义性,务必记住:当加号两边含有字符串(String)的时候,它们不会进行数学运算的加法,而会执行字符串连接。\u003C\u002Fp\u003E\u003Cp\u003E加号作为连字符大家想必早已经熟悉,那么减号(-)是怎么回事呢?为什么字符串减去一个数字的时候,会得到一个新的数字?\u003C\u002Fp\u003E\u003Cp\u003E其实是字符串在执行减法运算的时候,会先把自己转换成一个数字,然后再做运算,也就是说字符串”10”在做减法之前,会直接变成数字10,然后和后面的1相减,得出的结果自然就是数字9.\u003C\u002Fp\u003E\u003Cp\u003E这个不只是减法,实际上乘(*),除以(\u002F),求余(%)都是这样的步骤,先把字符串转换成数字,再做运算。\u003C\u002Fp\u003E\u003Cp\u003E需要注意的是,这里的字符串转成数字,并不是执行parseInt或者parseFloat等函数,而是使用了内部转换,规则看一下例子就明白了\u003C\u002Fp\u003E\u003Cbr\u003E\u003Ccode lang=\&js\&\u003E\&123\&
==& 123\n\&
==& 123\n\&
\& ==& 123\n\&a12\&
==& NaN\n\&1 2\&
==& NaN\n\&1,2\&
==& NaN\n\&1b2\&
==& NaN\n\&12c\&
==& NaN\n\&1.234\&
==& 1.234\n\&
1.234\& ==& 1.234\n\& .234\&
==& 0.234\n\u003C\u002Fcode\u003E\u003Cbr\u003E\u003Cbr\u003E\u003Cp\u003E规则还是很简单的吧,记住了这个就会明白字符串的四则的结果是怎么样的了\u003C\u002Fp\u003E\u003Cp\u003E那么除了字符串,其他基本类型,比如undefined,null,boolean,这些是如何运算的呢?\u003C\u002Fp\u003E\u003Cp\u003E其实很简单,只要记住下面即可\u003C\u002Fp\u003E\u003Cbr\u003E\u003Ccode lang=\&js\&\u003Eundefined ==& NaN\nnull ==& 0\ntrue ==& 1\nfalse ==& 0\n\u003C\u002Fcode\u003E\u003Cbr\u003E\u003Cbr\u003E\u003Cp\u003E知道了这些,我们下面来看看高级点的东西,比如array\u003C\u002Fp\u003E\u003Cbr\u003E\u003Ccode lang=\&js\&\u003Evar a=[10,11];\nvar b=1;\nconsole.log(a+b); \u002F\u002F \&10,111\&\nconsole.log(a-b);
\u002F\u002FNaN\nconsole.log(a&b);
\u002F\u002Ffalse\nconsole.log(a&b);
\u002F\u002Ffalse\n\u003C\u002Fcode\u003E\u003Cbr\u003E\u003Cbr\u003E\u003Cp\u003E上面把a换成一个数组,这个时候再对它进行运算,大家会看到结果已经完全变了,为什么会出现这个结果呢?\u003C\u002Fp\u003E\u003Cp\u003E(注意,以下说的基本类型指的是undefined,null,true,false,Number,String等)\u003C\u002Fp\u003E\u003Cp\u003E事实上,由于数组不是一个基本类型,所以JavaScript对它执行四则或者比较的时候,会预先执行一个叫ToPrimitive的步骤,就是先把数组或者对象转成一个基本类型。\u003C\u002Fp\u003E\u003Cp\u003E转换的步骤是这样的,第一步是执行 对象的valueOf函数,如果返回值是一个基本类型,那么就直接用这个返回值来运算\u003C\u002Fp\u003E\u003Cp\u003E如果valueOf函数得到的结果并不是基本类型,那么就会继续执行toString()函数,如果它的返回值是一个基本类型,那么就用这个返回值来运算。\u003C\u002Fp\u003E\u003Cp\u003E如果上面两个都没有满足,会抛出一个Cannot convert object to primitive value的类型错误\u003C\u002Fp\u003E\u003Cp\u003E上面的道理明白了以后,那么再回过头来看看那个数组的运算。\n这里的a定义是一个值为[10,11]的数组,我们首先看看它的valueOf的值,实际上,[10,11].valueOf()的返回值依旧是一个数\n组,而不是上面所说的基本类型,所以再查看一下它的toString()的值,结果是\&10,11\&,这是一个字符串,符号上面所说的基本类型,所以这个\n返回值是可以用来做运算的。\u003C\u002Fp\u003E\u003Cp\u003E所以上面的a在做运算的时候,都可以看成是一个字符串”10,11”来做运算,这样结果就不言而喻\u003C\u002Fp\u003E\u003Cp\u003E由于有字符串参与,所以加号是连字符,a+b的返回值是字符串\&10,111\&\n减法的时候,字符串先预转成数字,我们前面的规则告诉我们\&10,11\&这个字符串,预转数字的时候是NaN,对NaN执行任何四则运算结果都是NaN。\n比较的时候同减法,由于a这个时候相当于NaN,而NaN和任何数字(包括它自己)都不相等,对它做任何大小比较也都会返回false。\u003C\u002Fp\u003E\u003Cp\u003Eobject实际上在执行运算的时候和array相同,遵行同样的法则,先查看valueOf,不符合条件再查看toString,得到的基本类型再做运算。\u003C\u002Fp\u003E\u003Cbr\u003E\u003Ccode lang=\&js\&\u003Evar a={a:1,b:2,c:3};\nvar b=1;\nconsole.log(a+b); \u002F\u002F \&[object Object]1\&\nconsole.log(a-b);
\u002F\u002FNaN\nconsole.log(a&b);
\u002F\u002Ffalse\nconsole.log(a&b);
\u002F\u002Ffalse\n\u003C\u002Fcode\u003E\u003Cbr\u003E\u003Cbr\u003E\u003Cp\u003E上面的结果应该不用说都明白了吧,下面我们来看个稍微复杂的例子\u003C\u002Fp\u003E\u003Cbr\u003E\u003Ccode lang=\&js\&\u003Evar a={a:1,b:2,c:3,valueOf:function(){return 10}};\nvar b=1;\nconsole.log(a+b); \u002F\u002F 11\nconsole.log(a-b);
\u002F\u002F9\nconsole.log(a&b);
\u002F\u002Ftrue\nconsole.log(a&b);
\u002F\u002Ffalse\n\u003C\u002Fcode\u003E\u003Cbr\u003E\u003Cbr\u003E\u003Cp\u003E这个结果和第一个例子的结果完全一致,其原因在于我们重写了valueOf函数,它返回了一个基本类型数字10,这个返回值10由于是基本类型,所以可以用来做比较,那么下面对它做运算的时候,实际上都是用10这个数字来运算的。\u003C\u002Fp\u003E\u003Cp\u003E我们下面同时重写一下a的toString方法,让它返回一个字符串。\u003C\u002Fp\u003E\u003Cbr\u003E\u003Ccode lang=\&js\&\u003Evar a={a:1,b:2,c:3,valueOf:function(){return 10},toString:function(){return 'abc'} };\nvar b=1;\nconsole.log(a+b); \u002F\u002F 11\nconsole.log(a-b);
\u002F\u002F9\nconsole.log(a&b);
\u002F\u002Ftrue\nconsole.log(a&b);
\u002F\u002Ffalse\n\u003C\u002Fcode\u003E\u003Cbr\u003E\u003Cbr\u003E\u003Cp\u003E可以看到结果没有任何影响,其原因是valueOf返回了基本类型,后面的toString对于四则和比较运算已经没了意义。\u003C\u002Fp\u003E\u003Cp\u003E如果把valueOf去掉或者让valueOf返回一个非基本类型的话,就能看到另一番结果了。\u003C\u002Fp\u003E\u003Cbr\u003E\u003Ccode lang=\&js\&\u003Evar a={a:1,b:2,c:3,valueOf:function(){return []},toString:function(){return true} };\nvar b=1;\nconsole.log(a+b); \u002F\u002F 2\nconsole.log(a-b);
\u002F\u002F 0\nconsole.log(a&b);
\u002F\u002F false\nconsole.log(a&b);
\u002F\u002F false\nconsole.log(a==b);
\u002F\u002Ftrue\n\u003C\u002Fcode\u003E\u003Cbr\u003E\u003Cbr\u003E\u003Cp\u003E上面这里的运算,a就相当于一个数字1。\u003C\u002Fp\u003E\u003Cp\u003E我们上面说到的加号可以作为连字符,还有一种操作,比如[1,2,3].join(”)也可以把数组格式化成一个字符串,这个时候如果数组里有一个非基本类型的值,比如[1,{a:2},3]这样的数组,那么它执行join的时候是怎么样的呢?\u003C\u002Fp\u003E\u003Cp\u003E实际上还是需要执行ToPrimitive这个步骤,只不过这个时候是要把DefaultValue指定为String,所以它转换成字符串是这么\n一个顺序:先查看toString(),如果返回基本类型,那么就用这个返回值,如果不是,则查看valueOf(),如果返回基本类型,那么就用它,否\n则报typeerror的错误。\u003C\u002Fp\u003E\u003Cp\u003E和上面讲的四则的ToPrimitive顺序是反着的,大家要注意一下。\u003C\u002Fp\u003E\u003Cp\u003E总结一下:\u003C\u002Fp\u003E\u003Col\u003E\u003Cli\u003E数组和对象做四则或比较运算,会先查看其valueOf的返回值。\u003C\u002Fli\u003E\u003Cli\u003E返回值不是基本类型(undefined,null,true,false,Number,String)的时候,查看其toString()的值。\u003C\u002Fli\u003E\u003Cli\u003EtoString的返回值不是基本类型则报错,是则使用返回的值来运算。\u003C\u002Fli\u003E\u003Cli\u003E字符串或者其它非数字的基本类型执行预算的时候会预转为数字,规则在上面写了。 \u003C\u002Fli\u003E\u003Cli\u003E如果含有一个字符串,那么加号(+)的意义不再是四则加,而是连字符。 \u003C\u002Fli\u003E\u003Cli\u003ENaN是个特殊的数字,很多的时候字符串会变成它来进行运算,这个时候得到的结果是无意义的。 \u003C\u002Fli\u003E\u003Cli\u003E大小比较和减法类似,如果a-b是个大于0的数字,那么a就大于b,反之亦然。\u003C\u002Fli\u003E\u003C\u002Fol\u003E&,&updated&:new Date(&T16:34:24.000Z&),&canComment&:false,&commentPermission&:&anyone&,&commentCount&:1,&likeCount&:5,&state&:&published&,&isLiked&:false,&slug&:&&,&isTitleImageFullScreen&:false,&rating&:&none&,&sourceUrl&:&&,&publishedTime&:&T00:34:24+08:00&,&links&:{&comments&:&\u002Fapi\u002Fposts\u002F2Fcomments&},&url&:&\u002Fp\u002F&,&titleImage&:&https:\u002F\u002Fpic1.zhimg.com\u002F0cded3ad55abf_r.jpg&,&summary&:&&,&href&:&\u002Fapi\u002Fposts\u002F&,&meta&:{&previous&:null,&next&:null},&snapshotUrl&:&&,&commentsCount&:1,&likesCount&:5},&&:{&title&:&简说Racket的模块&,&author&:&browsnet&,&content&:&\u003Cp\u003E模块化的前提是多文件化,就是把不同的代码放在不同的文件里,以期获得良好的组织,现在任何一门语言都有这样的功能,就像C语言的#include \&stdio.h\&一样,racket的(require \&abc.rkt\&)也可以把同目录下的rkt文件导入进本文件里。\u003C\u002Fp\u003E\u003Cp\u003Erequire有多种不同的写法,而且可以使用连续的(require a b c)形式加载多个模块\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E(require \&.\u002Fabc.rkt\&)
字符串路径是常用的require方法,它会去寻找本文件目录下的相对路径,找到后加载它。\u003C\u002Fli\u003E\u003Cli\u003E(require abc) 这种加载方法一般称为库集(Library Collections),abc 为库集的id,实际上,查找库集一般是这么做的\u003C\u002Fli\u003E\u003Cul\u003E\u003Cli\u003E如果是abc后面没有\u002F,那么自动在abc补全为abc\u002Fmain,如果abc\u002Fxxx的话则跳过\u003C\u002Fli\u003E\u003Cli\u003E然后如果没有加后缀.rkt,则自动加上文件后缀名,比如上面会成为abc\u002Fmain.rkt\u003C\u002Fli\u003E\u003Cli\u003E系统会在两个目录下搜索abc\u002Fmain.rkt,一个是racket安装目录下的collects,还有一个是对应的个人文件夹,具体的路径可以用(get-collects-search-dirs)看到。\u003C\u002Fli\u003E\u003Cli\u003E找到了文件以后并加载。\u003C\u002Fli\u003E\u003Cli\u003E使用 (lib \&abc\&)后,步骤也和上面类似。\u003C\u002Fli\u003E\u003Cli\u003Eracket还提供一种叫planet的方式,是可以很方便的使用第三方库 (require (planet schematics\u002Frandom:1\u002Frandom))\u003Cbr\u003E\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cli\u003E(require 'abc) 这时后面的被称为quote id,它一般用于在本文件里require子模块或者其它模块\u003C\u002Fli\u003E\u003Cli\u003E require还提供一种访问submod的方法,下面会说到。\u003Cbr\u003E\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cp\u003E但光是把代码分散到各个文件是不够的,racket想做的更多,所以它加入了模块、访问控制、类集合等特性,实际上这些特点在其他语言上都有很多,不算稀奇,简要说一下它的特点。\u003C\u002Fp\u003E\u003Cp\u003E首先是模块,实际上在racket的,任何文件都是一个模块,在.rkt文件里常见的开头比如#lang racket或者#lang racket\u002Fbase,都是模块的缩略写法。hello文件里的#lang racket实际上等于下面的写法\u003Cbr\u003E\u003C\u002Fp\u003E\u003Ccode lang=\&racket\&\u003E(module hello racket\n
(define a 123)\n
(define b 234)\n
(define c (lambda (a b) (+ a b))))\n\u003C\u002Fcode\u003E\u003Cp\u003E在文件的任何一处都能定义一个module,module内部也还能继续定义module,它的代码独立,不会污染module外的内容。\u003C\u002Fp\u003E\u003Cp\u003E这种方式类似JAVA里在同一个文件里定义一系列辅助类,不过区别在于要想在同文件里使用新定义的module,必须要(require 'hello)才行。\u003C\u002Fp\u003E\u003Cp\u003Eracket的访问控制有几个方面\u003C\u002Fp\u003E\u003Col\u003E\u003Cli\u003Emodule外部的代码想要使用一个module,必须先要require它,同一个module内部的代码只有在第一次被require的时候才会执行,而且只会执行一次,这类似于Node.js的require('http')的作法,与之不同的是Node.js里的require不能局限于文件的部分代码。\u003C\u002Fli\u003E\u003Cli\u003Eprovide是控制module里导出的语句,也就是说,只有在module里(provide hello) 才能在其他module里使用hello这个定义。 \u003C\u002Fli\u003E\u003Cli\u003Emodule内部定义的module叫做子模块(submodule),它无法require到外面的module,这样也就无法使用到外面module导出的定义,但是可以在定义子模块的时候使用 (module* mysub #f ……)或者缩写为(module+ mysub ……),这样在内部就可以使用外部module的任何定义,甚至是没有被provide的。缺点是这种定义的子模块,无法被子模块外部的module require\u003C\u002Fli\u003E\u003Cli\u003Erequire后的module,无法用(set! a 123)或者(define a 123)这种形式重新赋值或者定义,其原因在于module可能被多个文件重用,必须保证内部常量或者函数没有被更改,如果确实要更改,需要使用compile-enforce-module-constants来开关\u003C\u002Fli\u003E\u003C\u002Fol\u003E\u003Cp\u003E模块的导出提供了访问控制,不过如果每个定义都要provide一下的话,那会显得很繁琐,所以racket提供了all-defined-out、all-from-out等形式,让用户能批量导出,甚至可以在导出的时候重新命名已有的定义\u003C\u002Fp\u003E\u003Cp\u003E有时候会碰到这么一个问题,就是(require a b)以后,a和b模块同时都有导出了一个定义 (hello),那么require之后就陷入了命名冲突,这时候解决办法是改名,所以require里可以用(rename-in )或者(only-in)来改变导入进来的定义。\u003C\u002Fp\u003E\u003Cp\u003E或者可以采用统一给模块a的定义加上前缀,没有命名空间的C语言或者Objective-C就是采用在变量前面加上私有前缀来避免冲突,在racket里,require时候可以统一给模块加上前缀,比如 (require (prefix-in m: abc))\u003C\u002Fp\u003E\u003Cp\u003E总体而言,racket的模块化做的还是不错的,而且module还有自动下载安装的特殊功能,在开发的时候可以节省不少时间。\u003C\u002Fp\u003E&,&updated&:new Date(&T16:21:57.000Z&),&canComment&:false,&commentPermission&:&anyone&,&commentCount&:0,&likeCount&:5,&state&:&published&,&isLiked&:false,&slug&:&&,&isTitleImageFullScreen&:false,&rating&:&none&,&sourceUrl&:&&,&publishedTime&:&T00:21:57+08:00&,&links&:{&comments&:&\u002Fapi\u002Fposts\u002F2Fcomments&},&url&:&\u002Fp\u002F&,&titleImage&:&https:\u002F\u002Fpic4.zhimg.com\u002F8facd09bc0f_r.jpg&,&summary&:&&,&href&:&\u002Fapi\u002Fposts\u002F&,&meta&:{&previous&:null,&next&:null},&snapshotUrl&:&&,&commentsCount&:0,&likesCount&:5},&&:{&title&:&DbC里的pre-conditions, post-conditions,invariants&,&author&:&browsnet&,&content&:&\u003Cp\u003EDesign by Contract(DbC,契约式设计)是一种编程思想,Eiffel语言的创建人提出了这一概念并将其融合进Eiffel,后来有两位专家Richard Mitchell和Jim Mckim写了一本相关的书,03年的时候孟岩翻译为中文版并发了几篇文章讲述C++里如何引入DbC。\u003C\u002Fp\u003E\u003Cp\u003EDbC在Eiffel语言、Perl、Clojure、racket里都有内置,在微软的 .NET4.0有个Code
Contracts也是类似的设计思想,本文以racket为例。\u003C\u002Fp\u003E\u003Cp\u003E 大体上有三个概念,pre-conditions (前置条件), post-conditions(后置条件) 和 invariants(不变性)。\u003C\u002Fp\u003E\u003Cp\u003E前置条件发生在函数被call的时候,而后置条件发生在函数return的时候\u003C\u002Fp\u003E\u003Ccode lang=\&racket\&\u003E#lang racket\n(module one racket\定义一个可供读写的level\n
(define level 0)\n
(define (set-level num)\n
(set! level num))\n
(define (get-level) level)\导出setter、getter\n
(provide set-level get-level)\n)\n;导入上面这个模块\n(require 'one)\n(get-level)0\n(set-level 10)\n(get-level)10\n\u003C\u002Fcode\u003E\u003Cp\u003E 上面这个例子,模块里有一个level变量,然后有一个setter和getter函数可供查询和修改,但考虑到racket是弱类型的语言,我们如果执行(set-level \&二十级\&)这样的语句,程序照样会执行成功,这明显是不对的。\u003C\u002Fp\u003E\u003Cp\u003E所以可以为racket代码加上一点简单的契约,让我们修改一下\u003C\u002Fp\u003E\u003Ccode lang=\&racket\&\u003E;导出setter、getter\n(provide get-level)\n(provide (contract-out \n
[set-level (-& exact-nonnegative-integer? any)]))\n\u003C\u002Fcode\u003E\u003Cp\u003E这里的-&是个契约连接符,racket还支持另外一种写法 (exact-nonnegative-integer? . -& . any),意思是一样的,最后一个表达式(这里是any)代表的是return值检查,也就是所谓的post-conditions(后置条件),其余部分则是函数调用时对参数进行检查,也就是所谓的pre-conditions (前置条件)。\u003C\u002Fp\u003E\u003Cp\u003E上面的代码的意思是为set-level加上一个契约,这里的pre-conditions是挨个检查set-level的参数,参数num的契约就是exact-nonnegative-integer?,它是个组合判断函数,它的意思是 (and (integer? v) (exact? v) (not (negative? v))) ,翻译过来就是 确切、整数、非负的意思,也就是所谓的非负自然数,比如0,1,2,3这类。\u003C\u002Fp\u003E\u003Cp\u003E契约里的any代表任意返回值,实际上any是不检查返回值的,也就是说在这里没有\u003Cbr\u003E后置条件。\u003C\u002Fp\u003E\u003Cp\u003E此时我们再执行(set-level \&二十级\&)的时候,会提示错误,这时候就能保证我们的等级是一个非负自然数。\u003C\u002Fp\u003E\u003Cp\u003E当然这样还是有些问题的,虽然等级是非负自然数,但也是有上限的,假如我们最高上限是60,如果是(set-levl 200)这样的语句也是明显不合逻辑的。\u003C\u002Fp\u003E\u003Cp\u003E所以我们需要进一步改进契约,给它增加一个上限\u003C\u002Fp\u003E\u003Ccode lang=\&racket\&\u003E;导出setter、getter\n(provide get-level)\n(provide (contract-out \n
[set-level \等级要是非负自然数且小于等于60\n
(-& (and\u002Fc exact-nonnegative-integer? (&=\u002Fc 60))\n
any)]))\n\u003C\u002Fcode\u003E\u003Cp\u003E 通过这么一个前置条件,我们已经能够保证set-level在调用的时候,参数必定是一个0到60之间的自然数,符合我们的需求。\u003C\u002Fp\u003E\u003Cp\u003E 那么后置契约顾名思义就是对返回值进行检查了,假设我们现在需要获取一个用户的名字,并利用这个名字执行其他动作,那么这个函数get-name就必须要对返回值负责,它要求返回一个不超过十个字符的字符串\u003C\u002Fp\u003E\u003Ccode lang=\&racket\&\u003E;定义并导出get-name\n(define\u002Fcontract (get-name)\n
(-& any\u002Fc \n
(and\u002Fc string? (string-len\u002Fc 10)))\n
name)\n\u003C\u002Fcode\u003E\u003Cp\u003E 这时候我们可以看到(-& ...)表达式里的最后一个参数,就是对return检查,此时他保证函数调用的结果一定是个不超过十个字符的字符串,如果不是,那么就是函数内部出了问题,和调用者无关。\u003C\u002Fp\u003E\u003Cp\u003E当对一个变量定义一个契约的时候,那么它在以后的生命里就必定要满足这个契约,这就是保持了它的不变性。\u003C\u002Fp\u003E\u003Cp\u003E举例说明\u003C\u002Fp\u003E\u003Ccode lang=\&racket\&\u003E;定义money并带上契约,要求大于等于0的整数\n(define\u002Fcontract money \n
(and\u002Fc integer? (&=\u002Fc 0) 10.0)\n;test\n;(set! money \&hello\&)\n;(set! money -4)\n;(set! money 10.01)\n(set! money 20);20\n\n\u003C\u002Fcode\u003E\u003Cp\u003E 我们保证了这个money的不变性,他不会因为值的改变而突破自己的契约。\u003C\u002Fp\u003E\u003Cp\u003E以上就是一些简单说明,racket里还支持较为复杂的契约定义,大多集中在函数的契约,比如可变参数的函数、带keyword的函数、带默认值的函数(一般用 -&* 标识符),case-lambda的函数,还支持-&i,-&d这些标识符,可以让参数因参数而改变(前置),返回值因参数而改变(后置)等等,总之可以保障大多数想法得以实现。\u003C\u002Fp\u003E\u003Cp\u003EDbC作为一种设计的方法论,曾经被提倡过,然而这个方法却并没有大热,原因可能在于设计契约的时候,契约本身可能会导致代码复杂,而且契约的前置、后置约束会降低程序整体性能,所以他虽然有好处,也有不好的一面,如何权衡,需要在程序设计的时候仔细定夺。\u003C\u002Fp\u003E&,&updated&:new Date(&T00:48:22.000Z&),&canComment&:false,&commentPermission&:&anyone&,&commentCount&:0,&likeCount&:2,&state&:&published&,&isLiked&:false,&slug&:&&,&isTitleImageFullScreen&:false,&rating&:&none&,&sourceUrl&:&&,&publishedTime&:&T08:48:22+08:00&,&links&:{&comments&:&\u002Fapi\u002Fposts\u002F2Fcomments&},&url&:&\u002Fp\u002F&,&titleImage&:&https:\u002F\u002Fpic4.zhimg.com\u002Ff2a745bbe788_r.jpg&,&summary&:&&,&href&:&\u002Fapi\u002Fposts\u002F&,&meta&:{&previous&:null,&next&:null},&snapshotUrl&:&&,&commentsCount&:0,&likesCount&:2},&&:{&title&:&魅族论坛的模拟登录示例(node.js)&,&author&:&browsnet&,&content&:&这是给群里的一哥们写的一个登录魅族社区的demo
\u002F\u002F所以请忽略~~\u003Cbr\u003E\u003Ccode lang=\&js\&\u003E\u002F\u002F 引证 \nvar http=require(\&http\&),https=require(\&https\&);\nvar querystring=require(\&querystring\&),url=require(\&url\&);\nvar UA='Mozilla\u002F5.0 (M Intel Mac OS X 10_10_2) AppleWebKit\u002F537.36 (KHTML, like Gecko) Chrome\u002F42.0.2311.39 Safari\u002F537.36';\nvar username=\&\&,password=\&\&;\n\u002F\u002F 登录到member.meizu.com\nlogin(username,password).then(function(data){\n
\u002F\u002F 通过跳转链接获取cookie\n
return getCookie(data);\n}).then(function(cookie){\n
\u002F\u002F 通过带cookie访问其他页面,比如通知\n
return getNotice(cookie);\n}).then(function(result){\n
\u002F\u002F 通知ajax的结果\n
console.log(result);\n}).catch(function(err){\n
console.error(err);\n});\n\n\u002F\u002F 登录到member.meizu.com\nfunction login(username,password){\n
var postData={\n
service:\&bbs\&,\n
appuri:\&http:\u002F\u002Fbbs.meizu.cn\u002Flogging.php\&,\n
useruri:\&http:\u002F\u002Fbbs.meizu.cn\u002F\&,\n
account:username,\n
password:password\n
var postString=querystring.stringify(postData);
var options = {\n
host:'member.meizu.com',\n
port:443,\n
method:'POST',\n
path:'\u002Fsso\u002Flogin',\n
headers:{\n
'Referer':'https:\u002F\u002Fmember.meizu.com\u002Fsso?appuri=http%3A%2F%2Fbbs.meizu.cn%2Flogging.php&useruri=http%3A%2F%2Fbbs.meizu.cn%2F&sid=&service=bbs&autodirct=trueg',\n
'Origin' : 'https:\u002F\u002Fmember.meizu.com',\n
'Host': 'member.meizu.com',\n
'Content-Type':'application\u002Fx-www-form-urlencoded',\n
'User-Agent:':UA,\n
'Content-Length':postString.length\n
return new Promise(function(resolve,reject){\n
var request=https.request(options,function(res){\n
var bufferArr=[];\n
res.on(\&data\&,function(data){\n
bufferArr.push(data);\n
res.on(\&end\&,function(){\n
var cookieArr = res.headers[\&set-cookie\&];\n
var cookie=trasCookie(cookieArr);\n
var data= Buffer.concat(bufferArr).toString();\n
var json=JSON.parse(data);\n
if(json.value){\n
resolve({url:json.value,cookie:cookie})\n
}else if(json.message){\n
reject(json.message);\n
}catch(err){}\n
reject(\&login failed\&);\n
}).on(\&error\&,function(err){\n
reject(err);\n
request.write(postString);\n
request.end();\n
})\n}\n\n\u002F\u002F 通过跳转链接获取cookie\nfunction getCookie(data){\n
var urlObj = url.parse(data.url);\n
var options = {\n
host:urlObj.hostname,\n
port:urlObj.port?urlObj.port:80,\n
method:'GET',\n
path:urlObj.path,\n
headers:{\n
'Host': 'bbs.meizu.cn',\n
'Cookie':data.cookie,\n
'User-Agent:':UA,\n
return new Promise(function(resolve,reject){\n
var request=http.request(options,function(res){\n
var cookieArr = res.headers[\&set-cookie\&];\n
if(!cookieArr)reject(\&no cookie\&);\n
var newcookie=trasCookie(cookieArr);\n
resolve(data.cookie+\&;\&+newcookie);\n
request.end();\n
})\n}\n\n\u002F\u002F cookie头的转换\nfunction trasCookie(cookieArr){\n
var cookie = cookieArr.map(function(val){\n
var arr=val.split(\&;\&);\n
return arr[0];\n
}).join(\&;\&);\\n}\n\n\u002F\u002F 通过带cookie访问其他页面,比如通知\nfunction getNotice(cookie){\n
var options = {\n
host:'bbs.meizu.cn',\n
port:80,\n
method:'GET',\n
path:\&\u002Fmisc.php?mod=message&type=notice_all&isindex=1\&,\n
headers:{\n
'Cookie': cookie,\n
'Connection':'close',\n
'Host':'bbs.meizu.cn',\n
'User-Agent:':UA\n
return new Promise(function(resolve,reject){\n
var request = http.request(options,function(res){\n
var bufferArr=[];\n
res.on(\&data\&,function(data){\n
bufferArr.push(data);\n
res.on(\&end\&,function(){\n
var data= Buffer.concat(bufferArr).toString();\n
var json=JSON.parse(data);\n
resolve(json);\n
}catch(err){}\n
reject(data);\n
}).on(\&error\&,function(err){\n
reject(err);\n
request.end();\n
})\n}\n\u003C\u002Fcode\u003E没用第三方模块,可以直接node meizu.js运行&,&updated&:new Date(&T16:53:35.000Z&),&canComment&:false,&commentPermission&:&anyone&,&commentCount&:3,&likeCount&:0,&state&:&published&,&isLiked&:false,&slug&:&&,&isTitleImageFullScreen&:false,&rating&:&none&,&sourceUrl&:&&,&publishedTime&:&T00:53:35+08:00&,&links&:{&comments&:&\u002Fapi\u002Fposts\u002F2Fcomments&},&url&:&\u002Fp\u002F&,&titleImage&:&https:\u002F\u002Fpic1.zhimg.com\u002Fe880dabdb83dabc4036e6d_r.jpg&,&summary&:&&,&href&:&\u002Fapi\u002Fposts\u002F&,&meta&:{&previous&:null,&next&:null},&snapshotUrl&:&&,&commentsCount&:3,&likesCount&:0},&&:{&title&:&Dagger2的简易流程&,&author&:&browsnet&,&content&:&现如今android开发和之前的有所不同了,gradle的方便让android项目使用第三方库变得非常简单,所以一个android项目引用几十个第三方库都是比较常见的,像官方support库、图片picasso库等等。\u003Cp\u003E现在还有一个变化就是注解从J2EE开发那边蔓延过来了,现在有不少热门的第三方库都使用了注解来减轻使用者的键入,不过随之而来的问题就是有可能会对阅读代码者提高些许障碍,如果不熟悉对应的库,就有可能摸不着头脑。\u003C\u002Fp\u003E\u003Cp\u003EDI(依赖注入)在J2EE和其他很多地方的框架早已出现,android这边的依赖注入其实也并不新鲜,由Square设计的Dagger库就是其中一员,目前Google接手并且实现了Dagger2,虽然项目还是推进中,但已经足可以满足开发需求了。\u003C\u002Fp\u003E\u003Cp\u003E要想用Dagger2来实现依赖注入,就必须知道它的两个重要组成部分:Module和Component。\u003C\u002Fp\u003E\u003Cp\u003EModule这个东西,是对应实例提供者的概念,当你需要自动把实例注入的时候,你写的是类名,那么这个对应类名的实例,就必须要有提供者,Module提供一个轻量级、可拆解的提供机制。\u003C\u002Fp\u003E\u003Cp\u003EComponent是连接实例提供者和实例消费者的桥梁,它需要指定一个或多个Module,也可以继承一个已有的Component。\u003C\u002Fp\u003E\u003Cp\u003E现在用一个简单的例子来说明Dagger2的惯用法,首先定义一个Module,这里面的两个以provide开头的方法,便是提供类的实类,方法的注解需要有@Provides,这里加上一个@Singleton注解,之后获取到的就会是同一个实例。\u003C\u002Fp\u003E\u003Cp\u003E我这里先定义的这个AppModule提供了两个可被注入的实例,一个是Context,还有一个是SharedPreferences。\u003C\u002Fp\u003E\u003Ccode lang=\&java\&\u003E\u002F\u002F AppModule.java\n@Module\npublic class AppModule {\n
private final A\n
AppModule(App app){ this.app =}\n
@Provides\n
@Singleton\n
Context provideAppContext(){\\n
@Provides\n
@Singleton\n
SharedPreferences provideSp(){\n
return PreferenceManager.getDefaultSharedPreferences(app);\n
}\n}\n\u003C\u002Fcode\u003E\u003Cp\u003E有了Module但是并不能用,还必须有个Component来做注入的桥梁,这个component也是未来你主要持有的变量,它是一个接口,只需要在上面加上@Component的注解就算定义完成了。\u003C\u002Fp\u003E\u003Ccode lang=\&java\&\u003E\u002F\u002F AppComponent.java\n@Singleton\n@Component(modules = AppModule.class)\npublic interface AppComponent {\n
void inject(BaseActivity activity);\n}\u003C\u002Fcode\u003E\u003Cp\u003E注意上面的inject方法里面的参数类型,这个是未来的注入点。\u003C\u002Fp\u003E\u003Cp\u003E现在就可以在自己的App类里面把component实例化,并提供接口暴露出去,以供给BaseActivity使用。\u003C\u002Fp\u003E\u003Ccode lang=\&java\&\u003E\u002F\u002F App.java\npublic class App extends Application{\nprivate AppC\n@Override\n
public void onCreate(){\n
component = DaggerAppComponent.builder()\n
.appModule(new AppModule(this))\n
.build();\n
super.onCreate();\n
public AppComponent getComponent(){\n
}\n}\n\u003C\u002Fcode\u003E上面的DaggerAppComponent实际上是Dagger2自动生成的类,工作是在编译期做,可能需要你ReBuild一下才能看到,它提供一个Build子类,里面可以指定对应的Module实例,这时候我们就可以借机把Application Context传给AppModule了。\u003Cp\u003E接下来就可以在BaseActivity.java使用依赖注入了。\u003C\u002Fp\u003E\u003Ccode lang=\&java\&\u003E\u002F\u002F BaseActivity.java\npublic abstract class BaseActivity extends AppCompatActivity{\n@Inject
protected SharedPreferences mSp;\n@Override\n
public void onCreate(Bundle savedInstanceState){\n
super.onCreate(savedInstanceState);\n
((App)getApplication()).getComponent().inject(this);\n
setContentView(getLayoutId());\n
ButterKnife.bind(this);\n
afterCreate();\n
protected abstract int getLayoutId();\n
public abstract void afterCreate();\n}\u003C\u002Fcode\u003E上面的SharedPreferences并没有在onCreate里初始化,而是使用了@Inject注解来实现依赖注入,它在所有继承BaseActivity的子类里都可以使用到。\u003Cp\u003EDagger2的Module除了支持普通的@Provides提供实例,还支持Lazy模式,通过Lazy模拟提供的实例,在@Inject的时候并不初始化,而是等到你要使用的时候,主动调用其.get方法来获取实例。\u003C\u002Fp\u003E\u003Cp\u003E其他的还有为了让你能够选择不同的注入实例而提供的@Named注解,以及在Inject时候可以每次都获得新的实例的@Provide注解,都是传统DI为考虑不同需求而列出来的特性,Dagger2上也可以方便使用。\u003C\u002Fp\u003E\u003Cp\u003EDagger2的使用方法其实并不复杂,但是代码显得不够直观,许多人在引入之前都会好奇:我直接在BaseActivity里持有一个单例不就行了么?为什么还要写这么多的类呢?\u003C\u002Fp\u003E\u003Cp\u003E其实解释起来还是老话,解耦、易替换,而且可控性比较强。\u003C\u002Fp\u003E\u003Cp\u003E我上面演示的只是一个Application层面的依赖注入,事实上我们可以有很多层面的注入手段,在实际开发当中,我们可以围绕着Activity、Fragment这些注入点来提供component,在比较大的应用上,它可以节省不少的重复代码。\u003C\u002Fp\u003E\u003Cp\u003EDagger2的依赖注入在其Component接口内部可以不需要@Inject注解直接注入,所以很多的时候Component里可以提供多个方法,比如这样写:\u003C\u002Fp\u003E\u003Ccode lang=\&java\&\u003E\u002F\u002F DemoComponent.java\n@Singleton\n@Component(modules = DemoModule.class)\npublic interface DemoComponent {\n
RetrofitManager getApiServer();\n}\n\u003C\u002Fcode\u003E\u003Cp\u003E上面就会在DaggerDemoComponent.java的实现里补上getApiServer方法的实现,注入一个RetrofitManage实例。\u003C\u002Fp\u003E\u003Cp\u003EModule的可拆分前面已经说了,一个module可以通过@Module(include)来引入另外几个module,这种可任意组合的特点可以用在做服务组合。\u003C\u002Fp\u003E\u003Cp\u003E引入Dagger2以后,代码结构也会随之改变,Activity那边的逻辑也会进一步减轻,特别是配合当下的MVP架构模式,可以让android app的代码更有条理,写代码之前也会思考更多。\u003C\u002Fp\u003E\u003Cp\u003E==============================\u003C\u002Fp\u003E\u003Cp\u003E2015年的最后一天就这样过去了,我无心写回顾或者总结,写篇文章算是告别这一年,新的2016年依旧充满未知,有小的期望,也有小的恐慌,明年,我是要改变改变自己了。\u003C\u002Fp\u003E\u003Cbr\u003E\u003Cp\u003E—— 2015 年12月31日晚\u003C\u002Fp\u003E&,&updated&:new Date(&T14:46:32.000Z&),&canComment&:false,&commentPermission&:&anyone&,&commentCount&:2,&likeCount&:25,&state&:&published&,&isLiked&:false,&slug&:&&,&isTitleImageFullScreen&:false,&rating&:&none&,&sourceUrl&:&&,&publishedTime&:&T22:46:32+08:00&,&links&:{&comments&:&\u002Fapi\u002Fposts\u002F2Fcomments&},&url&:&\u002Fp\u002F&,&titleImage&:&https:\u002F\u002Fpic1.zhimg.com\u002F8e0e9fd907fd24b03279_r.jpg&,&summary&:&&,&href&:&\u002Fapi\u002Fposts\u002F&,&meta&:{&previous&:null,&next&:null},&snapshotUrl&:&&,&commentsCount&:2,&likesCount&:25},&&:{&title&:&Dagger2的应用——MVP+Retrofit+RxJava&,&author&:&browsnet&,&content&:&传统Android开发里,Activity或Fragment这一层太重了,因为它承担了和界面交互、和数据交互等等一系列职责,所以一个Activity搞不好就有好几千行,各种职责的代码混在一块,不利用维护。\u003Cp\u003E所以MVP、MVVM这类开发模式现在更吸引开发者,它们用不同方式减轻了Activity的职责,MVVM现在主要是google自家推的数据绑定,虽然刚出来没有多久,也吸引了不少开发者的兴趣,未来可能会有所发展。\u003C\u002Fp\u003E\u003Cp\u003EMVP模式其实更容易理解,它把整个应用分成了三块,其中M代表着操纵数据的一组API,而V代表着操纵界面的一组API,P是Presenter的简称,它持有M和V这两组接口,自己提供一组方法供其它人调用,从而完成M和V的桥梁作用。\u003C\u002Fp\u003E\u003Cp\u003E一般情况下,M这个接口,是纯粹的API,它主要是包含两大部分:远程网络API和本地缓存,也就是所谓的DataLayer,它和Presenter的通信是单向的,也就是说只能Presenter持有DataLayer的API,Presenter可以随意调用这些API来获取数据。\u003C\u002Fp\u003E\u003Cp\u003E而V这个接口,通常是由Activity、Fragment、Adapter或者比较重的View来实现,它和Presenter互相持有,这样当发生界面交互的时候,它可以调用Presenter的方法,而Presenter同样也持有了V的api,当需要让界面发生改变的时候,就回调对应的方法,这样就发生了相互通信。\u003C\u002Fp\u003E\u003Cp\u003E这三者概念说清楚了以后,来谈谈具体的实现。\u003C\u002Fp\u003E\u003Cp\u003E首先是Presenter,我们实现个BasePresenter,它需要持有的一部分是DataLayer,而这个一般是网络API或者缓存API,我们用Retrofit来做网络API的实现,所以这里需要注入一个Retrofit Service。\u003C\u002Fp\u003E\u003Ccode lang=\&java\&\u003E\u002F\u002F BasePresenter.java\npublic abstract class BasePresenter {\n
@Inject public DemoService mA\n
@Inject public CookieM\n
public BasePresenter(){\n
App.apiComponent.inject(this);\n
}\n}\u003C\u002Fcode\u003E我上面用了一个Dagger2的Component,它提供注入两个api,一个是Retrofit service,还有一个是自己定义的CookieManager,这个CookieManager主要应用在登录、注册、换账号、退出登录这些Presenter里的。\u003Cp\u003EapiComponent在App那边build,它需要提供的两个类都是单例。\u003C\u002Fp\u003E\u003Cp\u003ERetrofit这部分,并不能直接build,这是因为默认没有接收和保存cookie,我们需要自己定义一个OkHttpClient,而这个OkHttpClient里得添加一个Interceptor,用于在请求开始的时候,给request添加一个Cookie头。\u003C\u002Fp\u003E\u003Cp\u003E这个Interceptor是个接口,我刚才自定义的CookieManager就实现了这个接口,由于Cookies是需要持久化的,我们就把它放在SharedPreferences里,这样以后就能随时读取到了。\u003C\u002Fp\u003E\u003Ccode lang=\&java\&\u003E\u002F\u002F CookieManager.java\npublic class CookieManager implements Interceptor {\nprotected Map&String,String& cookies = new HashMap&&(3);\n\n
private SharedP\n
public CookieManger(SharedPreferences sp){\n
this.sp =\n
String cookieString = sp.getString(\&cookies\&,\&\&);\n
initCookies(cookieString);\n\n
public String toString(){\n
StringBuilder sb = new StringBuilder();\n
for (Map.Entry&String, String& entry : cookies.entrySet()) {\n
sb.append(entry.getKey()+'='+entry.getValue()+\&;\&);\n
return sb.toString();\n
@Override\n
public Response intercept(Chain chain) throws IOException {\n
Request req = chain.request();\n
req=req.newBuilder()\n
.addHeader(\&Cookie\&,this.toString())\n
.build();\n
return chain.proceed(req);\n
}\n}\n\u003C\u002Fcode\u003E上面这个类省略了initCookies、save、clear、reset、add等操作cookie map的方法,可以自行添补。\u003Cp\u003E有了这个可以作为Intercetor的CookieManager,我们就可以用它来构造一个OkHttpClient,从而构造一个Retrofit实例,也就能构造一个Service API了。\u003C\u002Fp\u003E\u003Ccode lang=\&java\&\u003E\u002F\u002F ApiModule.java\n@Provides @Singleton\nCookieManger provideCookieManger(SharedPreferences sp){\n
return new CookieManger(sp);\n}\u003C\u002Fcode\u003E\u003Cp\u003E上面通过参数依赖注入了SharedPreferences的单例,这样我们就能初始化cookies了,有了这个Intercetor,我们就能构造出一个OkHttpClient了\u003C\u002Fp\u003E\u003Ccode lang=\&java\&\u003E\u002F\u002F ApiModule.java\n@Provides @Singleton\nOkHttpClient provideClient(CookieManger cm){\n
OkHttpClient client = new OkHttpClient();\n
client.interceptors().add(cm);\\n}\u003C\u002Fcode\u003E上面这个同样是通过Dagger2的参数依赖注入完成的,这里的OkHttpClient是OkHttp2.0带的。\u003Ccode lang=\&java\&\u003E\u002F\u002F ApiModule.java\n@Provides @Singleton\nRetrofit provideRetrofit(OkHttpClient client){\n
Retrofit retrofit = new Retrofit.Builder()\n
.baseUrl(API_BASE_URL)\n
.addConverterFactory(GsonConverterFactory.create())\n
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())\n
.client(client)\n
.build();\\n}\u003C\u002Fcode\u003E我们提供的这个Retrofit里添加了带Gson支持的Converter,并且由于我比较喜欢RxJava,所以也带了RxJavaCallAdapter的支持,需要注意如果要使用这两个,得在gradle那边加上\u003Ccode lang=\&groovy\&\u003Ecompile 'com.squareup.retrofit:adapter-rxjava:2.0.0-beta2'\ncompile 'com.squareup.retrofit:converter-gson:2.0.0-beta2'\u003C\u002Fcode\u003E现在有了Retrofit的单例,也就能提供出一个Presenter需要的实例了\u003Ccode lang=\&java\&\u003E\u002F\u002F ApiModule.java\n@Provides @Singleton\nDemoService provideService(Retrofit retrofit){\n
return retrofit.create(DemoService.class);\n}\u003C\u002Fcode\u003E这样我的ApiComponent就算完成了,它主要是提供了DemoService这个单例,把ApiComponent放在App的onCreate里build,随后就可以像开头一样,注入到BasePresenter类中了\u003Cp\u003E这样也就完成了Presenter里的DataLayer部分,在随后的Presenter里,我们可以随时使用mApi.getThread()这样的api来完成网络请求并拿到数据了。\u003C\u002Fp\u003E\u003Cp\u003E推荐在写Retrofit的Service Interface的时候,定义成Observable是最好的做法,非常方便的结合了RxJava,当你需要二次请求或者需要和缓存做结合的时候,RxJava这套链式写法太方便。\u003C\u002Fp\u003E\u003Cp\u003E接下来就说说Presenter持有的这个V的接口,通常情况下,我会把要用到的V 接口写在Presenter的里面,比如这样\u003C\u002Fp\u003E\u003Ccode lang=\&java\&\u003Epublic class LoginPresenter extends BasePresenter {\n
private LoginPresenter.View mV\n
public static interface View {\n
public void setEmailError(String error);\n
public void setPasswordError(String error);\n
public void showError(String error);\n
}\n}\u003C\u002Fcode\u003E这里提供的LoginPresenter.View接口,有是在LoginActivity那边需要实现的,我们在这个Presenter提供了一个login()方法,它会回调mView的方法,这些方法都是和界面相关的,在LoginActivity那边实现。\u003Cp\u003E注意到Activity剥离了与DataLayer交互的能力,在它的代码里,只能存在和界面相关的方法,它的职责完全限定在view范畴里了。\u003C\u002Fp\u003E\u003Cp\u003E由于View和Presenter相互持有,所以可能会导致内存泄漏的问题,这时候需要在Activity的onDestory那边做处理\u003C\u002Fp\u003E\u003Ccode lang=\&java\&\u003E@Override\npublic void onDestroy(){\n
super.onDestroy();\n
mPresenter.clearView();\n
mPresenter =\n}\u003C\u002Fcode\u003E还有你可能需要在某些情况下,停止网络请求,这个时候只需要对Retrofit的Observable做unsubscribe处理即可,它会自动调用service的cancel功能,这样就可以停止请求,非常方便。\u003Cp\u003E在基于Fragment构建的app里,也是把Fragment当成V API来处理,只要它实现了你自定义的接口就好。\u003C\u002Fp\u003E\u003Cp\u003EMVP这种模式分拆了Activity或者Fragment的职责,自己做中间层挡在了View和DataLayer之间,它的好处不仅仅是职责清晰,还具有可替换的好处,因为它和View之间的联系是通过接口来完成了,当你需要替换的时候,只需要按照同样的接口来实现即可。\u003C\u002Fp\u003E&,&updated&:new Date(&T09:47:44.000Z&),&canComment&:false,&commentPermission&:&anyone&,&commentCount&:10,&likeCount&:18,&state&:&published&,&isLiked&:false,&slug&:&&,&isTitleImageFullScreen&:false,&rating&:&none&,&sourceUrl&:&&,&publishedTime&:&T17:47:44+08:00&,&links&:{&comments&:&\u002Fapi\u002Fposts\u002F2Fcomments&},&url&:&\u002Fp\u002F&,&titleImage&:&https:\u002F\u002Fpic2.zhimg.com\u002Fc50202eec5b3f0ccee2c86add5acac13_r.jpg&,&summary&:&&,&href&:&\u002Fapi\u002Fposts\u002F&,&meta&:{&previous&:null,&next&:null},&snapshotUrl&:&&,&commentsCount&:10,&likesCount&:18},&&:{&title&:&RxJava的应用——以点赞后登录重试为例&,&author&:&browsnet&,&content&:&在应用开发的时候,其实有一种场景很常见,就是某项操作需要用户登录,而用户并没有登录,那么此时应弹出登录界面,并且在用户登录成功后,自动执行用户上次的操作,当然,如果用户在登录界面回退,也就不用执行后续操作了。\u003Cp\u003E以点赞为例,当用户点赞之后,正常情况下是Presenter发送请求给服务器,执行成功以后回调给View(Fragment或Activity),执行界面修改工作,也就是显示一个已赞的标志,并且赞数也加一。\u003C\u002Fp\u003E\u003Cp\u003E但如果用户没有登录,上面的流程就有问题了,在服务器那边会返回一个{needlogin:true}的标志之后,用户需要登录以后才能执行上面的操作,这个时候你可以在Presenter的Call Observable那边作过滤,比如这样\u003C\u002Fp\u003E\u003Ccode lang=\&java\&\u003Eprivate Observable&ServerResult& getApi(){\n
return mApi.like();\n}\nprivate void like(){\n
getApi()\n
.subscribeOn(Schedulers.newThread())\n
.observeOn(AndroidSchedulers.mainThread())\n
.filter((ServerResult result) -& {\n
if(result.needlogin == true){\n
needLogin();\u002F\u002F 弹出登录\n
}).subscribe();\n}\u003C\u002Fcode\u003E\u003Cp\u003E上面这样可以做到拦截未登录的情况,一旦发觉未登录,就直接执行needLogin()函数,startActivity一个LoginActivity,如果是登录状态那就继续执行后续逻辑。\u003C\u002Fp\u003E\u003Cp\u003E但是如果我们想更近一步,在用户登录之后,自动执行上面的点赞行为,就需要多花点心思了。\u003C\u002Fp\u003E\u003Cp\u003E首先我们要能获取到LoginActivity的登录结果,也就是登录是否成功\u002F失败。\u003C\u002Fp\u003E\u003Cp\u003E请求开始是由用户在Activity或者Fragment发起startActivityForResult,而请求结果是重写类的onActivityResult,我们以Fragment为例,说说怎么想把它Rx化。\u003C\u002Fp\u003E\u003Cp\u003E先弄一个工具类,叫ActivityLaucher,类的构造方法接受一个参数,也就是Fragment,这样我们可以就可以利用这个类来代理Fragment,这个类实现一个start方法,代理Fragment的startActivityForResult,并且在执行之后生成一个PublishSubject实例并返回,这样它就是一个Observable了\u003C\u002Fp\u003E\u003Cp\u003E再实现一个onActivityResult方法,用于在Fragment的onActivityResult接到结果之后,调用PublishSubject的onNext方法,把结果告知订阅者。\u003C\u002Fp\u003E\u003Ccode lang=\&java\&\u003Epublic class ActivityLauncher {\n
private Fragment mD\n
Observable&ActivityResult&\n
public ActivityLauncher(Fragment f) {\n
this.mDelegate =\n
public Observable&ActivityResult& start(@NonNull Intent intent, int requestCode, @Nullable Bundle options) {\n
mDelegate.startActivityForResult(intent, requestCode);\n
} catch (ActivityNotFoundException | SecurityException e) {\n
return Observable.error(e);\n
subject = PublishSubject.create();\n
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {\n
subject.onNext(new ActivityResult(resultCode, data));\n
subject.onCompleted();\n
}\n}\u003C\u002Fcode\u003E上面是个示例,具体还是需要考虑实际情况,比如view发起多个startActivityForResult的时候,view已经被destory的时候。\u003Cp\u003E我们有了这个ActivityLauncher之后,得在BaseFragment那边实例化一个launcher变量,并且重写BaseFragment的onActivityResult,调用launcher.onActivityResult,还要留一个接口给Presenter,比如startLogin什么的。\u003C\u002Fp\u003E\u003Cp\u003E这样我们就可以在Presneter那边,通过mView.startLogin来要求用户登录,并且它也是个Observable,可以在subscrible里获取结果。\u003C\u002Fp\u003E\u003Cp\u003E有了登录并且获取是否登录成功,我们还需要重新调用之前的操作,也就是递归调用一次方法。\u003C\u002Fp\u003E\u003Cp\u003E这时候要考虑到不能断链式,我们需要利用RxJava的Operator,它能够代理我们的Subscriber,这样我们就可以在这里面做手脚。\u003C\u002Fp\u003E\u003Ccode lang=\&java\&\u003Eclass MyOperator extends Observable.Operator&ServerResult, ServerResult&{\n
@Override\n
public Subscriber&? super ServerResult& call(final Subscriber&? super ServerResult& s) {\n
return new Subscriber&ServerResult&() {\n
@Override\n
public void onCompleted() {}\n\n
@Override\n
public void onError(Throwable e) {\n
if(!s.isUnsubscribed()) {\n
s.onError(e);\n
@Override\n
public void onNext(final ServerResult result) {\n
if(s.isUnsubscribed())\n
if(result.isNeedLogin()){\n
mView.startLogin().subscribe(\n
activityResult -&{\n
if(activityResult.isOk()){\n
getApi().subscribeOn(Schedulers.io()) \n
.observeOn(AndroidSchedulers.mainThread())\n
.subscribe(result1-& {
s.onNext(result1);\n
s.onCompleted();\n
s.onNext(result);\n
s.onCompleted();\n
s.onNext(result);\n
s.onCompleted();\n
}\n}\u003C\u002Fcode\u003EOperator的实例是用在Observable的lift方法上的,这样就可以在链式调用里使用。\u003Cbr\u003E\u003Ccode lang=\&java\&\u003Eprivate Observable&ServerResult& getApi(){\n
return mApi.like();\n}\nprivate void like(){\n
getApi()\n
.subscribeOn(Schedulers.newThread())\n
.observeOn(AndroidSchedulers.mainThread())\n
.lift(new MyOperator(mView))\n
.subscribe(ServerResult result) -& {\n\n
});\n}\u003C\u002Fcode\u003E可以看到在调用的时候非常方便,不过存在一个问题,那就是在登录成功回调里面,需要递归的调用getApi().subscripe,这样会严重影响我们的封装。\u003Cp\u003E为了避免这种情况,我们在登录成功的时候,抛出一个自定义的错误,比如说\&needRetry\&这个字符串。\u003C\u002Fp\u003E\u003Ccode lang=\&java\&\u003EmView.startLogin()\n
.subscribe(activityResult -&{\n
if(activityResult.isOk()){\n
s.onError(new Throwable(\&needRetry\&));\n
s.onNext(result);\n
s.onCompleted();\n
});\u003C\u002Fcode\u003E\u003Cp\u003E这样我们就可以在调用链上使用retry来重试了\u003C\u002Fp\u003E\u003Ccode lang=\&java\&\u003Eprivate Observable&ServerResult& getApi(){\n
return mApi.like();\n}\nprivate void like(){\n
getApi()\n
.subscribeOn(Schedulers.newThread())\n
.observeOn(AndroidSchedulers.mainThread())\n
.lift(new MyOperator(mView))\n
.retry((count,t)-&t.getMessage().equals(\&needRetry\&) && count == 1)\n
.subscribe(ServerResult result) -& {\n\n
});\n}\u003C\u002Fcode\u003E\u003Cp\u003E包装之后它就可以执行下面的逻辑:\u003C\u002Fp\u003E\u003Cp\u003E发起动作请求===&服务器判断是否登录===&已登录拿到结果===&回调view\u003C\u002Fp\u003E\u003Cp\u003E发起动作请求===&服务器判断是否登录===&未登录===&开启LoginActivity===&如果登录取消则无动作,如果登录成功===&重新发起动作请求===&已登录拿到结果===&回调view。\u003C\u002Fp\u003E&,&updated&:new Date(&T05:02:47.000Z&),&canComment&:false,&commentPermission&:&anyone&,&commentCount&:2,&likeCount&:36,&state&:&published&,&isLiked&:false,&slug&:&&,&isTitleImageFullScreen&:false,&rating&:&none&,&sourceUrl&:&&,&publishedTime&:&T13:02:47+08:00&,&links&:{&comments&:&\u002Fapi\u002Fposts\u002F2Fcomments&},&url&:&\u002Fp\u002F&,&titleImage&:&https:\u002F\u002Fpic2.zhimg.com\u002F250fc50386abce01d60fdae9f494a3be_r.jpg&,&summary&:&&,&href&:&\u002Fapi\u002Fposts\u002F&,&meta&:{&previous&:null,&next&:null},&snapshotUrl&:&&,&commentsCount&:2,&likesCount&:36},&&:{&title&:&谈谈RecyclerView在开发的那些事&,&author&:&browsnet&,&content&:&在传统的app开发中,大量的形式其实是和列表相关的,以前大家都用ListView,现在google弄出了一个新组件RecyclerView的support,大家也都换上了RecyclerView,这一块的业务逻辑也很重。\u003Cp\u003E一个普通的列表界面,假设是由一个Activity或者一个Fragment表现的,它可能需要满足这些情况:\u003C\u002Fp\u003E\u003Cp\u003E1. 进入界面后列表会自动获取第一页数据\u003C\u002Fp\u003E\u003Cp\u003E2. 列表可以上拉刷新\u003C\u002Fp\u003E\u003Cp\u003E3. 列表可能在头部含有其他的元素,可能是导航什么的\u003C\u002Fp\u003E\u003Cp\u003E4. 滚动到列表底部的时候自动加载更多\u003C\u002Fp\u003E\u003Cp\u003E5. 加载更多如果没有数据的话在底部提示已加载全部\u002F或者直接隐藏提示\u003C\u002Fp\u003E\u003Cp\u003E6. 在加载数据的时候显示一个正在加载的标识\u003C\u002Fp\u003E\u003Cp\u003E7. 加载出错的时候提示错误信息,并允许点击重试\u003C\u002Fp\u003E\u003Cp\u003E8. 如果这个列表需要登录后才能查阅,那提示需要登录,并在点击后跳出登陆窗口,成功登录以后自动加载并显示。\u003C\u002Fp\u003E\u003Cp\u003E9. 列表每一项的事件处理\u003C\u002Fp\u003E\u003Cp\u003E以上这些部分明显没有涵盖所有的列表需求,但它属于一个典型的列表,涵盖了基本的交互和表现,我们来简单说说这些功能的实现。\u003C\u002Fp\u003E\u003Cp\u003E先说文件组织,假设我们这个界面是个Fragment(或者Activity),那么它的layout里应该有两个view组件,一个是上拉刷新的view,比如说是support里自带的SwipeRefreshLayout ,还有一个就是RecyclerView。\u003C\u002Fp\u003E\u003Cp\u003E我们采用MVP来做代码组织,在fragment里,我拿到recyclerView之后,需要给它一个RecyclerView.Adapter的实例, 常做列表视图的人都知道,列表里的大部分业务逻辑实际上并不在Fragment里,而是在这个自定义的adapter类里。\u003C\u002Fp\u003E\u003Cp\u003E那么presenter的实例是交给Fragment持有还是交给Adapter持有呢?这个不一而足,有的人给fragment,有的人给adapter,还有的人两方都持有,都能调用presenter的方法。\u003C\u002Fp\u003E\u003Cp\u003E需要注意的是presenter这边还需要持有一个view接口,当你fragment和adapter都持有presenter的话,那view接口谁实现?\u003C\u002Fp\u003E\u003Cp\u003E我的建议是,交给Fragment持有,Adapter不要持有presenter,第一是为了避免混乱,第二就是能够更好的控制Fragment的其他元素。\u003C\u002Fp\u003E\u003Ccode lang=\&java\&\u003Epublic interface View extends StartLoginInterface{\n
public void addData(List&SimpleData& datas);\n
public void resetData(List&SimpleData& datas);\n
public void refreshDown();\n
public void changeLoadingStatus(LoadingStatus status);\n}\u003C\u002Fcode\u003E我在Presenter里定的View 接口也就四个方法,其中前两个方法顾名思义,就是在加载数据之后回调Fragment的方法,而Fragment再调用Adapter的方法,完成修改数据和通知变更。\u003Cp\u003E而refreshDown这个接口纯粹是为了下拉刷新而服务的,在下拉刷新完成之后,我得告知Fragment的refresh view,刷新已经成功了。\u003C\u002Fp\u003E\u003Cp\u003EchangeLoadingStatus这个方法,是为了显示加载状态\u002F当前信息的。\u003C\u002Fp\u003E\u003Cp\u003E那么我们的Presenter又有哪些方法可以被fragment调用呢?我们可以分析一下行为,在列表界面上我们需要的就是加载数据,所以一个loadData应该足够了,不过考虑到还有一个state的问题,我们可以加些辅助方法。\u003C\u002Fp\u003E\u003Cp\u003E哪些是state呢?一个是当前是否在加载数据,我们要保证多次调用同一个load的时候,不能有多条请求。\u003C\u002Fp\u003E\u003Cp\u003E还有就是是否有更多信息,这个是控制是否能继续滚动刷新的前提,也是显示“没有更多”的依据。\u003C\u002Fp\u003E\u003Cp\u003E还有就是控制翻页的state,可以是页码,也可以是其他能控制页面参数的变量,这样获取数据的时候才能接着取。\u003C\u002Fp\u003E\u003Cp\u003E切忌在其他地方(Fragment、Adapter)里保存状态信息,它们也不应该能直接操纵数据、状态。\u003C\u002Fp\u003E\u003Cp\u003Eadapter这边的交互动作,加载数据其实只有两种情况,一个是加载首页,一个是加载更多。\u003C\u002Fp\u003E\u003Cp\u003E下面分别说说上面那些需求的写法。\u003C\u002Fp\u003E\u003Cp\u003E\u003Cb\u003E1. 进入界面后列表会自动获取第一页数据\u003C\u002Fb\u003E\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cp\u003E这个容易,在fragment实例化presenter之后,就调用它的loadData()方法,获取数据,回调到resetData()之后再调用adapter的resetData方法,更新内部数组并且notifyDataSetChanged()。\u003C\u002Fp\u003E\u003Cp\u003E\u003Cb\u003E2. 列表可以上拉刷新\u003C\u002Fb\u003E\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cp\u003E我们使用上拉刷新组件来做,可以用google的也可以用第三方的,一般来说它只有两种方法,一种显示刷新状态,一种是取消刷新状态,所以我们可以在loadData这个Observable的finallyDo上加一段就好,这样不管成功还是失败,都会取消刷新状态。\u003C\u002Fp\u003E\u003Ccode lang=\&java\&\u003EObservable&Results& ob = getApi(page)\n
.subscribeOn(Schedulers.io())\n
.observeOn(AndroidSchedulers.mainThread())\n
.finallyDo(()-&{\n
isLoding =\n
mView.refreshDown();\n
});\u003C\u002Fcode\u003E\u003Cp\u003E\u003Cb\u003E3. 列表可能在头部含有其他的元素,可能是导航什么的\u003C\u002Fb\u003E\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cp\u003E如果列表上面是一些固定高度的元素,那么就直接在layout里定义就好了,毕竟fragment那边可以拿到这些view,也可以和presenter交互,如果是一些希望内置的列表 item,也可以在RecyclerView那边定义一个新的ViewHolder 类型,根据实例化的参数来决定放入的内容。\u003C\u002Fp\u003E\u003Cp\u003E新ViewHolder加入之后,在adapter的onBindViewHolder里需要注意那个position的处理,如果上头放几个的话,那么你item的positiion就有可能改变了,不能再直接list.get(position)了。\u003C\u002Fp\u003E\u003Cp\u003E\u003Cb\u003E4. 滚动到列表底部的时候自动加载更多\u003C\u002Fb\u003E\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cp\u003E这个需要监听RecyclerView的滚动事件,判断它是否到达底部,如果到达底部就调用mPresneter.loadMore(),这个滚动事件也是我为什么要把presenter放fragment而不是adapter的原因,如果你放adapter那边,你需要adapter持有当前的RecyclerView,它的layoutmanager这些东西,这和adapter本身的功能就相悖了。\u003C\u002Fp\u003E\u003Cp\u003E检测是否到达底部有个样板代码,就像这样\u003C\u002Fp\u003E\u003Ccode lang=\&java\&\u003EmList.addOnScrollListener(new RecyclerView.OnScrollListener() {\n@Override\n
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {\n
super.onScrolled(recyclerView, dx, dy);\n
int visibleThreshold = 5;\n
int totalItemCount = linearLayoutManager.getItemCount();\n
int lastVisibleItem = linearLayoutManager.findLastVisibleItemPosition();\n\n
if (totalItemCount &= (lastVisibleItem + visibleThreshold)) {\n
mPresenter.loadMore();\n
}\n});\u003C\u002Fcode\u003E\u003Cb\u003E5. 加载更多如果没有数据的话在底部提示已加载全部\u002F或者直接隐藏提示\u003C\u002Fb\u003E\u003Cbr\u003E\u003Cp\u003E我们Presenter加载数据之后会知道后面还有没有数据了,如果没有数据的话,我们应该截断loadMore的网络请求,并且改变状态提示。\u003C\u002Fp\u003E\u003Cp\u003E状态提示这个我其实也是放在RecyclerView里的,作为一个特殊的Item来处理,它也是一个特殊的ViewHolder,它的数据是一个自定义的接口LoadingStatus,这里面我定义了status和message两个信息。\u003C\u002Fp\u003E\u003Cp\u003E通过一个方法changeLoaddingStatus,我们可以改变这一条的状态。\u003C\u002Fp\u003E\u003Ccode lang=\&java\&\u003Epublic void changeLoaddingStatus(LoadingStatus status){\n
if(status == mLoadingStatus)\n
this.mLoadingStatus =\n
notifyItemChanged(getItemCount()-1);\n}\u003C\u002Fcode\u003E上面这里的notifyItemChanged的参数,实际上是因为我把这一条放在recyclerView的最后一栏,改变的时候也只要通知这一个Item就行了。\u003Cp\u003E我们在onBindViewHolder的时候,如果是LoadingStatus ViewHolder的话,我们判断当前持有的status数据,然后根据其status和text来决定其内容显示。\u003C\u002Fp\u003E\u003Cp\u003E比如说如果加载全部的话,我们就传一个{status:done,text:\&没有更多数据了\&}的对象给adapter,这样就会改变这一Item的表现,让它显示”已加载全部“或者把这Item直接隐藏。\u003C\u002Fp\u003E\u003Cp\u003E\u003Cb\u003E6. 在加载数据的时候显示一个正在加载的标识\u003C\u002Fb\u003E\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cp\u003E这一条可以用刷新组件来做,也可以用上面我们提到的changeLoaddingStatus,弄一个状态{status:loading},让我们的LoadingStatus ViewHolder显示正在加载的标识,当数据已经加载完成,我们可以把它隐藏。\u003C\u002Fp\u003E\u003Cp\u003E\u003Cb\u003E7. 加载出错的时候提示错误信息,并允许点击重试\u003C\u002Fb\u003E\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cp\u003E同样是利用changeLoaddingStatus,错误信息可能是服务器提供的,也有可能是网络超时\u002F未联网\u002F未取得网络权限等等,前者在Subscription的onNext里回调changeLoaddingStatus,后者的话在onError里。\u003C\u002Fp\u003E\u003Cp\u003E需要注意onError还有一种可能,那就是对应的activity\u002Ffragment被销毁了,这时候要注意不能直接调用mView.changeLoaddingStatus,得先判断它是否存在。\u003C\u002Fp\u003E\u003Cp\u003E\u003Cb\u003E8. 如果这个列表需要登录后才能查阅,那提示需要登录,并在点击后跳出登陆窗口,成功登录以后自动加载并显示。\u003C\u002Fb\u003E\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cp\u003E提示登录,其实也是一个错误信息,它和上面这个没啥区别,点击后跳出登陆窗口,成功登录以后自动加载并显示这个功能,其实是利用我之前写过的一篇文章里的知识来做的。详见\u003Ca href=\&http:\u002F\u002Fzhuanlan.zhihu.com\u002Fbrowsnet\u002F\& data-editable=\&true\& data-title=\&RxJava的应用——以点赞后登录重试为例\& class=\&\&\u003ERxJava的应用——以点赞后登录重试为例\u003C\u002Fa\u003E\u003C\u002Fp\u003E\u003Cp\u003E这个功能不仅仅可以用在加载列表页,还能用在所有需要登录的情况下。\u003C\u002Fp\u003E\u003Cp\u003E\u003Cb\u003E9. 列表每一项的事件处理\u003C\u002Fb\u003E\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cp\u003E上面我都没有提到点击重试是如何实现,众所周知,RecyclerView并没有提供onItemClick的方法,所以我们需要自己在onBindViewHolder的时候为holder.itemView来增加事件监听。\u003C\u002Fp\u003E\u003Cp\u003E这个时候又牵扯到一个问题,事件处理到底放在哪里合适?adapter?fragment?还是presenter?\u003C\u002Fp\u003E\u003Cp\u003E如果放在adapter里来处理,那么它的交互问题就有些棘手,我们需要处理相关的事件,可能会涉及到数据之类的。\u003C\u002Fp\u003E\u003Cp\u003E如果放在presenter的话,它可能需要增加多个接口用来处理不属于数据的工作。\u003C\u002Fp\u003E\u003Cp\u003E也有人把fragment作为事件处理工具,在adapter里他们直接用一句holder.itemView.setOnClickListener(mListener),这个mListener在adapter实例化的时候传进去的,通常就是fragment或者activity本身。\u003C\u002Fp\u003E\u003Cp\u003E这样做省事也是有缺点的,缺点就是如果OnClickListener里面实际上只会有一个view的参数,我们拿到这个view之后,需要先对它定位,找到对应的position,然后又更具position来找到对应的view的数据,最后才是对数据进行处理,这样不仅麻烦还不清晰。\u003C\u002Fp\u003E\u003Cp\u003E我的做法是通过接口中转,比如说定义一个 ItemClick.\u003C\u002Fp\u003E\u003Ccode lang=\&java\&\u003Epublic interface ItemEvent {\n
public void onItemClick(ItemModule item);\n
public void onLoadingStatusClick(LoadingStatus status);\n
public void onHeaderClick(HeaderModule header);\n}\u003C\u002Fcode\u003E\u003Cp\u003E这个接口的实现一般来说放在fragment里,然后同样通过在adapter实例化的时候传进,在adapter里,我们对ViewHolder的事件做处理,就可以不用管view了,直接用其数据\u003Cbr\u003E\u003C\u002Fp\u003E\u003Ccode lang=\&java\&\u003EloadingHolder.itemView.setOnClickListener(v -& {\n
eventHandler.onLoadingStatusClick(mLoadingStatus);\n});\u003C\u002Fcode\u003E这样事件就被放在Fragment来处理了,但是它拿到的参数直接是数据,而且我们不同的viewHolder的事件也区分开了。&,&updated&:new Date(&T01:44:48.000Z&),&canComment&:false,&commentPermission&:&anyone&,&commentCount&:5,&likeCount&:26,&state&:&published&,&isLiked&:false,&slug&:&&,&isTitleImageFullScreen&:false,&rating&:&none&,&sourceUrl&:&&,&publishedTime&:&T09:44:48+08:00&,&links&:{&comments&:&\u002Fapi\u002Fposts\u002F2Fcomments&},&url&:&\u002Fp\u002F&,&titleImage&}

我要回帖

更多关于 数据库截取字符串 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信