跳至主要内容

【转】strcpy如何实现

什么是契约――Eiffel的观点

    假设你现在正在面试,主考不紧不慢地给出下一道题目:"请用C语言写一个类似strcpy的函数。要考虑可能发生的异常情况。" 你会怎么做呢?很明显,对方不是在考察你的编程能力,因为复制字符串实在太容易了。对方是在考察你的编程风格(习惯),或者说,要看看你编码的质量。

    下面是多种可能的做法:

    void
    string_copy1(char* dest, const char* source)
    {
      assert(dest != NULL); /* 使用断言 */
      assert(source != NULL);
     
      while (*source != '\0') {
        *dest = *source;
        ++dest;
        ++source;
      }

      *dest = '\0';
    }

    void
    string_copy2(char* dest, const char* source)
    {
      if (dest != NULL && source != NULL) {  /* 对错误消极静默 */
        while (*source != '\0') {
          *dest = *source;
          ++dest;
          ++source;
        }

        *dest = '\0';
      }
    }

    int
    string_copy3(char* dest, const char* source)
    {
      if (dest != NULL && source != NULL) {
        while (*source != '\0') {
          *dest = *source;
          ++dest;
          ++source;
        }

        *dest = '\0';
        return SUCCESS;  /* 返回表示正确的值 */
      }                         
      else {
       errno = E_INVALIDARG;  /* 设定错误号 */
       return FAILED;         /*  返回表示错误的值 */
      }
    }
   
    // C++
    void
    string_copy4(char* dest, const char* source)
    {
       if (dest == NULL || source == NULL)
         throw Invalid_Argument_Error();  /*  抛出异常 */

       while (*source != '\0') {
         *dest = *source;
         ++dest;
         ++source;
       }

       *dest = '\0';
    }

    如果你是主考,不知道面对这样四份答卷,你的评分如何?当然,你可以心里揣着一个"标准答案","顺我者昌,逆我者亡"。但是如果以认真的态度面对这四份答卷,我想很多人都会难以抉择。

    因为这里涉及到了软件开发中的一个带有本质性的难题――错误处理。

    历来错误处理一直是软件开发者所面临的最大困难之一。Bjarne Stroustrup在谈到其原因时说道,能够探察错误的一方不知道如何处理错误,知道如何处理错误的一方没有能力探察错误,而直接采用防御性代码来解决,会使得程序的正常结构被打乱,从而带来更多的错误。这种困境是非常难以应对的――费心耗力而未必有回报。因此,更多的人采用鸵鸟战术,对可能发生的错误视而不见,任其自然。

    C++、Java和其他语言对错误处理问题的回答是异常机制。这种机制在正常的程序执行流之外开辟了专门的信道,专门用来在不同程序模块之间报告错误,解决上述错误探察与处理策略分散的矛盾。然而,有了异常处理机制后,开发者开始有一种倾向,就是使用异常来处理所有的错误。我曾经就这个问题在comp.lang.c++.moderated上展开讨论,结果是发现有相当多的人,包括Boost开发组里的很多专家,都认为异常是错误处理的通用解决方案。

    对此我不能赞同。并且我认为滥用异常比不用异常的危害更大。

    The Pragmatic Programmer是一本在国外程序员中间颇为流行的书,其中在讲到错误处理时,有一句箴言:
   
    "只在真正异常的状况下使用异常。"

    书中举了一个例子,如果你需要当前目录下的一个名叫"app.dat"的文件,而这个文件不存在,那么这不叫异常状况,这是你应该预料到的、并且显式处理的情况。而如果你要到Windows目录下寻找user.dat文件,却没找到,那才叫做异常状况――因为每一个正常运行的Windows系统都应该有这个文件。

    我非常赞成书中的那句忠告,可是究竟什么是"真正异常"的状况?书中的这个例子显然只是一个颇具感性的、寓言似的故事,具有所有寓言的共同特点――读起来觉得豁然开朗,收获很大,实际上帮不了你什么忙。这种例子对于我们的实际开发,仍然提供不了真正的帮助。

    究竟应该如何看待错误?怎样才能最好地错误处理?

    说实话,在这两个问题上,我们所见到的大部分语言都没有给出很好的回答。C秉承一贯风格,把所有的东西推给开发者考虑;Ada发明了异常,但是又为异常所累(知道阿里亚纳5火箭的处女航为什么失败吗?);C++企图将Ada的异常机制融合进自己的体系中,结果异常成了C++中最难以处理的东西;Java和C#显然都没有耐心重新考虑错误处理这桩事,而只是简单的将C++的异常机制完善化了事。

    与上述这些语言不同,Eiffel从一开始就把错误处理放在核心的位置上予以考虑,并以"契约"思想为核心,建立了整个的错误处理思想体系。在我了解的语言里,Eiffel是对这个问题思考最为深刻一个,因此,Eiffel历来享有"高质量系统开发语言"的声誉。(事实上,Bertrand Meyer很不喜欢别人称Eiffel为"编程语言",他反复强调,Eiffel是一个Software Development Framework。不过本文只涉及语言特性,所以姑且称Eiffel语言。)

    Eiffel把软件错误产生的本质归结与"契约"的破坏。Eiffel认为,一个软件能够正常运作,正确完成任务,是需要一系列条件的。这些条件包括客观运行环境良好,操作者操作正确,软件内部功能正确等等。因此,软件的正确运行,是环境、操作者与软件本身三方面合作的结果。相应的,系统的错误,也是由于三者中有一方没有正确履行自己的职责而导致的。细化到软件内部,每个软件都是由若干不同的模块组成的,软件的错误,是由于某些模块没有正确履行自己的职责。要彻底杜绝软件错误,只有分清各自模块的责任,并且建立机制,敦促各模块正确履行自己的责任,然后才有可能做到Bug-free。(鉴于系统中错综复杂的关系,以及开发者认识能力的局限,我认为真正无错误的系统是不可能的。但是当前一般软件系统中的质量问题远远比应有的严重。)

    如何保证各方恪守职责呢?Eiffel引入了契约(Contract)这个概念。这里的契约与我们通常所说的商业契约很相似,有以下几个特点:

