业务逻辑的强类型化(续)

时间:2022-09-24 23:24:59

作为一个好事者,我希望能够给我周边的人讲解这种技术。他们对C++很不熟悉,但熟悉C#。于是,我打算把这种技术移植到C#中,以便於讲解。说做就做。

我建了一个C#项目,把代码拷贝过去,然后着手修改,这样可以省些事。我立刻便遇到了问题。C#有泛型,相当于模板,但不支持非类型泛型参数,即int CurrType,只允许用一个类型作为泛型参数。这样我们就不能使用C++中耍的手法了(typedef currency<n>)。退而求其次,直接用类实现货币类型:

class RMB

{

public double _val;

}

class USD

{

public double _val;

}

这样太繁琐了,很多重复。我们可以用一个基类封装_val,货币类从基类上继承获得:

class CurrBase

{

public double _val;

}

class RMB : CurrBase

{

}

class USD : CurrBase

{

}

货币类都是空的,它们的存在只是为了创建一个新的类型。

现在处理货币转换问题。C#不能重载operator=,所以只能使用一个helper函数泛型asign代替:

class utility

{

     public static void asign<T1, T2>(T1 c1, T2 c2)

         where T1 : CurrBase

         where T2 : CurrBase

     {

         c1._val = c2._val * utility.e_rate[c2.CurID(),c1.CurID()];

     }

}

这个asign函数是个泛型,泛型参数分别代表了两个操作数,函数中执行了货币转换。为了能够在汇率表中检索到相应的汇率,我们必须为基类和货币类定义抽象函数:

    public abstract class CurrBase

    {

        public double _val=0;

        public abstract int CurID();

    }

    public class RMB : CurrBase

    {

        public override int CurID()

        {

            return 0;

        }

}

基类中声明了CurID()抽象方法,并在货币类中定义。这样,便可以用统一的方式进行货币转换了:

asign(rmb_, usd_);

还行,尽管不那么漂亮,但也还算实用。不过,当我多看了几眼代码后,便发现这里根本没有必要使用泛型。完全可以利用OOP的多态实现同样的功能:

     public static void asign(CurrBase c1, CurrBase c2)

     {

         c1._val = c2._val * utility.e_rate[c2.CurID(),c1.CurID()];

     }

不过也没关系,使用方式还没有变,代码反而更简单了。使用泛型毕竟不是我们的根本目的,对吧?

现在轮到运算符了。不过我不知该把泛型运算符定义在哪里。按C#文档里的要求,运算符必须是类的static成员。但我的泛型运算符是针对许多个货币类的,定义在任何一个中,对其他类似乎不太公平。于是,我决定尝试将其定义在基类里:

    public abstract class CurrBase

{

  

        public static CurrBase operator+<T1, T2>(T1 c1, T2 c2)

           where T1 : CurrBase

           where T2 : CurrBase

        {

           

        }

}

编译器立刻还我以颜色:操作符根本不能是泛型!好吧,不能就不能吧,继续退而求其次,用OOP

    public abstract class CurrBase

{

  

        public static CurrBase operator+(CurrBase c1, CurrBase c2)

        {

           

        }

}

不过,这次让步让得有点离谱。当我写下这样的代码时,编译器居然不认账:

rmb_=rmb_+usd_;

错误信息是:错误 CS0266: 无法将类型“st_in_cs.CurrBase”隐式转换为“st_in_cs.RMB”。存在一个显式转换(是否缺少强制转换?)

我非得采用强制类型转换,才能过关:

rmb_=(RMB)(rmb_+usd_);

太夸张了,这样肯定不行。于是,我*在每个货币类中定义operator+

class RMB : CurrBase

{

   public RMB operator+(RMB c1, USD c2)

   {

      

   }

   public RMB operator+(RMB c1, UKP c2)

   {

      

   }

}

这可不得了,我必须为每对货币类定义一个+操作符,+操作符的总数将会是货币类数量的平方!其他的操作符每个都是货币类数的平方。我可受不了!

好在,可爱的OOP为我们提供了一根稻草,使得每个货币类的每个操作符只需定义一个:

class RMB : CurrBase

{

   public RMB operator+(RMB c1, CurrBase c2)

   {

      

   }

}

这样,任何货币类都可以作为第二操作数参与运算,而操作符只需定义一个。这样的工作量,对于一个逆来顺受的程序员而言,还是可以接受的。很好,代码不出错了:

rmb_=rmb_+usd_;

但当我写下如下代码时,编译器又开始抱怨了:

ukp_ = rmb_ + usd_;

还是要求显示转换,除非我们为UKP定义隐式类型转换操作符:

class UKP

{

    public static implicit operator UKP(RMB v)

    {

       

    }

}

