代码之家  ›  专栏  ›  技术社区  ›  DanDan

使用强类型语言还有多远?

  •  42
  • DanDan  · 技术社区  · 14 年前

    假设我正在编写一个API,我的一个函数接受一个表示通道的参数,并且只在0到15之间。我可以这样写:

    void Func(unsigned char channel)
    {
        if(channel < 0 || channel > 15)
        { // throw some exception }
        // do something
    }
    

    还是我利用C++作为一种强类型语言,使自己成为一种类型:

    class CChannel
    {
    public:
        CChannel(unsigned char value) : m_Value(value)
        {
            if(channel < 0 || channel > 15)
            { // throw some exception }
        }
        operator unsigned char() { return m_Value; }
    private:
        unsigned char m_Value;
    }
    

    我的功能现在变成了:

    void Func(const CChannel &channel)
    {
        // No input checking required
        // do something
    }
    

    但这是彻底的过度杀戮吗?我喜欢自己的文档和它所说的保证,但是它值得为这样一个对象的构造和破坏付出代价吗,更不用说所有额外的输入了?请告诉我你的意见和备选方案。

    14 回复  |  直到 8 年前
        1
  •  60
  •   Sarfaraz Nawaz    12 年前

    如果您希望使用这种更简单的方法来概括它,这样您就可以从中获得更多的使用,而不是根据具体的事情来定制它。那么问题不是“我应该为这个特定的东西做一个全新的类吗?”但是“我应该使用我的工具吗?”后者总是对的。公用事业总是有用的。

    所以做一些类似的事情:

    template <typename T>
    void check_range(const T& pX, const T& pMin, const T& pMax)
    {
        if (pX < pMin || pX > pMax)
            throw std::out_of_range("check_range failed"); // or something else
    }
    

    现在你已经有了这个检查范围的好工具。您的代码,即使没有通道类型,也可以通过使用它变得更干净。你可以更进一步:

    template <typename T, T Min, T Max>
    class ranged_value
    {
    public:
        typedef T value_type;
    
        static const value_type minimum = Min;
        static const value_type maximum = Max;
    
        ranged_value(const value_type& pValue = value_type()) :
        mValue(pValue)
        {
            check_range(mValue, minimum, maximum);
        }
    
        const value_type& value(void) const
        {
            return mValue;
        }
    
        // arguably dangerous
        operator const value_type&(void) const
        {
            return mValue;
        }
    
    private:
        value_type mValue;
    };
    

    现在你有了一个很好的实用程序,可以做:

    typedef ranged_value<unsigned char, 0, 15> channel;
    
    void foo(const channel& pChannel);
    

    在其他场景中也可以使用。把它都放在 "checked_ranges.hpp" 归档并在需要时使用。做抽象从来都不坏,周围有实用程序也没有坏处。

    另外,不要担心开销。创建一个类只需要运行您无论如何都要做的相同代码。此外,干净的代码比任何其他代码都更受欢迎;性能是最后的问题。完成后,您可以让一个分析器测量(而不是猜测)慢部件在哪里。

        2
  •  27
  •   Jerry Coffin    8 年前

    是的,这个想法是值得的,但是(imo)为每个整数范围编写一个完整的、独立的类是没有意义的。我遇到了足够多的情况,需要有限范围整数,为此我编写了一个模板:

    template <class T, T lower, T upper>
    class bounded { 
        T val;
        void assure_range(T v) {
            if ( v < lower || upper <= v)
                throw std::range_error("Value out of range");
        }
    public:
        bounded &operator=(T v) { 
            assure_range(v);
            val = v;
            return *this;
        }
    
        bounded(T const &v=T()) {
            assure_range(v);
            val = v;
        }
    
        operator T() { return val; }
    };
    

    使用它的方式如下:

    bounded<unsigned, 0, 16> channel;
    

    当然,你可以得到比这更详细的说明,但这个简单的方法仍然可以很好地处理大约90%的情况。

        3
  •  14
  •   anon    14 年前

    不,这不是过度杀戮——你应该总是尝试将抽象表示为类。这样做有无数的原因,而且开销很小。不过,我会叫班级频道,而不是频道。

        4
  •  11
  •   Seva Alekseyev    14 年前

    不敢相信到目前为止还没有人提到过枚举。不会为您提供防弹保护,但仍然比普通整数数据类型更好。

        5
  •  6
  •   M. Williams    14 年前

    看起来有点过头了,尤其是 operator unsigned char() 访问器。您不是在封装数据,而是使明显的事情变得更复杂,而且可能更容易出错。

    像您的 Channel 通常是更抽象的东西的一部分。

    所以,如果你在你的 ChannelSwitcher 类,您可以在 信道切换器 的身体 (而且,你的typedef可能是 public )

    // Currently used channel type
    typedef unsigned char Channel;
    
        6
  •  6
  •   Tom Crockett    14 年前

    无论是在构造“cchannel”对象时抛出异常,还是在需要约束的方法的入口抛出异常,都没有什么区别。无论哪种情况,您都在进行运行时断言,这意味着类型系统确实对您没有任何好处,是吗?

    如果你想知道你走了多远 可以 与一个强类型的语言一起,答案是“非常远,但不是用C++。”静态约束一个约束所需要的那种能力,比如“这个方法只能用0到15之间调用”需要一些东西。 dependent types 也就是说, 依赖于值的类型 .

    为了把这个概念放到伪C++语法中(假装C++有依赖类型),你可以这样写:

    void Func(unsigned char channel, IsBetween<0, channel, 15> proof) {
        ...
    }
    

    注意 IsBetween 参数化为 价值观 而不是 类型 . 为了现在在程序中调用这个函数,必须向编译器提供第二个参数, proof ,必须具有类型 IsBetween<0, channel, 15> . 也就是说,你必须 证明 在编译时, channel 介于0和15之间!这种表示命题的类型的概念,其值是这些命题的证明,称为 Curry-Howard Correspondence .

    当然,要证明这一点很困难。根据问题域的不同,成本/效益比可以很容易地得到提示,而只需对代码执行运行时检查。

        7
  •  4
  •   SCFrench    14 年前

    有些东西是不是杀伤力过大,往往取决于许多不同的因素。在一种情况下,可能会造成过度杀戮,而在另一种情况下则不会。

    如果您有许多不同的功能,所有接受的通道都必须执行相同的范围检查,那么这种情况可能不会被过度破坏。channel类可以避免代码重复,还可以提高函数的可读性(例如,将类channel命名为channel而不是cchannel-neil b.是正确的)。

    有时,当范围足够小时,我将为输入定义一个枚举。

        8
  •  1
  •   mdma    14 年前

    如果为16个不同的通道添加常量,以及为给定值提取通道的静态方法(或在超出范围时引发异常),则这可以在不增加每个方法调用的对象创建开销的情况下工作。

    在不知道如何使用这个代码的情况下,很难判断它是否被过度杀戮,或者不适合使用。你自己试试——用char和typesafe类的两种方法编写几个测试用例——然后看看你喜欢哪个。如果你在写了几个测试用例后厌倦了它,那么最好避免它,但是如果你发现自己喜欢这个方法,那么它可能是一个管理者。

    如果这是一个将被许多人使用的API,那么也许打开它进行一些审查会给你有价值的反馈,因为他们可能非常了解API域。

        9
  •  1
  •   5ound    14 年前

    在我看来,我不认为您所建议的是一个很大的开销,但是对于我来说,我更喜欢保存类型并将0..15之外的任何内容都未定义的文档放入其中,并在函数中使用assert()来捕获调试生成的错误。我不认为增加的复杂性对已经习惯于C++语言编程的程序员提供了更多的保护,其中包含了许多未定义的行为。

        10
  •  1
  •   neal aise    14 年前

    你必须做出选择。这里没有银弹。

    性能

    从性能的角度来看,开销根本不会很大。(除非你必须计算CPU周期)所以很可能这不是决定因素。

    简单/易用性等

    使API简单易懂。 您应该知道/决定数字/枚举/类对API用户是否更容易

    维修性

    1. 如果你非常确定频道 类型将是中的整数 在可预见的未来,我会去 没有抽象(考虑 使用枚举)

    2. 如果您有大量的 有界值,考虑使用 模板(Jerry)

    3. 如果你认为,频道可以 可能有方法使其成为 马上上课。

    编码努力 这是一次性的事情。所以要经常考虑维护。

        11
  •  1
  •   Norman Ramsey    14 年前

    频道的例子很难做到:

    • 首先,它看起来像一个简单的有限范围整数类型,就像您在Pascal和Ada中找到的那样。C++不能让你说出这些,但是 枚举足够好。

    • 如果你再近一点看,会不会是那种 设计可能改变的决策? 你能开始用频率来指“频道”吗?通过电话通知(wgbh,请进)?通过网络?

    很大程度上取决于你的计划。API的主要目标是什么?成本模型是什么?是否会经常创建频道(我怀疑不是)?

    为了获得稍微不同的观点,让我们看看搞砸的代价:

    • 你把代表暴露为 int . 客户机编写了大量代码,接口要么受到尊重,要么您的库因断言失败而停止。创造渠道是非常便宜的。但是,如果你需要改变你做事的方式,你就会失去“向后的bug兼容性”,让那些草率的客户机的作者感到恼火。

    • 你要保持抽象。每个人都必须使用抽象(不是很糟糕),并且每个人都将在未来对抗API中的变化。保持向后兼容性是小菜一碟。但是,创建通道的成本更高,而且更糟的是,API必须仔细声明何时可以安全地销毁通道以及由谁负责决策和销毁。更糟的情况是,创建/破坏通道会导致严重的内存泄漏或其他性能故障,在这种情况下,您将返回枚举。

    我是一个马虎的程序员,如果是我自己的工作,我会使用枚举,如果设计决策发生变化,我会牺牲成本。但是,如果这个API作为客户提供给许多其他程序员,我将使用抽象。


    显然我是一个道德相对主义者。

        12
  •  1
  •   madoki    9 年前

    只有0到15之间的值的整数是无符号4位整数(或半字节、半字节)。我设想如果这个通道交换逻辑在硬件中实现,那么通道号可以用4位寄存器来表示。 如果C++作为一种类型,你就可以在那里完成:

    void Func(unsigned nibble channel)
    {
        // do something
    }
    

    遗憾的是,事实并非如此。您可以放宽API规范来表示通道号是以无符号字符的形式给出的,实际通道是使用modulo 16操作计算的:

    void Func(unsigned char channel)
    {
        channel &= 0x0f; // truncate
        // do something
    }
    

    或者,使用位域:

    #include <iostream>
    struct Channel {
        // 4-bit unsigned field
        unsigned int n : 4;
    };
    void Func(Channel channel)
    {
        // do something with channel.n
    }
    int main()
    {
        Channel channel = {9};
        std::cout << "channel is" << channel.n << '\n';
        Func (channel); 
    }
    

    后者可能效率较低。

        13
  •  0
  •   joe snyder    14 年前

    我支持您的第一种方法,因为它更简单、更容易理解、维护和扩展,而且如果您的API必须重新实现/翻译/移植/等等,它更可能直接映射到其他语言。

        14
  •  0
  •   Saher Ahwal    14 年前

    这是抽象的,我的朋友!处理物体总是比较整洁的。