文章教程

4.13技术解惑

8/31/2020 9:56:04 PM 人评论 次浏览

高级程序设计语言给我们带来的变革是在其语言环境中构建起了一个全新的、更抽象的虚拟计算模型。Smalltalk语言引入的对象计算模型从根本上改变了以前的传统计算模型。以前的计算模型突出的是顺序计算过程中的机器状态,而现在的对象计算模型突出的是对象之间的协作,其计算结果由参加计算的所有对象的状态总体构成。而由于对象本身具有自身状态,我们也可以把一个对象看成是一个小的计算机器。这样,面向对象的计算模型就演变成了许多小的计算机器的合作计算模型。图灵机作为计算领域内的根本计算模型,精确地抓住了计算的要点:什么是可计算的,计算时间和空间存储大小开销有多大。计算模型清楚地界定了可计算性的范围,也就界定了哪些问题是可求解的,哪些问题是不可求解的。OOP为程序员提供了一种更加抽象和易于理解的新的计算模型,但其本身并没有超越冯、诺依曼体系所代表的图灵机数学计算模型。所以我们不能期望OOP能帮助我们解决更多的问题,或者减少运算的复杂度。但OOP却能帮助我们用一种更容易被我们所理解和接受的方式去描叙和解决现实问题。

到此为止,面向对象编程的内容已经学完了,读者要深刻体会面向对象思想的重要性。面向对象思想改变了人们对编程的看法,将编程中的功能都用“对象”来处理。面向对象思想也使团队开发成为了可能,一个项目可以由多个编程人员独立完成不同的模块,这就为大型项目的时效性、可能性提供了保障。所以当今市面上的高级语言中,无论是Java、C++还是C#,都是面向对象的。

在编程语言中有一个不成文的规则,即一个函数只做一件事。例如,一个函数用来获取文件的大小,一个函数用来获取该文件的版本等信息,每个函数只做特定的事。但是随之新问题也来了,如果这样定义,每个函数都要打开这个文件,然后再关闭该文件……如此重复可能会浪费资源。当然,此时也许可以把所有代码放在一个函数中,但这样不仅可读性差,不便于维护,而且函数的灵活性也会降低。例如,当程序仅需要获取文件大小或文件版本信息时,难道还要另编写一个函数吗?其实减少几次打开和关闭操作,对程序的执行效应影响不大,因此,笔者建议,一般情况下,应优先考虑程序的可读性和维护性,不要为了效率而效率。

当给一个类编写一个函数,如果该函数需要访问某个实例的成员变量时,就可以将该函数定义成实例函数。一类的实例通常有一些成员变量,其中含有该实例的状态信息,而该函数需要改变这些状态,那么该函数也需要声明成实例函数。

如果该函数不需要访问某个实例的成员变量,也不需要改变某个实例的状态,那么就可以把该函数定义成静态函数。

(1)第一种情况:先声明实例,再调用实例函数。

当一个类有多个实例,例如学生这个类,可以有学生甲、学生乙、学生丙等实例,我们就可以先声明实例,然后再调用实例。在多线程的情况下,只要每个线程都创建自己的实例,那么此种方法通常是线程安全的。

(2)第二种情况:通过一个静态的实例调用实例函数。

这种情况比较特殊,通常是整个程序中该类唯一的一个实例,我们通过调用该实例的实例函数来改变该实例的某些状态。这个实例在多线程的情况下,通常是线程不安全的。除非给这个实例加锁,以防止其他线程访问该实例。

(3)第三种情况:直接调用静态函数。

这种情况下静态函数不需要去改变某个实例的状态,只需要得到少量的参数就可完成既定事情。例如判断一个文件是否存在,只要给出文件路径和文件名,就能知道该文件是否存在。

从表面上,引用参数和输出参数的区别是一个用关键字ref标示,一个用关键字out标示,但二者的根本区别涉及数据是引用类型还是值类型。

一般用这两个关键字你是想调用一个函数将某个值类型的数据通过一个函数后进行更改。传out定义的参数进去的时候这个参数在函数内部必须初始化。否则是不能进行编译的。ref和out都是传递数据的地址,正因为传递了地址,所以才能对源数据进行修改。

在一般情况下,当不加ref或者out关键字的时候,传递值类型数据传递的是源数据的一个副本。也就是在内存中新开辟了一块空间,在里面保存是与源数据相等的值。这也就是为什么在传递值类型数据时,如果不用return将无法修改原值的原因。但如果使用了ref或者out,一切问题就都解决了,因为它们传递的是数据的地址。

out和ref相比,还有一个用法就是可以作为多返回值来用。我们都知道函数只能有一个返回值,在C#里,如果想让一个函数有多个返回值,则可以使用关键字out。

