博文

LiteBlaster 正式发布(2007-6-9 17:22:00)

前些日子本人的网站终于有稳定的服务器了,所以狠狠心买了域名+四线空间+数据库

各位有空去逛逛~~~ http://www.auwaysoft.com/

现在介绍下 LiteBlaster

LiteBlaster 是一款基于 .NET Framework 原创的 ASP.NET 论坛系统。后台程序库采用 C#/VB.NET 编写,前台采用 DIV/TABLE+CSS+JavaScript 的形式,并灵活运用 AJAX 技术为客户端带来不一样的体验。

主要特性

  1. 基于 ASP.NET ,利用 ASP.NET 强大的基础框架可以高效完成数据库读写并提供便捷的扩展途径。
  2. 提供丰富的 CSS 内置样式,方便美工人员修改界面。
  3. AJAX 技术的引入(不影响传统浏览方式)令回帖、编辑帖子、修改设定等操作实现真正的“页面无刷新”,快速响应与低带宽一直是 LiteBlaster 的设计目标之一。
  4. 突破性地完整支持 MS Access、MS SQL Server 以及 MySQL 数据库,一份论坛程序同时具有三种数据库连接能力,一次程序扩展即可让三个数据平台受益。同时,论坛程序专门针对 MS SQL Server 以及 MySQL 提供了部分查询优化,提高查询速度。此外,您还可以使用与 MS Access 相兼容的数据库作为后台,论坛程序将以 MS Access 兼容模式访问该数据库(通过 OLE-DB)。
  5. 提供智能易上手的数据库备份、还原功能。
  6. 详尽的用户组管理及经验值管理策略让论坛管理更加积极主动,创新的单用户双用户组设计可以更加灵活地组织用户权限。
  7. 对敏感性的密码、提示问题答案等均采用 MD5 扩展方式不可逆加密保证用户信息的安全性,同时提供多个解决方案方便论坛管理人员维护论坛秩序使其时刻符合相关法律条文要求。
  8. 论坛程序相当多的内容都提供了 UBB 代码、表情符号支持,您可以通过灵活的 UBB 制作丰富多彩的富文本信息。
  9. 系统的经验值管理策略及多样的经验值奖励方案让论坛体验更加丰富。
  10. 最佳回复、快捷版面订阅、吸附式投票设计等特色功能使得论坛增色不少。

希望各位喜欢并支持!!

http://bbs.auwaysoft.com/逛逛或者去http://www.auwaysoft.com/LiteBlaster/查看更多信息


阅读全文(1540) | 评论:0 | 复制链接

.NET 窗体间传送数据方法谈 —— 迟来的补遗(2007-5-7 3:16:00)

其实这篇文章早就该写出来了。毕竟有关WinForm窗体间传送数据的帖子在.NET相关版面中相当有热度。

本人当初考虑起笔的时候,感觉这个东西不是三言两语就能搞定的,毕竟方法甚多,要说清楚也不那么容易。其次呢,我这人并不勤于笔墨,更新最快的live space频率最高的时候也就一星期一篇而已。

现在仔细思量了一下,还是早点出的好。于是打算结合之前遇到的问题,稍微将解决思路一带。复杂的技术就不再细说,重点提供各位几种实用易上手的方法。

注:

  1. 本文中提到的方法可能会不定期补充,毕竟我不能保证一次性总结那么全面(更何况我一直不擅长总结~~,大家发现什么其它方法或者俺没说清楚的地方给俺说声)。
  2. 以下所有方法均基于两个普通窗体类Form1、Form2(实例对应f1、f2);如果没有专门说明,这两个类均位于同一程序集中。
  3. 传送的数据均以32位整型为例。
  4. 如果没有单独说明,就按照双向传送数据对待。
  5. 本文中提到的数据传送方法并不仅单单适用于窗体间数据传送,很多方法同样适用于更为宽泛的类数据传送。

方法一:静态变量法

  • 优点:发挥静态变量的优势,很少代码就能搞定。
  • 缺点:静态变量也容易导致多个窗体共同访问时出现混乱,并且在这两个窗体类的多个实例之间传递的时候不具有相互独立性。

Form1或者同一程序集的其它类中声明一个静态中间变量,如

    class Form1 : Form{
        ...
        public static int internalVar;    // 也可以是internal static
        ...
    }

 然后,Form1以及Form2的实例自然可以方便地访问Form1.internalVar这个中间变量了。

方法二:非静态变量法

  • 优点:独立性好。
  • 缺点:需要额外建立新变量。主动方要事先获得被动方的引用。

Form2中声明一个中间变量,如

    class Form2 : Form{
        ...
        public int internalVar;    // 也可以是internal
        ...
    }

然后主动方只要在传送前获得Form2的实例f2就可以访问f2.internalVar了。

方法三:Tag法

  • 优点:独立性好,不用额外建立新变量。
  • 缺点:主动方要事先获得被动方的引用。此外,Tag是Object类型,读取时需要强制类型转换。
  • 注:也可以充分利用其它类似属性来实现,不过Tag属性存在的目的就是数据暂存,具有数据中立的特点,何乐而不为呢。

主动方(可以是Form1实例,也可以是任何其它的实例)某个方法中:

    Form2 f2 = new Form2();      // 或者其它方式获得被动方引用
    // do something or not
    f2.Tag = data;                         // 传送数据,f2可以在这句代码之后使用该数据
    // do something or not
    string data = (int)f2.Tag;        // 接收数据,主动方可以在这句话之后使用该数据

这样,主动方就可以实现数据的接收和传送。

方法四:窗体所有关系法

  • 优点:具有很高的独立性。
  • 缺点:主动方要事先获得被动方的引用。本方法会建立窗体所有关系。有可能需要建立额外的变量。
  • 注:本法可以基于(非)静态变量法、Tag法等方法。

本例以Tag法为例(省几行代码^_^):

假定已经获得主动方Form2的实例引用f2,假定被动方是当前实例。

    f2.Owner = this;

然后,f2内部要想访问被动方的话,只需this.Owner即可获得被动方实例,从而f2内this.Owner.Tag即中间变量。

同样,Form.ShowDialog(Form owner)方法支持自动设定Owner,也可以类似设定窗体所有关系。

方法五:构造函数法(非Form引用)

  • 优点:具有很高的独立性。
  • 缺点:如果构造函数参数传递的不是引用类型变量,那么只能实现单向传送;此外,此法只能在实例初始化的时候传送。(静态构造函数类似)

被动方新建一个构造函数,如

    class Form2 : Form{
        public Form2(int data){
            // 将接收的data存储到实例中或者直接处理(单向)
        }
    }
    class Form2 : Form{
        public Form2(ReferenceType data){
            // 由于此处data是某种引用类型,所以主动方和被动方同时引用一个变量,所以是双向的
        }
    }

方法六:构造函数法(Form引用)

  • 注:本法基于非静态变量法、窗体所有关系法以及构造函数法(非Form引用)中双向传送类别。

被动方声明一个中间变量,如

    class Form1 : Form{
        ...
        public int internalVar;    // 也可以是internal
        ...
    }

主动方新建一个构造函数,如

    class Form2 : Form{
        public Form2(Form1 f1){
            // 获得了被动方Form1的实例引用
        }
    }

这样,主动方可以通过访问被动方实例f1.internalVar即可实现访问。

方法七:方法传送法

  • 优点:具有很高的独立性。逻辑功能上具有自身的意义。
  • 缺点:如果方法参数传递的不是引用类型变量,那么只能实现单向传送(除非该方法有传回数据用的返回值)。(静态方法类似)
  • 注:本法类似于构造函数法。

被动方新建一个成员方法,如

    class Form2 : Form{
        public void GetData(int data){
            // 将接收的data存储到实例中或者直接处理(单向)
        }
        public void GetData(ReferenceType data){
            // 由于此处data是某种引用类型,所以主动方和被动方同时引用一个变量,所以是双向的
        }
    }

方法八:Remoting法

  • 优点:可以跨程序集甚至跨域进行数据传送。
  • 缺点:代码量较大,实现机理较为复杂;程序集内部使用速度较慢。

本方法涉及内容较多,暂不进行讨论。有兴趣者可以参考.NET Remoting的相关资料。

方法九:WCF法

  • 优点:同Remoting法。提供简便的分布式通信方式。
  • 缺点:同Remoting法。此外,该方法仅适用于.NET 3.0平台。
  • 注:WCF是Windows Communication Foundation的缩写,是.NET 3.0上的新一代分布式通信平台,有效地组织各类Windows平台之上的通信方式。

本方法涉及内容较多,暂不进行讨论。有兴趣者可以参考WCF的相关资料。


阅读全文(1802) | 评论:0 | 复制链接

Opera 之 Browser JavaScript(2006-12-11 17:07:00)

LiteBlaster 论坛程序新版本今天测试哈……
 
按照惯例,俺是要完全兼容 Opera 滴,不过测试的时候偶尔发现一句:
“Opera has modified the JavaScript on 192.168.1.111 (Rich text editor submit fix). See browser.js for details”
 
 
奇怪,以前从来没碰到过这个啊??还browser.js呢~~~不行,要坚决解决,最爱的 Opera 一定要全兼容啊……
 
去 Opera Community 和 Opera Support 上和 E 文奋战了 N 长时间才了解到:Browser JavaScript 是 Opera Fixing the World 计划的一部分,通过修改不兼容网站的 js 脚本来解决兼容问题。
 
我就更奇了怪了,我的网站差不多就是按照 Opera 标准做的,怎么不兼容了?
 
再翻 browser.js 源代码一看才恍然大悟,Opera 为了修复某网站的 rich text 提交问题而发布了兼容脚本,源码如下:
 }else if(  name.indexOf('/richtext.js')>-1  ){ // Bug 230549, bug 208097
   // Rich text editor checks for "On" where Opera and FireFox return "on". sigh.
   addPreprocessHandler( 'designMode != "On"', 'designMode != "on"', true, function(t){return t.indexOf('kevinroth.com/rte/demo.htm')>-1;} );
   opera.postError('Opera has modified the JavaScript on '+hostname+' (Rich text editor submit fix). See browser.js for details');
  }
 
而我的论坛因为有个脚本叫做 richtext.js 正好中奖(根据这句——name.indexOf('/richtext.js')>-1)。
 
SIGH!!!!!!
 
 
Opera 的这个功能还得智能化点啊……否则殃及池鱼,忙乎了我半天~~~无语

阅读全文(2796) | 评论:0 | 复制链接