1. 契约关系的双方是平等的,对整个bussiness的顺利进行负有共同责任,没有哪一方可以只享有权利而不承担义务。
2. 契约关系经常是相互的,权利和义务之间往往是互相捆绑在一起的;
3. 执行契约的义务在我,而核查契约的权力在人;
4. 我的义务保障的是你的利益,而你的义务保障的是我的利益;
 
    将契约关系引入到软件开发领域,尤其是面向对象领域之后,在观念上给我们带来了几大冲击:

1. 一般的观点,在软件体系中,程序库和组件库被类比为server,而使用程序库、组件库的程序被视为client。根据这种C/S关系,我们往往对库程序和组件的质量提出很严苛的要求,强迫它们承担本不应该由它们来承担的责任,而过分纵容client一方,甚至要求库程序去处理明显由于client错误造成的困境。客观上导致程序库和组件库的设计和编写异常困难,而且质量隐患反而更多;同时client一方代码大多松散随意,质量低劣。这种情形,就好像在一个权责不清的企业里,必然会养一批尸位素餐的混混,苦一批任劳任怨,不计得失的老黄牛。引入契约观念之后,这种C/S关系被打破,大家都是平等的,你需要我正确提供服务,那么你必须满足我提出的条件,否则我没有义务"排除万难"地保证完成任务。

2. 一般认为在模块中检查错误状况并且上报,是模块本身的义务。而在契约体制下,对于契约的检查并非义务,实际上是在履行权利。一个义务,一个权利,差别极大。例如上面的代码:
    if (dest == NULL) { ... }
这就是义务,其要点在于,一旦条件不满足,我方(义务方)必须负责以合适手法处理这尴尬局面,或者返回错误值,或者抛出异常。而:
    assert(dest != NULL);
这是检查契约,履行权利。如果条件不满足,那么错误在对方而不在我,我可以立刻"撕毁合同",罢工了事,无需做任何多余动作。这无疑可以大大简化程序库和组件库的开发。

3. 契约所核查的,是"为保证正确性所必须满足的条件",因此,当契约被破坏时,只表明一件事:软件系统中有bug。其意义是说,某些条件在到达我这里时,必须已经确保为"真"。谁来确保?应该是系统中的其他模块在先期确保。如果在我这里发现契约没有被遵守,那么表明系统中其他模块没有正确履行自己的义务。就拿上面提到的"打开文件"的例子来说,如果有一个模块需要一个FILE*,而在契约检查中发现该指针为NULL,则意味着有一个模块没有履行其义务,即"检查文件是否存在,确保文件以正确模式打开,并且保证指针的正确性"。因此,当契约检查失败时,我们首先要知道这意味着程序员错误,而且要做的不是纠正契约核查方,而是纠正契约提供方。换句话说,当你发现:
     assert(dest != NULL);
