计算机科学与技术


      Zhangdi 同学说,评论我的《Java 和 .Net 在异常处理机制上的区别》帖子说:我并不认为 C#(.NET)这样处理 Exception 是好的。我觉得这与 strong-type 的概念相冲突。几个问题:

  1. 在设计一个接口的时候,是不是需要同时告诉用户,这个接口有可能抛出什么类型的 Exception?

    答:这种说法自然是没错的,异常应该是接口设计的一部分,但是告知用户的方式可以有很多种。Java 的这种方式只是其中较为有效的方式之一,他通过编译器做出严格的限制,这其中兼有优劣。

  2. 如果需要,那么这个 Exception 类型是否应该成为一个接口声明的一部分?

    答:其实这还要从程序整体的设计角度来讨论。在 Java 中非 RuntimeException 其实已经不是一种普通的异常那么简单。因为最终在程序运行的时候,Java 不允许有非 RuntimeException 被抛出到虚拟机,这也就是说这种异常看上去只存在于设计时。他更像是程序运行期间的一种消息传递机制,只是首先这种消息“有别于(Except)”其他的返回类型,另外就是它会沿着调用栈强行向上追溯。显然这种异常更多的是在程序设计阶段起到操控程序非正常运行时行为的作用。因此他更像是一种接口,自然声明这样的接口也是理所应当的。

          但是 Java 还有 RuntimeException 的概念,而 .Net 或许就是没有区分他们而已,因为一旦程序遇到异常,无论是被程序本身恢复,还是被虚拟机捕捉到导致强行非法结束,都是可以接受的,毕竟对用户而言除了造成不方便,并不会有安全上的隐患。

  3. 一个已知的有可能发生的 Exception 被操作系统捕获是否可以说是用户在处理这个接口的声明的时候,出现了疏忽呢?

    答:这个自然,如果不是 Java 概念中的 RuntimeException,那么如果真有这样的异常被虚拟机捕捉到肯定就是 Bug,但至少是一个安全的 Bug。而同时,就像我在帖子里说到的,需要在开发难度和设计完整性上达到一定的平衡。.Net 这样自然忽视了后者,但同时显然使用就方便多了。

  4. strong-type 的目标是在编译器发现尽可能多的关于类型的错误,如果我们把接口的定义看作类型的一部分,那么是否可以说,这个有可能将 checked exception 暴露给操作系统的编译器在 strong-type 上有缺陷呢?

    答:没看懂!:-)

      还是那句话,我觉得我写这篇帖子已经足够公正了。.Net 在异常处理的设计上显然不如 Java 更趋近完美,但毕竟还是可以用的,还是好用的,剩下的瑕疵是可以通过程序员的经验,以及良好的开发习惯和管理来弥补的。

      关于 Java 和 .Net 优劣的争论一直在继续,而在异常处理方面体现得最为激烈,因为他们之间的差异是如此明显。.Net 晚于 Java 出现,那么 Java 对 .Net 就理应起到很重要的借鉴作用,但是伟大的 Anders Hejlsberg 为什么没有继续 Java 的实现方式,而是另辟蹊径,这是一个非常值得研究的问题。因为我们要承认一个真理:正确的东西大家都是一样的正确,错误的却各有个的错误。可以肯定这不可能是 Anders 的疏忽,那么他的道理究竟何在,或者说他们之间究竟有什么区别?

      在你能耐下心来看完这篇帖子之前,我想要明确告诉你一个结论:Java 和 .Net 在异常处理的本质上是没有区别的。

一、Java 是如何处理异常的

      如果一个 Java 方法要抛出异常,那么需要在这个方法后面用 throws 关键字定义可以抛出的异常类型。倘若没有定义,就认为该方法不抛出任何异常。如果从方法的入口和出口的角度去考虑一下这个规范,我们知道参数可以认为是方法的入口(当然某些情况下也可以是出口),而返回值则是方法的出口,这是在程序正常执行的情况下,数据从入口入,出口出。要是程序非正常执行,产生异常又当如何? 被抛出的异常应该如何从方法中释放出来呢? Java 的这种语法规范就如同给异常开了一个后门,让异常可以堂而皇之“正确”地从方法里被抛出。

      这样的规范决定了 Java 语法必须强行对异常进行 try-catch。设想一下,对于以下的方法签名:

public void foo() throws BarException { ... }