光有RMB的不行啊,还得有USD的、JPD…。不过这样的话,我们必须为每一个货币类定义所有其它货币类的类型转换操作符。又是一个组合爆炸。到这里,我已经黔驴技穷了。谁让C#不支持=操作符重载和操作符模板化呢。没办法,只能忍着点了。

不过,如果我们能够降低点要求,事情还是有转机的。如果我们不通过操作符,而是采用static成员方法,进行货币的运算的话,就可以省去很多代码了:

    public class utility

    {

        public static T1 asign<T1, T2>(T1 c1, T2 c2)

            where T1 : CurrBase, new()

            where T2 : CurrBase

        {

            c1._val = c2._val * utility.curr_rate[c2.CurID(),c1.CurID()];

            return c1;

        }

        public static T1 add<T1, T2>(T1 c1, T2 c2)

            where T1 : CurrBase, new()

            where T2 : CurrBase

        {

            T1 t=new T1();

            t._val=c1._val + c2._val *

               utility.curr_rate[c2.CurID(),c1.CurID()];

            return t;

        }

      

}

这里,我还是使用了泛型,因为这些函数需要返回一个值,只有使用泛型,才能返回一个明确的类型,以避免强制转换的要求。于是,赋值和计算的代码就成了:

asign(jpd_, asign(ukp_, add(rmb_, usd_)));//也就是jpd_=ukp_=rmb_+usd_

的确是难看了点,但是为了能够少写点代码,这也只能将就了。

好了,我尽了最大的努力,试图在C#中实现强类型、可计算的货币系统。尽管最终我可以在C#中开发出一组与C++具有相同效果的货币类(除了赋值操作以外),但需要我编写大量的代码,实现各种计算操作,以及货币类之间的类型转换操作(组合爆炸)。相比C++中总共200来行代码,的确复杂、臃肿得多。

我并不希望把这篇文章写成“C++ vs C#”,(尽管我很高兴看到C++C#J)。我希望通过对这样一个代码优化任务,显示不同技术运用产生的结果。同时,也可以通过这两种实现尝试的对比,了解泛型编程的作用,以及泛型编程对语言提出的要求。

毋庸置疑,C++采用了纯粹的泛型编程,因此可以对问题进行高度抽象。并利用问题所提供的每一点有助于抽象的信息,以最简的形式对问题建模。而作为以OOP为核心的语言C#,对泛型的支持很弱。更重要的是,C#的泛型对泛型参数的运用严重依赖於泛型参数的约束(where)。如果没有whereC#将泛型参数作为Object类型处理,此时泛型参数没有意义(我无法访问该类型的成员)。如果有了whereC#要求泛型参数必须同where中指定的类型有继承关系(如asign中的T1必须是CurrBase的继承类)。而泛型函数中对泛型参数的使用也局限在约束类型(即CurrBase)上。于是,我们可以直接用以基类(CurrBase)为参数的asign函数代替泛型版本的asign。由于C#对泛型参数的继承性要求,使得泛型被困住了手脚,无法发挥应用的作用。正由于这些问题,C++才采用了现在模板的形式,而没有采用同C#一样的泛型模式。

或许有人会说,既然OOP能解决问题(asign最初不需要泛型也行,但最终还需要泛型来控制返回值的类型),为什么还要GP呢?

对于这个问题,前面也给出了答案。由于C#的泛型不支持非类型泛型参数,因此迫使我们使用传统OOP的手段:利用基类实现货币类的实现,定义货币类来创建新类型,使货币强类型化,利用虚函数提供货币独有信息。仅这一层面,OOP方式已经大大不如GP方式了,GP仅定义了一个模板,所有的货币类型都是通过typedef一句语句产生,无需更多的代码。而OOP方式要求必须为每一个货币编写一个类,代码量明显多于GP方式。

此后,C++通过重载一组操作符模板,实现货币的运算。而货币模板以及生成货币类型的typedef都无须任何改变。而在C#中,由于不支持泛型操作符,*定义大量的特定于类型的操作符。所有运算操作符,在每个货币类中都必须重载一次。而转型操作符,则必须在每个货币类中重载n-1次。

换句话说,有n种货币,有m个操作符(包括转型操作符),那么就需要定义n+1个类(包括基类),n×m+n×(n-1)个操作符。假设n=10m=10,那么总共需要11个类定义,190个操作符重载!如果每个类定义需要20行代码,而每个操作符重载需要5行代码,那么总共需要1170行代码。如果货币数量增加,总的代码数将以几何级数的方式增长。

上面的计算表明,尽管 OOP 可以解决问题,实现我们的目标,但所带来的开发量和维护量却是难以承受的。而且, OOP 的方式扩展非常困难,随着系统规模的扩大,扩展将越来越困难。所有这些都表明一点,尽管 OOP 是软件工程的明星,但在实际情况下,很多地方存在着 OOP 无法解决或难以解决的问题。这也就是为什么业界的先锋人物不断拓展和强化泛型编程的原因。