这是 expressive types 系列的第一个帖子。也就是说,表达类型代表的东西,而不是它们的实现。通过使类型承载意义,从而提升了代码的可读性和表达性。
动机
你也许碰到过这种情况:你需要两种构造方式,但是它们类型相同。作为演示,现在以 Circle 为例:
假设这个类需要提供它的周长和面积,并且可以用它的半径构造:
class Circle
{
public:
explicit Circle(double radius) : radius_(radius) {}
void setRadius(double radius) { radius_ = radius; };
double getCircumference() const { return 2 * Pi * radius_; }
double getArea() const { return Pi * radius_ * radius_; }
private:
double radius_;
};
现在我们希望能够使用周长而不是半径构造对象。
周长也是使用 double 表示的,和半径一样。因此带来了一个问题:将会有两个具备相同参数的构造函数:
class Circle
{
public:
explicit Circle(double radius) : radius_(radius) {}
explicit Circle(double diameter) : radius_(diameter / 2) {} // This doesn't compile !!
...
}
编译将会出错,因为构造函数调用不明确:
Circle c(7) // is the radius 7 or is it the diameter ??
当然了,setter 没有这个问题:
void setRadius(double radius) { radius_ = radius; }
void setDiameter(double diameter) { radius_ = diameter / 2; }
上面的 setter 没有混淆问题。因为它们都附带了一个名字(setRadius 和 setDiameter)。这篇文章的重点是向你展示如何让构造函数也带有一个名字。
标签调度:并不是最好的办法
一些代码通过标记调度解决了这个问题。如果您从未听说过标签调度,您可以直接跳到下一节。否则你可能想继续阅读,以了解为什么这不是这里的最佳选择。
标签调度的想法是为每个原型添加一个参数,以消除调用的歧义。每个原型都将获得不同类型的参数,使它们在调用点可区分。附加类型不携带值。它只是专门用于原型。因此,创建了新的人工类型,既没有行为也没有数据,例如:
struct AsRadius {};
struct AsDiameter {};
构造函数将变为:
class Circle
{
public:
explicit Circle(double radius, AsRadius) : radius_(radius) {}
explicit Circle(double diameter, AsDiameter) : radius_(diameter / 2) {}
...
}
调用端变为:
Circle circle1(7, AsRadius());
Circle circle2(14, AsDiameter());
我看到这种技术有两个缺点:
它可能导致语法更笨拙
它不具备伸缩性。
如果您有多个构造函数和多个参数,您需要消除歧义,原型会变得越来越大。
在类型中携带含义
更好的选择是使用更具表现力的类型。仔细想想,您真正想要传递给构造函数的是半径(或直径)。但是通过上面的实现,你实际传递的是一个 double。的确,双精度是半径的实现方式,但它并没有真正说明它的含义。
所以解决方案是让类型具有表现力,也就是说让它告诉它代表什么。这可以通过围绕类型构建一个薄包装来完成,只是为了在它上面放一个标签:
class Radius
{
public:
explicit Radius(double value) : value_(value) {}
double get() const { return value_; }
private:
double value_;
};
类似的,对于直径:
class Diameter
{
public:
explicit Diameter(double value) : value_(value) {}
double get() const { return value_; }
private:
double value_;
};
然后,构造函数可以以这种方式使用它们:
class Circle
{
public:
explicit Circle(Radius radius) : radius_(radius.get()) {}
explicit Circle(Diameter diameter) : radius_(diameter.get() / 2) {}
...
}
调用侧为:
Circle circle1(Radius(7));
Circle circle2(Diameter(14));
现在,我们写的两个包装器非常相似,都需要泛化,这就是下一篇文章的主题:强类型。