暗含了两方面的意思:第一,该方法要抛出 BarException 类型的异常;第二,除了 BarException 外不能抛出其他的异常。而正是这第二点的缘故,我们要如何保证没有除 BarException 之外的任何异常被抛出呢? 很显然,就需要 try-catch 其他的异常。也就是说,一般情况下,方法不抛出哪些异常就要在方法内部 try-catch 这些异常。

      Java 这样的机制既有优点,也有缺点。先来说说优点:

  • 很显然,这种规范是由 Java 编译器决定的。倘若 Java 程序的入口点 main() 方法没有任何异常抛出,就是说要在 main() 方法内部,即整个程序内部捕捉所有的异常,否则将无法通过编译。这样编译器保证了程序对每个异常都有相应的计划和处理,不会有未处理的异常被泄露到虚拟机中,导致程序意外中断或退出,也就是增强了程序的健壮性。当然,Java 有 RuntimeException 的概念,这样的异常仍然可以随时被抛出到虚拟机中。
  • 强行 try-catch 要求把异常作为程序设计的一部分看待。就如同方法的参数和返回值一样,在编写一个方法时,要结合上下文做出通盘的考虑和打算。虽然异常是所谓的“意外情况”,但是这些“例外”理应是被我们全部了解并处理的。
  • 方便调试。异常理应在正确的位置被捕捉。当异常发生时,我们能更清楚的了解到其来源和相应处理程序的位置,而免去了在整个调用栈中摸索的麻烦。
  • 在不借助任何文档的情况下,从方法签名就可以知晓应该对哪些异常进行处理。

      Java 异常处理机制的这些优点也直接导致了他的致命弱点:将程序变得异常繁复。往往一个简单的程序,功能代码寥寥几行,而异常处理部分却占用了程序的绝大部分篇幅;同时导致缩进深度加深,既不利于书写,也不利于阅读。另外他的强行 try-catch 需要程序员有更高深的造诣,能够通盘考虑异常处理设计问题,这个在程序开始之初或者对于初学者是一个不小的门槛。这往往会阻碍其推广与发展,因为低水平初学者的信心往往因此而受到打击。然而对于高手来说,编译器是否能帮助他们找到未被处理的异常只是一个方便与否的问题,只要在编写方法时注意了异常处理,即便没有编译器的支持,情况也不会糟糕太多。反而倒是由于要遵循这样复杂的异常处理规范,以至于大多数人都可能为了图一时方便,对异常的基类型 Exception 或 Throwable 进行笼统地捕捉,这样做的危害就是那些你无法处理的异常被溺死在处理程序中,(按照异常处理原则,我们应该只捕捉那些可以被处理或恢复的异常,而把其他的异常继续抛出。至于这样做的优势,以及不这样做所带来的问题,不是一两句能够说清楚,这里就不展开讨论了。)导致程序的不稳定和不确定。既没有发挥 Java 语法在这方面的优势,反而增加了忧患。

二、.Net 是如何处理异常的

      一句话概括 .Net 的异常处理方式就是随心所欲。没有人要求你一定要抛出异常,也更没有人要求你一定要捕捉异常。未被捕捉的异常会被以 Unhandled Exception 的形式抛出到虚拟机中。在此我就要先解决一下文章开头提到的问题,为什么说 Java 和 .Net 这两种异常处理机制在本质上是相同的。可以从两个方面来考虑:

  1. 默认情况下。Java 在默认情况下 main() 方法是不抛出异常的,正如前面所说的,这要求所有的异常都必须在 main() 方法内部被捕捉;而 .Net 则没有这种约束,他的 Main() 以至于整个应用程序中的任何一个方法对异常都是完全开放的。这样来看,这两者刚好是对立互补的。
  2. 非默认情况下。Java 可以通过在 main() 方法后面加 throws 关键字使得整个应用程序对异常开放;而 .Net 则可以通过给应用程序域(Application Domain)的 UnhandledException 事件添加委托达到捕捉所有异常的目的,很显然这又是对立互补的。

