`
hqs7636
  • 浏览: 214871 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

D语言的陷阱

阅读更多
原文:http://colorful1982.blog.sohu.com/45473453.html

关注D语言已一月有余。最近又在翻看D语言规范,写些心得,以资纪念(本文代码采用C#命名规范)。

诚如D所介绍的那样,它是一门通用的系统和应用编程语言。俺最欣赏D能以原生语言的身份引入垃圾回收机制。不依赖于特定虚拟机的实现着实让俺兴奋了一阵。 垃圾回收是个古老话题,它的好处自不待言,N多语言都提供这种机制,但在原生语言中引入仍是凤毛麟角。听说C++0x标准正在准备引入垃圾回收机制,无疑D已经在这方面先行一步。

D借鉴了很多语言的长处,但在很大程度上保留了C/C++的观感。为了与C二进制兼容,采用了C99的数据类型;为了支持多种编程范式,沿袭了C++的模型。其中值得一提的是它的虚方法调用机制师从于Java。俺所说的是D在OOP上的理解。

现代编程语言基本都提供了OOP的编程机制,即封装,继承和多态。先声明一下,在这里我们讨论的主要是语言层面的OOP。设计模式提及的OOP是在编程语言提供的OO机制上的升华,是代码如何有效组织,与语言上的OO机制有很大不同。D语言采用单根+接口的继承机制。在多态上主要使用虚方法表和多接口来实现,而数据封装则主要通过它的attributes。

OK,下面我们先来看下D语言attributes语法层面上的小陷阱。

Attributes的定义如下:Attributes are a way to modify one or more declarations(D语言的attributes是用来修饰一个或多个声明的方式).

它通常形式如下:

attribute declaration; /* affects the declaration */
attribute: /* affects all declarations until the next } */
  declaration;
  declaration;
  ...
attribute /* affects all declarations in the block */
{
  declaration;
  declaration;
  ...
}
你可能会说,这不是已经解释的很清楚了吗?当然,对于1和3的声明方式,我们都很容易理解。但是第2种声明方式,我就犯迷糊了。我们不论在phobos还是tango库都可以找到大量的类似声明。

比如 fenv.d(为了方便观看,去除了注释):

/* 示例1 */
module std.c.fenv;
extern(C):
struct fenv_t
{
  version(Windows)
  {
    ushort status;
    ushort control;
    ...
  }
  ...
}
...
enum
{
  FE_INVALID = 0x01;
  ...
}
void feraiseexcept(int except);
...
再比如array.d。

/* 示例2 */
module std.array;
private import std.c.stdio;
class ArrayBoundsError: Error
{
   private:
     uint linum;
     char[] filename;
   public:
     this(...)
     {
       ...
     }
}
...
查阅源代码,这些都很容易理解。但是,这跟文档明显有出入。如果这不是语法陷阱,那么就是写文档的笔误了。

上面的是开胃小菜,真正的大餐来了,呵呵。

看一下下面这个示例。

/* 示例3 */
module sample;
import std.c.stdio;
int main(char[][] argv)
{
  TestClassA();
  return 0;
}
void TestClassA()
{
  A a = new A();
  printf("%*s",a.Method());/* 这里可以看出C和D处理字符串的区别 */
}
class A
{
  char[] Method(){return "Call member function Method() of class A.";}
}
函数TestClassA()会执行成功吗?答案是肯定的。因为在不带修饰符的情况下,D语言默认是public级别,不论对象是全局函数,结构还是类,成员函数。前面都好理解,但是连成员函数都默认是public,这就奇怪了。从OOP的角度来说,默认应该是保护级别的最大级别,尤其是在类中。在C++中,成员函数默认是private,这跟数据封装有关系。因为当程序员忘记修饰时,编译器会帮忙以免数据可以随意访问。当以后需求有变化时,再把它修正为public,这样对现存的客户程序都不会有兼容的问题。但是如果一旦把public修正为private时,麻烦就来了。继承的子类,客户程序等等都要在考虑之列。至于D为什么要把成员函数默认为public,俺不理解。另外俺认为良好的编程风格应该可以清晰表达代码的意图。D为了保持C/C++的观感,采取了上面的风格。俺不推荐。俺认为风格应该如下(以下所有的代码示例都会采用如下风格,并且除非采用C面向过程的结构化编程,不会再用到类似TestClassA()这种全局函数):

public class A
{
  public char[] Method(){return "Call member function Method() of class A.";}
}
下面再看一下这段代码示例。

/* 示例4 */
module sample;
import std.c.stdio;
int main(char[][] argv)
{
  TestCase test = new TestCase();
  test.TestClassA();
  return 0;
}
public class TestCase
{
  public void TestClassA()
  {
    A a = new A();
    printf("%*s",a.Method());/* 这里可以看出C和D处理字符串的区别 */
  }
}
public class A
{
  private char[] Method(){return "Call member function Method() of class A.";}
}
有过C++经验的程序员看到上面这段代码,会不会认为这是段错误代码,能通过编译吗?答案是上面这段代码不但能通过编译,而且运行良好。为什么会这样?D里面的private和C++/C#等语言private的语义稍有不同。在D中,private修饰的函数不仅可以被所在类的内部成员访问,甚至可以被同一模块内的其他成员访问。在同一模块内,它相当于C语言中被static修饰的函数,表达的是friend的语义。这一点跟Delphi很相似,只不过在Delphi中称其为单元(unit)。俺认为,D语言提供这个特性虽然方便了程序员编码,但也可能造成槽糕的代码组织和编程习惯。因为它破坏了OOP的封装性。所以,Delphi在其2005新版中增加了strict private来确保封装的严密。但在D中,目前还没有提供相似的功能。或许是D有意为之?俺建议,如果采用OOP,在模块内应人为限制private的语义(类C编程除外)。这是个无奈之举,最稳妥的办法是在语言机制上做出修改。

同理,protected也存在同样的问题。

到了这里,你可能会质疑示例3。D语言默认成员函数的访问级别应该是private才对啊,因为同一模块内,它可以随意访问。那么我们再修改一下示例3代码。

/* 示例5 */
module sample1;  //文件sample1.d
import std.c.stdio;
class A
{
  char[] Method(){return "Call member function Method() of class A.";}
}

module sample2;  //文件sample2.d
private import sample1;
int main(char[][] argv)
{
  TestClassA();
  return 0;
}
void TestClassA()
{
  A a = new A();
  printf("%*s",a.Method());
}编译运行示例5,我们发现依然能运行成功。如果修改Method()为private级别,则不会编译成功。这就说明前面的分析正确。

下面,我们来讨论一下D的继承机制。

/* 示例6 */
module sample;
import std.c.stdio;
int main(char[][] argv)
{
  TestCase test = new TestCase();
  test.Test();
  return 0;
}
public class TestCase
{
  public void Test()
  {
    TestClassA();
    TestClassB();
  }
  private void TestClassA()
  {
    printf("Call function TestClassA()...\n");
    A a = new A();
    printf("%*s",a.Method());
    printf("\n\n");
  }
  private void TestClassB()
  {
    printf("Call function TestClassB()...\n");
    B b = new B();
    printf("%*s",b.Method());
    printf("\n");
    printf("%*s",b.Method(1));
    printf("\n\n");
  }
}
public class A
{
  public char[] Method(){return "Call member function Method() of class A.";}
}
public class B : A
{
  public char[] Method(int i){return "Call member function Method(int) of Class B.";}
} 从C++的角度来看,上述代码并没有任何错误。但是在D中却不能编译通过。原因是B中并不存在有函数匹配Method()原型,所以b.Method()会调用不成功。奇怪,B明明继承父类A的Method()了啊。怎么会不能编译?

下面让我们修改一下示例6的代码。

/* 示例7 */
module sample;
import std.c.stdio;
int main(char[][] argv)
{
  TestCase test = new TestCase();
  test.Test();
  return 0;
}
public class TestCase
{
  public void Test()
  {
    TestClassA();
    TestClassB();
  }
  private void TestClassA()
  {
    printf("Call function TestClassA()...\n");
    A a = new A();
    printf("%*s",a.Method());
    printf("\n\n");
  }
  private void TestClassB()
  {
    printf("Call function TestClassB()...\n");
    B b = new B();
    printf("%*s",b.Method());
    printf("\n");
    printf("%*s",b.AnotherMethod());
    printf("\n\n");
  }
}
public class A
{
  public char[] Method(){return "Call member function Method() of class A.";}
}
public class B : A
{
  public char[] AnotherMethod(){return "Call member function AnotherMethod() of Class B.";}
}
这下总算可以编译运行了。郁闷了吧,哈哈。为什么示例6不能编译,而示例7可以?我们注意到两个示例有点小小的不同,就是示例6有重载方法,而示例7则没有。Bingo!原因就在于此。D认为如果你要重载父类的方法,就必须显式的声明它。这是个良好的习惯,但许多程序员一开始都很不适应(Delphi和VB程序员似乎不会有这个问题,因为它们重载要显式声明),呵呵。我们再次修改示例6的代码,以便让其重载方法可以运行。

/* 示例8 */
module sample;
import std.c.stdio;
int main(char[][] argv)
{
  TestCase test = new TestCase();
  test.Test();
  return 0;
}
public class TestCase
{
  public void Test()
  {
    TestClassA();
    TestClassB();
  }
  private void TestClassA()
  {
    printf("Call function TestClassA()...\n");
    A a = new A();
    printf("%*s",a.Method());
    printf("\n\n");
  }
  private void TestClassB()
  {
    printf("Call function TestClassB()...\n");
    B b = new B();
    printf("%*s",b.Method());
    printf("\n");
    printf("%*s",b.Method(1));
  }
}
public class A
{
  public char[] Method(){return "Call member function Method() of class A.";}
}
public class B : A
{
  alias A.Method Method;
  public char[] Method(int i){return "Call member function Method(int) of Class B.";}
} 最后,我们来看下D语言的多态。D语言实现多态主要是通过虚方法调用和多接口继承。此外,抽象类的使用也是实现多态的重要途径之一。多态问题非常复杂,很难一下说清楚。因此,我们重点考察D的虚方法调用和多接口继承(应用设计模式,抽象类也能发挥很大作用,但不在我们讨论范围之内)。

D语言的虚方法调用机制跟Java很相似,却与C++/C#背道而驰(这两种设计哲学孰优孰劣不予讨论)。D认为,所有非静态,非私有方法默认都是虚方法。需要说明的是,虚方法调用的开销要比非虚方法调用大的多。因此,D编译器在编译代码之前,会分析子类是否overridden父类的虚方法。如果没有,则编译成非虚方法。这样做的好处是不用再考虑应该把哪个方法设置为虚方法了,坏处是可能造成设计的不清晰和滥用。

接口既是表达多态的手段,也是实现契约编程的手段。接口实际上只是为一组方法签名指定一个名称的方式。这些方法根本不带任何实现。但是继承接口与继承父类截然不同。继承接口必须显式实现接口方法,而继承父类则不必显式实现。不管一个接口的契约说明有多么好,都无法保证任何人能100%正确实现它。COM就颇受这个问题之累,导致有的COM对象只能正确用于Microsoft Office Word或Microsoft Internet Explorer。此外,如果多个接口的方法签名相同,如何正确实现它也是个问题。值得注意的是,接口方法是虚方法。

下面的示例很好的说明了上述问题。

/* 示例9 */
module sampleford;
import std.c.stdio;
int main(char[][] argv)
{
  TestCase test = new TestCase();
  test.Test();
  return 0;
}
public class TestCase
{
  public void Test()
  {
    A a = new A();
    printf("%*s", a.Method());
    printf("\n");
    B b = new B();
    printf("%*s", b.Method());
    printf("\n");
    C c = new C();
    printf("%*s", c.Method());
    printf("\n\n");

    printf("---------Program executes succeeded.--------");
  }
}

public interface IA
{
  char[] Method();
}
public interface IB
{
  char[] Method();
}

public class A : IA
{
  public char[] Method(){return "Call member function Method() of class A.";}
}
public class B : A
{
  public override char[] Method(){return "Call member function Method() of class B.";}
}
/* C应该怎么实现 */
public class C : A, IA, IB
{
  /*
   * 奇怪的是竟然可以编译成功,不知道算不算是个Bug.
   * 但是调用不到这个方法.
   */
  alias A.Method Method;
  /*
   * 这个方法到底是谁的实现
   * 遗憾的是D还没有提供显式接口实现的特性
   * 所以目前不能区分到底实现的哪个接口方法
   */
  public override char[] Method(){return "Call member function Method() of class C.";}
}
D语言存在的陷阱不在少数。比如指针的陷阱,虽然比C++中减少了很多,但是只要是指针,就不可避免的存在问题,甚至新增了一个指向垃圾收集堆的新问题,幸运的是我们大部分情况下不需要动用指针这个超级武器。比如泛型编程,泛型已经逐渐成为编程主流,但是D当中的模板依然存在一定问题(这些问题有时间再撰文讨论)。俺只是讨论了D在OOP当中应该注意的问题,这些问题在其他编程语言中也或多或少的存在。

总之,D是一门发展中的语言,具有很大潜力。我很看好你呦!

分享到:
评论

相关推荐

    Python单选题库(4).docx

    以下选项中,不是 Python 语言保留字的是 A while B pass C do D except 3.关于 Python 程序格式框架,以下选项中描述错误的是 A Python 语言不采用严格的"缩进"来表明程序的格式框架 B Python 单层缩进代码属于之前...

    Python中的一些陷阱与技巧小结

    Python是一种被广泛使用的强大语言,让我们深入这种语言,并且学习一些控制语句的技巧,标准库的窍门和一些常见的陷阱。 Python(和它的各种库)非常庞大。它被用于系统自动化、web应用、大数据、数据分析及安全...

    《简单的逻辑学》D.Q.麦克伦尼

    美国著名逻辑学家、哲学教授D.Q.麦克伦尼,将一门宽广、深奥的逻辑科学以贴近生活、通俗易懂、妙趣横生的语言娓娓道来。它既没有刻板的理论教条,也不是正规的教科书,而是一本必不可多得的现实指南。正如著名行为学...

    《简单的逻辑学》D.Q.麦克伦尼 azw3

    美国著名逻辑学家、哲学教授D.Q.麦克伦尼,将一门宽广、深奥的逻辑科学以贴近生活、通俗易懂、妙趣横生的语言娓娓道来。它既没有刻板的理论教条,也不是正规的教科书,而是一本必不可多得的现实指南。正如著名行为学...

    清华大学Linux操作系统原理与应用

    5.2.2 对陷阱门和系统门的初始化 115 5.2.3 中断门的设置 116 5.3 中断处理 116 5.3.1 中断和异常的硬件处理 116 5.3.2 中断请求队列的建立 117 5.3.3 中断处理程序的执行 119 5.3.4 从中断返回 121 5.4 中断的下半...

    PCB技术中的数字电路抗干扰设计

    形成干扰的基本要素有三个:(1)干扰源,指产生干扰的元件、设备或信号,用数学语言描述如下:du/dt, di/dt大的地方就是干扰源。如:雷电、继电器、可控硅、电机、高频时钟等都可 能成为干扰源。(2)传播路径,指...

    Windows Sockets网络编程 可能是最清晰版本(Windows Sockets 2规范解释小组负责人亲自执笔。)总共4个包,part1

    《Windows Sockets网络编程》是WindowsSockets网络编程领域公认的经典著作,由Windows Sockets2.0规范解释小组负责人亲自执笔,权威性毋庸置疑。它结合大量示例,对WindowsSockets规范进行了...D.4 各种编程语言的使用

    Windows Sockets网络编程 总计4个包,part2

    《Windows Sockets网络编程》是WindowsSockets网络编程领域公认的经典著作,由Windows Sockets2.0规范解释小组负责人亲自执笔,权威性毋庸置疑。它结合大量示例,对WindowsSockets规范进行了...D.4 各种编程语言的使用

    ApSIC Xbench 3.0.1367 X64 破解版

    (*.xml)、Trados TagEditor 文件 (*.ttx)、Trados Word 未清理文件 (*.doc, *.rtf)、Trados Studio 文件 (*.sdlxliff, *.sdlproj)、SDLX ITD 文件 (*.itd)(注:该选项需要你的电脑安装 SDLX)、SDLX 记忆库 (*.mdb)、...

    闭合编译器:JavaScript检查器和优化器

    它不是从源语言编译为机器代码,而是从JavaScript编译为更好JavaScript。 它解析您JavaScript,对其进行分析,删除无效代码,然后重写并最大程度地减少剩下的内容。 它还会检查语法,变量引用和类型,并警告常见...

    C语言入门经典(第4版)--源代码及课后练习答案

    10.3.7 scanf()的陷阱 383 10.3.8 从键盘上输入字符串 383 10.3.9 键盘的非格式化输入 384 10.4 屏幕输出 389 10.4.1 使用printf()格式输出到屏幕 389 10.4.2 转义序列 391 10.4.3 整数输出 392 10.4.4 输出...

    c语言编写单片机技巧

    大概来说可以分成I/O型单片机、LCD型单片机、A/D型单片机、A/D with LCD型单片机等等。这些单片机的中文资料我们都公开在HOLTEK-p.htm" target="_blank" title="HOLTEK货源和PDF资料">HOLTEK网站...

    lisp:Leo Howell的Go中构建LISP的实现

    建筑LISP 目录内容 问题? 评论? 电子邮件 。版权本文档是Leo W.... 尽管我认为这是个不错的建议(尽管我自己并没有遵循),但是在解析诸如C之类的语言时需要付出大量的努力,以至于任何潜在的见解都有可能迷

    Reversing:逆向工程揭密

    10.5.5 陷阱标志 335 10.5.6 代码校验和 335 10.6 迷惑反汇编器 336 10.6.1 线性扫描反汇编器 337 10.6.2 递归遍历反汇编器 338 10.6.3 应用 343 10.7 代码混淆 344 10.8 控制流变换 346 10.8.1 暗晦谓词 346 10.8.2...

    通关!游戏设计之道 epub

    第8关 符号语言——HUD和图标设计 132 血槽 133 瞄准镜 134 弹药量 134 道具箱 135 记分牌 135 雷达/地图 137 情景提示 138 清爽的屏幕 138 图标无处不在 139 不要滥用QTE 143 HUD要放在哪 144 除了HUD...

    新编MCS-51单片机应用设计(清晰最新版)

    3.5 MCS-51汇编语言的伪指令 思考题及习题 第4章 MCS—51的中断系统 4.1 中断的概念 4.2 MCS-51中断系统的结构 4.3 中断请求源 4.4 扣断控制 4.4.1 中断允许寄存器m 4.4.2 中断优先级寄存器IP 4.5 中断响应 4.6 外部...

    C++大学教程,一本适合初学者的入门教材(part2)

    6.15 微妙的陷阱:返回对Private数据成员的引用 6.16 通过默认的成员复制进行赋值 6.17 软件复用性 6.18 有关对象的思考:编写电梯模拟程序的类 小结 术语 自测练习 自测练习答案 练习 第7章 类与数据抽象(二) 7...

    C++大学教程,一本适合初学者的入门教材(part1)

    6.15 微妙的陷阱:返回对Private数据成员的引用 6.16 通过默认的成员复制进行赋值 6.17 软件复用性 6.18 有关对象的思考:编写电梯模拟程序的类 小结 术语 自测练习 自测练习答案 练习 第7章 类与数据抽象(二) 7...

Global site tag (gtag.js) - Google Analytics