LiteBlaster 后期开发有感(2006-8-23 18:12:00)

 这个论坛程序折腾了我一个暑假,本来暑假就短……现在第一个版本终于全部封装完毕了,准备内部测试。

 我想开发中应当没有什么,一切都很顺。但到后期时间一紧就容易出乱子,眼看暑假就要OVER了,开发自然要逼快一下了,很重要的后期工作中出了些麻烦,在这里分享下顺便发点牢骚~~


 顺便提一下,LiteBlaster采用ASP.NET 1.1(毕竟.NET 2.0的服务器少啊……)作为服务器技术,页面采用传统Table布局和DIV混合的方式,用CSS和JS定位,并用AJAX给绝大部分模块作了个外壳。数据库设计兼容MS Access,MS SQL Server以及MySQL。

 这样形成了一个免费一条龙的开发、使用流程:开发主要用SharpDevelop、ASP.NET Matrix、Dreamweaver,使用的时候挂上Access或者MySQL,的确是免费……不过Dreamweaver是个麻烦事(要好多米啊)……不过也好替代,比如高级记事本之类的,我用的是EmEditor。因为这个项目的确用不到什么界面设计的地方,关键的用于元素定位js和css都是服务器动态分发的,Dreamweaver显然找不到这些文件,所以设计模式没啥用,一直就用代码模式在写……平时只修改一个文件的时候就直接用EmEditor了。其它的辅助工具像NUnit、FxCop等也是免费的。
 
 兼容多个数据库是比较头疼的地方,不过相信也是优势。需要加个中间层再作一些特殊处理。另外由于针对SQL Server和MySQL作了一些优化,再加上起初程序底层架构过于垂直化,所以中间层以上部分仍然有些地方需要跨过中间层之间访问数据模块。这铁定是今后重构的重点。

 在这里,我要把大多数的牢骚送给MySQL。

 这个暑假是我第一次使用MySQL,所以有很多都不大了解。当然MySQL的SQL语法自然是清楚了,其中最喜欢的就是LIMIT子句,分页太方便了。

 不过我在开发过程中一直使用Access来进行简单的单元测试和阶段测试,到最后一块测试的时候发现连接不上MySQL。仔细一查,发现OLE-DB连接服务MySQLProv要自己安装……我想没有哪个论坛管理者愿意到服务器上装点什么吧。

 那就找其它连接方式吧……ByteFX比较有名,不过主页上已经人去楼空,用它的老版本已经和MySQL 5.0不兼容了;CoreLabs商业化产品,和免费宗旨不合。最后只好用MySQL官方基于GPL的Connector/.NET,貌似它是利用ODBC连接的,稳定是好事,不过这效率……铁定打折扣。

 牢骚继续……Connector/.NET没有提供布尔类型,让人郁闷不已。毕竟后台中大量用了强制转换成布尔的代码。MySQL内部提供BOOL类型,看作TINYINT(1),这个Connector还真偷懒,一个整型传回来。改吧……强制转换全变成Convert.ToBoolean。
 然后说说TINYINT,Connector最后传给我一个SByte类型。它也真够可以的。SByte不是CLS兼容类型,偏偏多此一举。.NET官方都对应到Byte,这下好了,再改:Convert.ToByte。

 本以为这样可以了事了,论坛也跑起来了,谁知道每次到ExecuteScalar那里就挂。再检查下,MySQL果然和MS不是一个风格,MS对COUNT之类聚集函数返回的都是Int32,Connector给了我一个Int64。从中间层都转换掉吧……不是省油的灯。

 不过值得夸奖的是,mysqldump和phpMyAdmin都能很方便的导出脚本,省得我写代码,备份功能就好办了。而另一边SQL Server,我无语了……


 不过唯一的遗憾是目前测试了三个浏览器,只有Opera和IE完美兼容,Firefox存在一个兼容问题:由于AJAX运用,经常往DIV塞东西,innerHTML一塞多了,它就显示不出来了,把下部内容截断了,不像另外两个那样可以自动伸缩。如果有哪位老大知道如何解决,麻烦告诉我一声。


 对了,那个索引服务我都很服气了,平时容易出现删除文件之后仍然留在那里的现象,我忍了,现在边开发边测试论坛,DLL经常变动,索引一出问题,DLL就拒绝访问了,建议把ASP.NET Temporary Files文件夹放到索引服务不编录的行列中。


 虽然MySQL给我搞了N多大头,不过通过这个项目完全可以考虑一下ASP.NET+MySQL架构了,PHP+MySQL不再是不二之选。希望MS哪天考虑一下在.NET中官方支持MySQL。


阅读全文(1860) | 评论:0 | 复制链接

C# 3.0 特性预览(2006-5-20 23:57:00)

    声明:本文主要是根据微软《C# 3.0 Specification》文档翻译而成(本文内容并非完整按照该文档进行编排),并对msdn相关文档中的资料进行整理而成。若有翻译不妥之处,恳请指正。
    
    阅读本文前,需要了解:
    1、C# 3.0代号“Orcas”,是基于C# 2.0的扩展。提供了多种具有更高层次功能的类库。这些扩展允许构造组合(compositioanl)API来实现具有同关系型数据库及XML等领域相等效能的表达效力。
    2、LINQ项目可以看作是一个未来技术的演示项目,可以从MSDN网站上下载预览包。LINQ项目旨在扩展C#及VB.NET在语法上对语言集成查询的支持。借助这些特性,我们可以用类似SQL或者XQuery之类的语句进行代码编写。LINQ项目的内容不单独介绍,因为它对于C#中的特性主要就是C# 3.0中的语言集成查询特性。
    3、写这篇文章的目的很简单,就是希望有兴趣的朋友可以开始3.0的探索了,这样当3.0的编译器出台时不至于再赶时间学习。并不建议初学者花费精力来掌握本文内容,了解一下发展概况即可,否则很容易导致两头都搞不好。况且这并不是最基础的内容。
    4、本文内容仅基于预览版本内容(PDC 2005 Technology Preview),并非最终版本。C# 3.0完成后,有可能会增加或者更改某些特性。预览版本可能还不能支持C# 3.0中的某些内容,对于这些内容,将简单介绍。
    5、本文旨在将新的特性展现出来,针对每个特性并不进行深入的探讨,读者如果有兴趣可以自行参阅相关资料。
    
    那么,我们就开始吧。
    
    C# 3.0的扩展特性主要包括以下几点,我们在后面也会按照这个顺序进行介绍:
    1、隐式局部变量(implicitly typed local variables),通过初始化该局部变量的表达式自动推断出该变量的类型。
    2、扩展方法(extention methods),可以利用附加方法拓展已经存在的类型和构造类型。
    3、Lambda表达式(lambda expressions),匿名方法的革新,能够提供更好的类型推导以及到委托类型和表达式树的转换。
    4、表达式树(expression trees),允许Lambda表达式以数据(表达式树)的形式存在,而不是代码(委托)。
    5、对象初始化器(object initializer),简化了对象的构造和初始化。
    6、匿名类型(anonymouse types),由对象初始化器自动推断和生成的元组类型。
    7、隐式数组(implicitly typed array),一种数组创建和初始化的形式,可以从数组初始化器推导出数组的元素类型。
    8、查询表达式(query expressions),提供语言集成查询的语法,使得在编程中可以使用类似关系型(如SQL)以及层次(如XQuery)查询语言的代码。   
          
    
一、隐式局部变量
    在以前,如果我们要声明局部变量,比如:
    int i = 5;
    string str = "test";
    int[] numberss = new int[]{1, 2, 3);
    ArrayList list = new ArrayList();
    在C# 3.0,完全可以等价为
    var i = 5;
    var str = "test";
    var numberss = new int[]{1, 2, 3);
    var list = new ArrayList();
    我们可以看到,用一个简单的var就可以代替这么些类型(怀念Pascal中~~~)。因为,我们往往只要关注后面的赋值表达式即可推断出变量的类型,在这里也是如此。
    
    不过呢,自然要有运用条件:
    1、出于向后兼容的考虑,如果作用域中出现一个叫做var的类型,那么会优先使用var类型来声明该变量而不再推断表达式的类型。
    2、声明隐式变量的同时需要初始化。而初始化需要一个表达式,且该表达式不能是单独的对象或者集合初始化器,但可以是包含对象或者集合初始化器的new表达式。如var x;和var x = {1 ,2 ,3};都是错误的。
    3、表达式的编译时类型不能为空类型(null)。如var x = null;就是错误的。
    4、如果隐式变量的声明中包含多个声明符,那么这些声明符必须具备同样的编译时类型。
    
    此外,for语句中的初始化部分以及using语句的资源声明中,都可以使用隐式变量。同样的,使用foreach迭代也可以利用隐式变量:
    int[] numbers = {1, 2, 3, 4};
    foreach(var n in numbers) Console.WriteLine(n);
   
    
二、扩展方法
    通过在方法的第一个参数中用this关键字修饰,即可声明一个扩展方法。但是,扩展方法必须声明于静态类中。
    namespace TestSpace{
        public static class Extensions{
            public static int ToInt32(this string s){
                return int.Parse(s);
            }
        }
    }
    这些扩展方法享有普通静态方法等同的功能。此外,只要引入了扩展方法,我们就可以通过普通的实例方法调用语法来调用它们。
    
    那么,我们现在就介绍如何引入扩展方法。
    相信大家对于using很熟悉了吧。using System;相当于引入了System命名空间中的所有类型。不过呢,using在这里的意义可不单单是引入了类型,还引入了属于该命名空间下的扩展方法。扩展方法被引入之后,扩展方法中的第一个参数(即被this修饰的参数)的类型便多了这么一个附加方法。并且该附加方法的优先级比该类型的同名实例方法要低。比如要使用上例中的扩展方法,先引入:using TestSpace;然后可以这样调用:
    string str = "1234";
    int re = str.ToInt32();
    //上式等价于int re = Extentions.ToInt32(s);
    
    经过这两个例子,我们可以看出:
    1、扩展方法必须声明在静态类中(这样也能保证自身是静态方法)。
    2、扩展方法声明时至少有一个参数,即要附加的实例类型,并且要用this来修饰。
    3、扩展方法在作为附加方法调用时必然成为实例方法。
    4、扩展方法在作为附加方法调用时的优先级比同名的实例方法要低。即编译器默认调用实例类型的原有方法,如果找不到泽调用扩展方法。    
       
       