因此,就好像一个是“正反”,一个是“反正”,加在一起“正反反正”都是一样的,对于达到控制异常的目录来说,是没有区别的。

      很多 Java 爱好者都鄙视 .Net 的这种行为,一方面他令程序变得不够健壮,因为默认情况下没有强制的办法要求所有的异常都被处理,或被正确处理;另外,他为调试增加了困难,不借助文档或代码你将无法了解到一个方法可能抛出什么异常,而当一个异常被抛出的时候,同时异常处理代码又写得不够完善,你将不得不仔细查看整个调用栈来确定异常出现的位置,而对于这一点 Java 默认是强制的。

      但是 Anders 的想法总是有道理的。

  1. .Net 代码写起来非常容易。这是对于初学者,或者那些只是想实现一些测试性小功能的人而言,你完全没有必要考虑太多异常处理的细节,你要的就是写代码,然后让他跑起来。这样的简单性无疑是你希望看到的,这样的简单性无疑更有利于 .Net 在市场上的推广。由于他在这方面并没有什么理论上的漏洞,也就仍然适合构建庞大的项目,只是感觉没有那么舒服罢了。
  2. 一定程度上增加了程序的安全性。难道不捕捉异常可以被成为是安全的吗?这个话也许要从另外一方面来想,前面说过,有些 Java 程序员(绝对不占少数)为了图省事,在强行捕捉异常的压迫下,选择捕捉异常的基类型,也就是捕捉所有的异常。这样当有你无法处理的异常出现时,他们就溺死在了你的代码中,而外面的程序全然不知,还在以一种不确定的状态运行着,这就可能是危险的开始。而如果是 .Net,那么 Unhandled Exception 会被虚拟机捕获,导致程序异常退出,虽然这从面子上对于用户不是一个好的交代,但是深层次地他避免了程序在危险的状态下继续运行。

      总之,萝卜白菜各有所爱。我的这篇帖子力求公正地讨论了这个问题,希望能对你有所帮助。

      我终于决定不再继续看极限编程大师 Kent Beck 所写的那本 Test Driven Development: By Example 了。因为我实在无法忍受我与作者在软件开发流程模型上的分歧。但这并不妨碍我认同并实践作者所谓“测试先行”的理念。我是一个趋近完美主义者,因此我更喜欢像设计模式那样偏重设计的方法论,我不能认可一个靠不断修改而成长起来的软件。

      按照 Kent Beck 极端的测试驱动开发模型,一个软件应该是这样写出来的:

  1. 写测试代码。
  2. 编译测试代码,编译无法通过。(因为没有实现代码。)
  3. 写一份最简单的实现代码,让编译通过。
  4. 迭代地修改代码以减少实现代码的重复度,降低实现代码和测试代码之间的耦合度。

就是这样,一个软件被一点一点修改而成。他强调了在这个过程中有两个主意事项:第一,在添加任何功能之前都要先写测试,除非代码是用于调试的,这也就是所谓测试先行的概念;第二,当编译无法通过时尽量不要再写新的测试。

      我之所以不认同这个模型,就是因为其中间产生了大量的迭代过程,而这些过程很可能只在头脑中出现,或者根本就不曾出现。这些过程一方面增加了开发的消耗,也在某种程度上引入了更多的不确定性。但是其中测试先行的做法,能最大程度保证单元测试的完整性与正确性。

      因此,我理想中的开发流程模型应该是这样的:

  1. 用户需求调研,导出成软件需求
  2. 根据软件需求进行设计
  3. 根据设计编写测试用例
  4. 根据测试用例编写测试代码
  5. 构建软件框架使测试代码能够编译通过
  6. 写 Mock 来保证测试用例代码准确无误
  7. 按部就班地写程序,用测试用例来保证所写的代码准确无误

这其中可以按照极限编程的思想以一个可独立发布可运行的模块作为单位,这样可以有效地控制模块的大小,还可以避免出现过度设计的问题。

      《在 Sourceforge 项目空间上部署 MediaWiki》的第一部分在这里

      造成 MediaWiki 不能向 config 目录写文件的原因,估计是 Sourceforge 服务器没有开放这样的权限。幸亏,MediaWiki 提供了一个变通的办法。找到下面的这些代码,把他们注释掉:
