技术掘金,专注于Web互联网开发,分享技术,致力于打造好的技术博客
《渝李文集静日书 -- 赤水莲花》《渝李文集静日书 -- 蜡手馆的来客》《渝李文集静日书 -- 留守风花树洞的冬天》《渝李文集静日书 -- 留一颗掌心朱砂》《渝李文集静日书 -- 破天》《渝李文集静日书 -- 每一只鼹鼠都能恋爱》《渝李文集静日书 -- 一场游戏》《渝李文集静日书 -- 秦淮河的风月桃花》《渝李文集静日书 -- 烫暖爱情的牛肉汤》《渝李文集静日书 -- 悟空有没有爱过谁》《渝李文集静日书 -- 完美爱情童话》《渝李文集静日书 -- 双输戏》《渝李文集静日书 -- 夜奔》《渝李文集静日书 -- 七步成殇》《渝李文集静日书 -- 和幸福一起痛》《渝李文集静日书 -- 倾城》《渝李文集静日书 -- 日光碎片》《渝李文集静日书 -- 丢失的U字》《渝李文集静日书 -- 关于一棵菠菜的等待》《渝李文集静日书 -- 那些花儿老去》《渝李文集静日书 -- 一枚硬币背上的幸福》《渝李文集静日书 -- 宋恩南的夏日薄荷香》《渝李文集静日书 -- 那一年天使离去》《渝李文集静日书 -- 惊航》《渝李文集静日书 -- 寻找书童马自强》《渝李文集静日书 -- 失落的绿洲》《渝李文集静日书 -- 江湖这么大》
最近做一个Android联网抓包的工具，自己在网上搜索了好久，发现还是没有头绪，于是考虑在linux层上下功夫于是采用linux的tcpdump来实现了抓包的功能，用简单的话来定义tcpdump，就是：dumpthe traffic on anetwork，根据使用者的定义对网络上的数据包进行截获的包分析工具。作为互联网上经典的的系统管理员必备工具，tcpdump以其强大的功能，灵活的截取策略，成为每个高级的系统管理员分析网络，排查问题等所必备的东东之一。对于tcpdump详细的信息可以参考百度百科关于TCPDUMP的介绍我这里主要介绍一下如何在Android上使用tcpdump，我所采用的测试机是破解版G2和模拟器，具体的就是在APK程序里执行linux命令，tcpdump -p -vv -s0 -w /sdcard/capture.pcap 这样就能够将抓到的包在sdcard上写一个capture. pcap文件，而这个文件可以用wireshark在PC上打开进行查看，非常方便。具体的源代码如下，MainHook.javapackage zy.hook; import java.io.IOException; import android.app.Activity; import android.os.Bundle; import android.util.Log; public class MainHook extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); try { //Runtime.getRuntime().exec("su"); //Runtime.getRuntime().exec("dd if=/sdcard/tcpdump of=/data/local/tcpdump"); //Runtime.getRuntime().exec("chmod 6755 /data/local/tcpdump"); Runtime.getRuntime().exec("tcpdump -p -vv -s 0 -w /sdcard/capture.pcap"); Log.i("run","success!!!!!!!!!"); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); Log.i("run",e.toString()); } } } AndroidManifest.xml<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="zy.hook" android:versionCode="1" android:versionName="1.0"> <application android:icon="@drawable/icon" android:label="@string/app_name"> <activity android:name=".MainHook" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> <uses-sdk android:minSdkVersion="7" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> </manifest> 尤其不要忘记了权限的声明，运行程序我们就可以抓包了会在sdcard上生成capture.pcap文件，抓一段时间之后，将文件从sdcard上导出，然后我们用wireshark打开 可以非常方便的查看我们抓到的包
发布于：2007-10-19 21:37:031我接到梨落的竹筒信时，正在明月楼上搂着姑娘喝小酒。红衣的姑娘笑起来嗓门挺大，酒量也很好，我喝得有点头晕了，她还没醉，两只红艳艳的手指夹起一颗葡萄喂到我嘴边，唤，二公子……这时窗外有鸽子扑翅的声音，我探身看了看，果真看到傅家堡特有的蓝灰色信鸽。解开它脚上的信筒，里面有个白色小布卷，匝三道红线，看来是加急信。蘸点口水拆信，方才的姑娘从背后挤过来。我瞥了一眼信的内容，心里一咯噔，转身就把她给甩开了。这种时刻我可顾不上怜香惜玉，罩上袍子，蹬了马靴，一阵风似的朝马厩冲去。那封信还在我手里，细软的白布条上有泪痕，是梨落的泪，不到万不得已她不会写信给我，尽管只是简短的一行字，却硌得我的心像是要碎掉。信鸽冲天而去时我的马也在飞奔，游龙剑在腰间轻鸣。我心里除了愤怒还有担忧，甚至于，更深的悔恨也狠狠攫住了我。我不明白，大哥在得到这样一个绝世佳人之后，为什么还会陋巷寻花长街问柳，难道说堂堂傅家堡堡主夫人，那曾撩动无数江湖英雄竞折腰的美人毕梨落，会失色于烟视媚行的青楼女？而富可敌国的傅家堡里，那日间篱下饮酒，晚来红帐温香的生活，还不足以令大哥收敛心性，不足以令他待她，有多一点温情、多一点真心？当初大哥是极爱她的，甚至不顾性命之虞，调全堡上下七百多号习武之人，从天灵山上力克众人夺梨落回来。那场婚宴，在我记忆中是如此盛大欢喜，从傅家堡主城正厅铺出的红毯，穿城绕林，一直绵延到堡外三十里，即便帝王之家送迎嫁娶，那排场也不外如此。可是，梨落的信里却哀哀写道：兄纳妾在即，嫂望速归。嫂望速归，这四个字是一道魔咒，这称呼，比我腰间的游龙剑更锋利。悔恨铺天盖地袭来，我知，终其一生，无论如何，毕梨落是我心间难以拔除的一根玫瑰刺。2回到堡内大哥不在，正厅里只有管家廖文在等我。他走过来作了个揖说，二公子，夫人有请。整个傅家堡的气氛还是如常压抑，自梨落有孕搬出中房以来，大哥耐不住寂寞，整日里不是去坊间喝花酒，就是带明月楼的姑娘回来，在西侧倚翠居里厮混。为免梨落伤心，我交代下人不准多言妄行，由此独居东边玉箫阁的梨落少见少闻了许多和大哥有关的荒唐事，只是堡内从此也便寂寥下去。廖文带我去玉箫阁，一路上他回了几次头，我知他有话要问我，果然，他在廊外花间停下来，指指楼上，二公子，你打算如何？他指的是梨落。我犹疑片刻，坦白地对他说，阿文，我无能为力。廖文看了我一眼问，你不打算带她走？我心里揣满了悲伤与无奈，我说整个江湖都是人，人人都有一张嘴，我与梨落，要走到哪里去呢？廖文叹口气说，担着携嫂私奔的罪名，傅家堡二公子怕也确是难在江湖立足。我顿了顿说，长宁毕竟是我大哥，前事我已然对他不住，如今再不可令他难堪。这番话，廖文是明白的，在傅家堡里，我从未把他当做外人。那年梨落进傅家门，喜堂之上，风吹喜帕的瞬间，我便深知那双波光潋滟的眼能带给我怎样的劫。那夜我平生第一次大醉，是廖文扶我回房。那时他还不是傅家堡总管，只是个刚进堡的普通下人，所有人都在为大哥和梨落欢庆祝福，只有他注意到已醉得不省人事的我，整夜待在我房里，陪我至天明，并听我叫了一整夜梨落的名。我不拿他当外人，升他做管家，只因这波涛暗涌的宏城巨堡内，我需要有这么一个人来懂我。我曾深深追悔，原本天灵山一战，我与大哥打过赌，不管生死都要拿下。那场大战的赌注有两个，一个是绝世美人毕梨落，另一个，是我腰上这把游龙软剑。那一战，我们败了江南三杰，降服漠北双煞，削了崆峒弟子的鼻子，断了唐门女掌门的手臂，还送久负盛名的武林第一庄——凌霄山庄庄主夫妇一并归西。华山、青城、武当通通拜于我们脚下，最后是少林是空大师，他不为美人而来，却为游龙软剑——这把传言中能守城护主的绝世名剑，是可以统领江湖号令群雄的千年至宝，就算是空，也不免心动。而我悔的，正是战胜之后我与大哥的选择。我得到了游龙软剑，他得到了毕梨落。那一天的风很大，我们站在高高的天灵山顶上，他抱着白狐软裘里没有露出面容的梨落，而我，还在为腰间的游龙剑喜不自胜。那时我还不曾这样刻骨铭心地爱过一个人，还不曾懂得，绕指柔比金刚圈更易将一个人的心禁锢深锁。3梨落比我出堡之前更瘦了，我方推门而入，她已经把自己紧紧嵌在我怀里。我极艰难地叫了一声大嫂，她一颤，随后抬头望着我，眼眸里是一片荒凉。她说你还要我怎样？同意他纳妾？眼睁睁看着一个烟花女进堡，而后为他生养子嗣，将我与孩子一步步排挤出去？我办不到，死也不同意。我还是抱住了她。我说你不要胡思乱想，你是大哥的正妻，孩子是大哥的长子，在傅家堡里，你永远都是当家主母，任何人，任何事，都不比你对大哥来得重要。梨落退开，眼泪从她面颊上狠狠砸下来。她摇头说，可是长风，这是你的孩子啊。竟是如此。这比大哥纳妾的消息更令我震惊，我的脑子似要炸开。我想起很久以前那段狂乱的耽溺于鱼水之欢的日子。那时梨落与大哥也不过新婚，却终日以泪洗面，大哥追问，她说思念天灵山上临冬绽放的梅花，大哥便于一个晴朗的冬日上山，打算亲自为她折梅移植到堡内。而一夜大雪封山，他进去，便困在山中七日不得出。在他离开后的第一个飞雪夜，我卧房的门被梨落敲开，她站在风雪里，如一只失群孤鸿，那一夜，我没让她离开。唯此七日，让我知道了灵肉相契是多么令人心驰神往的境地，但此后我知道不会再有了，不会再有任何一个女人叫我迷乱如斯，而毕梨落，这个女人我却永不可得。她说她爱我，我令她想起她父亲。当年在天灵山，她父亲也是这样力敌群雄，拔走五灵教圣坛上的游龙剑。他气宇轩昂地走到母亲面前，只轻轻挽一个剑花，便看到了面纱下母亲那张精致绝伦的脸。他问她，这么多年了，你还愿意跟我走吗？母亲没有回答，他于是笑，用游龙软剑挑断自己双脚的脚筋。他说我的大仇已报，如今，还是我留下陪你吧。这个故事令她心碎也心醉，她说他们就这样由仇人变成爱人，在天灵山上厮守半生，直到另有仇家寻来，双双辞世。梅花窗下，她偎在我怀里问，有一天，你会带我走吗？我拔出那把游龙剑，我说毕梨落，江湖这么大，走到哪里去呢？是，江湖这么大，但何处令我们容身？我心如火焚，这就是大哥要纳妾的原因吗？或者对梨落，他亦绝望，但傅家堡的声誉不能毁，所以他甘愿绿云压顶，独自神伤。而梨落始终在哭，她说，我不能看着长宁娶来新人承袭傅家堡的一切，我死无碍，但孩子呢？待到傅家再添人丁，便是孩子的死期。她殷殷地望着我，眼里燃起一道光，她说带我走吧，如果有你，我不会想要什么傅家堡，就让那女人占尽一切，而我们寻个没有纷争的地方，像我父母一样逍遥一世可好？我脑子里还是乍听到的各种消息，江湖这么大，但要如何才能两全？迟疑的片刻，我看见梨落眼里的光，暗了下去。4我从玉箫阁出来，大哥和廖文站在正厅外的石阶上说话。大哥怀里还搂着一个红衣的姑娘，那样子看起来喝了不少酒。经过我身边的时候我跟他打了声招呼，他面孔赤红，应声时喷了我满脸酒气。他怀里的姑娘正嘻嘻笑，与我擦肩时我看见了她手上的蔻丹，还听见她说大公子，待会儿我喂你吃葡萄。他们走后廖文同我说，大哥吩咐他去厨房点两盅冰糖雪蛤，稍后会亲自给夫人送去。看得出他对梨落还是很体贴的，廖文说，一日夫妻百日恩啊。结果大哥带回的姑娘令他没时间去送雪蛤，梨落从我手里接过青花瓷盅，眼里再也没有了光亮。入夜后我怎么也睡不着，想起大哥带回的那个姑娘，终于还是穿上衣服，跨马直奔明月楼。那姑娘白日里陪我喝过花酒，入夜时刚从傅家堡返回。她为我斟酒，我说下午怎么见着我不打招呼？她笑嘻嘻地说，吃醋了？我讪笑，下午你和我大哥做什么了？她这回笑得更欢，她说从明月楼出去的姑娘，能干什么？我问，大哥要纳的小妾可是你？她像是听了个笑话，笑不可抑地说，你大哥几时说过要纳妾？要是我，他还能在事毕管我讨红药，还让我喝下一盅加了红药的冰糖雪蛤？我震惊地掀翻了她。这一次我连马都顾不上骑，直接展开轻功，直扑傅家堡。5我果真没来得及。等我到达玉箫阁，大哥和廖文也在，而梨落躺在床上，面白如纸。明月楼姑娘们用于堕胎的红药，果然药效奇快。夜里我和大哥在桃林里喝酒，他醉了，醉后就容易说真话。他说天灵山那一战，我受的伤令我终生不可能有子嗣，长风你说，梨落这个孩子，会是谁的？原来他并不知我与梨落的事。稍停他又说，是谁的都不重要，所幸梨落还属于我。我懊恼地问，你既爱她，为何还要纳妾？他无话可说，等我看时，他已伏在石桌上睡着了。第二日我醒来时日当正午，正梳洗，马厩处传来一片嘈杂。我匆匆跑去，未到近前，突听一声马的长嘶，心里一凛，就有婢女在尖叫：大公子坠马了！大哥已经不行了。一只散落的马蹄铁磕进他后脑，两枚锈铁钉还在上面。他的胸腹又受烈马狂踏，当我扶起他时，他全身破败如絮，笼罩着的净是酒气。我哀号，哭着大声唤他名字。他给了我最后一眼，那一眼充满疼爱、怜惜、眷恋，直至最后转化为惊惧。我还来不及回顾他那一眼中的变化，只觉得自脊背到胸口骤然一凉，下意识回头看时，却见刚刚尖叫的婢女，也正被梨落一剑洞穿左胸，缓缓倒在血泊中。插在我胸上的那把剑令我无力还击，在最后一点力气消失之前，我的呼哨唤来了我的马，在跃上马背的那一刻，黑暗如巨网一般牢牢笼住了我。6傅家堡一夜之间，连失二主。关于傅家兄弟的传闻，江湖上公认，故事的开始与明月楼的艳姬小红有关：自早春傅家主母毕梨落怀孕以来，傅大公子就难耐春宵寂寞，迷上了明月楼的头牌花魁小红，还预备择日迎回为妾。谁知这小红姑娘又是傅二公子的旧相好，所以一时间兄弟反目。武艺略胜的傅二公子用游龙剑取了大哥性命，自己愧恨难当，从此隐于世外，杳无音信。但也有人说，是傅家堡主母下过追缉令，昔日那个手执游龙剑独步天下的傅二公子，如今只如丧家之犬，惶惶不可终日。在那之后，明月楼的花魁小红也难觅芳踪，甚至整个明月楼都在一夜之间人散茶凉，凋敝歇业。隆冬时节，堡主夫人毕梨落产下傅长宁遗腹子，此后萧条数月的傅家堡再振声威，全国各处银庄赌场生意兴隆。有人觊觎傅家堡盛大家业与绝世的毕梨落，但无奈堡内单习武之人便七百有多，何况还有忠心护主的管家廖文和游龙剑，故几次挑衅后，终未得逞。江湖传闻从来如此，足以采信，但难以知晓是否真相。悠游客的出现也是这样，关于他来自天灵山的传闻，也无人知道是否贴近真相。他在城里杀了很多人，杀完一个，就会在尸体上留下一朵梅花。见过梅花的人都说它们来自天灵山，是经过寒冬磨砺才开得出的那种花。所以悠游客也决定，等到飞雪时节血洗傅家堡。等待的时候他回想很多事，周身扬起一股肃杀的风。他的确去过天灵山，下山的途中，采了一些临冬怒放的红梅花。他其实不想这么无所遏制地杀人，但他们令他愤怒，一句闲言，一句碎语，通通都是被杀的理由。早在失去游龙软剑的那一刻，他就变成了一个肆无忌惮的杀手。7人们不再叫我傅二公子，他们叫我悠游客。在天灵山，我明白了很多事。在我看到当初激战后留在石壁上的那些刀斧印，当我看到昔日五灵教圣坛下埋下的四具白骨，我便明白了真相：那四具白骨，一对是毕梨落的双亲，一对，是我父母。当年到天灵山找五灵教寻仇的，是我傅家堡。大哥那年不过十三四岁，习武初成，便也同去，他便是在那场对决之中第一次见到毕梨落。后来我曾听大哥说起，两对仇家尽管都深受对方巨创，临死前却化尽戾气，彼此相惜，含笑而去。决战后大哥独自归来，研习武艺，挑起傅家堡全副重担，十年后，他与我再战天灵山，夺得了五灵教两样至宝。难怪他会毫不犹豫地选择毕梨落，早于我十年，她便已是他心里唯一选择。然而是毕梨落误解了那一场对决，为上一辈早已化解的恩怨抱恨而来，离间大哥与我，令他与我在江湖之上留下青楼浪荡名，然后杀掉大哥嫁祸于我，以小红为引，从此令我有家归不得，而如此一个傅家堡，尽数归为毕姓。无论如何，我决意去找她。8既然恨我傅家，你为何还留下孩子？那一夜你不是服下了大哥央我送去的冰糖雪蛤？你是企望他日我若真能归来，会看在孩子的分上饶过你吗？我的竹剑抵住毕梨落的喉咙时有些抖，但仍问得干净利落。她不慌亦不乱，只抬头看看我，然后又埋下头，就着灯火绣手中婴孩的小衣。我手里的剑又紧了紧。我说你的想法未免天真，自从大哥在我怀里不明真相地死去，我对你，所剩的便只有恨。她白皙的脖子渗出血，血落在小衣上时她抬头微笑，她说你以为长宁不知道吗？直到那天早上，他能想到成全你我的方式，就是死。我大惊失色，更深的迷雾却再次笼罩住我。从看到梨落将剑刺进婢女胸膛的那一刻开始，一幕一幕退后。在那时，有人同时刺穿了我的身体，并将我腰间的游龙剑抽走。然后是冰糖雪蛤，然后是明月楼的小红。再然后，有一个人在我还未进入玉箫阁之前，曾站在楼下问我打算怎么办，他意有所指，他怂恿我带梨落私奔。在我叫出这个名字时，廖文的笑在房里响起。傅家堡的家丁武师们随同他从门窗外涌入，他们手上刀剑的寒光雪亮，他说二公子，好久不见。9天灵山上傅二公子杀了武林第一庄庄主楚一笑夫妇，却不想于乱中逃走的，还有凌霄山庄少庄主楚廖文。傅长宁成亲之日，楚廖文化名进了傅家堡，一心复仇。直至他听闻了傅二公子口中女子的名字，漫漫长夜中，便有了一个计划。我的竹剑对准了手执游龙剑的楚廖文。傅家堡七百多号习武之人是我调教出来的，靠他们对付我，我又怎会深陷囹圄。楚廖文追我至天灵山顶，那把游龙剑寒光闪闪，但我知道，他赢不了我。他的剑距离我的咽喉不过寸许，这令他笑得很畅快，像一种夜行兽的嚎叫。我只有最后一问，毕梨落，她如何会听命于你？而我的孩子，你又为何要留下他？哈，到现在你还如此天真。楚廖文笑道，若不是那孩子姓楚，我怎会始终按兵不动？我和你一样，会殚精竭虑护着他，直到他长大成人，顺理成章袭下你傅家堡全副身家。你以为我与毕梨落素不相识？傅二公子，江湖艰险，靠早已四分五裂的五灵教，如何能佑护年幼的毕梨落在失掉双亲后顺利长大？如果不是我凌霄山庄，毕梨落早已化灰化尘，游龙剑也早就遗落江湖。是我父母在五灵教圣坛前受挚友托孤，十年来，靠了天下第一庄的名号暗中维持五灵教不灭。我与梨落从小一起长大，她早已暗许我楚家，十年后天灵山一战，说是各路豪杰争相一战，事实上，父亲早已得到游龙剑，而毕梨落也会在众人面前，成为我楚廖文的妻子。他紧了紧手中的剑，又道：但我想不到的是你们傅家堡，你兄弟二人竟不惜与整个武林为敌，也要从我手中夺得梨落与游龙剑。我就这样眼睁睁看着你大哥迎娶梨落，而你仗着游龙剑称霸江湖。若不是那一夜你酒后失言，至多我侥幸悄悄取你性命，然后一生接受你大哥的追杀。我不想过那样的日子，原本这一切，都该属于我凌霄山庄，都该是我的！他张狂地大笑，但笑声未止，我竹剑轻轻一拨，抵住我喉咙的游龙软剑像一只骤然折返的白鹰，在他不可思议的表情里，剑刃回挫，自剑柄而出，深深埋进了他的喉咙。我说过，他赢不了我。不懂剑的人，绝世名剑在手，也不能识得其中精妙。如他一样，将剑尖对准敌人的时候，也将死亡对准了自己。而扑上来的那身白衣却惊住了我，那是毕梨落。我拔出埋于楚廖文喉中的游龙剑，再一次用剑刃抵住了她的喉咙。10她说长风，你为什么不带我走呢？血从她的喉间滴落到那身白衣上，像天灵山上熬过苦寒盛开的红梅。我说没有用的，你的柔情，再也摧毁不了我。她凄厉地一笑，说，你要杀我？我的手很抖，但仍坚决地说了一个字，对。她说那么你忘了，在傅家堡的那七日。那飞雪的七日，你对我说过，你爱我一生一世。我说不要相信一个男人的誓言，就像不要相信一个女人的眼泪一样。如果傅家堡里的那个你从头到尾都是一个阴谋，你如何还配得起我对你的爱。她又笑了笑。她说这一幕很熟悉，二十多年前，想必我父亲也是这样用剑指着我母亲，而后来，他却用指着她的剑，断了自己双足。我说，痴情的故事已经打动不了我，他们有爱，我们，只有恨。说这话的时候我的剑已经划破了她的喉咙，但下一秒，她倒下去的身体让我感到双足与心一样有碎裂的疼痛。跪倒在她身边的时候她手里那柄金光闪闪的小剑上沾满了血，她说原来你也不懂游龙的秘密，这把剑中剑，才是真正的游龙。她满足地闭上了双眼，鲜血奔涌时她发出微弱的喟叹。她说你终于也被游龙断了双足，从此后，就让我们像我父母一样，在这天灵山上相依相守。我挣扎着抱起她尚有微温的身体，我哭了。她说我负了楚廖文和你大哥，我只是一个想得到所爱的寻常女子。我曾无数次幻想你能带着我归隐天灵山，但你始终不是父亲那样至情至性之人，肯为一个女人抛下一切。是失望令我狠心，是绝望，令我顺从了廖文。这一刻，我不得不承认，对她的爱，我甚至比不上大哥深。她最后一次看了看我，说，但我没有骗你，在楚廖文面前，我的谎言保住了那个孩子，我们的孩子，他会在傅家堡里，安然长大。天灵山上风雪怒号，她合上眼的时候，我想起一句话。傅家堡飞雪七日里，她曾在梅花窗下殷殷问我，长风，带我走好吗？我摇摇头说，毕梨落，江湖这么大……
随着类的数量增加，创建二进制的组件就变得很平常了：你想分离部分功能。所有不同的是，二进制组件可以让你独立的保存这些离散的功能。创建的组件程序集可以方便的共享逻辑，方便交叉语言编程，以及方便布置。在.Net程序就是组件包，每一个程序什么样可以随时更新和发布。你应该已经知道，基于程序集之间的应用程序是多么容易更新呀，程序集之间的偶合是多么好呀!最小偶合就是说更多的是减少了程序集之间复杂的依懒关系。同时也意味着你可以简单的更新小块新的程序集。这一章就是关于创建简单易用，容易布置，以及容易更新的程序集。.Net环境中的应用程序可以由多样的二进制组件组成。随后，你可以独立的更新这些组件，也就是可以在一个多程序集应用程序中安装与更新一个程序集。但你必须明白一件事，那就是CLR是如何发现和载入这些程序集的。在你创建这些二进制组件时，你也就必须创建符合这些明确期望的组件。接下来就介绍这一概念。CLR并不会在程序启动时加载全部的引用程序集。更合适的作法是当运行时须要程序的成员时，加载器才解决程序集的引用问题。这可能是调用一个方法或者访问数据，加载器先查找引用的程序集，然后加载它，然后JIT编译须要的IL。当CLR须要加载一个程序集时，先是断定那个文件要加载。程序集的元数据包含了对所有其它程序集的引用记录，这个记录有强名称和弱名称之分。一个强名称(程序集)由四部分组成：程序名，版本号，语言属性(译注：这里的语言范指区域性特定的信息，如关联的语言、子语言、国家/地区、日历和区域性约定)，以及公开密钥。如果程序集不是一个强名称程序，那么就只记录了程序集名。如果你的程序是一个强名称程序集，那么你的程序集就不太可能被一些恶意的组件(程序集)所取代。强程序集还可以让你用配置文件对组件的版本进行配置，是新的，还是先前的。确定正确的程序集以后，CLR接来就断定这个程序集是否已经在当前的应用程序中加载。如果是，就用原来的。如果不是，CLR就继承查找程序集。如果程序是强名称的，CLR就先在全局程序缓存(GAC)中查找，如果不在GAC中，加载器就检测代码目录(codebasedirectory，译注，这里只是译为代码目录，其实并不是源代码所在的目录)中的配置文件，如果当前代码目录存在，那就是唯一被搜索程的目录了。如果在代码目录中没有找到所要的程序集，那么加载就失败。应用程序目录。也就是与主应用程序集同在的位置。语言目录。这是一个在应用程序目录下的子目录，这个目录与当前语言名匹配。程序集子目录。这是一个与程序集同名的子目录，这两个可以组合成这样：[语言]/[程序集名]私有的运行目录(binPath)。这是一个在应用程序配置文件中定义的私有目录。它同样可以和语言和程序集名组合：[bin目录]/[程序集名], 或者[bin目录]/[语言],还可以是：[bin目录]/[语言]/[程序集名]。从这个讨论中你应该明白三个内容：第一，只有强名称程序集才能放到GAC中。其次，你可以通过配置文件来修改默认的行为，从而升级程序中个别的强名称程序集。第三，强名称程序集可以阻止对程序集的恶意篡改，从而提供更安全的应用程序。了解CLR是如何加载程序集的，有利于在实际操作中考虑如何创建和更新组件。首先，你应该使用强名称程序集，把所有的元数据信息都记录下来。当你在VS.net中创建一个项目时，你应该把assemblyInfo.cs文件中的所有属性都填满，包括完整的版本号。这会让你在后面的升级中更简单的。VS.net在assemblyInfo.cs中创建三个不同的部份，首先就是信息报告：[assembly: AssemblyTitle("My Assembly")] [assembly: AssemblyDescription ("This is the sample assembly")] #if DEBUG [assembly: AssemblyConfiguration("Debug")] #else [assembly: AssemblyConfiguration("Release")] #endif [assembly: AssemblyCompany("My company")] [assembly: AssemblyProduct("It is part of a product")] [assembly: AssemblyCopyright("Insert legal text here.")] [assembly: AssemblyTrademark("More legal text")] [assembly: AssemblyCulture("en-US")] 最后一条，AssemblyCulture只针对本地化程序集。如果你的程序不包含任何的本地化资源，就空着。否则就应该遵从RFC1766标准填写语言描述信息。接下来的部份就是版本号，VS.net是这样记录的：[assembly: AssemblyVersion("1.0.*")] AssemblyVersion包含4个部份：主版本号.副版本号.编译号.修订号，星号就是告诉编译器用当前时间来填写编译号和修订号。编译号就是从2000年1月1号起的天数，而修订号则是从当天凌晨起的秒数除以2。这样的算法可以确保修订号是不断递增的：每次编译后的号码都会比前一次高。(译注：我有一点不明白的，就是如果我把本机时间修改了呢？或者两台机器上的时间不一致，会有什么问题呢？当然，这并不会有什么太大的问题。)对于这一版本号的算法，好消息是两个编译的版本不会有完全相同的版本号。坏消息就是在你编译并发布后，要记住这个版本号。我个人比较喜欢让编译器给我生成编译和修订号。通过记录发布时的编译号，我就知道最后的版本号了。我从来不会忘记在我发布一个新程序集时修要改它的版本号。但也有例外，COM组件是在你每次编译后自动注册的。如果还让编译器自己生成编译号后然后注册它，那么很快就让注册表里填满了无用的信息。最后一部份就是强名称信息：[assembly: AssemblyDelaySign(false)] [assembly: AssemblyKeyFile("")] [assembly: AssemblyKeyName("")] 应该考虑为所有的程序集都创建强名称，强名称程序集可以防止别人的篡改，而且可以为单个应用程序独立更新个别程序集。然而，你应该在ASP.net应用程序中避免使用强程序集；局部安装的强名称程序集不会正确的加载。同样，强名称程序集必须用AllowPartiallyTrustedCallers特性进行修饰，否则它们不能被非强名称程序集访问(参见原则47)。实际在更新一个组件时，公共的和受保护的接口部份必须在IL语言层上是兼容的。也就是说不能删除方法，不能修改参数，不能修改返回值。简单的说，没有一个组件愿意在引用你的组件后要重新编译。你可以添加配置信息来修改引用的程序集。配置信息可以存储在不同的位置，这取决于你想如何更新组件。如果是一个简单的应用程序，你可以创建一个应用程序配置文件，然后把它存放在应用程序目录就行了。如果是配置所以使用同一个组件的应用程序，你可以在GAC中创建一个发布策略文件。最后，如果是全局的修改，你可以修改machine.config文件，这个文件在.Net进行时的配置目录里(参见原则37)。实际操作中，你可能从来不用修改machine.config文件来更新你的程序集。这个文件包含了基于整台机器的信息。你可以通过应用程序配置文件来更新单个应用程序配置，或者使用一个发布策略来更新多个程序公用的组件。应该考虑为所有的程序集都创建强名称，强名称程序集可以防止别人的篡改，而且可以为单个应用程序独立更新个别程序集。然而，你应该在ASP.net应用程序中避免使用强程序集；局部安装的强名称程序集不会正确的加载。同样，强名称程序集必须用AllowPartiallyTrustedCallers特性进行修饰，否则它们不能被非强名称程序集访问(参见原则47)。实际在更新一个组件时，公共的和受保护的接口部份必须在IL语言层上是兼容的。也就是说不能删除方法，不能修改参数，不能修改返回值。简单的说，没有一个组件愿意在引用你的组件后要重新编译。你可以添加配置信息来修改引用的程序集。配置信息可以存储在不同的位置，这取决于你想如何更新组件。如果是一个简单的应用程序，你可以创建一个应用程序配置文件，然后把它存放在应用程序目录就行了。如果是配置所以使用同一个组件的应用程序，你可以在GAC中创建一个发布策略文件。最后，如果是全局的修改，你可以修改machine.config文件，这个文件在.Net进行时的配置目录里(参见原则37)。实际操作中，你可能从来不用修改machine.config文件来更新你的程序集。这个文件包含了基于整台机器的信息。你可以通过应用程序配置文件来更新单个应用程序配置，或者使用一个发布策略来更新多个程序公用的组件。这是一个XML配置文件，它描述了存在的版本信息和升级后的信息：<dependentAssembly> <assemblyIdentity name="MyAssembly" publicKeyToken="a0231341ddcfe32b" culture="neutral" /> <bindingRedirect oldVersion="1.0.1444.20531" newVersion="1.1.1455.20221" /> </dependentAssembly> 你可以通过这个配置文件来标识程序集，旧版本，以及升级后的版本。当你安装和更新了程序集后，你就要更新或者创建一个恰当的配置文件，然后程序程序就可以使用新的版本了。如果你的软件是一个程序集的集合：你希望个别的更新它们。通过一次更新一个程序集，你须要做一些预先的工作，那就是第一次安装时应该包含一些必要的支持升级的信息。
Effective C# 原则29：仅在对基类进行强制更新时才使用new修饰符
你可以用new修饰符来重新定义一个从基类中继承来的非虚成员。你可以这样做，但并不意味着需要这样做。重新定义非虚方法会导致方法含意的混乱。如果两个相关的类是继承关系，那么很多开发人员可能会立即假设两段代码块是做完全相同的事情，而且他们也会这么认为：object c = MakeObject( ); // Call through MyClass reference: MyClass cl = c as MyClass; cl.MagicMethod( ); // Call through MyOtherClass reference: MyOtherClass cl2 = c as MyOtherClass; cl2.MagicMethod( ); 一但使用了new修饰符以后，问题就完全不一样了： public class MyClass { public void MagicMethod( ) { // details elided. } } public class MyOtherClass : MyClass { // Redefine MagicMethod for this class. public new void MagicMethod( ) { // details elided } } 这样的实际操作会让很多开发人员迷惑。因为当你在同一个对象上调用相同的函数时，一定希望它们执行同样的代码。但实际上是，一但你用不同的引用来调用同名的函数，它们的行为是不一样的，这感觉非常糟糕。它们是不一致的。一个MyOtherClass类型的对象所表现的行为会因为你引用的方式不一样而有所不同。这就是new修饰符用在非虚成员上的后果。其实这只是让你在类的名字空间中添加了一个不同的方法(虽然它们的函数名是相同的)。非虚方法是静态绑定的，不管哪里的代码，也不管在哪里引用，MyClass.MagicMethod()总是严格的调用类中所定义的函数。并不会在运行时在派生类中查找不同的版本。另一方面，虚函数动态的。运行时会根据不同的类型对象调用不同的版本。建议大家避免使用new修饰符来重新定义非虚函数，这并不要太多的解释，就像推荐大家在定义一个基类时应该用虚方法一样。一个类库的设计者应该按照合某种约设计虚函数。也就表示你期望任何派生类都应该修改虚函数的实现。虚函数的集合就相当于是定义了一个行为的集合，这些行为是希望在派生中重新实现的。设计默认的虚函数就是说派生可以修改类中的所有虚的行为。这确实是说你不想考虑所有派生类可能要修改行为的分歧问题。相反，你可以把时间花在考虑把什么样的方法以及属性设计成多态的。当然，只有它们是虚行为的时候才能这样做。不要考虑这样会限制类的用户。相反，应该认为这是给类型的用户定义行为提供了一个入口向导。有且只有一种情况要使用new修饰符，那就是把类集成到一个已经存在的基类上时，而这个基类中已经使用了存在的方法名，这时就要使用new了(译注：就是说基类与派生类都已经存在了，是后来添加的继承关系，结果在添加继承关系时，发现两个类中使用了同样的方法名，那么就可以在派生类中添加一个new来解决这个问题)。因为有些代码已经依懒于类的方法名，或者已经有其它程序集在使用这个方法。例如你在库中创建了下面的类，使用了在另一个库中定义的BaseWidget：public class MyWidget : BaseWidget { public void DoWidgetThings( ) { // details elided. } } 你完成了你的widget,而且用户可以使用它。然而你却发现BaseWidget公司发布了一个新的版本。而这正是你所渴望的，于是你立即购买并编译你的MyWidget类。结果失败了，因为BaseWidget的家伙们已经添加了他们自己的DoWidgetThings方法：public class BaseWidget { public void DoWidgetThings() { // details elided. } } 这是个难题，你的基类中隐藏了一个方法，而这又是在你的类的名字空间中。有两个方法解决这个问题，一个就是修改你的类中的方法名：public class MyWidget : BaseWidget { public void DoMyWidgetThings( ) { // details elided. } } 或者使用new修饰符：public class MyWidget : BaseWidget { public new void DoWidgetThings( ) { // details elided. } } 如果你可以拿到所有使用MyWidget类的源代码，那么你应该选择修改方法名，因为这对于今后的运行会更简单。然而，如果你已经向全世界的人发布了MyWidget类，这会迫使所有用户来完成这个众多的改变。这正是new修饰符容易解决的问题，你的用户不用修改DoWidgetThings()方法而继续使用它。没有人会调用到BaseWidget.DoWidgetThings()方法，因为(对于派生类而言)它们根本不存在。在更新一个基类时，如果发现它与先前申明的成员发生了冲突，可以用new修饰符来解决这个问题。当然，在某些时候，你的用户可能想调用基类的Widget.DoWidgetThings()方法，这时你又回到了原来的问题上：两个方法看上去是一样的，但其实是不同的。考虑到new修饰长期存在的歧意问题，有时候，还是在短期上麻烦一下，修改方法名为上策。(译注：长痛不如短痛。呵呵)new修饰符必须小心谨慎的使用。如果它是有歧意的，你就在类上创建了个模糊的方法。这只有在特殊情况下才使用，那就是升级基类时与你的类产生冲突时。即使在这种情况下，也应该小心的使用它。最重要的是，其它任何时候都不要用它。
转换操作是一种等代类型(Substitutability)间操作转换操作。等代类型就是指一个类可以取代另一个类。这可能是件好事：一个派生类的对象可以被它基类的一个对象取代，一个经典的例子就是形状继承。先有一个形状类，然后派生出很多其它的类型：长方形，椭圆形，圆形以及其它。你可以在任何地方用图形状来取代圆形，这就是多态的等代类型。这是正确的，因为圆形就是一个特殊的形状。当你创建一个类时，明确的类型转化是可以自动完成的。正如.Net中类的继承，因为System.Object是所有类型的基类，所以任何类型都可以用System.Obejct来取代。同样的情况，你所创建的任何类型，也应该可以用它所实现的接口来取代，或者用它的基类接口来取代，或者就用基类来取代。不仅如此，C\#语言还支持很多其它的转换。当你为某个类型添加转换操作时，就等于是告诉编译器：你的类型可以被目标类所取代。这可能会引发一些潜在的错误，因为你的类型很可能并不能被目标类型所取代(译注：这里并不是指继承关系上的类型转换，而是C\#语言许可我们的另一种转换，请看后文)。它所的副作用就是修改了目标类型的状态后可能对原类型根本无效。更糟糕的是，如果你的转换产生了临时对象，那么副作用就是你直接修改了临时对象，而且它会永久丢失在垃圾回收器。总之，使用转换操作应该基于编译时的类型对象，而不是运行时的类型对象。用户可能须要对类型进行多样化的强制转换操作，这样的实际操作可能产生不维护的代码。你可以使用转换操作把一个未知类型转化为你的类型，这会更加清楚的表现创建新对象的操作(译注：这样的转换是要创建新对象的)。转换操作会在代码中产生难于发现的问题。假设有这样一种情况，你创建了如图3.1那样的类库结构。椭圆和圆都是从形状类继承下来的，尽管你相信椭圆和圆是相关的，但还是决定保留这样的继承关系。这是因为你不想在继承关系中使用非抽象叶子类，这会在从椭圆类上继承圆类时，有一些不好实现的难题存在。然而，你又意识到每一个圆形应该是一个椭圆，另外某些椭圆也可能是圆形。(译注：这一原则中作者所给出的例子不是很恰当，而且作者也在前面假设了原因，因此请读者不要对这个例子太钻牛角尖，理解作者所在表达的思想就行了，相信在你的C\#开发中可能也会遇到类似的转换问题，只是不太可能从圆形转椭圆。)这将导致你要添加两个转换操作。因为每一个圆形都是一个椭圆，所以要添加隐式转换从一个圆形转换到新的椭圆。隐式转换会在一个类要求转化为另一个类时被调用。对应的，显示转化就是程序员在代码中使用了强制转换操作符。public class Circle : Shape { private PointF _center; private float _radius; public Circle() : this ( PointF.Empty, 0 ) { } public Circle( PointF c, float r ) { _center = c; _radius = r; } public override void Draw() { //... } static public implicit operator Ellipse( Circle c ) { return new Ellipse( c._center, c._center, c._radius, c._radius ); } } 现在你就已经实现了隐式的转换操作，你可以在任何要求椭圆的地方使用圆形。而且这个转换是自动完成的：public double ComputeArea( Ellipse e ) { // return the area of the ellipse. } // call it: Circle c = new Circle( new PointF( 3.0f, 0 ), 5.0f ); ComputeArea( c ); 我只是想用这个例子表达可替代类型：一个圆形已经可以代替一个可椭圆了。ComputeArea函数可以在替代类型上工作。你很幸运，但看下面这个例子：public void Flatten( Ellipse e ) { e.R1 /= 2; e.R2 *= 2; } // call it using a circle: Circle c = new Circle( new PointF ( 3.0f, 0 ), 5.0f ); Flatten( c ); 这是无效的，Flatten()方法要求一个椭圆做为参数，编译器必须以某种方式把圆形转化为椭圆。确实，也已经实现了一个隐式的转换。而且你转换也被调用了，Flatten()方法得到的参数是从你的转换操作中创建的新的椭圆对象。这个临时对象被Flatten()函数修改，而且它很快成为垃圾对象。正是因为这个临时对象，Flatten()函数产生了副作用。最后的结果就是这个圆形对象，c，根本就没有发生任何改变。从隐式转换修改成显示转换也只是强迫用户调用强制转换而以：Circle c = new Circle( new PointF( 3.0f, 0 ), 5.0f ); Flatten( ( Ellipse ) c ); 原先的问题还是存在。你想让用户调用强制转换为解决这个问题，但实际上还是产生了临时对象，把临时对象进行变平(flatten)操作后就丢掉了。原来的圆，c，还是根本没有被修改过。取而代之的是，如果你创建一个构造函数把圆形转换成椭圆，那么操作就很明确了：Circle c = new Circle( new PointF( 3.0f, 0 ), 5.0f ); Flatten ( new Ellipse( c )); 相信很多程序员一眼就看的出来，在前面的两行代码中传给Flatten()的椭圆在修改后就丢失了。他们可能会通过跟踪对象来解决这个问题：Circle c = new Circle( new PointF( 3.0f, 0 ), 5.0f ); // Work with the circle. // ... // Convert to an ellipse. Ellipse e = new Ellipse( c ); Flatten( e ); 通过一个变量来保存修改(变平)后的椭圆，通过构造函数来替换转换操作，你不会丢失任何功能：你只是让创建新对象的操作更加清楚。(有经验的C++程序可能注意到C\#的隐式转化和显示转换都没有调用构造函数。在C++中，只有明确的使用new操作符才能创建一个新的对象时，其它时候不行。而在C\#的构造函数中不用明确的使用关键字。)从类型里返回字段的转换操作并不会展示类型的行为，这会产生一些问题。你给类型的封装原则留下了几个严重的漏洞。通过把类型强制转化为其它类型，用户可以访问到类型的内部变量。这正是原则23中所讨论的所有原因中最应该避免的。转换操作提供了一种类型可替代的形式，但这会给代码引发一些问题。你应该已经明白所有这些内容：用户希望可以合理的用某种类型来替代你的类型。当这个可替代类型被访问时，你就让用户在临时对象上工作，或者内部字段取代了你创建的类。随后你可能修改了临时对象，然后丢掉。因为这些转换代码是编译器产生的，因此这些潜在的BUG很难发现。应该尽量避免转换操作。
发布于：2007-9-26 20:18:16我只记得那一天晴好。那一天，我的骆驼死了。是正午，太阳很大，它倒下去的时候，发出了一声沉重的哀鸣。尽管我背上的包袱很重，水囊里一滴水也没有了，但我还是决定停下来，为它在原地掘一个坑，令它的尸骨有一处不受烈日曝晒的安息地。神诫，常怀恻隐之心，我是记得的。而只此一念，这个埋尸坑，令我青史留名。忘记说，史，不仅是竹简麻编的中国通史。亦忘记说，名，我的名是，斯文?赫定。王胤我叫王胤，绿林山人。假如你还记得多年前的那场绿林之战，你就会对我的姓肃然起敬。我的祖籍在新市，这样说你可能更为明白，在新市，没有人不知道王姓人家，我的先祖正是王匡和王凤，在外戚王莽当政之际，是他们带领挖野荸荠尚不能为生的农民发动了一场声势浩大的起义，而鏖战两万官军的那场战争，有人称为，绿林之战。这是我先祖积下的功德，他们的侠义精神至今在江湖流传，所有生于草莽仗剑为民的人，都被称为“绿林好汉”，而我亦有幸，沾染祖上无上荣光。我的身份是游侠，这是一个令人欣慰的职业。很多人对游侠的含义有误读，他们认为，游，则为游手好闲；侠，即是惹是生非。他们所能理解的游侠，既有别于官府差官，又不同于义军将士，当然也达不到发个“巨子令”就威震江湖号令群雄的地步。所以游侠是一种被忽视进而被藐视的职业，甚至于，它竟算不上一种“职业”。但祖上有遗训告知后辈，江湖之上，只有游侠一业，得为存，得为寿，得为公。那年是绿林之战的第二年，绿林山上发生了一场罕见的瘟疫。五万绿林军几乎死掉一半，剩下的只好兵分三路，转战汉江两岸。此后刘秀在南阳郡舂陵乡随同起兵，破落贵族刘玄成了更始帝，拜我先祖为上公。但所谓天道大行的起义军，却在不久后刘秀自封大汉光武帝，我先祖与赤眉军将领樊崇一起被杀后，分崩瓦解。王道，是野心家的王道；江湖，是求胜者的江湖。这番道理，我懂得。故此，从我出生之时起，我便在流浪。流浪对于一个游侠来说，就好像空气水分一样不可缺少。我从汉江浪迹至夷地，又从南疆跋涉至塞外，行走，当然是没有意义，假如一定要给上一个，或许是，逃避。逃避一种渴望，我深知。我的内心，潜伏有扬名立万的狂热因子，这与游侠精神是绝对背离的，这也与祖训背离。有时候我在独行的路上，耐不住庞大而漫长的寂寞，就会背一本残破帛书上写的话，那段话记载了我先祖的身后事：身首异，四肢残，烹脑焚心，佐以甘食；又以皮为鼓，骨为槌，隆隆进舞，王尊堂前，顶骨为杯，割腥啖膻，笑若枭鸣……每每背到此处，我的心里，就升起一种难言的悲凉。这悲凉与想要扬名的狂热化为两股气流，在我血液之中逆行冲撞，然后通通汇至左胸，就好像要咆哮而出一样。很多时候我运气打坐，诵武功心法，疑心这就是武艺精进时高手们常说的气通任督二脉，但那股气，一直都没有真正从我左胸喷出，而做为一名游侠，我真正仰赖着行走江湖的，非难驭的无形之气，而是背上那把有形之剑。我的剑，名唤纯钧。当年勾践想用纯钧换得骏马良田和大城，薛烛却说，这把剑是天人共铸的不二之作。为铸这把剑，千年赤堇山山破而出锡，万载若耶江江涸而出铜。铸剑之时，雷公打铁，雨娘淋水，蛟龙捧炉，天帝装炭。铸剑大师欧冶子承天之命呕心沥血，与众神铸磨十载此剑方成。剑成之后，众神归天，赤堇山闭合如初，若耶江波涛再起，欧冶子也力尽神竭而亡。故此，纯钧已成绝唱。纯钧是我祖上留下，在中原腹地，它从未开光。但终于，我还是用它杀过人，在边塞，一队异族人。那日我正在关内一间小店饮酒，酒后，我原打算离开中原，去塞外隐居。这些年走南闯北，令我会得各种异族语言和习俗，塞上牧马，是我能想到的又一种游侠生活。那家店卖关中有名的烧酒，没有名字，只是辛辣醇厚，用红釉瓷瓮盛着，不设碗，就着喙形瓮口饮，甚是爽快。我有好酒量，还有好的品酒本事，尤其是对酒香酒味，能称上过喉不忘。这酒分外特别，令我肠胃妥帖，我便额外叫了半斤牛肉，欢喜大啖。正嚼得欢，嗖的一声，门外酒招飞了进来，断掉的半截竹枝牢牢斜插进我面前剩下的半块牛肉里。我大骇，想不到这边关之地，还有这样的高手。这时候外面的喊杀声已起，我探头朝门外看，正是一队剽悍的异族骑兵，在追杀一个浑身染血的男人。男人脚程略快，进了店，拴门，店内冷清，只得我与老板小二三人。老板和小二早已吓得钻进案桌底下瑟瑟发抖，我坐在那里，手放到碗边的剑囊上。男人也是异族人，扑过来，用番语喊：“酒窖在哪？酒窖在哪？”我侠义心又起，走过去扒开案桌。老板一边尿裤子一边指：“内房有一只铺苍狼皮的红木躺椅……”我搀着男人进去，我知道，他想借酒窖逃过门外追兵。凡边塞有名的酒馆，店家一定会有祖传秘酿做酒种，为防外泄，常在地底做一个迷宫一样的酒窖。窖内四通八达，只有一个出口通往藏秘酿的地穴，那穴为了酿酒时的通风防潮，通常是留出了风口在戈壁里，或者朝向一面丘壁，或者通往一片胡杨林，假如进了酒窖找到出口，这一场劫难，该是能避过了。我与那男人进了酒窖，在窖口，隐约还能听见骑兵破门的声音。寻找秘酿风口的时候，男人的面色逐渐变白。他抓住我的手，豆大的冷汗开始往下滴。他说，楼兰内讧，你救我，救楼兰。我心里一动，问他，你是何身份？他呕出一口浓血，说，我是楼兰雏君，摩怗。我胸间突然就有气血翻涌，于是俯下身，将他背到了我背上。我说过，我有好的品酒本事，对酒香酒味过喉不忘。在黑暗的沙石暗穴里，我从店家为混淆盗种人而放置的不同窖藏当中，分辨出一种特别的香味，那种香，不若别者混有黄土瓦瓮的泥腥味，却夹杂着红釉瓷瓮独有的澈洌醇美。况且，习武令我兼有一副好耳力，于难辨的咝咝漏风声里，令一个江湖客找到秘酿所在，实在不是难事。我们躲过了追兵。那一夜，出风口的时候月亮特别圆。大漠里静悄悄，偶有狼啸。我重返了一趟酒家，那队骑兵正宿在店里，大概是预备死守到天明。老板和小二的尸体被挂在原来挂酒招的木柱上，影子投在沙地里，分外可怖。毫无疑问，我用纯钧杀死了那一队异族骑兵。天将明的时候，我从酒窖出来，取走了一样东西。索依娜很多时候，我觉得我的生活太过沉闷。你要知道，我说的是生活。我的生活其实很简单，我是一个平凡的牧羊女。从我有记忆开始，我和母亲就在这片草原上牧羊了。这里的景色非常美，雪川高原，碧野苍茫，牛羊遍布，河水涓涓。它有一个更美的名字——krora'ina——楼兰。每天清晨太阳初升，那种金，便会注满草叶间的朝露。这时候我会赶着羊群走过原野，远处的雪山在金色阳光下显得圣洁神秘，纯净的融水顺着冰川崩塌的裂缝流进浅浅河道。河水蜿蜒，从我身边静静流过，不远处，就是楼兰圣地——蒲昌海。楼兰人的先祖们从遥远而寒冷的异域迁移而来，曾在孔雀河、铁板河一带过着临水而居半耕半牧的游牧生活。白天在水草丰美的河滩上放牧牛羊，晚上燃起篝火，跳欢快的吐火罗舞。他们的生活单纯快乐，直到一场战争降临于乐土。那场战争，发生在多年以前，后来，君王领着他们，告别了飘摇不定的游牧生活，在蒲昌海边定居下来，建立了昌盛的楼兰城。从那时候起，人人害怕战争，但有时我会想，战争，它也常和一些旷世奇情连在一起。当然我不懂我到底是不是期盼战争，这样想是非常罪恶的，但我知道在中原，有一句话叫做“冲冠一怒为红颜”，这句话或许在很多年以后会被某个历史事件盗用，但此刻我知道它，我知道它的含义，是说美人与英雄。他们都说我是美人，与中原的“红颜”一样的美人。但他们说楼兰没有英雄，楼兰的英雄生死未卜，他是楼兰的雏君，也是楼兰的希望。这么说英雄和美人是没有故事的，那么这沉闷的生活，总该是有故事发生。我不知道为什么会有这样的预感，也许是从那个早晨开始。那天我的羊群在蒲昌海边喝水，我在岸边的胡杨林里剥一片叶子做口笛。当我用口笛吹出第一声响的时候，我看到一个浑身染血的男人。那个男人的装扮很奇怪，他应该是从楼兰以外的地方来。他有一匹马，马也受了伤，马背上驮着一只笨重的黑色布袋，足有整个马背那么长。他从马上翻身下来，取了一只水囊，走到湖边打水喝。他喝水的样子很豪爽，就像是楼兰的骑士在行军帐外饮酒一样，但他又不像他们那样鲁莽地发出啧啧声，他的喉头，有一种好听的咕嘟声响。喝完水他坐下来脱夹衣，这时候我才看到，他的背上有一道非常深的刀伤，该是楼兰的骑士用“白狼斩”砍伤的。“白狼斩”是楼兰男人对付最强悍的敌人时所惯用的武器，这么说，他该是楼兰的敌人了。他背对着我，用打湿的布条清洗伤口，姿势很别扭。不知为什么，我心里一点惧怕都没有，走过去接过他手里的布，轻轻为他擦起伤口上的血渍来。他也不动，不说话，等我擦完，他转头看我一眼，披上衣服，就上马走了。走的时候他回了一下头，我正站在水边，他在胡杨林里。他看着我，把手里那只装满水的水囊丢了过来，然后笑了一下。等我捡起水囊的时候，他已经消失在胡杨林里，我突然想到，帮助一个楼兰国的敌人，到底是不是对的？而这以后我时常做梦，梦见那男子的面容。假如我猜得没错，他应该是去过中原的班库老爹所讲的那种游侠。他的面容很冷，受伤的背上有一只缠满布条的剑囊，他不爱说话，但我为他擦背的时候，他背上瞬间纠结僵硬的肌肉告诉我，他是热血的。我随身挎着那只水囊，那里面的水，我一滴都舍不得喝。我总疑心有什么事情要发生，这件事，会像他的剑一样，划破我沉闷的放牧生活。很快，这件事真的发生了。楼兰的雏君归国，肃清所有叛乱之人，那些阴谋篡位的、绝地追杀的、阻拦归国的，通通被处死在楼兰城楼上。他们的尸首被秃鹫啄食，楼兰城的上空，有很长一段时间散发着腐尸的味道。当然这些与我无关，与我有关的是，我见到了楼兰新君，并且留在王殿里。摩怗我觉得我越来越像个侠客，是那种世人口中肆意妄为的江湖游侠。我历经浩劫回了宫，继位，屠杀，有点中原人快意恩仇的意思，想必是在边关待太久的缘故。在边关，是发生了一点事情的，所以我带了一个人回来，是这个人成就了我。内乱平定以后，我的性情改变了很多。我变得寡言，少出行，并厌倦看似热闹的臣下朝见。我的睡眠变得很好，尽管仍时常做梦，梦却很美。有一次我梦到一个姑娘，她有很浓密的长发，而且沉静的面容是我喜欢的样子，她在梦里望着我，嘴角有点微微上扬。那天醒来，我照了照铜镜，问王胤说，此梦何解？他答道，凤求凰，战事平。我也认为这是个吉兆。接下来的一整个夏天，楼兰城的上空都是腐尸的味道，白露的时候，我决定举行一场祭祀。那一天，我独自去了蒲昌海边的胡杨林，我没有骑马，只牵着一匹老迈的骆驼，骆驼背上，驼着王胤的黑布袋。就在那一天，我见到了梦中的姑娘。这一次，她的面容有点哀伤，她正在吹一只用胡杨树叶做成的口笛，而她的腰上挎了只熟悉的水囊。我一直站在水边听她吹口笛，那一天城内的火光格外璀璨。远处传来闷重的牛角号声，不同于战时的嘹亮绵长，应该是尼雅的新君派来求和的使者到了。但我决定暂且不去理会，在胡杨林里我埋下那只黑布袋以后，又听了一会儿口笛声，才悄悄离开。很快我便知道，那个湖边吹笛的姑娘叫索依娜，是个平凡的牧羊女。但我觉得她身上的情致韵调，有点如同中原江南的闺阁女子。如果按照王胤的记忆来说，她与汉江边的采莲女更为神似，她们不仅有相同娇艳的芙蓉面，她们更有一双同样柔若水软如酥的白玉手。有一日晨起梳头，面对铜镜，我听见王胤说了句，你该迎她入王殿。于是那日黄昏，我在有些森冷的殿外，等到了被骆驼车载来的索依娜。在很长一段时间里，我喜欢在羊皮纸上画一种画。当我从楼兰国的四疆巡查归来，就会把游历过的山川地貌画成一幅精致的工笔图。在图上，我习惯用汉文题字，我新调来的近身随侍时常会讨好我，说我的汉文又精进了不少。不做这些事的时候，我喜欢看索依娜跳舞。成群的仆役和婢女鱼贯而出，只余下我和她。她用沙漠中少见的温泉沐浴，然后会涂上从安息运来的昂贵乳香。大多时候她爱穿红色舞衣，跳欢快的吐火罗舞，这时候我看着她，会想到中原流传的“红颜”一词。她在很多场战事以后为我跳舞，每一次想起吐火罗舞，就会想起楼兰四周的敌人还有很多。日渐强大的尼雅，狡黠的米兰，还有同样虎视耽耽的疏勒、龟兹、洛浦、若羌，夜深的时候我在红帐里抱着她，会回想起王胤的话——何处不江湖，何处不杀戮。但我们从不裸呈相对，也极少有深入交谈。只有一次索依娜问我，你入过边关，是否见到江湖？中原是否有句话，叫做人在江湖，身不由己？我说江湖不是用来见的，江湖，是用来趟的。我说我是君主，循王道，为权累，就是我的江湖；而你是否见过一个叫王胤的男子，他是游侠，剑光之下快意恩仇，为情累，就是他的江湖。索依娜没有说话，她的身体有点凉，还有一种香。那一夜王殿之外有枭声，在睡梦中我意识到索依娜起身走到窗边，摸出她的口笛，吹出三声奇怪的哨响。而后她急速消瘦，开始厌倦王殿里的生活。边关战火又起，竟是尼雅来袭。我不再有时间分给她，她便常常独自一人，骑着骆驼回家探望母亲。母亲我是异族女子，长到索依娜这么大之前，我的少女时光都是在离楼兰甚远的尼雅城中度过，我曾是尼雅城郊的一个乡间纺纱女。那时候我的家在莽莽昆仑山下，尼雅河从门前流过。这条绿色的生命线，从吕什塔格冰川上流下来，绵延两百多公里，流经绿洲的中心大麻扎。我对那片绿洲的记忆很深，那里有遍布的苇草和葱绿的胡杨木，还有成群的牛羊悠游在最肥美的草圃上。在房前，我栽种了很多桑树，每天清晨我会采下鲜嫩的桑叶来喂蚕。在蚕宝宝咀嚼桑叶的沙沙声中，我开始挑线，纺纱织棉。每隔一段时间，我会将织好的纱和布拿去尼雅城卖，再带回一些食物和丝线。夜晚我喜欢在羊油灯下为丈夫缝制锦袍，想到他，我觉得甜蜜。他是个勇猛的弓箭手，能够百步穿杨。当他从君王的军队里回来时，常带来鲜美的野味。那时候我们生活很平静，入夜的时候，他喜欢燃起篝火为我烤羊腿，我就牵上他那匹高大漂亮的栗色骏马去河边饮水。但当我们的女儿降生以后，变故开始降临。尼雅和楼兰、Supis之间有了一场大的战事，他被急招入殿接受最残酷的训练，他的脸上常有了倦意和风霜。那年四月，尼雅和楼兰的战争仍在继续，烽烟四起，哀鸿遍野。到了月中，Supis人突然向且末袭来。君王大惊，他被王母调到备战的队伍之中，不久，Supis联合楼兰、于阗入侵尼雅。战事无可避免。临行那天夜里，我亲手将织好的箭袋系在他背上。他说他会平安归来，我抱着女儿站在门口，看他踏着满地破碎月光离去的背影，夜风把那扇门扉吹得咿呀作响。那一战尼雅大败，战火焚城，君王和王母信了巫师，一夜之间，在城中寻齐了十名老妇，十名少女，十名女婴。我织好的锦锻被覆盖在高高的神坛上，火焰冲天。巫师念着诡谲的文字，三十只次第错开的木桩上，萦绕着哀号、悲泣、婴啼。在那片呼号声中，有一息婴儿的啼哭叫我心碎。战火和祭火同时烧热了这片土地，也烧疯了这片土地。乐曲欢快，舞蹈妖冶，王母举着夜光杯，高呼尼雅万岁，十名战死疆场的士兵头颅被抛向烈焰，其中，有我熟悉的面容。那一夜，我听见不肯瞑目的魂灵在哭。尼雅不是永远书写神话的国度。这场战争中，鲜血没有换来君王酒杯里斟满的葡萄酒和殿堂上蹁跹起舞的娇艳少女。楼兰攻破了尼雅的防御点，向都城开进。鲜血和死亡，战争和屠杀。烟雾笼罩的尼雅城，那些歌舞升平，那些苍翠繁华，终于都成为昔日荣光，沉埋地下。我成了战俘，和族人一起。我们千里跋涉，来到这个眼泪一样的湖泊。我无端想起“一将功成万骨枯”这句话，假如我是个诗人，这句诗，大概会令我名扬千古。我常跟索依娜说，不要忘记，你身上还留着尼雅人的血。她有时候会问我，母亲，我救了那个人，是对，还是错。她说的那个男人，是一个中原来的游侠。在楼兰王摩怗入宫那日，他的骆驼车经过了城郊的草原，我在帐里看不见车中的他，但我知道，他对摩怗一定很重要，否则摩怗怎么可能将车辇让给他，而自己为他牵驼马。他是个神秘而低调的男人，从他入王殿那日起，所有人就再也没在王殿以外的地方见过他，而摩怗议政时，也从来不见他参与。但谁都知道，他是摩怗的参军，摩怗的臂膀，摩怗的脑袋。平内乱，定四疆，摩怗离不开他。班库大叔从中原回来，他的面色很憔悴。他说原来王道亦江湖，王者王，寇者寇，杀人与被杀，同样为名利，同样为权谋。如若我们要成事，这个叫王胤的游侠，或许成也他，败也他，因为这道理，他太明白。于是在索依娜入王殿之前，我给了她一种药。这种药有特别的香，浸在肌肤里，用者无恙，闻者，会有微微麻痹和幻觉。后来班库大叔告诉我，尼雅新君派遣的使者将会回国。那天我们挤在城门口，目送他的骆驼车离开，有女人轻声哭泣，那些眼泪，是战俘思归的眼泪。不久，尼雅进攻楼兰。班库我有半生的时间，是在中原游历。有时候我觉得自己，就像中原人口中的游侠。我经历过很多战争，其中有一场让我刻骨铭心。在那场战争中，我失去了我的女儿，她被尼雅君王献祭给主战神，烈火中她的哀号好像一直回响在我耳边。那年秋天，我在战火堆中捡到了一个婴儿。她的装束非常华贵，但浑身染满了鲜血。她应该是战后遗孤，我把她抱回了家。之后为了谋生，我决定沿着那条中原人贩运丝绸的路线，把安息的香料运到中原换回豆种和布帛。而那个小小女婴，我便托付给了沙塔。当我回来的时候，尼雅城却几乎半毁。沙塔和许多尼雅人成了战俘，还有那个小婴儿，从此后在蒲昌海边为楼兰人牧羊。我也去了蒲昌海，那里有一片巨大的绿洲。这些年来我从中原学到许多剑法，清晨的时候，我喜欢在胡杨林里一边听索依娜吹口笛，一边对着湖水的倒影练剑。我知道那个背着纯钧的男人，他不是普通的游侠。我也知道边关那家小酒馆里，有一队楼兰骑士死状惨烈，他们每一个人，都被一剑封喉，但不见血光。夜很深，我习惯在深夜里等鹰归，那只鹰从王殿来。三年前，我从丝绸之路上的骆驼商队里，买回来一只鹰。这只鹰非常奇怪，它能辨识声音和气味，但是目盲。索依娜入王殿的时候，我把这只鹰交给了沙塔，并让她转交给索依娜一种香，然后我还教会索依娜吹一种口哨，有点像胡杨树叶吹出的口笛声响。我最后一次贩运香料和布帛，是在尼雅使者回国以前。那一次我经过我的故国尼雅，在城中逗留了整整三日。这三日里我见到了尼雅新君，他与昏庸的老君王完全不一样。他野心勃勃，特训的军队整装待发。他答应我一定会让战俘们都回国，所以我决定助他一臂之力。在深夜，我等索依娜从王殿里传出的羊皮图纸。那上面记载着楼兰的每一处军事要塞，也记载着所有的布兵情况和攻守线路。摩怗为防身边人，每一次战役都换过新的图纸，且全部用汉文标注，他的汉文造诣又精进，设局用兵也完全不是原来做雏君时候的风格，从旧的羊皮图纸上看，王胤改变他很多。我说过，强国大事，或许成也王胤，败也王胤。但我相信索依娜，最后这一张图纸，她一定会送得出。摩怗这份担子很沉重，我从登上王位的第一天起就知道。有时候我脑子里会想起一本破败帛书上的话，那是描写王胤先祖的一段话。那率众揭竿而起的绿林好汉，拜公封侯的上善之人，最后也终不免剖心挖脑、身首异处，而头骨在为王者的手中，就着歌舞，被当成酒杯。在史册之中，没有千秋的霸业；在江湖之上，没有永远的英雄。这样的想法令我觉得悲凉，觉得悲凉的时候我就更珍惜那少许的欢乐时光。那段时光是索依娜带给我的，她是个难得的好姑娘。有时候我在想，其实在我内心深处，或许对功名权力并没有那么深的渴望。万人之上，总有一天也会成黄土一垅；名垂青史，终也不过史册上据有方寸之地。一个人，和浩大时空、喧嚣江湖相比，就好像一滴水较之于整片沙漠，此生以外，谁理得生前身后事呢？实在只有现时欢乐，才是令生命丰美的绿洲。这片绿洲，我已经拥有。我在王殿的密室里，为索依娜用胡杨木刻了一只小舟，我用了一种特别的工具，它不太应手，以至于我两只手的虎口处，伤痕累累。但是我喜欢看索依娜划着它，在清晨的蒲昌海里悠游放歌，她的头发很长，喜欢用清澈的湖水洗净，然后涂上一种好闻的香料。她的歌声叫人流连，有一种口笛一样婉转绵长的味道，我站在岸边，听胡杨林里的叶子簌簌地落。这时候我会忘记自己的身份，像个普通男人一样。我想要带着她驶入蒲昌海波光的最深处，用一世的光阴，守护这种宁静和餍足。在离王殿不远的地方，有楼兰国最宏大的一座佛寺。高高佛塔上挂着巨大铜铃，塔尖直指苍天。傍晚的时候，寺院的钟声会响遍整个楼兰城，如果我离开王殿私服巡游，便会听见暮色中的钟鸣。在这种时候，索依娜应该独自去寺院里听晚课，有时候她会留宿，为前方的战事整夜祈祷。我开始睡不好觉。我的弟弟昆谒出征，带着我最新绘制的羊皮图纸，但那一战，他败了。王殿里的气氛很凝重，臣子们在疑心，有人走漏军情。他们的目光投向我，那含义是，王的身边有奸细。我杀了我的近身随侍，在城门处，和臣子们一起，拦住了驾车回家的索依娜。王殿卫队搜遍了骆驼车而无所得，两个搜身的婢女也摇头说，索依娜的身上，没有任何物什与战事有关。昆谒和臣子们的目光黯下去，我牵着索依娜的手，带她回殿。楼兰军继续战败，我们的每一个计划，尼雅统领仿佛都了如指掌。国内开始有反声，我决定发动最后一战。这一战，我要亲自领兵，就在城郊蒲昌海边。若成，楼兰国疆固守，若败，我将尸骨无存。出兵前夜，我在密室里，对着铜镜，亲自细心绘制了一幅最新的作战图，我把这张图卷在里衣袖筒里，这是万无一失的地方，即使在我与索依娜欢好的时候，我说过，我也从不除下我贴身的上衣。那一夜甚为疯狂，索依娜在我怀里，柔如蒲昌海水，粲若水面星光。在我跌入梦境之前，我听到了窗外夜枭的啼叫，那时候还该是前半夜，夜花的香，很特别。我在合上双眼的时候笑了笑，用袖筒里放有羊皮图纸的那只手，抱紧了索依娜。天明尼雅大军压境，朝堂上昆谒连同一帮臣子奉上一把“白狼斩”，要求我歃血盟誓，与边疆的楼兰将士一起，迎击敌军。那一天我穿着黄金铠甲，索依娜在我装有羊皮图纸的腰袋上系上象征着凯旋归来的金丝带。她站在王殿高高的台阶上，长发飞扬。我跨上战马的时候回了一下头，她的眼里都是泪水，而我看见她的身后，王殿檐角，一轮红日如血。索依娜他走的时候我在王殿台阶上，身后那一轮如血红日，像是一个征兆。然后我飞奔至佛寺，有悠长钟声响起，我知道，他不在，他也不会在了。这一次，我不是为他，我是为母亲、班库老爹还有无数留在楼兰城的尼雅战俘祈祷。眼泪打湿了我面前的蒲团，我开始明白，那个一心想求得不平凡生活的索依娜，是多么幼稚可笑。没有人知道，昨夜，我的心里是如何挣扎，如何痛如刀绞。当盲鹰带着那张羊皮图纸而去的时候，我在死一般的黑暗和静寂里，想起那些在佛寺度过的快乐时光。那个我在蒲昌海边遇见的男子，我知道他就是游侠王胤。在我踏入王殿的那一夜，摩怗夜游未归，在森冷的殿堂回廊上，王胤紧紧抱住了我。那冰冷的石头房，暗苔的凉沁入我骨髓，但我的心像火一样烧。他牵着我的手，在王殿通往佛寺的地下甬道里笑和奔跑，当他压我在潮湿石壁上密密亲吻的时候，我听见他背上的剑发出嗡嗡声响。那一夜在密室，红纱散落一地。我摸着他背上的那道刀伤，一颗心，是醉的。从那夜以后，王殿之上，殿前回廊，石头甬道，佛寺密室，我想他，像一滴水想念一片绿洲，在这浩瀚沙漠上，他就是我唯一绿洲。我劝他离开楼兰，离开摩怗，与我一起去尼雅，或是去到边关开一家小酒馆。我这里有母亲给我的一种香，有一次我把它放了一点在他给我的水囊里，水就变得甘醇异常，还有淡而奇特的酒香，原来，那竟是一种酒种。王胤喝过那种酒，那一次是摩怗入睡后，我们从佛寺溜出去泛舟蒲昌海的夜里。他的武艺真高强，竟然能抱着我掠过城头，去胡杨林里找到那只小舟。在星光璀璨的湖面上，他喝了一口那种酒，然后点头说，好酒。他握住了我的手，说等战事一结束，我们就去边关，开一家专卖这种酒的酒楼。为了这句话，我希望战事告停的渴望瞬间膨胀到比母亲和班库老爹更强。我一次次在与摩怗欢好之后，令他闻到我发上的香，然后在他沉睡之后，盗走他放在从来不让我进去的议政房里的羊皮图纸。于公，我身上留着尼雅人的血，于私，楼兰战败，摩怗不在，王胤才是自由的，才能与我留居边关，牧羊塞外。只有那一次，我犹豫过。那是摩怗的弟弟昆谒上战场的前夕，我知道，那一次战役很重要，臣子们已经对每次落败的原因大为置疑，如果再败，恐怕以我与摩怗的亲密程度，会成为头号嫌疑。但我没想到，在城门口摩怗当众的搜查，竟能为我脱罪。那些羊皮图纸我自然不会通过贴身的方式带出去，那只只能辨认我哨声和香气的鹰，是最好的传送工具。那天摩怗牵着我的手回王殿，这个男人的掌心，有一种难以抑制的微微颤抖。不久之后，王胤不再来佛寺，后来，我想我明白了那颤抖的含义。摩怗亲征的第九日，尼雅大军开进到离蒲昌海二十里地远的地方，整个楼兰城一片恐慌。那些早已习惯安定生活的民众，每天聚集到王殿门口吵闹，甚至诅咒正为他们出生入死的君王。这时候我血已冷，我无法责怪那些人对摩怗的不敬，虽然我知道，那个温柔又阴鸷的男子正血洒疆场。那夜我做了一场噩梦，醒来时，窗外冷月如水。第二天，我收到摩怗从疆场上传来的信。那是一封用珐卢文写成的书信，送到我面前的时候，已经染满鲜血。看的时候我屏住呼吸，那上面写：楼兰战败，尼雅军即将越湖入城。在那封信的信套里，还有一小片皮纸，上面的话是写给我的：索依娜，我的骨骸在蒲昌海底守护，我的魂灵将随着你安居边关。屠刀血刃，我已厌倦，那一壶好酒，你为我留着。我抓着那张皮纸，呕出满口鲜血。我如何能料到，败的是你，你仍战败！我没有准备离开，我将楼兰战败的消息发布给城中百姓。摩怗的臣子们即刻回应，拥戴昆谒成为新王。尼雅的军队已经到达蒲昌海岸，马匹和骆驼行走过的地方，卷起滚滚黄沙。母亲和班库老爹传信给我说，索依娜，离开楼兰回去尼雅，那里才是我们的家。可是那样虔诚的尼雅族人，那样信任新君的母亲，就在涉湖而去的那一天，我在胡杨枯萎的岸边，看到他们惨白的面容和冰冷的躯体。而隔岸的尼雅统领还在疯狂叫嚣：射杀他们，射杀他们，叛逆的臣子不容归来！原来对战争任何美好的信仰和渴望都是愚蠢而天真，十数张羊皮图纸出卖的情报，换来的不过是母亲和族人冰冷的尸体，而这竟是尼雅的箭，将他们断送在楼兰的土。那些平日里衣冠楚楚的臣子，跪在高高殿堂上发抖，眼里写满对战事的恐慌。他们异口同声地对昆谒说，离开这里吧，我们迁到像天堂一样的鄯善河流域去，我们可以在那里建立世界上最强大最富有的国家，我们可以叫它鄯善国。战争不灭，战火不熄，普天之下，安有乐土？我在佛寺里静坐，等待那最后的时刻。满城血雨腥风，尼雅军队的马蹄践踏过积淀了千年的楼兰城，将手中寒光闪闪的弯刀挥向来不及逃离的妇孺。四面是鲜血，处处有死亡。我的心在那一刻碎成尘埃。佛塔上的钟声还在响。我跪在蒲团上，听门外的厮杀声渐息。尼雅统领恭敬地走到我面前，他说，想必你就是那个摩怗临死时还念念不忘的女子，那么，请上路吧。尼雅军队的马车行驶在黄沙滚滚的大漠中，烽烟刚刚熄灭，我深爱的人永远不会归家。经过蒲昌海的时候，我想起他说到的骨骸，我想有一天，我要带着他去边关，为他烫一壶我囊中的好酒。王胤如果我一直是那个无牵无挂的江湖游侠，我的一生，会不会充满矛盾，如此跌宕并充满奇迹与悲凉。当尼雅统领的弯刀砍下来的时候，我突然想起了先祖的遗训：江湖之上，只有游侠一业，得为存，得为寿，得为公。是他们早知道，无所求才会无所失，无所倚才会自当强。但我悟晚了这个道理，我悟得更晚的，是野心，终于要败给柔情万丈。我是王胤，也是摩怗。一切起于边关酒窖里那个异族男子的一句话，他说他是楼兰雏君，他是摩怗。那一天，他受了很重的伤，我背他找到了地穴的风口。但当我转身拉他出风口的时候，他襟上呕出的鲜血在月光下刺红了我的眼，我拔出背上的纯钧，插在他胸口上。那夜我回酒馆，将所有楼兰骑兵杀死，然后我重入酒窖，在风口处扎好摩怗的尸体，并剥下了他的面皮。我是游侠，品酒、识毒、杀人和易容，对我来说就像空气和水分一样惯常。我带着摩怗没有面皮的尸体到了楼兰，在入城的时候，城内已然大乱，摩怗的面皮，为我招来一场追杀。我受了重伤，“白狼斩”几乎砍掉我半只臂膀。它在我背上留下了一道很深的伤口，在蒲昌海边，是索依娜为我清洗包扎。如果我知道，不久以后这个善良的异族女子会成为我一生中最致命的伤，我会在那时候，留给她封喉的刀光而不是一只水囊。我说过我的体内，总有一种悲凉和渴望扬名立万的狂热在相互冲撞，我就在这矛盾的冲撞中，扮演了王胤和摩怗。每天清晨对着铜镜里那张摩怗的面孔，我会听到王胤在我心里说话。到最后，我分不清自己到底是向往平静的摩怗，还是野心勃勃的王胤。伤好之后，我用纯钧杀死了很多反摩怗的楼兰人，然后我牵着装有摩怗尸体的骆驼车，以礼遇贤士的方式，把他的尸体扮成神秘低调的王胤入了王殿。从边关西行以来，我知道是这个被我杀死的男人成就了我，所以我对他有一份难以言说的亲密感和愧疚，在胡杨林里，那只装有他尸体的黑布袋，被我好好掩埋。我在那里再次遇见了索依娜，很快，我迎她进王殿。在红围帐里，我是宠爱她的摩怗，在佛寺密室里，我是迷恋她的王胤。但我想不到，她会是窃走羊皮图纸的人，假如不是那壶酒，以后的事还会不会发生？那壶酒，我入喉即知，它的香，与我梦中闻到的香是同一种。不光是在梦中，我的议政房，放羊皮图纸的地方，也曾有过这种香，而那间房，我从来不让任何人进去。我便知道，盗图人，是索依娜。感谢那壶酒，想必沙塔没有告诉索依娜，那种香的解法。用者无恙，就是这四个字。我喝下那香调制的酒，那种香带来的麻痹和幻觉，就不再有了。令昆谒出征的那晚，我看着她将用红墨拓下的羊皮图纸系在鹰腿上，但我没有说话，只是在她打开甬道去往佛寺密室以后，恢复了王胤的样子，避开守殿人去见她。我用了计，这计与她的香一样，令人麻痹。我在城门口，当众搜查了她的骆驼车。我向所有人宣布，她在军情泄露一事上，是无辜的。那天我牵着她的手回殿，我的手有一点颤抖，那种悲凉的感觉又涌上来，即使那样相爱，也不免相互算计，使尽阴谋。那以后，我总是幻听，听见王胤的声音说，成王败寇。于是我于悲凉之外，被索依娜背叛的心化成一股熊熊怒火，我决定抛开对她的感情，亲自出征。出征之前，我在议政房待了一天。我画了最详尽细致的用兵图和记下了全部的攻略计划，然后按照完全相反的做法，拓出一幅假兵图。其实从我知道索依娜盗图以后，就再也没有绘过用兵图。所有的布兵计划都留在我脑子里，这一次绘完以后，我用火烧掉了那张真图。我把假兵图藏在袖筒里，带着它，与索依娜欢爱。那一夜竟非常怪，我没有闻到惯常的香，却感到非常疲倦，那场疯狂的床第之欢让我睡得很沉。坠入睡梦之前，我感觉到索依娜侧身，她柔软的双手摸进了我的袖筒里，窗外花香袭人，时时传来夜枭的啼叫，有鹰扑拉拉扇翅的声音，我睡得很安心。醒来的时候索依娜为我把羊皮图纸装在腰袋里，并在上面系上象征凯旋的金丝带。我走的时候她望着我，眼里竟然有泪光。那一刻我想告诉她她的族人即将落败，我想告诉她，我本无意利用她，让她卷入这场残酷的战争中来。但不想，落败的是我。当我在蒲昌海岸边的胡杨林里遭遇到伏击，我就知道，她送出去的那张图，是假的。一切都与我料想的相反，我拓了假的兵图令她盗走，但她又是出于什么样的目的，再次反拓了图纸，令送出的那张图，奇迹般和我的用兵计划一模一样。后来我打开腰袋里的图纸，那上面有两处被眼泪濡湿过的痕迹，一处是蒲昌海，一处是佛寺密室。我便知道，她识穿了两个我，她反拓图纸，是宁肯牺牲族人，也要帮我的。但这一分刻骨柔情，却敌不过天命。反拓又反拓的图纸，恰恰是真的用兵图。纯钧第一次染满了那么多血，但是来不及了，刀落的时候，我想起初见索依娜的那天，她抱着小羊，笑着，一步一步，毫无惧意地向我走来。而那终是我逐猎江湖，失落的最后一片绿洲。索依娜我从尼雅军行进的队伍中逃了出来，行走在黄沙漫漫的大戈壁上。偷到的骆驼早已累得无力前行，踟躇着发出阵阵哀鸣。这里离蒲昌海还很远，头顶是骄阳如火。我的骆驼已经完全走不动了，在我偷走它之前，它已重伤。在军队里，这种受伤的骆驼唯一的用处是放干血给将士们喝，剩下的肉和尸骨，就无情地丢弃在荒漠中被太阳晒成干尸。我的眼前是一望无际的黄沙，见不到任何活着的生命。战争和沙漠是最无情的杀手，我不知道在这看似平静的黄沙下，掩埋着多少森森白骨。我是在那一夜，知道王胤和摩怗，其实是同一个人。当我的手伸向他袖筒里的羊皮图纸的时候，他脸上的笑让我有点迟疑。我留了很多的耐心等他睡得更沉，确定他睡去之后，我干脆解开了他的里衣。当他从不在我面前袒露的背脊暴露在亮白的月光之下，就在那一刻，熟悉的刀伤像闪电一样，划破了我的心脏。我还看到了他虎口的旧伤，该是摩怗用纯钧为我刻胡杨木舟留下的伤。我第一次那么近地贴近这张摩怗的面孔。在鬓角，我看到微微翻起的皮屑。在这张面孔之下，藏着一个我深爱的男人，我不管他是好是坏，是温情是残忍，那张图，我不能再真的泄露出去。于是我反拓了那张从他袖筒里偷出的羊皮图纸，天明之战，哪怕尼雅士兵血流成河，但神原谅我，我只是想要他平安归来。但他终于消失，就像一颗水珠消失于沙漠。他留给我的水囊还在，我这样辛苦跋涉，想要找到他，我们说过，要在边关开一家酒馆，夜里，我为他煮酒，他为我舞剑。受伤的骆驼在我身边悲鸣，它已经无法前进。将一个垂死的生命丢弃在不见绿洲的地方是最残忍的惩罚，我腰间的弯刀刺进它仍喘息着的喉咙，不久，它将化做一堆静默的白骨。终于我喝了一口酒，我不知道是否看见了那片最后的绿洲。蒲昌海的水静静流淌，王胤说，他的骸骨深埋湖底。我穿着红色舞衣，发间插一支蓝色鸟羽。在胡杨林里我捡到那把纯钧，人亡，剑无光。湖水很冰凉，在纯钧的嗡嗡声里，我开始歌唱。我愿与王胤在这里睡去，从今以后，再不允许有谁将我们轻易惊醒。愿以永远的沉睡，换他展眉一笑，愿世上再无纷争，人心再无江湖。绿洲历史常是一个怪圈，一场战事，一个国度，一段时光，最后留名的人，是我。我叫斯文?赫定，是一个来自瑞典的探险家。我在沙漠中行走，想要找到传说中的绿洲，然后我的骆驼死了，埋葬它的时候，我发现了藏在沙砾下的楼兰文明遗迹。这年是1900年，一年以后，我在此发掘出一个佛寺，三座殿堂，还有烽火台、大量木雕构件、五铢钱以及一封珐卢文书信。我还发现了一块残破的锦缎，隐隐有异族文字，有人译出来，说是在楼兰时代某年，上主于战乱中，在尼雅境内遗失襁褓中的爱女。原来此地，曾是一片绿洲。而那个在古代被成为蒲昌海的湖泊，我在其中进行过我的“处女航”。我将它重新定名为罗布泊，在其间，我找到一具发间插有鸟羽的——楼兰新娘。
ICloneable看上去是个不错的主意：为一个类型实现ICloneable接口后就可以支持拷贝了。如果你不想支持拷贝，就不要实现它。但你的对象并不是在一个“真空”的环境中运行，但考虑到对派生类的些影响，最好还是对ICloneable支持。一但某个类型支持ICloneable,那么所有的派生类都必须保持一致，也就是所有的成员必须支持ICloneable接口或者提供一种机制支持拷贝。最后，支持深拷贝的对象，在创建设计时如果包含有网络结构的对象，会使拷贝很成问题。ICloneable也觉察到这个问题，在它的官方定义中有说明：它同时支持深拷贝和浅拷贝。浅拷贝是创建一个新的对象，这个新对象对包含当前对象中所有成员变量的拷贝。如果这些成员变量是引用类型的，那么新的对象与源对象包含了同样的引用。而深拷贝则可以很好的拷贝所有成员变量，引用类型也被递归的进行了拷贝。对于像整型这样的内置类型，深拷贝和浅拷贝是一样的结果。哪一种是我们的类型应该支持的呢？这取决于类型本身。但同时在一个类型中混用深拷贝和浅拷贝会导致很多不一致的问题。一但你涉及到ICloneable这个问题，这样的混用就很难解脱了。大多数时候，我们应该完全避免使用ICloneable，让类更简单一些。这样使用和实现都相对简单得多。任何只以内置类型做为成员的值类型不必支持ICloneable;用简单的赋值语句对结构的所有值进行拷贝比Clone()要高效得多。Clone()方法必须对返回类型进行装箱，这样才能强制转化成一个System.Object的引用。而调用者还得再用强制转化从箱子中取回这个值。我知道你已经有足够的能力这样做，但不要用Clone()函数来取代赋值语句。那么，当一个值类型中包含一个引用类型时又会怎样呢？最常见的一种情况就是值类型中包含一个字符串：public struct ErrorMessage { private int errCode; private int details; private string msg; // details elided } 字符串是一个特殊情况，因为它是一个恒定类。如果你指定了一个错误消息串，那么所有的错误消息类都引用到同一个字符串上。而这并不会导致任何问题，这与其它一般的引用类型是不一样的。如果你在任何一个引用上修改了msg变量，你会就为它重新创建了一个string对象(参见原则7)。(译注：string确实是一个很有意思的类，很多C++程序员对这个类不理解，也很有一些C\#程序对它不理解，导致很多的低效，甚至错误问题。应该好好的理解一下C\#里的string(以及String和StringBulider之间的关系)这个类，这对于学好C\#是很有帮助的。因为这种设计思想可以沿用到我们自己的类型中。)一般情况，如果一个结构中包含了一个任意的引用类型，那么拷贝时的情况就复杂多了。这也是很少见的，内置的赋值语句会对结构进行浅拷贝，这样两个结构中的引用变量就引用到同个一个对象上。如果要进行深拷贝，那么你就必须对引用类型也进行拷贝，而且还要知道该引用类型上是否也支持用Clone()进行深拷贝。不管是哪种情况，你都不用对值类型添加对ICloneable的支持，赋值语句会对值类型创建一个新的拷贝。一句概括值类型：没有任何理由要给一个值类型添加对ICloneable接口的支持!好了，现在让我们再看看引用类型。引用类型应该支持ICloneable接口，以便明确的给出它是支持深拷贝还是浅拷贝。明智的选择是添加对ICloneable的支持，因为这样就明确的要求所有派生类也必须支持ICloneable。看下面这个简单的继承关系：class BaseType : ICloneable { private string _label = "class name"; private int [] _values = new int [ 10 ]; public object Clone() { BaseType rVal = new BaseType( ); rVal._label = _label; for( int i = 0; i < _values.Length; i++ ) rVal._values[ i ] = _values[ i ]; return rVal; } } class Derived : BaseType { private double [] _dValues = new double[ 10 ]; static void Main( string[] args ) { Derived d = new Derived(); Derived d2 = d.Clone() as Derived; if ( d2 == null ) Console.WriteLine( "null" ); } } 如果你运行这个程序，你就会发现d2为null。虽然Derived是从BaseType派生的，但从BaseType类继承的Clone()函数并不能正确的支持Derived类：它只拷贝了基类。BaseType.Clone()创建的是一个BaseType对象，不是派生的Derived对象。这就是为什么程序中的d2为null而不是派生的Derived对象。即使你克服了这个问题，BaseType.Clone()也不能正确的拷贝在Derived类中定义的\_dValues数组。一但你实现了ICloneable,你就强制要求所有派生类也必须正确的实现它。实际上，你应该提供一个hook函数，让所有的派生类使用你的拷贝实现(参见原则21)。在拷贝时，派生类可以只对值类型成员或者实现了ICloneable接口的引用类型成员进行拷贝。对于派生类来说这是一个严格的要求。在基类上实现ICloneable接口通常会给派生类添加这样的负担，因此在密封类中应该避免实现ICloneable接口。因此，当整个继承结构都必须实现ICloneable时，你可以创建一个抽象的Clone()方法，然后强制所有的派生类都实现它。在这种情况下，你需要定义一个方法让派生类来创建基类成员的拷贝。可以通过定义一个受保护的构造函数来实现：class BaseType { private string _label; private int [] _values; protected BaseType( ) { _label = "class name"; _values = new int [ 10 ]; } // Used by devived values to clone protected BaseType( BaseType right ) { _label = right._label; _values = right._values.Clone( ) as int[ ] ; } } sealed class Derived : BaseType, ICloneable { private double [] _dValues = new double[ 10 ]; public Derived ( ) { _dValues = new double [ 10 ]; } // Construct a copy // using the base class copy ctor private Derived ( Derived right ) : base ( right ) { _dValues = right._dValues.Clone( ) as double[ ]; } static void Main( string[] args ) { Derived d = new Derived(); Derived d2 = d.Clone() as Derived; if ( d2 == null ) Console.WriteLine( "null" ); } public object Clone() { Derived rVal = new Derived( this ); return rVal; } } 基类并不实现ICloneable接口;通过提供一个受保护的构造函数，让派生类可以拷贝基类的成员。叶子类，应该都是密封的，必要它应该实现ICloneable接口。基类不应该强迫所有的派生类都要实现ICloneable接口，但你应该提供一些必要的方法，以便那些希望实现ICloneable接口的派生类可以使用。ICloneable接口有它的用武之地，但相对于它的规则来说，我们应该避免它。对于值类型，你不应该实现ICloneable接口，应该使用赋值语句。对于引用类型来说，只有在拷贝确实有必要存在时，才在叶子类上实现对ICloneable的支持。基类在可能要对ICloneable进行支持时，应该创建一个受保护的构造函数。总而言之，我们应该尽量避免使用ICloneable接口。
一、数据准备 文章标题 文章分类 发布时间 操作 测试 测试 测试 测试 测试 测试 测试 测试 测试 测试 测试 测试 文章标题 文章分类 发布时间 操作 测试 测试 测试 测试 测试 测试 测试 测试 测试 测试 测试 测试 文章标题 文章分类 发布时间 操作 测试 测试 测试 测试 测试 测试 测试 测试 测试 测试 测试 测试 文章标题 文章分类 发布时间 操作 测试 测试 测试 测试 测试 测试 测试 测试 测试 测试 测试 测试 测试3 测试 测试 测试 二、操作 //1.鼠标移动行变色 $("#table1 tr").hover(function(){ $(this).children("td").addClass("hover") },function(){ $(this).children("td").removeClass("hover") }) $("#table2 tr:gt(0)").hover(function() { $(this).children("td").addClass("hover"); }, function() { $(this).children("td").removeClass("hover"); }); //2.奇偶行不同颜色 $("#table3 tbody tr:odd").css("background-color", "#bbf"); $("#table3 tbody tr:even").css("background-color","#ffc"); $("#table3 tbody tr:odd").addClass("odd") $("#table3 tbody tr:even").addClass("even") //3.隐藏一行 $("#table3 tbody tr:eq(3)").hide(); //4.隐藏一列 $("#table5 tr td::nth-child(3)").hide(); $("#table5 tr").each(function(){$("td:eq(3)",this).hide()}); //5.删除一行 // 删除除第一行外的所有行 $("#table6 tr:not(:first)").remove(); //6.删除一列 // 删除除第一列外的所有列 $("#table6 tr td:not(:nth-child(1))").remove(); //7.得到（设置）某个单元格的值 //设置table7,第2个tr的第一个td的值。 $("#table7 tr:eq(1) td:nth-child(1)").html("value"); //获取table7,第2个tr的第一个td的值。 $("#table7 tr:eq(1) td:nth-child(1)").html(); //8.插入一行： //在第二个tr后插入一行 $("插入3插入插入插入").insertAfter($("#table7 tr:eq(1)")); <"script>
Effective C# 原则26：用IComparable和IComparer实现对象的顺序关系
你的类型应该有一个顺序关系，以便在集合中描述它们如何存储以及排序。.Net框架为你提供了两个接口来描述对象的顺序关系：IComparable和IComparer。IComparable为你的类定义了自然顺序，而实现IComparer接口的类可以描述其它可选的顺序。你可以在实现接口时，定义并实现你自己关系操作符(\<，\>，\<=，\>=)，用于避免在运行时默认比较关系的低效问题。这一原则将讨论如何实现顺序关系，以便.Net框架的核心可以通过你定义的接口对你的类型进行排序。这样用户可以在些操作上得更好的效率。IComparable接口只有一个方法：CompareTo()，这个方法沿用了传统的C函数库里的strcmp函数的实现原则：如果当前对象比目标对象小，它的返回值小于0；如果相等就返回0；如果当前对象比目标对象大，返回值就大于0。IComparable以System.Object做为参数，因此在使用这个函数时，你须要对运行时的对象进行检测。每次进行比较时，你必须重新解释参数的类型：public struct Customer : IComparable { private readonly string _name; public Customer( string name ) { _name = name; } #region IComparable Members public int CompareTo( object right ) { if ( ! ( right is Customer ) ) throw new ArgumentException( "Argument not a customer", "right" ); Customer rightCustomer = ( Customer )right; return _name.CompareTo( rightCustomer._name ); } #endregion } 关于实现比较与IComparable接口的一致性有很多不太喜欢的地方，首先就是你要检测参数的运行时类型。不正确的代码可以用任何类型做为参数来调用CompareTo方法。还有，正确的参数还必须进行装箱与拆箱后才能提供实际的比较。每次比较都要进行这样额外的开销。在对集合进行排序时，在对象上进行的平均比较次数为Nxlog(N)，而每次都会产生三次装箱与拆箱。对于一个有1000个点的数组来说，这将会产生大概20000次的装箱与拆箱操作，平均计算：Nx log(n)有7000次，每次比较有3次装箱与拆箱。因此，你必须自己找个可选的比较方法。你无法改变IComparable.CompareTo()的定义，但这并不意味着你要被迫让你的用户在一个弱类型的实现上也要忍受性能的损失。你可以重载CompareTo()方法，让它只对Customer对象操作：public struct Customer : IComparable { private string _name; public Customer( string name ) { _name = name; } #region IComparable Members // IComparable.CompareTo() // This is not type safe. The runtime type // of the right parameter must be checked. int IComparable.CompareTo( object right ) { if ( ! ( right is Customer ) ) throw new ArgumentException( "Argument not a customer", "right" ); Customer rightCustomer = ( Customer )right; return CompareTo( rightCustomer ); } // type-safe CompareTo. // Right is a customer, or derived from Customer. public int CompareTo( Customer right ) { return _name.CompareTo( right._name ); } #endregion } 现在，IComparable.CompareTo()就是一个隐式的接口实现，它只能通过IComparable接口的引用才能调用。你的用户则只能使用一个类型安全的调用，而且不安全的比较是不可能访问的。下面这样无意的错误就不能通过编译了：Customer c1; Employee e1; if ( c1.CompareTo( e1 ) > 0 ) Console.WriteLine( "Customer one is greater" ); 这不能通过编译，因为对于公共的Customer.CompareTo(Customerright)方法在参数上不匹配，而IComparable. CompareTo(objectright)方法又不可访问，因此，你只能通过强制转化为IComparable接口后才能访问：Customer c1; Employee e1; if ( ( c1 as IComparable ).CompareTo( e1 ) > 0 ) Console.WriteLine( "Customer one is greater" ); 当你通过隐式实现IComparable接口而又提供了一个类型安全的比较时，重载版本的强类型比较增加了性能，而且减少了其他人误用CompareTo方法的可能。你还不能看到.Net框架里Sort函数的所有好处，这是因为它还是用接口指针(参见原则19)来访问CompareTo()方法，但在已道两个对象的类型时，代码的性能会好一些。我们再对Customer结构做一个小的修改，C\#语言可以重载标准的关系运算符，这些应该利用类型安全的CompareTo()方法：public struct Customer : IComparable { private string _name; public Customer( string name ) { _name = name; } #region IComparable Members // IComparable.CompareTo() // This is not type safe. The runtime type // of the right parameter must be checked. int IComparable.CompareTo( object right ) { if ( ! ( right is Customer ) ) throw new ArgumentException( "Argument not a customer", "right"); Customer rightCustomer = ( Customer )right; return CompareTo( rightCustomer ); } // type-safe CompareTo. // Right is a customer, or derived from Customer. public int CompareTo( Customer right ) { return _name.CompareTo( right._name ); } // Relational Operators. public static bool operator < ( Customer left, Customer right ) { return left.CompareTo( right ) < 0; } public static bool operator <=( Customer left, Customer right ) { return left.CompareTo( right ) <= 0; } public static bool operator >( Customer left, Customer right ) { return left.CompareTo( right ) > 0; } public static bool operator >=( Customer left, Customer right ) { return left.CompareTo( right ) >= 0; } #endregion } 所有客户的顺序关系就是这样：以名字排序。不久，你很可能要创建一个报表，要以客户的收入进行排序。你还是需要Custom结构里定义的普通的比较机制：以名字排序。你可以通过添加一个实现了IComparer接口的类来完成这个新增的需求。IComparer给类型比较提供另一个标准的选择，在.NetFCL中任何在IComparable接口上工作的函数，都提供一个重载，以便通过接口对对象进行排序。因为你是Customer结构的作者，你可以创建一个新的类(RevenueComparer)做为Customer结构的一个私有的嵌套类。它通过Customer结构的静态属性暴露给用户：public struct Customer : IComparable { private string _name; private double _revenue; // code from earlier example elided. private static RevenueComparer _revComp = null; // return an object that implements IComparer // use lazy evaluation to create just one. public static IComparer RevenueCompare { get { if ( _revComp == null ) _revComp = new RevenueComparer(); return _revComp; } } // Class to compare customers by revenue. // This is always used via the interface pointer, // so only provide the interface override. private class RevenueComparer : IComparer { #region IComparer Members int IComparer.Compare( object left, object right ) { if ( ! ( left is Customer ) ) throw new ArgumentException( "Argument is not a Customer", "left"); if (! ( right is Customer) ) throw new ArgumentException( "Argument is not a Customer", "right"); Customer leftCustomer = ( Customer ) left; Customer rightCustomer = ( Customer ) right; return leftCustomer._revenue.CompareTo( rightCustomer._revenue); } #endregion } }最后这个版本的Customer结构，包含了RevenueComparer类，这样你就可以以自然顺序-名字，对对象进行排序；还可有一个选择就是用这个暴露出来的，实现了IComparer接口的类，以收入对客户进行排序。如果你没有办法访问Customer类的源代码，你还可以提供一个IComparer接口，用于对它的任何公共属性进行排序。只有在你无法取得源代码时才使用这样的习惯，同时也是在.Net框架里的一个类须要不同的排序依据时才这样用。这一原则里没有涉及Equals()方法和==操作符(参见原则9)。排序和相等是很清楚的操作，你不用实现一个相等比较来表达排序关系。实际上，引用类型通常是基于对象的内容进行排序的，而相等则是基于对象的ID的。在Equals()返回false时，CompareTo()可以返回0。这完全是合法的，相等与排序完全没必要一样。(译注：注意作者这里讨论的对象，是排序与相等这两种操作，而不是具体的对象，对于一些特殊的对象，相等与排序可能相关。)IComparable 和IComparer接口为类型的排序提供了标准的机制，IComparable应该在大多数自然排序下使用。当你实现IComparable接口时，你应该为类型排序重载一致的比较操作符(\<, \>,\<=, \>=)。IComparable.CompareTo()使用的是System.Object做为参数，同样你也要重载一个类型安全的CompareTo()方法。IComparer可以为排序提供一个可选的排序依据，这可以用于一些没有给你提供排序依据的类型上，提供你自己的排序依据。