三、Lambda表达式
    通过C# 2.0带来的匿名方法,我们已经发现了简化代码的一种方法。但是,这样的做法仍然有些冗长且具有强制性,所以C# 3.0中又引入了Lambda表达式。
    
    Lambda表达式写成参数列表形式,后面紧跟“=>”符号,再跟上一个表达式或者代码块。只要看一下后面给出的例子,Lambda表达式的写法就很容易掌握,所以这里不再给出完整的Lambda表达式定义式。
    并且,Lambda表达式中的参数可以是显式,也可以是隐式的。显式参数列表中参数类型是显式指定的,而隐式参数列表中的参数类型会根据上下文而定(特别地,当Lambda表达式转换为兼容委托时,由这个委托提供参数的具体类型)。
    此外,如果参数列表中只有一个隐式的参数,那么圆括号可以省略,如:
    ( param ) => expression
    可简化为
    param => expression
    
    那么,我们来看一下例子:
    (int x) => x + 1    //显式参数列表
    x => x + 1        //隐式参数列表,由于只有一个隐式的参数,所以没有圆括号
    () => Console.WriteLine("Hello")        //无参数
    (int x, int y) => x + y    //两个显式参数
    x => { return x + 1; }    /*带有代码块的隐式Lambda表达式。但该功能目前尚不被PDC 2005 Technology Preview所支持,所以下文中将不再讨论该功能的运用。*/
    相信大家看完这些例子以后就知道怎么使用Lambda表达式了吧(注意:这里说的只是表达式,仅是语句的一部分,相信细心的同志们已经发现例子中没有结尾分号了吧)。
    然后我们看一下将Lambda表达式融入语句中的示例,同时给出用匿名方法实现的等效做法来作对比(其中利用Func泛型委托):
    //sample 1
    Func<int, int> add = x => x + 1;    //隐式表达式
    //等价于
    Func<int, int> add =
        delegate(int x){
            return x + 1;
        };
    
    //sample 2
    Func<string, bool> filter = s => s == "123";    //隐式表达式
    //等价于
    Func<string, bool> filter =
        delegate(string s){
            return s == "123";
        };
    由此可以看出,Lambda表达式简化了的确不少。更重要的是,它可以将一段匿名方法简化为一个表达式,这样就为了嵌套使用提供了保障,我在后面的其它部分中也会涉及到嵌套调用。
    
    
四、表达式树
    表达式树并不是PDC 2005标准的一部分,这里将简单阐述。
    表达式树最根本的运用就是System.Query.Expression<D>类型(有的材料上是System.Expressions.Expression<T>)。它为Lambda表达式提供了一个内存中高效的数据表示形式。
    对于下面这个Lambda表达式:
    Func<int, int> f = x => x + 1;
    这是代码(委托)形式。而
    Expression<Func<int, int>> e = x => x + 1;
    就是一个对表达式树的引用。
    
    Lambda表达式可以直接执行,如int re = f(1);
    但是表达式树的引用不可以,即int re = e(1);是错误的。
    
    那么如何运用表达式树呢?
    表达式树可以看作是对表达式委托的结构体现。看看下面这个例子(转),估计就比较有些理解了:
    Expression<Func<int, boo>> filter = n => n < 5;
    BinaryExpression body = (BinaryExpression)filter.Body;
    ParameterExpression left = (ParameterExpression)body.Left;
    ConstantExpression right = (ConstantExpression)body.Right;
    Console.WriteLine("{0} {1} {2}", left.Name, body.NodeType, right.Value);
    表达式树的这种数据形式在后面将要谈到的数据查询方面是一个很好的辅助工具。   
       
       
五、对象和集合初始化器
    使用对象初始化表达式可以方便地初始化一个或者多个对象的属性或者域。
    初始化表达式由“{”和“}”进行封闭,同时内部的成员初始化器用“,”进行分隔。成员初始化器包括成员名称、“=”和初始化的值。
    比如,对于类Dog:
    public class Dog{
        private string name;
        private int age;
        public string Name{
            get{
                return name;
            }
            set{
                name=value;
            }
        }
        public int Age{
            get{
                return age;
            }
            set{
                age=value;
            }
        }
    }
    这样初始化:
    Dog dog = new Dog{
        Name = "Toddy", Age = 5
    };
    等价于
    Dog dog = new Dog();
    dog.Name = "Toddy";
    dog.Age = 5;
    当然,嵌套的初始化也是允许的。
    
    同样,集合初始化器使得集合可以像数组那样初始化:
    List<int> list = new List<int>{1, 2, 3, 4};
    就这么简单……
    
    
六、匿名类型
    基于我们往往注重一个类型的结构而不是名称这个事实,C# 3.0引入了匿名类型。
    隐式局部变量由编译器自动推测变量的类型,而匿名类型是让编译器自动推测初始化表达式的类型。
    如第5节中的Dog类,我们其实也可以利用匿名类型来进行初始化:
    var dog = new {Name = "Toddy", Age = 5};
    我们可以看到,上例中省略了类型名。
    有了匿名类型,我们无需了解这个类型叫做什么名字,只要符合这个结构即可。
    
    
七、隐式数组
    从前面的种种特性中可以看到,利用编译器根据上下文推导来识别是C# 3.0的很多更新的基础,本节的隐式数组也是如此。
    隐式数组会自动根据数组内容来识别数组类型。
    我们以前这样写:
    int[] arr = new int[]{1,2,3};
    现在可以这样:
    var arr = new[] {1,2,3};        //一定要有[]操作符,参见隐式局部变量的注意部分
    
    但是必须要注意:
    1、数组中的内容均可以隐性转换为某一类型。
    2、最终类型不可为null。       
             
 
八、查询表达式
    个人认为,C# 3.0这些新特性的引入,不单单是为了在某种程度上简化平时的代码开发量。更重要的是,它们为查询表达式的方方面面都提供了良好的基础。
    
    查询表达式能够在语言中使用类似SQL或者XPath/XQuery的语法进行数据查询。
    
    查询表达式以from子句开始,以select或者group子句结束。大致组成顺序是(可选):from子句、where子句、orderby子句、select子句和where子句以及into子句。
    
    看一个例子:
    string[] Weekdays = {
        "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"
    };
    IEnumerable<string> enum_day =
        from day in Weekdays
        where day[0] == 'T'
        orderby day
        select day.ToUpper();
    foreach(var s in enum_day){
        Console.WriteLine(s);
    }
    你将会得到:
    THURSDAY
    TUESDAY
    
    如何?Amazing!
    
    那么,这样的语句为什么能够编译呢?
    其实,编译器在编译的时候进行了转换。上例中的enum_day的定义部分可转换为:
    IEnumerable<string> enum_day = Weekdays
        .Where(day => day[0] == 'T')
        .OrderBy(day => day)
        .Select(day => day.ToUpper());
    这样就清楚了吧(“括号里面的表达式什么?”不会吧,复习一下Lambda表达式吧)。
    
    当然了,像SQL这样的语句进行嵌套、组合等可以产生很多有趣的查询方法,C# 3.0中的查询方式虽然仅是简化版的SQL语法,不过经过嵌套、组合等方法当然可以形成复杂的表达式,这就靠大家今后巩固了。

附:
《C# 3.0 Specification》
http://download.microsoft.com/download/9/5/0/9503e33e-fde6-4aed-b5d0-ffe749822f1b/csharp%203.0%20specification.doc


阅读全文(2127) | 评论:1 | 复制链接

C# 2.0 特性介绍(2006-4-23 1:35:00)

注意:C# 2.0大纲根据网络资料收集整理而成,并在各个部分加以个人的理解以及运用。本文中的内容并不能够达到完整的层次,但是覆盖最基本的要求,并且往往可以通过本文的内容举一反三使得自己提升到更高的层次。如果有不足之处,恳请指点。

    C# 2.0需要支持.NET Framework 2.0的编译器,对于MS来说就是VS 2005,开源社区的SharpDevelop2也是不错的选择,但是Borland最新的C# Builder 2006暂不支持.NET 2.0。在这里列举这些特性的目的就是让各位已经开始使用VS 2005等开发工具的朋友可以在短时间熟悉新语言环境,运用新的语言特性,高效率完成任务。
   
    C# 2.0引入了很多语言扩展,最重要的就是泛型(Generics)、匿名方法(Anonymous Methods)、迭代器(Iterators)和不完全类型(Partial Types)。
    1. 泛型允许类、结构、接口、委托和方法通过它们所存贮和操作的数据的类型来参数化。泛型是很有用的,因为它提供了更为强大的编译期间类型检查,需要更少的数据类型之间的显式转换,并且减少了对装箱操作的需要和运行时的类型检查。
    2. 匿名方法允许在需要委托值时能够以“内联(in-line)”的方式书写代码块。匿名方法与Lisp语言中的拉姆达函数(lambda functions)类似。
    3. 迭代器是能够增量地计算和产生一系列值得方法。迭代器使得一个类能够很容易地解释foreach语句将如何迭代他的每一个元素。
    4. 不完全类型允许类、结构和接口被分成多个小块儿并存贮在不同的源文件中使其容易开发和维护。另外,不完全类型可以分离机器产生的代码和用户书写的部分,这使得用工具来加强产生的代码变得容易。
   
    不过呢,虽然具有这些改进(有的需要从.NET内部进行革新),但是在源代码级别将保持高度的兼容性,所以完全可以放心遗留代码可以在新的环境中通过编译(不过不能保证能否良好运行)。
   
    下面,我将分别阐述这4种新特性。