if( !is_writable( "." ) ) {
    dieout( "<h2>;Can't write config file, aborting</h2>
 
    <p>In order to configure the wiki you have to make the
    <tt>config</tt> subdirectory writable by the web server. Once configuration is done you'll move the created
    <tt>LocalSettings.php</tt> to the parent directory, and for added safety you can then remove the
    <tt>config</tt> subdirectory entirely.</p>
 
    <p>To make the directory writable on a Unix/Linux system:</p>
 
    <pre>
    cd <i>/path/to/wiki</i>
    chmod a+w config
    </pre> );
}

      这样一来,当目录没有写权限的时候,操作不会被取消,而是生成的配置文件会以文本的形式被打印在页面上。把这些代码拷贝、粘贴到一个名为 LocalSettings.php 的文件,然后上传到 MediaWiki 的根目录,在我们这个例子中也就是 /home/groups/s/sa/sample/htdocs/wiki/ 就可以了。

      最后还需要解决一下 session 的问题:

      由于 Sourceforge 使用了服务器集群,所以把 session 保存在内存中是有问题的,所以需要用文件来存储 session:

  1. 在 /tmp/persistent/ 下面创建一个用来保存 session 的目录,例如 /tmp/persistent/sample/sessions。这里的 sample 就是你的项目的 unix name。因为这个名称是唯一的,所以不用担心会跟别人的重复。但注意千万不要把它建在你的 project 目录下,因为他们是不能被 web 服务器写入的。
  2. 确保这个目录是可写的,也就是执行一下 chmod a+w /tmp/persistent/sample/sessions。sample 仍然是项目的 unix name。
  3. 告诉 MediaWiki 使用这个目录来存放 session,方法是在 LocalSettings.php 文件的开始处添加 session_save_path(”/tmp/persistent/sample/sessions/”); 注意不要丢掉 sessions 后面的那个 “/”。

      如果你这样做之后,在编辑或查看页面的时候退出,碰到空白页面,请把 ini_set( “include_path”, “.:$IP:$IP/includes:$IP/languages” ); 添加在上一步的 session_save_path 之前。

      这样,你的 Wiki 就基本上可以在 Sourceforge 的项目空间上运行了,不过还有很多问题这里没有涉及到,比如:如何配置邮件系统、如何修改网站的 logo、或者是一些碰到的其他的问题。更多内容请详见 Running MediaWiki on Sourceforge.net。(中国大陆地区访问可能会有障碍。)

      在开始部署之前,有一些准备工作要做:

  1. 你需要在 Sourceforge 上面拥有自己的帐号,并加入到一个项目,而且你的帐号在该项目中需要有访问 shell 的权限。同时确保该项目已经开通 MySql 数据库。
  2. 到 MediaWiki 的官方网站去下载 MediaWiki 1.6.8 版本。注意千万不要下载最新版本,因为 MediaWiki 从 1.7 版以后需要 php 5 的支持,而 sourceforge 服务器上部署的仍然是 php 4,因此无法运行最新版本。
  3. PuTTY 网站上下载 PuTTY.exePSFTP.exe

      为了方便,假设我在 Sourceforge 上拥有用户 usr,密码为 pwd,而且参与的项目为 sample。该项目的 MySql 数据库具有读写权限,可以添加、删除表和索引,并可以锁定表的用户为 mysqlusr,密码 mysqlpwd。

把下载来的 mediawiki-1.6.8.tar.gz 文件上传到项目空间,具体做法如下:

  1. 运行 psftp.exe
  2. 执行 open shell.sourceforge.net
  3. 按照提示输入你在 sourceforge 上注册的用户名,例如 usr
  4. 输入用户密码,例如 pwd
  5. 进入项目的 htdocs 目录,执行(以 sample 项目为例) cd /home/groups/s/sa/sample/htdocs/
  6. 把 mediawiki-1.6.8.tar.gz 复制到 psftp.exe 所在目录
  7. 运行 put mediawiki-1.6.8.tar.gz
  8. 待上传完成,关闭 psftp

解压已经上传的 mediawiki-1.6.8.tar.gz 文件:

  1. 运行 putty.exe
  2. 在 PuTTY Configuration 界面的 Host Name 中输入 shell.sourceforge.net,确认 Protocal 选中 SSH,然后点 Open
  3. 按照提示输入用户名,例如 usr
  4. 然后输入用户密码,例如 pwd
  5. 进入项目的 htdocs 目录,执行(以 sample 项目为例) cd /home/groups/s/sa/sample/htdocs/
  6. 执行 tar -xvzf mediawiki-1.6.8.tar.gz
  7. 将解压后的目录重命名为 wiki,执行 mv mediawiki-1.6.8 wiki

      登录 phpMyAdmin,创建一个 Database,例如 wikidb。当然用 sourceforge 提供的 MySql 服务创建 Database 时,命名都需要有一个前缀,在此忽略。

      打开浏览器,登录项目 Wiki,例如 http://sample.sourceforge.net/wiki。此时由于 MediaWiki 尚未配置,信息提示让你进入配置页面,点击进入配置页面,发现出错了。大意是说你的 config 目录没有写权限,让你在 MediaWiki 的安装目录执行 chmod a+w config 命令更改权限。但是,尽管你按照提示完成了设置,错误依旧。

      本文的第二部分在这里

      新版本的 JSON 修改了 API,将 JSON.stringify() 和 JSON.parse() 两个方法都注入到了 Javascript 的内建对象里面,前者变成了 Object.toJSONString(),而后者变成了 String.parseJSON()。

      不可否认,经过这样的修改,JSON 的接口确实比以前漂亮且好用多了,对于这样的对象:

var obj = new Object;
var arr = new Array();
arr[0] = "a";
arr[1] = "b";
obj["key1"] = arr;
obj["key2"] = "c";

以前的代码得这样写:

var jsonString = JSON.stringify(obj);
var myObj = JSON.parse(jsonString);

而现在的代码是:

