SICP学习笔记(2.4.1~2.4.3)
SICP学习笔记(2.4.1~2.4.3)
周银辉
1,基于类型的分派
在SICP的“抽象数据的多重表示”中,我们看到对于“复数”而言我们既可以采用“直角坐标”也可以采用“极坐标”,也许有朋友会迷惑“干嘛非要采用两种表示并存的形式,直接使用直角坐标不就可以了?并且我们还额外可以提供一个采用极坐标的构造函数来免去用户手动转换坐标系的麻烦,一切关于极坐标的选择函数和构造函数仅仅是我们提供给用户的糖衣”。的确,这是可行的,对于这个例子我也还挺鼓励这样做的。连SICP作者自己也承认了这是一个不太实际的例子。作者之所以仍然采用了这个例子,我觉得可能是因为本书到目前为止还没有引入继承或类似的概念而已。这里为了将事情讲得更“实际”一点,将不采用复数作为例子,并且我们还会看到继承的影子。
2.4.1~2.4.3这几节的内容可以提炼成两个字:分派(或者称Dispatch)
在程序设计语言中,“分派”的通俗解释可以认为是“方法(函数)查找”,当在某个对象上调用某个方法时,如果存在多种可能性,则需要一种机制来查找到合适的方法以便确定代码执行路径。常见的例子是对那些被“重载”或被“重写”的方法的调用。
1.1,静态分派:
如果对函数的查找工作在编译时期就能够确定下来,那么我们将这样的分派工作称为“静态分派”。C++,Java,C# 等语言的对重载函数的分派就是静态的,比如:
{
public:
void Play(Mp3 mp3){}
void Play(Avi avi){}
}
//....
Player p;
p.Play(mp3File)
在p.Play(...)中我们确定到底调用哪个Play方法,但这项工作在编译时便可以完成,不必等到运行时。这是因为编译器在我们的背后玩了一点点小把戏,编译时编译器将重载方法的每个版本重命名了,比如void Play(Mp3 mp3)就可能被编译成void void_Play_Mp3(Mp3 mp3),而Avi版本的则被编译成void void_Play_Avi(Avi avi)。如此这般,Play的两个重载版本实际上就变成了完全不同的两个函数,调用起来就很简单了。
1.2,动态分派
如果对函数的查找工作不能在编译时确定,那么将分派工作推迟到运行时也许能解决问题(注意,是“也许”,在运行是否能分派成功取决于具体语言是否具备相关功能。如果语言本身没有提供相关功能,这就需要程序员编写代码来手工分派,用一堆又一堆的if/else,或其他“高级技巧”吧,稍后会看到)
作为大多面向对象语言最基本性质的“多态性”是动态分派的一个例子(但动态分派不仅限于“多态”)。
{
public virtual void Play() {}
}
class Mp3Player : Player
{
public override void Play(){}
}
class AviPlayer : Player
{
public override void Play(){}
}
//….
Player p = new Mp3Player();
p.Play();
上面代码中,对于p.Play()的调用取决于p在运行时的实际类型,所以要在运行时才能确定(new关键字出卖了一切,new表示动态分配内存)。所以其属于动态分派。Java的动态分派是在运行时根据对象的实际类型和被调用的方法的签名这两个元素为依据按照继承关系从当前类型开始向上查找,直到某个类实现了该签名的方法,如果找到Object类型都还没找到,则抛异常。 C++则是通过“虚函数表”来实现的,稍后会讲。
1.3,单分派 和 多重分派
观察上例中的p.Play();我们发现,对于Play的确定仅仅取决于p的运行时类型这一个条件,这样的分派又称之为单分派(或单重分派,Single Dispatch)。相反地,如果我们将Player的Play()函数修改为void Play(IMedia media):
Player p ;
IMedia m;
//….
p.play(m)
对于Play的调用则取决于p以及m这两个元素的运行时类型,那么我们将这种需要由多个条件才能确定的分派称之为多重分派(Multiple Dispatch)
C++,C#,Java 在静态分派时是可以进行多重分派的(比如对重载的方法、模板方法等进行分派的时候),而在动态分派时则只能进行单分派(比如对能够被重写的方法进行分派时),不过据说C#4.0在动态多分派上有所改进(?)。
为啥非要拿动态多分派来说事呢,看看下面的代码:
{
class Program
{
static void Main()
{
ShapeBase rect = new Rectange();
ShapeBase circle = new Circle();
ShapeBase triangle = new Triangle();
rect.MergeWith(circle);
rect.MergeWith(triangle);
triangle.MergeWith(circle);
//....
}
}
abstract class ShapeBase
{
public abstract void MergeWith(ShapeBase sharpToMerge);
}
class Rectange : ShapeBase
{
public override void MergeWith(ShapeBase sharpToMerge)
{
//do merge here
}
}
class Circle : ShapeBase
{
public override void MergeWith(ShapeBase sharpToMerge)
{
//do merge here
}
}
class Triangle : ShapeBase
{
public override void MergeWith(ShapeBase sharpToMerge)
{
//do merge here
}
}
}
假设我们规定:不同类型的图形之间的合并操作是不一样的,并且某些特殊种类的图形之间不能相互合并。那么要为ShapeBase编写的每个子类编写MergeWith方法开始变得有些让人厌烦
如果我们像下面这么书写代码的话,其中的包含的危机是多么可怕:
{
var type = sharpToMerge.GetType();
if(type == typeof(Rectangle))
{
//do merge with rectangle
}
else if (type == typeof(Circle))
{
//do merge with Circle
}
else if …
//…
}
当然,你可以运用些设计模式甚至是反射技术来替换上面的代码使其看上去更漂亮些,但设计模式感觉总是在玩花招以转移注意力而非专注于解决问题本身(设计模式对美的追求和数学对美的追求似乎朝着两个刚好相反的方向在进行,孰美?)
而动态语言(比如Python)在这方面要做得好一些,比如Python有multimethods 来改进动态性。multimethods和SICP 2.4.3中“数据导向的程序设计”思想几乎一模一样,所以,即便你所使用的静态语言不具备动态多分配的能力,使用“数据导向的程序设计”思想来模拟一个multimethods也是一件和容易的事情。
2,数据导向的程序设计
注意到,上面所讲到的Merge方法, 我们是使用子类重写父类方法的形式进行的,发现需要写许多的If else, 并且当新增加一种图形的时候,每个子类都要被修改以便提供新的else分支来适应新的图形类型。 那么,有朋友可能会想到:如果我一共一个ShapeManager专门来处理Merge方法呢,并且ShapeManager提供许多重载方法,当增加图形种类的时候,我们是否可以通过增加一些Merge的重载版本来解决呢,我们试试:
假如我们的MergeManager如此写(C#代码):
{
public void DoMerge(Rectange rect, Circle circle)
{
Console.WriteLine("Rectangle Merge With Circle");
}
public void DoMerge(Triangle triangle, Circle circle)
{
Console.WriteLine("Triangle Merge With Circle");
}
}
static void Main()
{
ShapeBase rect = new Rectange();
ShapeBase circle = new Circle();
ShapeBase triangle = new Triangle();
ShapeManager manager = new ShapeManager();
manager.DoMerge(rect, circle);
manager.DoMerge(triangle, circle);
}
看上去,还蛮不错的。
可惜,上面的代码来编译都通不过!
为啥?
注意到了嘛, 声明rect时其静态类型是ShapeBase,其是被new出来的,所以我们知道其运行时类型是Rectangle,但,编译器不知道。调用ShapeManager的Merger方法时,由于其存在多个重载版本,编译器需要去进行“分派”以便查找到正确的方法,C#在重载分派时采用的是“静态分派”,所以其会根据rect的静态类型ShapeBase去查找合适的匹配,在这里,其没有找到,所以通不过编译。
这是静态语言(C++,C#,Java…)普遍存在的问题。
那应该怎么处理呢, 难道非要写那么多丑陋的if / else. 不必着急。
选项一:如果你喜欢Java以及设计模式,可以玩visitor 模式这样的魔术
选项二:如果你喜欢C++ 并且熟悉虚函数表,可以看看周大侠的C++双重分派实现
选项三:如果你看过SICP,知道Python的multiMethod,那么可以看看下面的代码,它工作得很好:
using System.Collections;
using System.Collections.Generic;
namespace ConsoleApplication1
{
class Program
{
static void Main()
{
ShapeBase rect = new Rectange();
ShapeBase circle = new Circle();
ShapeBase triangle = new Triangle();
ShapeManager manager = new ShapeManager();
manager.AddRule<Rectange, Circle>(() =>
{
Console.WriteLine("Rectangle Merge With Circle");
});
manager.AddRule<Triangle, Circle>(() =>
{
Console.WriteLine("Triangle Merge With Circle");
});
manager.Merger(rect, circle);
manager.Merger(triangle, circle);
manager.Merger(triangle, rect);
Console.ReadKey();
}
}
class TypePair
{
public TypePair(Type t1, Type t2)
{
Type1 = t1;
Type2 = t2;
}
public object Type1
{
get;
private set;
}
public object Type2
{
get;
private set;
}
}
class ShapeManager
{
private IDictionary cache =
new Dictionary<TypePair, Action>();
public void AddRule<T1, T2>(Action act)
{
cache.Add(new TypePair(typeof(T1),typeof(T2)), act);
}
public void Merger(ShapeBase s1, ShapeBase s2)
{
Type t1 = s1.GetType();
Type t2 = s2.GetType();
foreach (TypePair key in cache.Keys)
{
if (key.Type1.Equals(t1) && key.Type2.Equals(t2))
{
var d = cache[key] as Action;
d.Invoke();
return;
}
}
Console.WriteLine("There is no appropriate Merage for " + t1.Name + " and "+t2.Name);
}
}
abstract class ShapeBase
{
}
class Rectange : ShapeBase
{
}
class Circle : ShapeBase
{
}
class Triangle : ShapeBase
{
}
}
3,虚函数表
本来想写点什么的,但发现这篇文章(C++虚函数表解析)已经写得非常详细了,可以参考它。