`
hqs7636
  • 浏览: 220921 次
  • 性别: 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是一门发展中的语言,具有很大潜力。我很看好你呦!

分享到:
评论

相关推荐

    C/C++语言中“?” “:”表达式的陷阱

    ### C/C++语言中“?”“:”表达式的陷阱 #### 一、引言 在C/C++编程中,问号冒号表达式(也称为条件运算符或三元运算符)是一种常用的语法结构,其格式为:`条件表达式 ? 表达式1 : 表达式2`。当条件表达式的...

    D源码欢迎下载呵呵呵呵呵

    D语言是一种现代的、系统级的、通用的编程语言,由 Walter Bright 创建,设计目标是结合C++的性能和程序员生产力,同时避免其一些复杂性和陷阱。D语言的特点包括: 1. **高性能**:D语言编译器能够生成高效的机器...

    超级经典c语言陷阱考试题

    超级经典C语言陷阱考试题 C语言是一种古老而强大的编程语言,自从发明以来,C语言已经成为计算机科学和软件工程的基础。但是,C语言也存在一些易错和易混淆的知识点,这些陷阱经常出现在大学考试和社会招聘考试中。...

    Card游戏D版

    它具有高效的编译器,可以生成接近原生代码的性能,并且支持静态类型检查和垃圾回收,这使得D语言在游戏开发中颇为适用。 开发卡牌游戏,首先需要理解游戏的基本规则和逻辑。卡牌游戏通常包含以下几个核心元素: 1...

    死亡陷阱2-解锁密

    总的来说,"死亡陷阱2-解锁密"这款游戏的开发很可能使用了Java作为主要编程语言,并通过精心设计的类和方法实现了丰富的游戏功能,包括玩家解锁隐藏内容的机制。每个.class文件都承载着游戏的一部分,共同构成了这...

    2dgame2d游戏素材

    这部分可能涉及Cocos2d-x、Unity、Godot等引擎的脚本语言,如Lua、JavaScript或Python。 开发者在获取这些素材后,需要根据自己的游戏引擎和开发工具进行适当的导入和配置。例如,Unity支持导入多种格式的图像和...

    C语言笔试陷阱与难点第一阶段

    C语言作为一门历史悠久且应用广泛的编程语言,在计算机科学领域占据着极其重要的地位。对于初学者而言,掌握C语言的基础知识并熟悉其陷阱和难点至关重要。本文将深入探讨C语言中的几个常见陷阱和难点,帮助读者更好...

    形式语言与自动机理论试题答案解析.doc

    - (T) 正确,集合{a,b,c,d,e}上的二元关系可以通过组合这些元素来构造。 - (T) 对于任何非空集合A,空集Φ是A的子集。 - (F) 给定的文法G不是右线性文法(RG),因为它包含左递归(AS)。 - (F) 正确,3型语言...

    C语言陷阱解读

    ### C语言陷阱解读 #### 一、概述 C语言作为一种高效且广泛应用的编程语言,在软件开发领域占据着重要地位。然而,尽管C语言简洁且功能强大,但它也存在一些潜在的陷阱和缺陷,这些陷阱可能会导致程序员在编写代码...

    2015_2016学年高中语文2.3迷幻陷阱_“误读”和“异读”课时训练新人教版选修语言文字应用

    在语文学习中,"误读"和"异读"是两个重要的概念...同时,了解并掌握中国的姓氏文化也是重要的,因为许多姓氏如“解”、“单”、“任”等在特定情况下会有异读,这不仅丰富了我们的语言知识,也加深了对传统文化的理解。

    Java中常见的陷阱题及答案

    Java编程语言虽然强大且广泛应用,但在实践中也存在一些容易让人踩坑的地方。下面我们就来深入探讨一下Java中常见的陷阱及其解决方案。 1. **找奇数的陷阱** ```java public static boolean isOdd(int i){ ...

    java中for循环删除集合陷阱

    在Java编程语言中,使用for循环来遍历并删除集合元素时,需要注意一个常见的陷阱,即在循环过程中直接修改集合的结构可能导致意外的结果。这个问题主要出现在增强for循环(也称为foreach循环)和常规for循环中,但...

    CT107D.zip

    学习51单片机编程,通常会涉及到汇编语言和C语言,其中C语言更为常用,因为它具有更好的可读性和可移植性。 在此次蓝桥杯竞赛中,参赛者可能需要编写控制51单片机执行特定任务的代码,例如数据采集、处理、显示或...

Global site tag (gtag.js) - Google Analytics