var jsonString = obj.toJSONString();
var myObj = jsonString.parseJSON();

      前者 JSON 作为一个工具类,提供一组方法实现转换功能,而后者让你觉得对象天生就具有这样的方法。如果对于编译语言来说,这可能没什么问题,但是对于 Javascript 问题就来了。考虑下面的代码片段:

var obj = new Object(); 
for (var k in obj) {
    document.write(k + "<br>");
}

      如果是一个干净(没有包含任何其他 Javascript 类库)的 Javascript 执行环境,这段代码应该不会输出任何内容,因为 obj 是空的;但是当代码中包含了新版本的 JSON 后,你会发现结果是 toJSONString。很多情况下我们会用 Javascript 的 Object 来模拟 Map,那么上面的这段代码的功能就是遍历此 Map,一个空的 Map 怎么会凭空出来了一个成员?这正是由于新版本的 JSON 的实现所决定的:

Object.prototype.toJSONString = function () {}

      JSON 并不是第一个采用这种写法的 Javascript 类库,始作俑者就是我文章标题中提到的 prototype。prototype 为 Object 注入了 extend 和 instanceof 等方法来支持 Javascript 的面向对象扩展,这样写的后果就是造成了非常严重的兼容性问题。你有没有注意到为什么有些 Javascript 的类库特别声明”使用了 prototype”或者说是”与 prototype 兼容”?因为如果他使用了 prototype 那么你需要在用此类库的时候格外小心,像上面那样的代码不能正常工作;而如果说其与 prototype 兼容,那么可能该类库并没有使用 prototype 但是你使用 prototype 并不会影响他的正常运行。

      prototype 固然是个成功且应用广泛的 Javascript 框架,但是这并不能说明他解决问题的思路就是正确的。开发编译语言程序的人们都知道要以组织或公司的名称作为命名空间前缀,来避免冲突,奈何如此著名的两大 Javascript 框架提供方反而积极去打破这个规则呢?难道实现了功能就可以不考虑兼容性的问题吗?

      为什么这么多互联网公司都在不遗余力地做着重复性劳动,看看这个公式也许就有点理解了。

      什么是搜索?一个汇集互联网内容的大型工具。网络是自由且开放的,但是你不去找他,他不会来找你。拥有了搜索,就是拥有了互联网中所有的资源,就相当于拥有了一个无比庞大的集贸市场。

      但一个再大的集市,如果没有人也是不顶用的。这就是社区。也许说“社区”更容易让人理解“人”在其中所处的地位。当然我更倾向于使用一个目前互联网上发了烧的词语,那就是 Web 2.0。当然 Web 2.0 不同于社区,但我理解中的 Web 2.0 同样是以人为本的。他更强调信息的收集、共享和流通,这些动作都是以人为主体的。所以 Web 2.0 是一个人问概念,而绝非异步 Javascrip 和 XML 这么简单。

      举个小例子,就拿刚刚结束的世界杯来说,订阅比赛结果应该是大家都享受过的服务。那么,你想没想过:当一场比赛结束之后,你订阅的信息怎么就发生变化了呢?也许这个问题太弱智了,你都不屑回答:当然是有人改过的。那么好我不否认你的答案,世界杯人人关注,而且信息量小,会有一些人花上一两秒的时间中去更新一个比分,你在服务的下游自然就收到了结果。但是倘若一个信息量庞大的服务,比如网络音乐,无数的歌手,无数的专辑,我想不会有人愿意专门去做更新这样的事情。但是你可能很愿意为你喜欢的歌手或专辑花一些时间去整理。想要把这些零星的内容组织起来,形成一个有规模的服务,就只能靠搜索了。

      拥有了搜索是拥有了内容,拥有了社区就是拥有了人。人和内容都有了,互联网也就在如来佛的手掌中了。

firefox_bites_ie_awesome.jpg
      我经常根据一个人所使用的浏览器来评价某个人。不管你信不信,你对浏览器的选择往往反映出了你的个性。

IE 5.0:

      你使用电脑仅仅是为了即时聊天,写写电子邮件和博客。你顽固地拒绝升级你那老旧的 WIN98,因为你并不需要太多的功能而且认为 WIN98 已经工作地很好了。你同时可能不使用任何杀毒软件,你只是每个月让你的儿子,侄子或朋友把把病毒清理干净而已。

IE 6.0:

      你很可能并不知道什么叫做“浏览器”并且认为 IE 就是因特网。你对技术没有清晰的概念,而且你通常对电脑感到畏惧。同样的,你使用电脑也仅仅是为了即时聊天,写写电子邮件和博客。也许你的朋友曾不断地向你提及“被炒鱿鱼的狐狸”(Fired Fox),但你一直不明白那到底是什么,也不准备在它上面花时间。