报错时,你要做的不是去修改你的string_copy函数,而是要让任何代码在调用string_copy时确保dest指针不为空。


4. 我们以往对待"过程"或"函数"的理解是:完成某个计算任务的过程,这一看法只强调了其目标,没有强调其条件。在这种理解下,我们对于exception的理解非常模糊和宽泛:只要是无法完成这个计算过程,均可被视为异常,也不管是我自己的原因,还是其他人的原因(典型的权责不清)。正是因为这种模糊和宽泛,"究竟什么时候应该抛出异常"成为没有人能回答的问题。而引入契约之后,"过程"和"函数"被定义为:完成契约的过程。基于契约的相互性,如果这个契约的失败是因为其他模块未能履行契约,本过程只需报告,无需以任何其他方式做出反应。而真正的异常状况是"对方完全满足了契约,而我依然未能如约完成任务"的情形。这样以来,我们就给"异常"下了一个清晰、可行的定义。

5. 一般来说,在面向对象技术中,我们认为"接口"是唯一重要的东西,接口定义了组件,接口确定了系统,接口是面向对象中我们唯一需要关心的东西,接口不仅是必要的,而且是充分的。然而,契约观念提醒我们,仅仅有接口还不充分,仅仅通过接口还不足以传达足够的信息,为了正确使用接口,必须考虑契约。只有考虑契约,才可能实现面向对象的目标:可靠性、可扩展性和可复用性。反过来,"没有契约的复用根本就是瞎胡闹。(Bertrand Meyer语)"。

    由上述观点可以看出,虽然Eiffel所倡导的Design By Contract在表象上不过是系统化的断言(assertion)机制,然而在背后,确实是完全的思想革新。正如Ivar Jacoboson访华时对《程序员》杂志所说:"我认为Bertrand Meyer的方向――Design by Contract――是正确的方向,我们都会沿着他的足迹前进。我相信,大型厂商(微软、IBM,当然还有Rational)都不会对Bertrand Meyer的成就坐视不理。所有这些厂商都会在这个方向上有所行动。"(参见《程序员》2002年第11期,P22)。

 

续篇-契约思想的一个反面案例   myan(原作)   

刚刚发表了《什么是契约》一文,突然发现自己通篇都在写理论,没有实例来证明。所以赶快补充一个反面案例――C++ IOStream。说是反面,不是因为IOStream库设计得不精彩(恰恰相反,你很难找到比IOStream设计更为精彩的C++库了),而是想展示一下,在没有契约概念的思想体系里,组件设计将为权责不清的错误处理付出多大的代价。

 

作者Blog:http://blog.csdn.net/myan/

评论

此博客中的热门博文

【转】AMBA、AHB、APB总线简介

AMBA 简介 随着深亚微米工艺技术日益成熟,集成电路芯片的规模越来越大。数字IC从基于时序驱动的设计方法,发展到基于IP复用的设计方法,并在SOC设计中得到了广泛应用。在基于IP复用的SoC设计中,片上总线设计是最关键的问题。为此,业界出现了很多片上总线标准。其中,由ARM公司推出的AMBA片上总线受到了广大IP开发商和SoC系统集成者的青睐,已成为一种流行的工业标准片上结构。AMBA规范主要包括了AHB(Advanced High performance Bus)系统总线和APB(Advanced Peripheral Bus)外围总线。   AMBA 片上总线        AMBA 2.0 规范包括四个部分:AHB、ASB、APB和Test Methodology。AHB的相互连接采用了传统的带有主模块和从模块的共享总线,接口与互连功能分离,这对芯片上模块之间的互连具有重要意义。AMBA已不仅是一种总线,更是一种带有接口模块的互连体系。下面将简要介绍比较重要的AHB和APB总线。 基于 AMBA 的片上系统        一个典型的基于AMBA总线的系统框图如图3所示。        大多数挂在总线上的模块(包括处理器)只是单一属性的功能模块:主模块或者从模块。主模块是向从模块发出读写操作的模块,如CPU,DSP等;从模块是接受命令并做出反应的模块,如片上的RAM,AHB/APB 桥等。另外,还有一些模块同时具有两种属性,例如直接存储器存取(DMA)在被编程时是从模块,但在系统读传输数据时必须是主模块。如果总线上存在多个主模块,就需要仲裁器来决定如何控制各种主模块对总线的访问。虽然仲裁规范是AMBA总线规范中的一部分,但具体使用的算法由RTL设计工程师决定,其中两个最常用的算法是固定优先级算法和循环制算法。AHB总线上最多可以有16个主模块和任意多个从模块,如果主模块数目大于16,则需再加一层结构(具体参阅ARM公司推出的Multi-layer AHB规范)。APB 桥既是APB总线上唯一的主模块,也是AHB系统总线上的从模块。其主要功能是锁存来自AHB系统总线的地址、数据和控制信号,并提供二级译码以产生APB外围设备的选择信号,从而实现AHB协议到APB协议的转换。 ====================================

