找回密码
 立即注册
首页 业界区 业界 如何设计一门语言(三)——什么是坑(面向对象和异常处 ...

如何设计一门语言(三)——什么是坑(面向对象和异常处理)

挽幽 2025-5-29 16:35:55
在所有的文字之前,我需要强调一下,我本人对structure typing持反对态度,所以就算文中的内容“看起来很像”go的interface,读者们也最好不要觉得我是在赞扬go的interface。我比较喜欢的是haskell和rust的那种手法。可惜rust跟go一样恨不得把所有的单词都缩成最短,结果代码写出来连可读性都没有了,单词都变成了符号。如果rust把那乱七八糟的指针设计和go的那种屎缩写一起干掉的话,我一定会很喜欢rust的。同理,COM这个东西设计得真是太他妈正确了,简直就是学习面向对象手法的最佳范例,可惜COM在C++下面操作起来有点傻逼,于是很多人看见这个东西就呵呵呵了。
上一篇文章说这次要写类成员函数和lambda的东西,不过我想了想,还是先把OO放前面,这样顺序才对。
我记得我在读中学的时候经常听到的宣传,是面向对象的做法非常符合人类的思维习惯,所以人人喜欢,大行其道,有助于写出鲁棒性强的程序。如今已经过了十几年了,我发现网上再也没有这样的言论了,但是也没有跟反C++的浪潮一样拼命说面向对象这里不好那里不好要废除——明显人们还是觉得带面向对象的语言用起来还是比较爽的,不然也就没有那么多人去研究,一个特别合用来写functional programming的语言——javascript——是如何可以“模拟”面向对象语言里面的常用操作——new、继承和虚函数覆盖了。
所以像面向对象这种定义特别简单的东西,语法上应该做不出什么坑的了。那今天的坑是什么呢?答案:是人。
动态类型语言里面的面向对象说实话我也不知道究竟好在哪里,对于这种语言那来讲,只要做好functional programming的那部分,剩下的OO究竟要不要,纯粹是一个语法糖的问题。在动态类型语言里面,一个类和一个lambda expression的差别其实不大。
那么静态类型语言里面的面向对象要怎么看待呢?首先我们要想到的一个是,凡是面向对象的语言都支持interface。C++虽然没有直接支持,但是他有多重继承,我们只需要写出一个纯虚类出来,就可以当interface用了。
在这里我不得不说一下C++的纯虚类和interface的这个东西。假设一下我们有下面的C#代码:
  1. interface IButton{}
  2. interface ISelectableButton : IButton{}
  3. interface IDropdownButton : IButton{}
  4. class CheckBox : ISelectableButton{}
  5. class MyPowerfulButton : CheckBox, IDropdownButton
  6. {
  7.     // 在这里我们只需要实现IDropdownButton里面比IButton多出来的那部分函数就够了。
  8. }
复制代码
我们先不管GUI是不是真的能这么写,我们就看看这个继承关系就好了。这是一个简单到不能再简单的例子。意思就是我有两种button的接口,我从一个实现里面扩展出一个支持另一种button接口的东西。但是大家都知道,我那个完美的GacUI用的是C++,那么在C++下面会遇到什么问题呢:
#region 抱怨
一般来说在C++里面用纯虚类来代替interface的时候,我们继承一个interface用的都是virtual继承。为什么呢?看上面那个例子,ISelectableButton继承自IButton,IDropdownButton继承自IButton。那么当你写一个MyPowerfulButton的时候,你希望那两个接口里面各自的IButton是不一样的东西吗?这当然不是。那如何让两个接口的IButton指向的是同一个东西呢?当然就是用virtual继承了。
好了,现在我们有CheckBox这个实现了ISelectableButton(带IButton)的类了,然后我们开始写MyPowerfulButton。会发生什么事情呢?
猜错了!答案是,其实我们可以写,但是Visual C++(gcc什么的你们自己玩玩就好了)会给我们一个warning,大意就是你IDropdownButton里面的IButton被CheckBox给覆盖了,再说抽象一点就是一个父类覆盖了另一个父类的虚函数。这跟virtual继承是没关系的,你怎么继承都会出这个问题。
但这其实也怪不了编译器,本来在其他情况下,虚函数这么覆盖自然是不好的,谁让C++没有interface这个概念呢。但是GUI经常会碰到这种东西,所以我只好无可奈何地在这些地方用#pragma来supress掉这个warning,反正我知道我自己在干什么。
C++没有interface的抱怨到这里就完了,但是virtual继承的事情到这里还没完。我再举一个例子:
  1. class A
  2. {
  3. private:
  4.     int i;
  5. public:
  6.     A(int _i)i:(_i){}
  7. };
  8. class B : public virtual A
  9. {
  10. public:
  11.     B(int _i):A(_i){}
  12. };
  13. class C : public virtual A
  14. {
  15. public:
  16.     C(int _i):A(_i){}
  17. };
  18. class D : public B, public C
  19. {
  20. public:
  21.     D():B(1), C(2){}
  22. };
复制代码
大家都是知道什么是virtual继承的,就是像上面这个例子,D里面只有一个A对象,B和C在D里面共享了A。那么,我们给B和C用了不同的参数来构造,难道一个A对象可以用不同的参数构造两次吗,还是说编译器帮我们随便挑了一个?
呵呵呵呵呵呵呵呵,我觉得C++的virtual继承就是这里非常反直觉——但是它的解决方法是合理的。反正C++编译器也不知道究竟要让B还是C来初始化A,所以你为了让Visual C++编译通过,你需要做的事情是:
  1. D()
  2.     : A(0)  // 参数当然是胡扯的,我只是想说,你在D里面需要显式地给A构造函数的参数
  3.     , B(1)
  4.     , C(2)
  5. {
  6. }
复制代码
#endregion
大家估计就又开始吵了,C++干嘛要支持多重继承和virtual继承这两个傻逼东西呢?我在想,对于一个没有内建interface机制的语言,你要是没有多重继承和virtual继承,那用起来就跟傻逼一样,根本发挥不了静态类型语言的优势——让interface当contract。当然,我基本上用多重继承和virtual继承也是用来代替interface的,不会用来做羞耻play的。
当我们在程序里面拿到一个interface也好,拿到一个class也好,究竟这代表了一种什么样的精神呢?interface和class的功能其实是很相似的
interface IA:只要你拿到了一个IA,你就可以对她做很多很多的事情了,当然仅限大括号里面的!
class C : IA, IB:只要你拿到了一个C——哦不,你只能拿到interface不能拿到class的——反正意思就是,你可以对她做对IA和IB都可以做的事情了!
所以contract这个概念是很容易理解的,就是只要你跟她达成了contract,你就可以对她做这样那样的事情了。所以当一个函数返回给你一个interface的时候,他告诉你的是,函数运行完了你就可以做这样那样的事情。当一个函数需要一个interface的时候,他告诉你的是,你得想办法让我(函数)干这样那样的事情,我才会干活。
那class呢?class使用来实现interface的,不是给你直接用的。当然这是一个很理想的情况,可惜现在的语言糖不够甜,坚持这么做的话实在是太麻烦了,所以只好把某些class也直接拿来用了,GUI的控件也只好叫Control而不是IControl了。
其实说到底class和interface有什么区别呢?我们知道面向对象的一大特征就是封装,封装的意思就是封装状态。什么是状态呢?反正云风一直在说的“类里面的数据”就不是状态。我们先来看什么是数据:
  1. struct Point
  2. {
  3.     int x;
  4.     int y;
  5. };
复制代码
这就是典型的数据,你往x和y里面随便写什么东西都是没问题的,反正那只是一个点。那什么是状态呢:
  1. struct String
  2. {
  3.     wchar_t* buffer;
  4.     int length;
  5. };
复制代码
String和Point有什么不一样呢?区别只有一个:String的成员变量之间是满足一个不变量的:wcslen(buffer) == length;
如果我们真的决定要给String加上这么个不变量的话,那这里面包含了两点:
1:buffer永远不是nullptr,所以他总是可以被wcslen(buffer)   
2:length的值和buffer有直接的关系
如果你要表达一个空字符串,你总是可以写buffer=L””,不过这就要你给String再加上一些数据来指明这个buffer需要如何被释放了,不过这是题外话了。我们可以假设buffer永远是new[]出来的——反正这里不关心它怎么释放。
这个不变量代表什么呢?意思就是说,无论你怎么折腾String,无论你怎么创建释放String,这个等式是一定要满足的。也就是说,作为String外部的“操作人员”,你应当没机会“观测”到这个String处于一个不满足不变量的状态。
所以这两个成员变量都不应该是public的。因为哪怕你public了他们其中的一个,你也会因为外部可以随意修改它而使他进入一个不满足不变量的状态。
这代表了,为了操作这些成员变量,我们需要public一些函数来给大家用。其实这也是contract,String的成员函数告诉我们,你可以对我(String)做很多很多的事情哦!
这同时也代表了,我们需要一个构造函数。因为如果我们在创建一个String之后,实例没有被正确初始化,那么他就处于了一个不满足不变量的状态,这就不满足上面说的东西了。有些人喜欢带一个Init函数和一个基本不干什么事情的构造函数,我想说的是,反正你构造完了不Init都不能用,你为什么非要我每次创建它的时候都立刻调用Init这么多次一举呢?而且你这样会使得我无法对于一个这样的函数f(shared_ptr x)直接写f(make_shared(new ClassThatNeedInit))因为你的构造函数是残废的!
有些人会说,init没有返回值,我不知道他犯了错误啊——你可以用Exception
还有些人会说,exception safe的构造函数好难写啊——学啊我艸
但是这样仍然有些人会负隅顽抗,都这么麻烦了反正我可以用对Init和返回值就好了——你连exception safe的构造函数都不知道怎么写你怎么知道你可以“用对”它们
#region 题外话展开
但是有些人就喜欢返回error,怎么办呢?其实我们都很讨厌Java那个checked exception的对吧,要抛什么exception都得在函数签名里面写,多麻烦啊。其实这跟error是一样的。一个exception是可以带有很多丰富的信息的——譬如说他的callstack什么的,还可以根据需要有很多其他的信息,总之不是一个int可以表达的。这就是为什么exception【通常】都是一个类。那如果我们不能用exception,但是也要返回一样多的信息怎么办?你只好把函数的返回值写得相当的复杂,譬如说:
  1. struct ErrorInfoForThisFunction
  2. {
  3.     xxxxxxxx
  4. };
  5. template<typename R, typename E>
  6. struct ReturnValue // C++没有好用的tuple就是卧槽
  7. {
  8.     bool hasError;
  9.     R returnValue;
  10.     E errorInfo;
  11. };
  12. ReturnValue<ReturnType, ErrorInfoForThisFunction> ThisFunction( ... ); //我知道因为信息实在太多你们又要纠结返回struct还是它的指针还是ReturnValue里面的东西用指针还是用引用参数等等各种乱七八糟的事情了哈哈哈哈哈哈
复制代码
于是现在出问题了,我有一个ThatFunction调用ThisFunction,当错误是一种原因的时候我可以处理,当错误是另一种原因的时候我无法处理,所以在这种情况下我有两个选择:
1:把错误信息原封不断的返回   
2:把ThisFunction的错误信息包装成ThatFunction的错误信息   
不过我们知道其实这两种方法都一样,所以我们采用第一种:
[code]struct ErrorInfoForThatFunction{    yyyyyyyy};ReturnValue
您需要登录后才可以回帖 登录 | 立即注册