IE 7.0:

      你认为你站在了技术的最前沿,同时认为微软是地球上最伟大的公司。至于那个邪恶的 “Lenoux” 操作系统(音同 Linux)则是由恐怖分子编写出来的。你在卧室的墙上张贴了斯蒂夫·鲍尔默(微软首席执行官)的海报,并希望自己在未来能成为第二个比尔·盖茨。你一想到 “Vista” 便会激动地浑身颤抖、坐立不安。

Firefox 1.x:

      你很可爱而且有点傻里傻气的,并为 FireFox 感到骄傲。你是开源运动的强烈支持者,你认为理乍得·马修·斯托曼才是“真正的男人”。你其实并不关心 FireFox 是不是比 IE 更安全,更快速——你会一直使用 FireFox 哪怕它的效率比 IE 低上十倍。你只是因为你得到了一个免费、开源并拥有庞大技术支持社区的浏览器而感到高兴。无论任何时候你都会安装至少 7 个必不可少的扩展。

Firefox 2.0 Beta:

      在白天你是个程序员,到了晚上你就成了一个开源软件开发者。要不,你就是一个疯狂的 Firefox 粉丝。你热衷于上报你遇到的每一个 Bug,很可能你已经发布了至少一个开源项目的补丁。你喜欢对程序修修补补,而且丝毫不会在意在自己的电脑上运行 beta 版软件。毕竟,发现新的 Bug 和修改最新的软件对你来说充满了乐趣。

Mozilla:

      从一开始你就在使用 Mozilla。你认为 FireFox 宣传地过了火,相对于 FireFox 你更愿意去使用旧版的 Netscape。你并不认为 Mozilla套装(Moz Suite)是个负担——事实上你更喜欢一个集成了邮件客户端、IRC 聊天客户端和网页编辑器的浏览器。你很不理解为什么有些人宁愿去挑选一个功能很少的浏览器而不是选择 Mozilla。在其他的方便你更像一个 Firefox 用户——你喜欢开源、你喜欢你的浏览器扩展、等等——或许你会说 Firefox 用户的口味和你非常相似。总之,你们在使用一个令人钦佩的、功能强大的、gecko 内核的浏览器,与此同时很多人仍然在他们的 IE 浏览器里挣扎。

Opera:

      你并不关心 Firefox 之流,你所需要的只是一个世界上最好的浏览器——对你来说那就是 Opera,而你很可能早在 Opera 收费时就购买了它。如果有一个 Firefox 粉丝对你的浏览器评头论足,你就会打开一个 ACID2 测试,然后以此来驳倒他。你知道什么是你所需要的(一个快速、支持标准的浏览器),你也明白怎样得到它。你对浏览器大战丝毫不感兴趣,虽然你有一点点希望
Firefox 获胜,因为如果那样的话会有更少的网页开发者制作只兼容 IE 的页面。

Netscape 8.x:

      你是一个刚刚得到一台新电脑的老资历网民,虽然你对互联网知道的并不多,但你却清楚地记得你需要 Netscape 去使用它。你并不明白人们谈到的 IE 和那个叫 Fire 什么的东西到底是什么,而且搞不清楚奥普拉·温弗瑞(Oprah,一个脱口秀主持人,音同 Opera)和因特网有什么关系,你所知道的就是点开那个大大的 “N”,然后变成”在线”。你认为史蒂文的关于网络的演讲很有道理。

Netscape 7 和更老的版本:

      参见 IE 5.0。

AOL Explorer:

      曾经有一天你安装了最新的 AIM 客户端,然后这个东西就成了你的默认浏览器。你非常讨厌它,但你却不知道怎样才能把它变回去。你甚至不知道你怎样才能向你的那些电脑高手朋友们描述这个问题,当你想得到帮助时你也许会像这样提问:“你能把这个新的网络,呃,变回原来的那个旧网络吗?”他们只会瞪着你,然后装作不明白你在说什么。他们或许并不像他们自己所说的那样了解电脑。

AOL Suite:

      你很可能仍然在使用 AOL 的拨号网络,不然的话,你就是觉得在你使用宽带网络之后仍然需要 AOL。有人告诉过你其实你上网是不需要用 AOL 拨号的,但你无法想象这是怎么一回事。这看起来很难做到,而且似乎是非法的。

Safari:

      恭喜你!你是一个 Mac 用户并享受着那个名字给你带来的好处和好心情。你喜欢 OSX,并且永远不会使用 Windows。Windows 对你来说实在是太过丑陋和低效,你更喜欢 Mac 的简洁和清晰,而 Safari 就是一个为你工作的浏览器。你从不会烦心去寻找另一个浏览器,因为你对你现在拥有的一切已经非常满意,你也不会因为世界而改变它。

