弧度和角度的角度类

6

我希望创建一个角度类,可以使用弧度或者角度进行初始化,但我不确定这是否是一个好主意。我考虑的是以下内容:

类图

class Radian;
class Degree;

/**
  * My angle class.
  **/
class Angle
{
    private:
        float m_radian;

    public:
        explicit Angle(const Radian& rad);

        explicit Angle(const Degree& deg);

        float GetRadian(void) const
        {
            return this->m_radian;
        }

        float GetDegree(void) const
        {
            return ToDegree(this->m_radian);
        }

        bool IsEqual(const Angle& angle, float epsilon = 0.001f) const
        {
            return Abs<float>(this->m_radian - angle.m_radian) < epsilon;
        }

        void Set(const Angle& ang);

    protected:
        Angle(void) : m_radian(0.0f)
        {}

        Angle(float rad) : m_radian(rad)
        {}
};

class Radian : public Angle
{
    public:
        Radian(void)
        {}

        Radian(float r) : Angle(r)
        {}
};

class Degree : public Angle
{
    public:
        Degree(void)
        {}

        Degree(float d) : Angle(ToRadian(d))
        {}
};

/// Trigonometric functions.
float Sin(const Angle& angle);
...

这样我在代码中就不会混淆弧度和角度单位。但是,这是一个好的设计选择吗?还是应该采用其他设计方案?


如果你已经有一个Angle类,为什么还需要DegreeRadian类呢? - Beta
3
我个人认为这个三角形是一种有趣的图案,以令人兴奋的方式违反了面向对象的原则。我最初是在Eiffel中看到它的,与复数相关,自那以后我就爱上了它。 - SáT
1
@Beta 我的 Angle 类是一个泛化类。因此,当我想要使用度角时,我可以使用 Degree(90) 而不是 Angle(ToRadian(90))。我希望有一种方法来避免弄混弧度和度数。如果只使用 Angle,我可能会写错 Angle(90) 这样的代码。 - Lucas Lima
2
我认为像这样的想法是面向对象思想在严肃科学计算中没有产生影响的原因之一。我不认为单位转换是足以证明需要两个新类的重要行为差异。 - duffymo
4个回答

16

在这里你根本不需要任何继承。一旦对象被构建,你就不再关心它们之间的区别 - 它们的行为完全相同。因此,你唯一需要解决的问题是如何以易读的方式构造Angle对象。

通常的解决方案是使用命名构造函数

class Angle
{
    public:
        static Angle fromRadians( float v );
        static Angle fromDegrees( float v );
        // ...

    private:
        Angle( float rad );
        // ...
};

不直接调用构造函数,而是提供具有表达性名称的工厂函数。因此,您可以编写:

void f( Angle::fromDegrees( 3.0 ), Angle::fromRadians( 17.0 ) );

+1 - 没错。这里不需要两个类。任何做过科学计算的人都不会这样做。 - duffymo

11

我认为在这里并不需要使用继承。对于使用你的类来说,唯一需要注意的是获取一个角度 - 它最初是以度数还是弧度表示无关紧要。

免责声明:我以前做过这个。完全相同的用例。我的解决方案是让构造函数接受两个参数:一个数字和一个单位枚举。我会这样使用我的类:

Angle a(1.2345, Angle::Radians);
std::cout << a.radians() << a.degrees() << sin(a);

如果你想要方便地从常见单位创建角度,那么它们只能是帮助方法。不需要单独的类。

Angle r = Radians(2.3);
Angle d = Degrees(180);

无论如何-这是我过去一直使用并感到开心的。希望它能帮到你!


这似乎也是个不错的解决方案。但是,在这种情况下,您必须传递两个参数来初始化一个 Angle 对象。如果您需要加载很多角度,这样做是否会有问题?并且构造函数中有一个“if”语句来处理角度种类? - Lucas Lima
1
+1,继承是用于修改行为的,而在这里没有任何差异可以证明需要使用继承。 - casablanca
2
@LucasNunes - 你可以通过将Angle::UNIT设置为乘数(或者是一个乘数表的索引)来避免条件语句。因此,如果你将角度存储为弧度,那么Angle:Radians就是1.0Angle::Degrees就是0.0174532925等等。 - Xavier Holt
2
@LucasNunes - 关于有两个参数的问题:我认为像这样的函数将被编译器内联,因为它非常小,而且在输入为弧度时,乘以1会被优化掉。但是,如果您想确保弧度初始化快速,您有选择。您可以使用“命名构造函数”,就像Frerich建议的那样,或者使用一个友元辅助方法,该方法调用仅具有一个(弧度)参数的私有构造函数。在所有其他单位中,乘法是不可避免的。 - Xavier Holt

6
我建议这样做:
class Radians {
    explicit Radians(float a) : angle_(a) {}
    Radians(Degrees a)        : angle_(a * PI/180.f) {}
    operator float()          { return angle_; }
private:
    float angle_;
}

class Degrees {
    explicit Degrees(float a) : angle_(a) {}
    Degrees(Radians a)        : angle_(a * 180.f/PI) {}
    operator float()          { return angle_; }
private:
    float angle_;
}

这将使函数的自然单位成为其接口的一部分,我认为这是件好事。你不应该编写一个检查所给角度类型并进行不同计算的Sin函数。要么编写两个版本,并让编译器完成工作:

float Sin(Radians x);
float Sin(Degrees x);

或者您可以编写一个函数(使用实现所需的任何类型 - 可能是弧度):
float Sin(Radians x);

重点不是你可以拥有一个“抽象”的角度(我认为这不是一个有用的概念),而是要避免混淆度数和弧度,并使两者之间的转换隐式进行。使用抽象基类Angle会增加语法噪音(你需要到处使用引用),并且可能会降低性能。这种解决方案还允许你在所需的单位中存储角度,而不是希望你得到“快速路径”。

2
+1 "重点不在于你可以拥有一个“抽象”的角度(我认为这不是一个有用的概念),而在于避免混淆度数和弧度,并使两者之间的转换隐式进行。" - 正中要害 - Pharap
1
我认为在这种情况下,隐式转换运算符有点危险。例如,Radians{1} + Degrees{45}可以编译通过,但结果并不直观。最好的做法可能是重载必要的运算符或使转换显式(或只提供一个getter)。你怎么看? - mtvec
1
这段代码无法编译,因为它是一个递归定义。 - pingu

1
我会使用角度作为对象,并使用不同的getter setter来表示相同的内部角度值,包括度数和弧度。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接