由此可见,引用参数在调用之前就初始化。这参数一般情况下是从外部向内部传递数值时使用,对于托管代码加ref和不加基本相同。

而输出参数不需要输入确定的值,实际的对象是在方法内部初始化,由方法内部给这种参数赋值。一般是调用该方法之后,需要方法输出一些数据的时候使用。因为有时候方法的返回值可能用作他用,而这时还想让方法输出其他的数据,就可以使用out参数了。

在C#程序中,不能在密封类型中声明虚拟成员。假如某公共类型是密封的,并且声明了既virtual又非final的方法。该规则不报告委托类型的冲突,委托类型必须遵循此模式。类型将方法声明为虚方法,使继承类型可以重写虚方法的实现。根据定义,不能从密封类型继承,这使得密封类型上的虚方法没有意义。C#编译器不允许类型与该规则冲突。

要修复与该规则的冲突,需要使方法成为非虚方法,或使类型可继承。建议读者不要禁止显示此规则发出的警告。使类型保持当前状态可能引发维护问题。

下面的代码演示了一个与该规则冲突的类型。

using namespace System;
namespace DesignLibrary
{ 
  public ref class SomeType sealed
  {
  public:
    virtual bool VirtualFunction() { return true; }
  };
}

在C#中,公共类型为sealed,并且声明了受保护的成员或受保护的嵌套类型。该规则不报告Finalize方法的冲突,该方法必须遵循此模式。类型声明受保护的成员,使继承类型可以访问或重写该成员。按照定义,不能从密封类型继承,这意味着不能调用密封类型上受保护的方法。C#编译器对此错误会发出警告。

要想修复与该规则的冲突,需要将成员的访问级别改为私有,或使类型可继承。建议读者不要禁止显示此规则发出的警告。使类型保持当前状态可能引发维护问题。

下面的代码演示了一个与该规则冲突的类型。

using System;
namespace DesignLibrary
{
  public sealed class SealedClass
  {
   protected void ProtectedMethod(){}
  }
}

在面向对象程序设计语言中,类(Class)实际上是对某种类型的对象定义变量和方法的原型。它表示对现实生活中一类具有共同特征的事物的抽象,是面向对象编程的基础。

类是对某个对象的定义。它包含有关对象动作方式的信息,包括它的名称、方法、属性和事件。实际上它本身并不是对象,因为它不存在于内存中。当引用类的代码运行时,类的一个新的实例,即对象就在内存中创建了。虽然只有一个类,但能从这个类在内存中创建多个相同类型的对象。

可以把类看作“理论上”的对象。也就是说,它为对象提供蓝图,但在内存中并不存在。从这个蓝图可以创建任何数量的对象。从类创建的所有对象都有相同的成员:属性、方法和事件。但是,每个对象都象一个独立的实体一样动作。例如,一个对象的属性可以设置成与同类型的其他对象不同的值。

举例1:若要理解对象与其类之间的关系,可想象一下小甜饼成型机和小甜饼。小甜饼成型机是类。它定义每个小甜饼的特征,如大小和形状。类用于创建对象。这些对象就是小甜饼。

举例2:可以把汽车看作一个类,但是你不知道是什么汽车,究竟是奔驰还是QQ呢?所以需要实例化一个类,实例后的就是实例对象。

pulic class Car//这是一个类
{
  Car BENQ = new Car() ; //BENQ就是实例后的对象
}

因为全局命名空间以外的某命名空间包含的类型少于5个,所以不要在密封类型中声明虚拟成员。需确保每个命名空间都有一个逻辑组织,并确保将类型放入稀疏填充的命名空间是存在有效理由的。命名空间应包含在大多数情况下要一起使用的类型。当类型的应用程序互斥时,这些类型应位于不同的命名空间中。例如,System.Web.UI命名空间包含在Web应用程序中使用的类型,System.Windows.Forms命名空间包含在基于Windows的应用程序中使用的类型。即使两个命名空间都具有控制用户界面外观的类型,这些类型也并非设计为在同一个应用程序中使用,因此位于不同的命名空间中。谨慎组织命名空间可以增强功能的发现能力。通过检查命名空间层次结构,库使用者能够定位实现功能的类型。

要想符合上述原则,设计时类型和权限应不合并到其他命名空间中。这些类型位于主命名空间下自己的命名空间中,而且这些命名空间应分别以.Design和.Permissions结束。

要修复与该规则的冲突,需尝试将包含少量类型的命名空间合并到一个命名空间中。在命名空间不包含与其他命名空间中的类型一起使用的类型时,可以安全地禁止显示此规则发出的警告。


教程类别