Konqueror:

      你是一个 linux 用户,并且打心底就是个极客(?)。你认为 KDE 是最好的桌面环境,并且因此而鄙视 Gnome。你喜欢一个同时是文件管理器、ftp/scp 客户端、smb 分享客户端、PDF 文档查看器和其它很多东西的浏览器。你喜欢向你的朋友炫耀 KDE 的网络透明度,你仅仅通过浏览器在你的网页服务器上编辑一个 HTML 文件,保存它,然后又在浏览器里重新载入修改后的文件。你日常使用的绝大多数软件都以 K 字大头(Kmail, Kontact, Kdevelop, Koffice 等等)。

Lynx:

      你肯定是个骗子,你真的想让我相信你使用一个文字浏览器来浏览所有网页?尤其是一个不支持 javascript, frames, css 甚至连 tables显示都有问题的浏览器?说真的,我可以相信你一直使用 VI(一个编辑器),用 Mutt 或 Pine 做你的主要邮件客户端,但你不可能让我相信你使用 lynx 作你的主浏览器。如果你真的做到了,那么你就是我一生中见到过的最最执着的极客了。向你脱帽致敬!

      如果你不同意上面的话,请留言好让我知道。如果你被我不幸言中,那么请停止使用那个该死的浏览器并换一个真正的浏览器吧。也请你自由地给我漏掉的浏览器作简短的描述。

      免责声明:我不清楚是谁制作的那个 FireFox 图像(就是本文开头的那个),有个人在留言本中曾使用它作头像。我向那个作者致以崇高的敬意,如果我能找到他的话。

      谢谢你们所有的评论,让我们开公布诚吧——我并没有说 Lynx 是一个差劲的浏览器,事实上我在很多不同的方面都经常使用它。我只是怀疑是否有人把它作为主浏览器。如果你是的话,向你脱帽致敬!你比我执着多了。

现在我补充一些漏掉的比较流行的浏览器:

Flock:

      他们也许会称你为 Web 2.0 先生。你所使用浏览器表明一点:你的足迹遍布 flickr, del.icio.us, youtube 和其它一打的网站。你认为 Firefox 还不错,但它并不不能让你在弹指间就能完成写博客、照片共享、标签和网络书签等等功能。你希望在你的脑袋里植入一块芯片,这样你就能一直连接到网络,而且能使用 24/7 移动博客。当一些目光短浅的人告诉你 Flock 只不过是 Firefox 的修改版时,你会赶走他们并说他们不能以更宽广的视野看东西。

Epiphany:

      你是一个 Gnome 用户并为之自豪。你认为 KDE 简直是地狱里出来折磨人的东西,并且热衷于向人们解释 KDE 必须经过几个小时的修改才能使用,至于那些说 KDE 马上就能用的人则是可耻的骗子。你希望所有东西能更加简单和直观——那就是你为什么选择了 Gnome,这同样也是你使用 Epiphany 的原因。你试过 Mozzila 和 Firefox,但你发现它们实在是臃肿、丑陋和麻烦。你的桌面就像你的书桌一样整齐有序。

Maxthon and Avant:

      你也许有些疑惑,虽然你喜欢IE并且不会换用别的浏览器,也不会担心网站会出现渲染不正常的错误,更不会担心它像其它内核浏览器那样不支持 ActiveX 控件,但你心底还是羡慕那些使用可以做到标签页浏览和其它很酷的功能的浏览器的朋友。你承认 IE 有点落后于时代,而你想要一些更加现代的东西,同时也不想放弃正常显示一些网站。Maxhton/Avant 让你拥有了世界上最好的两项功能——舒适温暖的IE渲染引擎和其它浏览器里非常酷的功能。当 IE7 发布正式版时你就会换用 IE7。

Sea Monkey:

      你很喜欢简单的软件套装,对你来说把浏览器和电子邮件客户端分开是不可理喻的。你以前习惯于使用 Mozilla,但 Sea Monkey 发布后你很快便换了口味并不再回头。你认为“Sea Monkey”是浏览器中有史以来最酷的名字。

w3m:

      你一生的大部分时间都在当系统管理员。你很少看见阳光,因为你一天中大部分时间花在大型服务器的周围。如果周围没有电脑风扇的“嗡嗡”声你便无法入睡。哪怕是在夏天你每天也不得不穿一件暖和的夹克,因为服务器机房里的冷气开的是如此之高,如果你不加以注意的话便很容易感冒。年轻的极客们都向你看其,并试图模仿你——而你一直也不知道这是为什么。