【转】GPIO编程模拟I2C入门

ARM编程:ARM普通GPIO口线模拟I2C  请教个问题: 因为需要很多EEPROM进行点对点控制,所以我现在要用ARM的GPIO模拟I2C,管脚方向我设 置的是向外的。我用网上的RW24C08的万能程序修改了一下,先进行两根线的模拟,SDA6, SCL6,但是读出来的数不对。我做了一个简单的实验,模拟SDA6,SCL6输出方波,在示波 器上看到正确方波,也就是说,我的输出控制是没问题的。 哪位大哥能指点一下,是否在接收时管脚方向要设为向内?(不过IOPIN不管什么方向都可 以读出当前状态值的阿) 附修改的RW24C08()程序: #define  SomeNOP() delay(300); /**/ /* *********************************  RW24C08   **************************************** */ /**/ /* ----------------------------------------------------------------------------- ---  调用方式:void I2CInit(void)   函数说明:私有函数,I2C专用 ------------------------------------------------------------------------------- -- */ void  I2CInit( void ) ... {  IO0CLR  =  SCL6;      // 初始状态关闭总线  SomeNOP();  // 延时   I2CStop();  // 确保初始化,此时数据线是高电平 }   /**/ /* ---------------------------------------------------------------------------- ----  调用方式:void I2CStart(void)   函数说明:私有函数,I2C专用 ------------------------------------------------------------------------------- -- */ void  I2CStart( void )

【转】cs8900网卡的移植至基于linux2.6内核的s3c2410平台

cs8900网卡的移植至基于linux2.6内核的s3c2410平台(转) 2008-03-11 20:58 硬件环境:SBC-2410X开发板(CPU:S3C2410X) 内核版本:2.6.11.1 运行环境:Debian2.6.8 交叉编译环境:gcc-3.3.4-glibc-2.3.3 第一部分 网卡CS8900A驱动程序的移植 一、从网上将Linux内核源代码下载到本机上,并将其解压: #tar jxf linux-2.6.11.1.tar.bz2 二、打开内核顶层目录中的Makefile文件,这个文件中需要修改的内容包括以下两个方面。 (1)指定目标平台。 移植前:         ARCH?= $(SUBARCH) 移植后: ARCH            :=arm (2)指定交叉编译器。 移植前: CROSS_COMPILE ?= 移植后: CROSS_COMPILE   :=/opt/crosstool/arm-s3c2410-linux-gnu/gcc-3.3.4-glibc-2.3.3/bin/arm-s3c2410-linux-gnu- 注:这里假设编译器就放在本机的那个目录下。 三、添加驱动程序源代码,这涉及到以下几个方面。(1)、从网上下载了cs8900.c和cs8900.h两个针对2.6.7的内核的驱动程序源代码,将其放在drivers/net/arm/目录下面。 #cp cs8900.c ./drivers/net/arm/ #cp cs8900.h ./drivers/net/arm/ 并在cs8900_probe()函数中,memset (&priv,0,sizeof (cs8900_t));函数之后添加如下两条语句: __raw_writel(0x2211d110,S3C2410_BWSCON); __raw_writel(0x1f7c,S3C2410_BANKCON3); 注:其原因在"第二部分"解释。 (2)、修改drivers/net/arm/目录下的Kconfig文件,在最后添加如下内容: Config ARM_CS8900    tristate "CS8900 support" depends on NET_ETHERNET && A