一、泛型
    在Ada实现泛型、C++实现Template之后,Java在编译器层次实现泛型,C#也在.NET底部提供泛型支持。可以预见,语言动态化是一种趋势,泛型也将为C#的更加灵活提供支持与动力。
   
    为什么要利用泛型呢?曾经我们一直都在设想拥有一个通用数组,可以存放任意类型的东西。在以前,我们可以用object[]或者ArrayList来实现。但是这样的话,我们在取出里面的元素时,都需要进行貌似无所谓的类型错误检查,如下例:
    ArrayList list = new ArrayList();
    myClass cls1 = new myClass();
    list.Add(cls1);
    myClass cls2 = (myClass)list[0];        //要进行类型转换
    而当具体类型为int等值类型时就更不妙了,存入需要进行装箱,取出还要拆箱,这样不仅需要在装箱、拆箱耗费时间来进行内存分配,还要进行类型错误检查。
    此外,这种方法还不能强制数据的种类。比如,上例中,如果取出时
    string str = (string)list[0];
    这样在编译的时候不会出现任何问题,直到运行之后才会出现InvalidCastException异常。这对于日常的编程调试是十分不利的。而泛型可以杜绝这种问题的出现。
   
    那么我们现在看一下一个典型的泛型类(List是ArrayList的等效泛型类)的构成:
    public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IList, ICollection, IEnumerable{
        public void Add (T item){...}
        public T this [int index] { get{...} set{...} }
        ...
    }
    这里只举出一个Add方法和一个索引属性,不过足够说明问题。
    那么,我们用List<int>初始化之后,List<T>中的所有类型参数T都被实际的类型名int所替换。相当于变成:
    public class List<int> : IList<int>, ICollection<int>, IEnumerable<int>, IList, ICollection, IEnumerable{
        public void Add (intitem){...}
        public int this [int index] { get{...} set{...} }
        ...
    }
    这样定义一个泛型类List<T>,就可以使用近乎于无数个类似的构造类型(每个构造类型都是相互独立的)。比如,List<int>在相关部分只处理int,而List<string>则专门处理string类型。不会有object转换,也更不会有装箱、拆箱,无效的类型转换也会在编译时检测到。
    List<int> lis t= new List<int>();
    list.Add(123);
    int a = list[0];        //无需拆箱等多余操作
    myClass cls = list[0];    //错误,类型不匹配
   
    现在需要讲一下替代T的实际类型的概念,实际类型其实要求非常低,只要是真实存在的符合T的约束条件(约束在后面会讲到)的可用类型即可。在符合约束条件的前提下,如int,Control,甚至是List<int>,int[][]等均不成问题。所以,泛型带来的便利今后将深刻地体验到。
   
    一个泛型类可以有多个类型参数,比如
    public class GenericSample<T1, T2, T3, T4>{...}
    在使用构造类型时,自然要全部给与适当的实际类型。比如GenericSample<int, string, long, char>等。当然,系统不在乎你是否赋予同样的实际类型。即使用GenericSample<int, int, int, int>,只要符合你的需要,自然是合理有效的。
   
    前面已经讲过,构造类型就是一个独立的类型了。所以在使用它的时候只要完整运用即可。比如List<int> list=new List<int>();中List<int>就作为一个整体来看待,所以构造类型的基本运用应该就比较清楚了。相信这一点是需要大家明确的,不然闹出像“List()<int>”这样的笑话是挺麻烦的。
   
    也许在某些情况下,我们并不关心整个类的改变类型重用,而是认为一个方法即可满足要求,这时候我们就可以使用泛型方法。
    比如下例中,ConCat函数将两个变量分别调用ToString()的返回值进行连接,然后返回连接结果:
    public string ConCat<T1, T2>(T1 var1, T2 var2){
        return var1.ToString() + var2.ToString();
    }
    这样,就实现了ConCat对于任何类型的复用。
   
    同样的,我们也可以按照这样的思路实现泛型委托。
    相信大家在编写事件会发现,参数表往往是(object sender, EventArgs e)之类的形式,而e的类型有时是EventArgs,有时是MouseEventArgs等等。那么,我们在编写委托的时候就需要变更一个类型就编写一个新的委托,但是这样的意义并不大。
    .NET 2.0中提供了一个EventHandler泛型委托,定义如下:
    public delegate void EventHandler<TEventArgs> (Object sender, TEventArgs e) where TEventArgs : EventArgs
    大家暂时不要去理会where子句的意义,其它部分的内容相信很快就能了解了吧。
    利用这个EventHandler,就能够通过实现不同的构造类型来完成新的事件的定义,而无需重新编写一个委托。(不过呢,MS在写.NET 2.0的Forms时,并没有采用这种方式,仍然按照以前的方法写了一堆委托,估计是出于兼容遗留代码的考虑)
    下例就是利用EventHandler实现MouseClick的方法:
    public delegate void MouseEventHandler (Object sender, MouseEventArgs e);
    public event MouseEventHandler MouseClick;
    //上为.NET中的实现方法
    //下为利用泛型委托的实现方法
    public event EventHandler<MouseEventArgs> MouseClick;
   
    此外,我们往往要对泛型类型参数进行约束,因为并非所有的类型都能够合法完成本泛型类的操作。在这里,我们就需要利用where上下文关键字对类型参数进行约束。
    比如,如果设计一个函数
    SetControlEnabled<TControl>(TControl ctrl, bool enabled){
        ctrl.Enabled = enabled;
    }
    用来设定控件的Enabled属性。这时,并不是所有类型都可以代替TControl,当string代入时,显然会由于缺少Enabled属性而出错。
    所以,只要约束TControl的实际类型必须继承自Control即可:
    SetControlEnabled<TControl>(TControl ctrl, bool enabled) where TControl : Control {
        ctrl.Enabled = enabled;
    }
    编译器会对不符合要求的类型代入发出错误警告。
    这种约束不仅可以运用在泛型方法中,也可以在泛型类中运用,自然还可以在泛型委托中运用(如前面提到的EventHandler泛型委托)。
    另外,每个类型参数都可以设定各自的约束(只设定部分参数自然也可以),不过如果要设定多个类型参数,那么每个类型参数都需要对应一个where子句。
   
    以下,将对泛型的具体运用作一下基本的补充:
    1.由于泛型类也具有普通类的效力,所以在共同作用域中,不要定义重名的类(GSample和GSample<T>以及GSample<T, K>均属于重名现象)。而对于方法、委托而言,有可能通过代入实际类型而导致签名一致的方法均无效,如(T1 param1, T2 param2)和(int a, int b)有可能签名相同(当然,这里的“一致”、“相同”建立在非泛型的合法方法重载基本理论之上,譬如ref与out视为等价签名)。方法、委托的重名规则同样适用于继承用的基类和基接口,如I<T>和I<K>重名。
    2.类型参数不能作为基类或者基接口,如class GSample<T> : T {}就是错误的。
    3.类的类型参数不得与类或者成员的名称相同。
    4.类的类型参数也有其作用域,覆盖类体、约束子句、基类部分以及嵌套类型,但不覆盖到其派生类。在嵌套类型中的作用效果等同于变量在类与嵌套类中的作用效果。
    5.前面已经提到,一个泛型类的构造类型是相互独立的,所以泛型类各个构造类型的静态组成部分(如静态构造函数、静态成员等)均是相互独立的。
    6.应用程序的入口点函数不能处于一个泛型类中。


二、匿名方法
    其实匿名方法说起来也很简单,就是一种不需要名称的内联委托。
   
    以前,我们要写下面这些代码:
    public class Sample : Form{
        Button btn;
        public Sample{
            btn = new Button();
            btn.Click += new EventHandler(btn_Click);
        }
        private void btn_Click(object sender, EventArgs e){
            this.Close():
        }
    }
    在上例中,为了一个简单的this.Close;而新建一个带有两个参数的方法,而当我们运用匿名方法之后,会变得很简单:
    public class Sample : Form{
        Button btn;
        public Sample{
            btn = new Button();
            btn.Click += delegate{
                this.Close();
            };
        }
    }
    这样我们可以看到,匿名方法适用于这种不想浪费一个单独方法来完成一项简单操作的情形。同样我们也可以感觉到,匿名方法其实用起来也很简单。
   
    一个匿名方法包括关键字delegate以及一个可选的参数表。在上例中,我已经将参数表省略了,然而如果遇到方法中需要访问参数的情形,则必须提供完整的参数表:
    btn.Click += delegate(object sender, EventArgs e){
        ((Button)sender).Enabled = false;
    };
   
    当然,方法设计也可以如此:
    delegate void DelegateSample();
    static void Main(){
        DelegateSample d = delegate{
            Console.WriteLine("Hello");
        }
        d();
        Console.WriteLine("Again...");
        d();
    }
    这样,就无需单独新建一个方法却可以反复重用这一部分代码。
   
    怎样设计才是一个合法的匿名方法呢?需要满足两点:
    1.参数表匹配。
    2.匿名方法的返回值可以显示转换为目标委托的返回值,或者均无返回值。
   
    此外,C# 2.0还提供方法的自动转换(方法组转换),比如最开始的例子中:
    btn.Click += new EventHandler(btn_Click);
    其实可以直接写为:
    btn.Click += btn_Click;
    编译器会自动进行判断。
   
   
三、迭代器
    我想大家都应该用过foreach吧。其实foreach就是一种迭代的调用。在这里,我们将讨论一个基本迭代的组成。
   
    为了实现枚举,一个可枚举的(enumerable)的集合要有一个无参的、返回枚举器(enumerator)的GetEnumerator方法。
    而要获得枚举器,我们就需要一个迭代器。所谓迭代器,就是一个可以产生有序的值序列的语句块。这里,我们就需要引入yield上下文关键字。其中,yield return产生本次迭代的值,yield break指示本次迭代完成。
    创建迭代器一般有两种方法:
    1.对可枚举接口(即IEnumerable和IEnumerable<T>)实现GetEnumerator方法,然后对该类直接使用foreach迭代。
    2.创建一个返回可枚举接口(即IEnumerable和IEnumerable<T>)的函数,对该函数的返回值使用foreach迭代。
    通过这两种方式,访问数据集合。
    以下,将分别举例阐述:
   
    1.实现GetEnumerator方法。
    public class ListSample<T> : IEnumrable<T>{
        private T[] items;
        ...//some methods to modify items
        public IEnumerator<T> GetEnumerator(){
            for(int i=0; i<items.Length; i++){
                yield return items[i];
            }
        }
    }
    这样便完成了迭代器,然后便可以通过foreach语句迭代该类返回items中的所有元素。
    ListSample<int> list = new ListSample<int>();
    foreach(int i in list){
        Console.WriteLine(i);
    }
   
    2.创建迭代函数
    public class ListSample<T>{
        private T[] items;
        ...//some methods to modify items
        public IEnumerable<T> Iterator(){
            for(int i=0; i<items.Length; i++){
                yield return items[i];
            }
        }
    }
    这样,我们就可以通过Iterator的返回值进行迭代。
    ListSample<int> list = new ListSample<int>();
    foreach(int i in list.Iterator()){
        Console.WriteLine(i);
    }
   
    第一种方法的优点就是设计方便简单,直接实现IEnumerable或者IEnumerable<T>即可;第二种方法的自由度大,可以自行为函数设定参数表,来自定义迭代的范围等等。
   
    然后说一下基本注意事项吧:
    1.yield语句不能用于匿名方法,yield语句不能用于finally块中,yield return不能用于try...catch块中。
    2.迭代函数的参数中不能出现ref或者out。
   
    其它的内容,诸如MoveNext()等方法的具体实现以及异常处理块中的情况讨论等问题由于较为复杂且平时不常用到,这里暂不讨论。有兴趣可以参考微软相关文献。


四、不完全类型
    C#允许在一个代码文件中存放多个类,但这样往往不便于类的管理,所以一向是提倡一个文件中只存放一个类。不过呢,随着类规模的不断膨胀,一个文件中存放一个类也有些显得臃肿,或者是在某个角度上不便于代码的组织。
    因此,C# 2.0中引入了不完全类型的概念,即启用了新的修饰符partial。借助该修饰符,我们可以在多个文件中存放一个类,每个文件只包含该类的某些功能。当然,它并不单单可以修饰class,还可以修饰struct和interface。
    正如VS 2005对Windows窗体代码的组织那样,界面部分代码被单独存放在一个文件中,其它代码存在另外一个文件中,这样正符合平时大多只关注非界面代码的现实。
   
    由于这一部分涉及的内容比较少,就无需展开来讲,下面将阐述一下注意事项:
    1.partial必须直接位于class,struct,interface之前。
    2.不可用partial扩展已经编译的类型。
    3.运用partial之后,对类型的任何编辑(成员、特性、修饰符、基类、约束等)将合并。
    4.运用partial的类型要一块进行编译。
    5.运用partial的类型必须处于同一命名空间之中。