K-Meleon:

      你对长时间等待浏览器启动感到很不耐烦,甚至IE的启动速度对你来说也太慢了点。这也是为什么你的浏览器会预读取页面,然后仅仅花费十亿分之一秒去载入页面。你的生活节奏非常快,根本没有时间去等待浏览器慢慢启动。你可以花费几个小时去设置 Windows 注册表来提升程序的响应速度、载入时间,并减少所有程序的超时时间。

Dillo:

      你从心底就是一个喜欢低资源占用的人,你喜欢让你的程序更加小巧和快速。你最喜欢运行 IceWM 或 Windowmaker,同时嘲笑那些臃肿的桌面环境像 KDE 或 Gnome。你以本地 Linux/BSD guru 著称。

      中文翻译地址: 一点笔记

      原文英文地址: Terminally Incoherent

      不要以为这是件容易的事情,先来看一个正确的 Equals 方法应该具备的条件:

  1. obj.Equals(obj) 应该永远返回真
  2. obj1.Equals(obj2) 和 obj2.Equals(obj1) 应该返回相同的结果
  3. 如果 obj1.Equals(obj2) 而 obj2.Equals(obj3) 那么 obj1.Equals(obj3) 应该也为真

别以为我的智商就小学水平,回去看看你的代码,这些都能做到吗?写一个具备如此条件的 Equals 方法不是件容易的事情!从实现角度说,一般来说分为两种情况:

一、给直接继承自 Object 基类的类型重写 Equals 方法

public class MyType {
    private int x;
    private string s;
 
    public override bool Equals(object obj) {
        // 如果 obj 是空的话,就直接返回假
        // 因为当前对象不会是空,否则在还没有调用 Equals 方法之前就会先抛出空指针异常
        if (obj == null) return false;
 
        // 如果 obj 与当前对象不属于同一个类型,也直接返回假
        // 显然两个不同类型的对象不可能相等
        if (this.getType() != obj.getType()) return false;
 
        // 这个强制类型转换是不会抛出异常的
        // 因为你已经知道 obj 属于该类型
        MyType o = (MyType)obj; 
 
        // 最后逐个比较他们的成员变量是否相等就可以了
        if (this.x != o.x || this.s != o.s) return false;
 
        return true;
    }
}

二、给间接继承自 Object 基类的类型重写 Equals 方法

// MyType2 继承自 MyType,而不是直接继承自 Object
public class MyType2 : MyType {
    private DateTime d;
    
    public override bool Equals(object obj) {
        // 如果基类认为这两个对象不相等
        // 直接返回假
        if (!base.Equals(obj)) return false;
 
        // 两种实现方法的区别仅在于此,下面都是一样的
        // 直接继承自 Object 的类型之所以没有上面这一段
        // 是因为 Object 判断两个对象是否相等的逻辑非常简单
        // 通常就是判断他们的引用是否相等。因为当前对象和 obj 很有可能是不同的引用
        // 所以这种情况下,你的 Equals 方法绝大多数情况下都会返回假
        // 如果 obj 是空的话,就直接返回假
        // 因为当前对象不会是空,否则在还没有调用 Equals 方法之前就会先抛出空指针异常
        if (obj == null) return false;
 
        // 如果 obj 与当前对象不属于同一个类型,也直接返回假
        // 显然两个不同类型的对象不可能相等
        if (this.getType() != obj.getType()) return false;
 
        // 这个强制类型转换是不会抛出异常的
        // 因为你已经知道 obj 属于该类型
        MyType o = (MyClass)obj;
 
        // 最后逐个比较他们的成员变量是否相等就可以
        if (this.d != o.d) return false;
 
        return true;
    }
}

参考了 Jeffry Richter 的 Applied Microsoft.NET Framework Programming 一书。

      今天享受了一下 Google 的 WebMasters 服务,再加上 Google Analytics,真有 WebMater 的感觉。

      在这个服务里面,为了网络爬虫能够更好地抓取页面,Google 需要你提供 SiteMap,一个 XML 格式文件。试了 Google 推荐的几个程序和服务,感觉都不是很好,鉴于 dasBlog 是开源的,简单研究了一下,为其添加了生成 SiteMap 的功能。

      首先,下载 net.tangrui.DasBlog.Web.Services.dll 文件,将其放到 dasBlog 的 bin 目录下。

      然后,修改 web.config 文件,在 <httpHandlers> 标签下,添加这样的节点:

<add verb="*" 
  path="sitemap.ashx" 
  type="net.tangrui.DasBlog.Web.Services.SiteMapHandler, net.tangrui.DasBlog.Web.Services" />

      最后,访问 http://yourwebsite/sitemap.ashx 就可以了。

      如果需要,可以到这里下载源代码。

Next Page »