D语言的陷阱

卓嘉良
2023-12-01
原文: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是一门发展中的语言,具有很大潜力。我很看好你呦!
 类似资料: