作为一个好事者,我希望能够给我周边的人讲解这种技术。他们对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>(T<chmetcnv w:st="on" unitname="C" sourcevalue="1" hasspace="True" negative="False" numbertype="1" tcsc="0">1 c</chmetcnv>1, T<chmetcnv w:st="on" unitname="C" sourcevalue="2" hasspace="True" negative="False" numbertype="1" tcsc="0">2 c</chmetcnv>2)
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>(T<chmetcnv w:st="on" unitname="C" sourcevalue="1" hasspace="True" negative="False" numbertype="1" tcsc="0">1 c</chmetcnv>1, T<chmetcnv w:st="on" unitname="C" sourcevalue="2" hasspace="True" negative="False" numbertype="1" tcsc="0">2 c</chmetcnv>2)
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>(T<chmetcnv w:st="on" unitname="C" sourcevalue="1" hasspace="True" negative="False" numbertype="1" tcsc="0">1 c</chmetcnv>1, T<chmetcnv w:st="on" unitname="C" sourcevalue="2" hasspace="True" negative="False" numbertype="1" tcsc="0">2 c</chmetcnv>2)
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>(T<chmetcnv w:st="on" unitname="C" sourcevalue="1" hasspace="True" negative="False" numbertype="1" tcsc="0">1 c</chmetcnv>1, T<chmetcnv w:st="on" unitname="C" sourcevalue="2" hasspace="True" negative="False" numbertype="1" tcsc="0">2 c</chmetcnv>2)
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)。如果没有where,C#将泛型参数作为Object类型处理,此时泛型参数没有意义(我无法访问该类型的成员)。如果有了where,C#要求泛型参数必须同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=10,m=10,那么总共需要11个类定义,190个操作符重载!如果每个类定义需要20行代码,而每个操作符重载需要5行代码,那么总共需要1170行代码。如果货币数量增加,总的代码数将以几何级数的方式增长。
上面的计算表明,尽管OOP可以解决问题,实现我们的目标,但所带来的开发量和维护量却是难以承受的。而且,OOP的方式扩展非常困难,随着系统规模的扩大,扩展将越来越困难。所有这些都表明一点,尽管OOP是软件工程的明星,但在实际情况下,很多地方存在着OOP无法解决或难以解决的问题。这也就是为什么业界的先锋人物不断拓展和强化泛型编程的原因。
分享到:
相关推荐
在本案例中,我们将深入探讨如何使用Python3.x与PyQtChart库来创建一个数据可视化界面,并实现业务逻辑与用户界面的分离。PyQtChart是一个强大的模块,它提供了丰富的图表类型,如线图、柱状图、饼图等,使得在...
10. **用户界面设计**:用户界面需直观易用,能够清晰显示可用的媒体类型、下载进度和状态,以及提供个性化设置选项。 综上所述,"基于下载类业务的媒体类型适配方法及系统"是一个涵盖了媒体识别、设备兼容、内容...
在Android开发中,断点续传是一项非常实用的技术,它允许用户在下载或上传文件时中断操作,并在后续时间点继续未完成的部分,无需重新开始整个...在实际项目中,可以结合具体需求和业务逻辑,灵活调整和扩展这些技术。
MyBatis是一个优秀的持久层框架,它简化了SQL操作,将Java对象与数据库表字段映射,使得开发人员可以专注于业务逻辑,而不是繁琐的JDBC代码。SpringBoot集成MyBatis,可以方便地管理数据库连接和事务,提供便捷的...
1. **模型-视图-控制器(MVC)模式**:Struts2.0遵循MVC设计模式,将业务逻辑、数据处理和用户界面分离,提高了代码的可维护性和可重用性。模型代表业务数据和逻辑,视图负责展示,控制器则协调模型和视图的交互。 ...
- **编写Action类**: 包含业务逻辑的方法,返回Result类型以指示下一步操作。 - **配置Action**: 在struts.xml中配置Action,包括URL映射、结果页面等。 - **视图渲染**: 使用JSP、FreeMarker或其他模板技术来...
开发者应设计并实现严谨的权限控制模型,进行代码审查以发现潜在的业务逻辑漏洞。 以上内容仅是Web应用安全领域的冰山一角,实际环境中还需要结合OWASP(开放网络应用安全项目)的最佳实践,使用自动化工具进行安全...
酒店管理系统通常采用三层架构设计:表现层(Presentation Layer)、业务逻辑层(Business Logic Layer)和数据访问层(Data Access Layer)。表现层负责用户交互,业务逻辑层处理业务规则,数据访问层则用于数据库...
1. JAVA编程语言:系统主要使用JAVA进行开发,利用其面向对象的特性,实现业务逻辑的模块化。 2. JDBC:用于连接数据库,执行SQL语句,进行数据的增删改查操作。 3. Servlet与JSP:构建服务器端逻辑,处理HTTP请求...
它将应用程序的业务逻辑、用户界面和数据处理分离,使得开发更加模块化。在.NET MVC框架中,模型负责管理数据和业务逻辑,视图负责展示数据,而控制器则协调这两个组件并处理用户输入。 图片和文件上传是Web应用...
系统采用三层结构设计,分别是用户界面层、业务逻辑处理层和数据存储层,这种设计增强了数据安全性并提高了程序的可扩展性。 接下来,我们探讨系统中的用例模型。用例图展示了系统的主要功能,其中汽车租赁是核心...
- 三层架构设计:表现层(客户端)、业务逻辑层(客户端)、数据访问层(服务器),分离职责,提高可维护性。 4. **功能模块设计** - **基础设置**:包括酒店信息、房间类型、价格策略等初始化设置。 - **业务...
本系统采用三层架构设计,包括表现层(UI)、业务逻辑层(BLL)和数据访问层(DAL)。表现层负责用户交互,业务逻辑层处理业务规则和流程,数据访问层则处理与数据库的交互。这种架构有利于代码复用,提高软件的可...
这些结构使得程序可以根据条件和迭代逻辑执行不同的分支,实现复杂的业务逻辑。 在学习结构化程序设计时,掌握程序文件的操作、变量的使用以及基本控制结构是基础,进一步还需要了解过程和用户自定义函数,它们可以...
它将控制层逻辑与视图层分离,使得开发人员可以更专注于业务逻辑,而无需关心视图的实现。 3. MyBatis:是一个持久层框架,它允许开发者编写SQL语句并将其与Java代码结合起来。MyBatis消除了几乎所有的JDBC代码和...
该系统利用jsp技术处理前端展示,后端则借助于Struts框架来实现业务逻辑控制,体现了JavaWeb开发中的MVC模式。 在JavaWeb开发中,文件管理系统的核心功能通常包括文件的上传、下载、删除、搜索以及权限管理。...
1. **MVC模式**:Struts2遵循Model-View-Controller(MVC)设计模式,将业务逻辑、数据和用户界面分离,提高了代码的可维护性和可重用性。 2. **Action类**:在Struts2中,Action类是处理用户请求的核心组件,它...
总的来说,JavaSE实现的汽车租赁系统是一个综合运用了面向对象编程、数据结构、业务逻辑、用户交互等多方面技术的实例。通过这样的系统,我们可以学习到如何在实际项目中有效地组织和管理代码,理解面向对象设计原则...
- **业务逻辑组件**:处理业务逻辑。 - **用户界面组件**:负责显示界面并与用户交互。 #### 十、类图 类图用于描述系统中类的结构以及它们之间的关系。例如: - **Car**:代表汽车的信息。 - **Customer**:代表...