补充:Nullable
    .NET 2.0中提供了Nullable泛型结构,Nullable类。
    C# 2.0也从语法上支持Nullable。
   
    这里只讨论Nullable泛型结构,因为Nullable类是Nullable泛型结构的补充。
   
    我们知道,引用类型可以被赋予空值(null),而值类型不可。但是利用Nullable之后,值类型也可以这么做。运用Nullable很简单,只需在声明类型后面加上一个问号即可。如
    private int? sample;
    然后在操作时只需要注意两个属性:一个是HasValue,用于指示当前是否存在值;一个是Value,如果存在值则返回值,否则抛出异常。
    C#也有替代的方案,那就是??运算符。
    Console.WriteLine("sample is {0}", sample ?? "NULL");
    ??运算符检查左边的Nullable是否含有值,如果则返回值,否则返回运算符右边的操作数的值。


阅读全文(2784) | 评论:1 | 复制链接

论Borland转让IDE业务(2006-2-14 14:07:00)

   太平洋时间2006年二月8日凌晨1时,Borland发表声明:Borland以1亿美元收购测试工具厂商Segue,同时为了促进Borland向应用软件周期管理(ALM:Application Lifecycle Management)厂商转型,Borland决定出售自己曾经赖以生存的IDE(Integrated Develop Environment)业务。(原文见

   这次出售的IDE产品线包括Delphi, C++Builder, C#Builder, JBuilder (和Peloton), InterBase, JDataStore, nDataStore, Kylix以及Turbo系列产品及工具。

 

一、我记忆中的Borland

   Borland曾经是专门研发IDE的厂商。在当年,IDE市场是一个竞争非常激烈的大环境。在其中,有Borland, HP, Intel, IBM, Microsoft, SUN, Symantec等一群重型厂商。在打拼多年之后,能够还留有较多实力应该是只剩下Borland和Microsoft了。而后者并不是以IDE为盈利点。只有Borland是利用自己的IDE研发实力证明了自己的存在价值。

   Borland的IDE产品线享有盛誉,从最老的Turbo系列(Turbo C和Turbo Pascal更是经典)到目前的Borland Developer Studio等都深受广大开发者的喜爱。

   Turbo C, Turbo Pascal到现在一直都有很多人依然在研究(尽管已经早已超出了产品生命周期);C++ Builder见证了Borland研发C++的实力;Delphi更是将一个即将死亡的语言(Pascal)挽救了回来并让其成为了主流语言,并打败了RAD鼻祖级软件、Microsoft力推的VisualBasic,同时曾力挽狂澜挽救了风雨飘摇的Borland,并在今年迎来了她的10岁生日;Delphi向Linux移植的产品Kylix(尽管这可能是一个错误的决定)很快成为了Linux上的首选开发工具;JBuilder更是值得所有Borlander自豪的产品,它在竞争激烈的Java开发市场中将SUN的Workshop打得无回手之力,将Microsoft的Visual J++和Symantec的Visual Cafe淘汰出局,也让IBM的VisualAge for Java从此一蹶不振且贡献给开源(成为后来的Eclipse的主要部分)……

   Borland的开发团队被誉为是天才的团队。这其中有很多家喻户晓的人物,也有很多人因为Borland而实现了个人价值。

   Borland是一个重视开发者的团队,总给人平易近人的感觉,没有高调的言论,没有无聊的敷衍……

   但这真正成为了记忆。

 

二、为什么要放弃IDE业务

   由于Borland一直坚守单IDE业务,所以业务增长并不是很快。加上多元化也是一个跨国公司业务增长的需求,所以Borland也一直在尝试转型。

   1998年的时候,Borland曾更名为Inspire,准备转型成为中间件厂商,相信这个大家应该都有印象,但很快就失败了,2年后又把名字改了回来。2000年的时候,Borland还打算过要并购Corel,当然最后是不了了之了。当然,Borland也曾经打算在桌面办公软件方面跟Microsoft较真,不过受制了自身规模,这项计划当然不可行。

   此外,Borland的产品线最近出现了一些问题:C# Builder叫好不叫卖,C++ Builder X的发布反响并不好,Delphi向.NET的迁移并不是很顺利(尽管兼容路线很值得人称赞),JBuilder面对昔日对手重装上阵的Eclipse似乎没有什么能力再次保持较大差距……

   似乎来自开源社区的压力是最大的。开源社区的IDE做得越来越出色,因此愿意掏腰包来购买IDE的人也越来越少了。Borland的IDE业务在财政上甚至出现了近千万美元的大幅度亏损。

   本来很赚钱的IDE业务,却让已经很累的Borland的开发队伍没有得到多少眷顾。大家真的都很累了。(见李维的Blog

   当然原因不止这些,我认为还有一个主要原因:对于日益复杂的软件开发工程,单单IDE业务是不能满足人们的需要的。为了在新一轮的竞争中生存,必须整合其它元素。比如IBM巨资收购了Rational,Microsoft投入大量人力物力研发VSTS,Borland收购TogetherSoft等。

   因此,Borland需要转型为ALM厂商。

   这样说的话,Borland像以前一样,扩展自身业务不就行了,干吗要放弃自己的IDE业务呢?因为限于自身的规格和目前的经济实力,Borland目前不能同时推进两种重型业务。

 

三、Borland的IDE业务花落谁家

   Borland目前仍在寻觅IDE产品线的买主,真正的销售过程也不会在短期内完成(毕竟Borland的IDE业务具有相当的规模)。

   大家都有很多猜测,有需要整合IDE来完善自身产品线的,在这其中最大胆的猜测就是Google。设想如果Google买下了这系列享有盛誉并曾经独占鳌头的IDE开发技术,那么Google和Microsoft的平衡天平就要倾倒了,那么……

   Borland也要等买主明确了以后才能有一个明确的发展计划。

 

四、展望

   相信ALM的发展会给Borland带来新的活力。

   最后,借用David I的结束语:“Go Borland. Go New Company. Go Developers!!!”(David I的相关文章见


阅读全文(2589) | 评论:1 | 复制链接

串行事务调度初窥(2005-7-12 18:55:00)

“事务(Transaction)”应该可以认定是一个数据库术语了,但在本文中它可被宽泛地认为是一个可供调度的信息集合,被交通员调度的车辆便是生活中的例子。
“串行”是对目前大多数人进行开发的串行机而言的,即在每一个时间步只执行一个指令,操作一个数据。也就是说,本文不涉及并行算法。
本文旨在介绍事务调度的最基本的几种管理方案。此方案没经过特殊优化,模型很可能效率不高,但足以描述几种调度思想。
下面,我们将从简易的队列开始模拟事务的调度。


一.队列
队列(Queue)是基本的算法结构之一,采用FIFO(First In First Out)机制,体现传统的“先到者先得”的思想,要求事务执行的优先级以事务加入的时间为线索。
调度模块通常有两部分组成:监听和事务处理。以后各节均以此为模版讲解。
首先,应当阐明本例中可用到的队列方法:Empty()返回一布尔值,标明队列是否为空;Front()返回队列的队头事务;EnQueue(Code c)将事务t插入队尾;DeQueue()将队头删除。
然后,假定当前用于缓存事务的队列为TransQ,那么监听代码可写作:
public void Listen(){
    Code c;//c用于储存属于某个事务的指令
    while(AppRun){
        //AppRun是一个布尔变量,用于终止程序

        //得到要加入的事务
        AcceptNewTransaction(out c);

        //加入事务
        TransQ.EnQueue(c);
    }
}
再写出处理代码:
public void Handle(){
    while(AppRun){
        if(!TransQ.Empty()){

            //执行队头事务
            Execute(TransQ.Front());

            //出队
            TransQ.DeQueue();
        }

    }
}
注意:监听与处理是在两个不同线程中运行。(显而易见 :-P)
这种队列的运行效果便如公路收费站单车道一样,先到的车辆先通过收费站。


二.优先队列
在做饭时,要煮米,但却发现还没淘米,因此若按上一节所示的队列进行便行不通了:首先想到煮米,所以按时间顺序煮米就会发生在淘米前。这时,我们需要一个自定义优先级的组合——优先队列。
先介绍一下优先队列的方法:Empty()返回一个布尔值,指示队列是否为空;Insert(Code c,ThreadPriority p)将t插入到优先极为p的位置;Pop()用于返回并删除当前优先级最高的事务。
假定优先队列为TransP,可写出监听代码:
public void Listen(){
    Code c;
    ThreadPriority p;
    while(AppRun){
        AcceptNewTransaction(out c,out p);

        //加入事务
        TransP.Insert(c,p);
    }
}
再写出处理代码:
public void Handle(){
    //处理事务
    while(AppRun)
        if(!TransP.Empty())
            Execute(TransP.Pop());
}
其中,优先队列优先级的设定方案可以说在一定程度上影响着整体效率。例如,采用序号便牌,那么,序号的维护便成了重点,若每次执行Pop()时,都要将所有序号重新计算,就是个很低效的方法。
注意:没有人可以容忍自己的事务长时间在调度区挂起,应该使最终的调度时间相对于事务运行时间忽略不计。


三.多队列
平时坐车时可以发现,多车道的公路上常标有快车道、重型车道等特殊车道用来满足不同需求。
事务调度时也可根据需求进行这种变化,如等级划分。我们现在将以等级为依据实现多队列。
所谓“等级”,实际上就是优先级,只不过是队列级的优先级。比如说,管理员的事务优先级总比用户的要高,所以分开处理便可。
我们现在将第一节的代码变形,以达到代码复用的目的:
public void Listen(ref Queue q){
    Code c;
    while(AppRun){
        AcceptNewTransaction(out c);
        q.EnQueue(c);
    }
}

public void Handle(ref Queue q){
    while(AppRun){
        if(!q.Empty()){
            Execute(q.Front());
            q.DeQueue();
        }
    }
}
再写出包装函数:
public void Listen1(){
    Listen(ref q[1]);
}
…//类似的
public void Listenn(){
    Listen(ref q[n]);
}


public void Handle1(){
    Handle(ref q[1]);
}
…//类似的
public void Handlen(){
    Handle(ref q[n]);
}
再写一个线程启动函数:
public void NewThread(ThreadStart ts1,ThreadStart ts2,ThreadPriority p){
    Thread t1=new Thread(ts1);
    Thread t2=new Thread(ts2);
    t1.Priority=p;
    t2.Priority=p;
    t1.Start();
    t2.Start();
}
这样,便可以利用线程的优先级作用为队列的等级。
也就是说,调度模块运行后,只需调用以下代码:
NewThread(new ThreadStart(Listen1),new ThreadStart(Handle1),p1);

NewThread(new ThreadStart(Listenn),new ThreadStart(Handlen),pn);
手动或动态加入队列,即可实现多队列。
注意:多线程应用时要注意细节,犹应考虑资源的利用。


四.多优先队列
同样,优先队列也可以分等级,来完成更为复杂的运用。
实现多优先队列请参见二、三节内容。


五.逻辑调度
使是上,数据库中的调度应当是最复杂的了,因为这其中牵扯狠要命的环节——锁机制(时间戳协议等与此相仿)。我们依靠锁来完成对共享数据的并发操作。本节将分块对此调度的串行基本思想进行初步探讨。
1.为什么要调度
数据库要同时承载多到成百上千的连接,甚至是负责管理成百上千个指向同一块资源的请求。N多个请求怎能同时串行处理,所以,坦然面对生活,想想如何调度吧。再者,自己发出的消息一直没有回音一定使你很无奈吧。所以,采用调度模块,及时对死亡的请求进行回滚才是明智之举。
2.为什么要利用锁
既然同一块数据可能被多个线程操作,那么同时进行的指令极有可能发生冲突。而基于锁的协议就可以有效的杜绝这种冲突的发生。此外,锁还可以用于权限的实行。
调度模块在执行事务时会检测该事务所申请的锁型及操作与目标数据所标配的锁型的兼容性,必要时进行回滚。
3.有哪些功能型锁
锁的类型经过4个方面去定义:可读性及其共享性、可写性及其共享性。注:只有在可读或可写的条件下,相应的共享性才有意义。如共享可读排他可写、排他可读不可写便是两种锁型。
但是,我们只讨论两种最常用的锁:共享锁(共享可读不可写)和排他锁(共享可读排他可写)。对于他俩而言,一个数据可只拥有任意个共享锁或只拥有一个排他锁。
4.锁的实例
假令共享锁为S1,排他锁为S2,事务T1、T2同时对某一数据项进行操作,则
    T1    T2
1    grant S1    
2        grant S1
3    read    
4        read
可以正常运行,而
    T1    T2
1    grant S1    
2        grant S1
3    read    
4    grant S2    
5    write    
在运行到4时出现错误。因为T1申请排他锁,但T2还持有共享锁,所以申请被拒绝。
当然,
    T
1    grant S1
2    write
运行到2时出错,因为S1是共享锁。
5.逻辑顺序
下面,我们将讨论insert,delete,read和write的冲突问题。
insert:
若数据不存在,则应先执行insert,然后执行其它命令;反之,在数据存在时执行insert将出错。
delete:
read,write在delete后执行将出错;若数据不存在而执行delete将出错。
read,write:
数据不存在时执行read和write将出错。
根据上述规则,我们可以轻易对下述事务的正确性做出判断。
    T
1    grant S2
2    write
3    delete
4    read
第4行出错,同样
    T
1    insert
2    read
3    delete
4    delete
第4行出错。
6.代码实现
本块代码将用队列实现,由于本文只是简要概述,所以代码中将不考虑原子性及回滚等内容。
Public void Listen(){
    Code c;
    while(AppRun){
        AcceptNewTransaction(out c);
        TransQ.EnQueue(c);
    }
}

public void Handle(){
    CodeLockInfo cl;//事务当前锁型
    DataLockInfo dl;//数据当前锁型
    Code c;
    while(AppRun){
        if(!TransQ.Empty()){
            c=TransQ.Front();
            TransQ.DeQueue();
            cl=GetCodeLockInfo(c);
            dl=GetDataLockInfo(c.Target);
                //Code.Target指向待操作数据
            ConfirmCompatibility(cl,dl);
                //若锁不相容则抛出异常,CI
            ConfirmCode(c.CodeName,Exists(c.Target));
                /*根据逻辑顺序,判断当前指令与目标数据是否相容,不相容则抛出异常,CII*/

            //全部无异常,则运行
            Execute(c);
        }
    }
}
其中,CI、CII语句处要设置错误处理,若发生错误,可根据事务日志进行回滚等恢复操作。
当然,可以根据需求对代码进行变形,使用优先队列、多队列、多优先队列等。但要注意:若采用多监听、多处理模式,势必增加单个事务的运行时间,要求更多的系统资源,但却提高了调度模块的负载能力,减少了等待时间。所以,同时运行几个模块应当慎重考虑。
7.有必要的补充
刚才的代码在实际运行时还会遇到一个很严重的问题。我们知道,每个事务都应当相对独立,所以当N个事务同时操作一个数据项时,只要有一个事务使用了write指令,那么所有事务均因此会出现冲突。
为了更为简单地分析这个问题,我们只考虑read和write语句。
假设T1和T2同时操作某一数据项,C1和C2分别为T1和T2的语句。
I.C1=read,C2=read:
    不发生冲突。
II.C1=read,C2=write:
    若C1在C2前运行,则不冲突;否则C1返回的是C2设定的值,并非T1原先想要的(因为事务相互独立)。
III.C1=write,C2=write:
    铁定冲突,最终值由较后运行的决定。
当然,所有分析均在锁兼容的情况下进行,否则申请共享锁却调用write在本块没有意义。
那么,I是最放心的,III是最痛心的,看看实例吧。
    T1    T2
1    read    
2    write    
3        read
4        write
5    read    
在5处冲突,可这样解决:
    T1    T2
1    read    
2    write    
3    read    
4        read
5        write
当然,这是最极端的情况。因为解决方案中T1和T2相当于单独连续运行。不过,单独连续运行绝对可以避免此类冲突,但并发的优势却不再存在。所以,只有到实在无法调换时再采用此法。


经过一番探讨,相信大家已经清楚事务调度的意义和启蒙性的有关实现方法了吧。数据库及其思想是十分诱人的,而其中的调度思想的重要性也完全可以与I/O设计思想比肩。愿此文可以对大家有所启示。

阅读全文(2871) | 评论:2 | 复制链接

虚拟机文化(2005-5-17 21:42:00)

注:文中的“虚拟机”指软件开发框架及现实的virtual machines。但所述特性并非适用于所有虚拟机。


现今的虚拟机技术日臻成熟,虚拟机的应用也越来越广泛。为什么呢?因为人们在总体上利大于弊的判断基础上逐渐接受了虚拟机。然而,人们往往去探讨如何更好地使用虚拟机来架构自己的程序,却没有认真研究软件开发中虚拟机思想的运用。
在这里,我将借剖析虚拟机的优势来引出部分有关思想。


一.跨平台
“跨平台执行”应当是每一个桌面用户的愿望:用着Windows的简便,想着MacOS的绚丽;WinXP够优秀,却难以运行早期Windows的非兼容程序及LE格式程序。于是乎,我们用VirtualPC和VMware去构建一个虚拟的操作系统,在新系统中执行我们的目的命令来完成相应操作;我们用WinXP的兼容性模式调整工具来运行早期Windows的非兼容程序;Lindows(现已更名为Linspire)也提供了折中的方案。
但是,这样只是委曲求全的方法,最根本的方法是:拿起电话,把那个程序员吼起床,然后告诉他“给我准备好XX平台上的程序”。当然,这显得不可理喻:一方面,没人有心情听你这样啰嚎;另一方面,构建跨平台的程序所需的技术又怎是一朝一夕完成的,应从基础库基准上按多平台需要进行开发。
“跨平台开发”绝对是每一个桌面开发者的美梦:有谁不想让自己的一份代码在多个平台跑起来?
wxWindows框架让纯编译的C++可以一码多平台,Delphi2005也可让代码分别编译为Windows和Linux的本地代码,这怎么不令人心动!.net Framework的开源实现mono以及JavaVM更令代码的多平台运行达到一个新的层次,让代码与操作系统相分离,并提供成千上万个类来提升开发生产力。
多平台是为了什么?简而言之,即提高市场适应力,更是提高竞争力。金山与IBM携手推出WPSOfficeStorm也可说明这一点。OpenOffice已成为昨日黄花,微软势必让其从Linux上绝迹,而Storm的推出压制了几乎所有Linux版中文字处理套件,市场反响很强烈。
这是从商业角度考虑的结果,如果从版本进化角度考虑,可以参见[二]。


二.无函数不定性
在NT技术刚被人们接受的时候,使用最多的Win32系统还是Win9x。32位函数如RegCreateKeyEx等均有安全选项(参数形式),然而Win9x并没有对这些参数予以实现,所以传入null即可获得最高权限;而Win2000等NT系统均对安全选项有严格要求,null代表最低权限。而刚入NT门的程序员往往由于工作惯性,懒得定义安全选项,导致程序在NT系统上运行失败。此为参数空实现。虚拟机框架中所有有关参数均按框架要求传递,一经测试即可发现参数问题。
Windows中提供的Win32API中同义异名函数颇多,如创建文件命令有lcreat,CreateFile和CreateFileEx。三者参数并不一致。lcreat是16位遗留函数,只有文件名和基本文件属性两个参数;CreateFile和CreateFileEx均为32位函数且参数均比lcreat参数详细,同时CreateFileEx比CreateFile更多了设置参数。然而要创建一个简单文本文档,3者皆可等质量完成任务。于是乎,很多程序员要选择简易的lcreat函数了。但是,没过多久,上司要求生成的文本文档应具有某些高级属性,所以lcreat可以下台了。没过几年,64位的创建文件函数出现了。为了实现64位功能,又得再放弃32位的CreateFile和CreateFileEx。如此波折,程序的类似修改令程序员叫苦不迭。此为函数同义多名。虚拟机框架将此类函数均指定为一个函数并设参数为可选或多个不同功能的函数(创建函数、属性设定函数等)。从此不必再为多个同义函数的选取而烦恼。
Win9x与NT的技术差异,还有基础编码字符集的选择差异。Win9x以ASCII为底,而NT以Unicode为底。但Win32函数中同时存在ASCII传参和Unicode传参的同种函数,在系统中以后缀A和W相区分,既FuncA和FuncW位Func的两种编码传参的各自的版本。2个函数,只有字符编码不同,若重新实现,则违背了代码复用原则,所以采用以下方法
//Win9x
void FuncA(…){
    …//procedure
}
void FuncW(…){
    UnicodeToAscii(…);
    FuncA(…);
}


//NT based on
void FuncA(…){
    AsciiToUnicode(…);
    FuncW(…);
}
void FuncW(…){
    …//procedure
}
然而在Win9x中调用FuncW以及在NT中调用FuncA必然会耗资源在字符编码转换上。此为函数功能赘余。对此,.net Framework以可拓展为出发点全面采用Unicode。因此只会有一种传参类型,程序员毋需考虑A/W问题。
.net Framework和JavaVM等虚拟机、框架让编码放在框架中而非操作系统中,编码时只要考虑框架变更即可,无需为操作系统API的诸多问题烦恼。因此,虚拟机让我们向函数的不定型说再见。当然,这也显示出虚拟机上构建的简易性。

<思想列举>
[一][二]中所提到的均为虚拟机为架构于其上的软件提供更强的环境适应性。当然,环境适应性很大程度上与开发简易性成正比。我们可将其引入到实际开发中。
首先要提的是对A/W及其类似问题的常见方案(很老掉牙的)。以获得Windows各版本注册表大小为例,由于注册表各文件(System.dat,User.dat等)在各系统中分布、组成不同,所以可分别编译(预处理)
#if defined(Win9x)
    #define GetRegSize RegV4Size
#elif defined(WinNT)
    #define GetRegSize RegNTSize
#else
    #define GetRegSize RegV5Size
#endif
获一次编译
int GetRegSize{
    //获取OS版本
    char Os[]=GetCurrentOS;
    //获得Registry大小
    switch(Os){
        case “V4”:
            return RegV4Size;
        case “NT”:
            return RegNTSize;
        default:
            return RegV5Size;
    }//switch
}//GetRegSize
两种方法如何舍取,当然由大家自己定夺。
其次,应当为特定功能的函数增加可选参数以便扩展、减少拓展成本,便不至于出现lcreat,CreateFile,CreateFileEx及类似的事例了。以创建文件为例(CreateFile):
//C++(无可选参数)
int CreateFile(char* FileName,int FileAttr);//此为最标准的形式
//C++(不支持重载)
int CreateFile(char* FileName,int FileAttr,int Opt[]);
//C++(支持重载)
int CreateFile(char* FileName,int FileAttr,int Opt1);
int CreateFile(char* FileName,int FileAttr,int Opt1,int Opt2);

//C#除了重载法之外,还可
int CreateFile(string FIleName,int FileAttr,params int[] Opt){…}


三.共享组件
VC++6.0风靡之时,MFC的运用可谓是到了一个高峰。使用VC++6.0开发的程序俯拾即是,所以几乎每个Windows系统中均由mfc40.dll等MFC组件。
VisualBasic6.0的高校RAD功能真正成熟起来,所以也占去大片市场。Windows中装有msvbvm60.dll等VB Runtime组件也不足为奇。
这些虚拟机涵盖了基本操作,所以“一OS一VM”即可。也因此,所有用VB写的程序均只用同一份虚拟机,从而大大减小了空间需求。一份虚拟机固然不大,但要让每一个程序都制作这样一个库,试想一个系统中成百上千个程序,那便有成百上千个库,磁盘空间占用可想而知。
.net Framework令多语言共享一个平台,这更向前了一步。它充分利用组件复用思想,单System根下的类便已成百上千,功能之强大不言而喻。基于.net的程序甚至无需对.net进行二次开发即可完成所有任务。框架松紧相结合地定义了这个系统的总体设计思路。此外,虚拟机还可利用自身已拓展的特性,轻易让多个组件完成进程内外通信,让复用设计的实施高效简易。

<思想列举>
[三]强调组件的复用。
对于以文件为组织单位的组件来说,放在共享目录中最易维护。Windows自身已提供了3个共享目录:{Windows},{System(32)},{Program Files}\Common Files。当然,新建一个类似MySharedComponents的文件夹也不失为一个好方法。我们可以将多套软件或程序共用的组件放在共享目录中。例如皮肤库,只需不断更新这一个库,即可令所有有关兼容工程同步更新。
对于框架这种设计思路,我们更应当将它引入到大中型规模的软件开发中。对内一般采用类继承方式,抽象出基类,以便于继承时所涉及的类的更新。对外一般采用接口,是松散度升高,以便于软件的拓展以及插件的实现。例如,制作一个数据库引擎:引擎内部先设计基本I/O方案,然后继承该方案编写该格式与其它数据库格式的转换方案;引擎对外提供I/O调用接口,外部程序可以依此动态调用不同版本该引擎的接口即可实现数据文件版本转换功能。


四.增强的API库
开发人员之所以采用库,是因为库中提供较多且较强的类来完成一些系统API不易完成的任务。例如MFC和.net库即可说明这一点。
所谓的“增强”有以下几点。
1.函数增强:
[二]已详细分析。
2.函数统一归类:
Win32API函数往往只在kernel32.dll,user32.dll,gdi32.dll等少数DLL文件中,而Win32API共有上百个函数。可想而知,一个DLL中囊括了大批的函数。所以直接获得某个函数类的所有函数(如有关I/O的)并不容易。而虚拟机可轻易做到(如.net中的System.IO和VB VM的FileSystemObject),因为虚拟机已经将所有框架API编类(如.net)或打包(如J2SE)。因此,我们不用再为了查函数名去翻SDK了。
3.基于对象操作:
用Win32API开发,那真是“句柄”满天飞,还得是不是得校验返回的错误代码或用GetLastError查错。有了虚拟机API,可以完全以对象为作用点,并可以使用结构化错误处理法。面向对象,开发更轻松。

<思想列举>
[四]中讲到了使用虚拟机API开发程序与使用传统API进行开发的不同。这主要是体现在面向对象这一特性的加强。
在过去的某段时间,很多程序员,尤其是使用诸如VB6及更早版本进行开发的,十分偏爱模块(Module)而不重视类(Class),或者总是制造巨型类(类中的函数满满的)。他们认为编写调用(或类内部调用)代码使用这种方法所需代码量较小。毕竟是用类时的确少麻烦,要求先声明类变量,考虑早期或晚期绑定,初始化,然后才能调用其中的函数,最后还要销毁,如(VB6):
Dim cls As New clsSample
Cls.Proc    ‘invoke a procedure
Set cls=Nothing
而用公共模块只需:
Proc    ‘invoke only
但是,程序在初始化是会加载所有模块,类在初始化时会加载所有子内容。可想而知,滥用模块以及制造巨型类会造成加载负担和内存浪费。毕竟从书架抽出一本书再放回容易,而把整架子书全拿出后再全部放回可不是一件易事。此外,在一个模块中找到一个函数也不易,维护上就出现漏洞。
所以我们应尽可能细地把关联功能的函数放在一个类中,再将关联功能的类放到一个命名空间中。这样,检索容易,虽然代码量有所提高,但类的特性以及上几段中所提的会令你日后的开发受益无穷。
不单是这样,我们在操作时若还再依赖句柄之类的东西进行标识就显得不明智了。
例如标记一个文件。若是用文件句柄,那么它只能起到一个志向的作用,我们还需制作一个类来操作这个句柄;我们换个思路想的话,制作针对文件的类,类中包含一个私有变量来记录文件句柄,这样声明类变量的同时,也可完成句柄的获取(这包含重构思想),且直接用变量进行调用(如var.proc(…))更符合OOP思想(这是桌面开发应当考虑的)。


五.宏观可控性
操作系统有“设置选项”、“控制面板”等调节系统的方案,以维护直接基于OS的程序的运行。但毕竟大多数设置是用于丰富用户体验的,而调节程序的设定往往繁琐且不可预知。而虚拟机的设定往往偏向于开发者的使用需要并允许编写代码进行修改。例如,.net Framework的.net Framework Configuration以及J2SE的J2SE Console便是如此。
所谓“可控性”,包括“监视”与“控制”。这两点很是显然:大部分可执行代码以及全部伪码均要经过虚拟机处理,所以“监视”是必然,从中调整代码即“控制”也自然毫无疑问。
所谓“宏观”,即通观基于该平台运行的一切操作。
因此,有了“宏观可控性”,可以轻易监控并调试线程、堆栈、CPU时间片等事项。开发效率也因此而飞升。

<思想列举>
[五]诠释了精细选项及后台支持对于应用的影响。
用户总希望知道程序在做什么并相让程序按自己的意愿来执行。所以尽自己所能去充实程序行为,丰富用户体验是有必要的。这样做会让程序更有吸引力。这也是Office等套件市场效应好的原因之一。这同样也是不少软件提供Console之类程序的原因。
强仅的软件表现并不单是表面的程序行为,大型任务的完成往往要依赖后台组件的运行。因为后台组件一般无界面显示,可节约资源提高速度;再者,后台代码可以单独出来有针对性地进行优化。所以我们应当将可后台执行的前台代码尽可能地重构到后台,单独进行代码、进程维护。


六.程序行为可评估
[五]重于使用人员的因素而此节重于机器的执行因素(以平台性虚拟机为对象)。
1.错误评估:
再Win32上,如果程序出错且无相应错误处理,那么只能以运行失败收尾,但没人会给你余地去释放堆栈。而在.net和J2SE上,如果真的山穷水尽程序终止,那么程序会令GC清理有关资源,因为GC有能力评估程序所有堆栈。
2.安全评估:
每一块代码都会由平台执行,所以如果程序试图使用非安全代码(如指针),那么虚拟机会牢牢盯上它直到访问违规或非安全代码结束。这样,虚拟机有能力在程序运行前、运行中及运行后保证系统的稳定。同样,如果程序的访问受权限约束,虚拟机也会做出相应处理以确保系统资源的安全。
3.回调评估:
回调(Callback)让程序异步访问等功能的轻松实现成为可能。然而,以往的回调机制总令人使用时惴惴不安。首先要提的就是回调指针,这个自然不言而喻;再者,繁琐的参数设定和不同的功能函数总让人头疼。现在好了,.net使用委托(Delegate)来完成回调操作。这样,一般情况下,只是用委托对象即可,即使使用指针,托管堆也会保证delegate指针的安全;再者,delegate令回调操作与平常函数操作基本无异,简单明了。

<思想列举>
[六]中涉及了程序在运行时的行为处理方法。
首先,我们绝对要详细地为代码套上结构化错误处理代码,在运行速度有所保证的前提下尽量让错误发生的所有方向均不超出控制范围。最后,还要在程序开始时编写GPF(General Protection Fault,一般保护性错误)处理代码,防止由不可知因素(如没捕捉到的异常、函数库的缺陷等)导致程序失败,尽可能地释放资源以使危害降到最小。
再者,我们在写程序是一定要时刻考虑代码的执行权限及可分配给用户的权限,限定程序运行规则(如安全模式下不可使用打印机)和用户操作权限,使程序在使用弹性与安全性中取一个适当的点。


当然,上述的所有“思想列举”仅是所有可衍生出的思想中的一小部分。谨以此希望大家日后多思多实践多借鉴,不断探寻良好的开发方法论来提升开发效率。

阅读全文(3281) | 评论:0 | 复制链接

Humane Programming——人性化编程(2005-3-26 9:53:00)

Humane Programming——人性化编程
    尽管现今各国均投入大量人力物力去研发所谓的真正的“人工智能”,但大家每日所对的计算机始终是个死物——一个让它做它才做的死物,甚至懂得罢工偷懒的死物。
    因此,支持库(Java VM,VB Runtime,.net Framework等)总是机械地吞吐着指令集中的动作。即使是那高智能的优化器,也只不过是将某句或多句汇编代码与优化库案例作对照,然后拟出相应的优化代码罢了(当然也并非如此简单)。
    那么,对于下面的案例,优化器又能做什么呢?
    小A为某网站的用户管理系统制作支持模块,具体来说,就是去写程序来管理用户信息。小A采用顺序文本文档为载体,亲自编写了针对顺序文本的繁琐的基本操作模块。但是运行之后,查找匹配速度极慢,I/O超出负荷,缓存文本大小呈几何级数增长。总而言之,该产品很不尽人意。
    然后,小A找来了优化器套件,优化器针对程序中所调用的函数库进行了部分更改后,带宽占用下来了、CPU占用率也下来了,但是最要命的速度与空间占用问题却得不到解决。这真是个耗时又不便利的豆腐渣工程。
    其实,我们早就发现,如果小A采用MySQL等专业数据库引擎作为后台支持,不仅有关代码的编写容易、维护方便,而且也不会出现速度滞后、空间利用差的问题。可是,优化器却不能为小A指出一条明路。
    尽管上面这个案例很愚蠢,但它也从一个方面向我们阐述:人对软件过程又绝对的作用。这就是Humane Programming的宗旨,要让每一句有效代码都人性化,让每一个开发阶段人性化,我们应当进行人性化编程。
    以下是我对Humane Programming的诠释。


一.不要总为简短的语句沾沾自喜
    似乎程序设计的进步总是伴随着编码量的减少。VB.net2005为我们带来了My命名空间,借助My.Forms可以像在VB6中一样不用发愁窗体间的交流,借助My.Computer可以轻易访问本机资源……这无疑是令人振奋的,VB.net2003中部分臃肿的代码得以瘦身,清爽干净。
    但并非所有简化方案都令人满意。
    VB6中提供IIf函数,可以将赋值型IF…ELSE…END IF语句在一行内完成,例如
    IF a>b THEN
        max=a
    ELSE
        max=b
    END IF
    就等价于
    max=IIf(a>b,a,b)
    乍看之下,不禁令人心动,但是再看下面这段代码
    IF i>=0 AND i<=2 THEN    ‘i的类型为Integer
        re=a(i)    ‘a是以0为下界,2为上界的一维数组
    ELSE
        re=a(0)
    END IF
    转换之后变为
    re=IIf(i>=0 AND i<=2,a(i),a(0))
    好像没有问题吧。不,别忘了IIf是一个函数,VB Runtime会先将函数的所有参数压入堆栈然后才跳入函数代码区。也就是说,IIf的调用会先令系统取得所有参数。那么,举个例子,当i=-1时,IIf的三个参数变为False,a(-1)和a(0)。然而a(-1)是无意义的,这时会发生下标越界的运行时错误。哦,这该死的逻辑错误,编译器在检查时是不会发现它的,运行时无奈的错误提示才会给你自以为聪明的想法泼上冷水。你会发现转换之前的语句是多么的美好。
    还有,复杂的语言也总会给你无限的空间来组装自己的代码,使它更精简。例如(C++)
    int a[100];
    for(int i=0;i<100;i++){
        if(i%2==0){
            *(a+i)=0;
        }else{
            *(a+i)=1;
        }//if
    }//for
    等价于
    int a[100];
    for(int i=0;i<100;i+=2)
        *(a+i)=0;
    for(int i=1;i<100;i+=2)
        *(a+i)=1;
    上两段代码均可简化为
    int a[100];
    for(int i=0;i<100;i++)
        *(a+i+1-2*(i%2))=(i+1)%2;
    但是可想而知,不可能一眼明白简化后的代码的意图,这也很不易维护,需要一定的注释,而这又将花费不少精力,然而却没有开发和运行效率上的提升,何苦而为之?
    此外,从事多语言交互开发的人有时也会有以下的困惑([游智超2005]):
    func(a<b,c>(d))
    上面这句代码应当如何理解?
    在C#2.0中func只有一个参数,a十一个泛型类,d是变量而b和c是替代泛型类型的真实类型或类;在Java中,func有两个参数,第一个是a<b的布尔值,第二个是c>(d)的布尔值。
    所以,如果只熟悉一门语言也就罢了,而经常使用多语言进行软件开发的人一定要重视这类情况,或许多写几行代码反而一目了然。
    由此可得,机器固定的运行模式会让聪明的人犯错。


二.以人为本
    程序的最终使用权归用户所有,所以,产品的好坏往往由用户作出最终的判断。而用户做出的第一判断往往是对GUI等界面的感受。
    如果界面能响应人的需求并且考虑到人的弱点,那么它就是以人为本的。[Raskin2004]
    人性化编程要求产品与用户有人性化的交流。
    依然举个例子,小B是数据库开发高手,他现在要为家庭主妇们设计一款家用财会管理软件,用户小C女士试用了这款产品。
    小C现在想要在2005年1月1日的记录下新建一条支出信息:“购买鸡蛋1斤——3元”。小C单击“添加支出项目”命令之后弹出一个输入框,提示信息是“请为您新建的字段命名”。“字段?我好像没要使用一个什么叫‘字段’的东西吧。” 小C被小B不考虑用户适应能力的产品搞糊涂了,只好怏怏地取消了操作。
    “字段”是我们最常见的数据库用语,但是我们在设计程序界面交互时不应当把用户定位成自己,不应当把自己的感受当作用户的感受,而应当认真分析使用人群的运用能力以及工作领域,否则自己的作品只会以失败告终。
此外,我们还应当考虑到用户的所有可能操作。
    比方说,有一个输入框供用户输入数字。那么,我们应当保证从输入框传到处理模块的数据是合法的数字形式,因此:如果输入框为空,则传入0;如果有非法字符,则进行剔除;如果数位超过要求,则应作相应的舍取;如果小数点前后出现无数字的情况,则应考虑是否补0;……
    总而言之,设计界面时,我们应当做到以人为本。我们是为用户而不是为机器装配软件。


三.不要被机器所禁锢
    在这里,我首先要重申文章开头所揭示的事项:不要过分依靠优化器。当然,到了“听天命”的地步,也只能用优化器了。
    我再举一个优化器造成困扰的粒子(C++)。
    void sample(){
        int pw;            //用于储存密码的变量
        pw=GetPassword;    //获取相关密码
        ……            //进行一些需密码的简单无停顿(即无对话框等)的操作(CodeI)
        pw=0;            //置pw为0(CodeII)
        ……            //进行无需密码的复杂或有停顿的操作(CodeIII)
    }//sample
    我们不想让CodeI部分所用的pw的值被用户所知,但是我们是无法奈何调试器的。而幸运的事,大多数用户并不会使用复杂的调试器,最多是使用内存查看器。而人一般来不及在CodeI短暂运行期中抓拍内存,只有在CodeIII部分才有可能。所以,只要利用CodeII将pw设为真实密码以外的数,便可以令拍下的内存中的pw值为错误值(此例中设为0)。
    然而,有些优化器认为CodeII之后没有再用到pw变量,所以CodeII没有用处,于是便在优化过程中删去了CodeII部分来完成它自以为是的提速。结果可想而知。
    我们在研究一下优化器以外的情况。
高级语言到汇编代码的转换是单向的,机器无法将汇编代码自动转换为高级语言代码,除非中间有人的参与。[Kaspersky2004]
    为什么会这样呢?就是因为机器不会思考,而人会!这应当是我们所反复念及的。机器无法将简易的汇编代码变成具有分支等具体结构的高级语言,而人经过分析对照,便可以尝试进行几种转换方案,直至可行性被验证为止。
    我们的聊天机器人也是如此,您可以尝试将一个关键词放入不同的句子中与机器人聊天。看到结果了吧,一样的回复信息!
    因此,我们不应当过分依赖所谓的智能工具。在编程过程中,我们应当学会运用工具来提高效率和质量,但是离开了工具就活不下去便是不对的了。人才是生产力中的决定性因素!


四.寻求针对自身的解决方案
    现今,OOP、XUL、UML等都炒得很火,大批有关资料在市面上涌现出来。大批人蜂拥而至。不过,你的确需要吗?
    比如说,.net的兴起使得大批的应用迁移到.net上——这当然是一个趋势,但是像Adobe,Macromedia和金山这样的软件大厂为什么没有将核心产品转移呢?因为还有大比例的Win32用户,这一点是必须考虑的!
    再举例说,如果要设计一个整数加减处理模块,可是你却要同时加上小数处理模块,理由是为了方便今后的拓展。然而,这样却拖延了开发进度,减慢了模块处理速度。况且需求是不断变更的,说不定在最坏的情况下,研发部放弃了这款产品,那时还怎么会有机会去拓展?
    就自身而言,我是一个自由开发者,不参与团体开发,所以完全有理由将团体开发方法束之高阁,同时也省去了不少学习时间。
    同样,如果linux 2.6的新特性对你很有帮助,那么升级是有必要的;否则,2.4已经十分稳定耐用,何必为了“新”而去放弃早已熟习的环境,去花费时间适应新的环境呢?


五.为自己的软件设计好未来
    如同父母总努力为孩子营造良好氛围去铺砌孩子们的未来,我们也应当为自己的软件的未来做好准备。
    [四]中提到了一个为软件未来着想却不可行的案例,案例失败的原因是对需求评估的不合理。我们提倡在需求计划稳定的前提下,为软件的发展拟定下1个版本甚至下几个版本的计划(用户暂不需要或较难实现的计划可归在下几个版本中进行)。这样,自己便有充足的时间去研究提前拟定的计划的可行性及易用性,并能在实现之前及时做出更改。此外,还可以在研究计划的同时产生有它引发的其他想法或思路。于是,一环套一环,推动产品的不断进步。
    当然,诸如及时添加注释、产品类说明等有关方便软件维护这类老掉牙的事项更不必赘述。


    好了,我已将我所提的人性化编程诠释完了。人性化编程不但是一个方法论,它应当贯穿软件的设计、开发、维护及用户的最终使用等多个阶段。谨以此文愿大家今后能高效进行更多体现人这一要素的软件开发项目。




参考资料

[游智超      2005]探讨和比较Java和C#的泛型机制(Generics)
[Raskin      2004]人本界面——交互式系统设计
[Kaspersky   2004]黑客反汇编揭秘
[段钢        2003]加密与解密(第二版)
[Sommerville 2000]Software Engineering(Sixth Edition)
http://msdn.microsoft.com
http://kernel.org

阅读全文(3081) | 评论:0 | 复制链接