由于clr不支持多继承,所以通过接口提供了“缩水版”的多继承
并且继承与派生的格式与C++也有所区别
1、在c++中,如果基类的某个函数是virtual的,则继承类中与其相同声明和名字的函数默认就是基类对应的虚函数
2、在c#中,必须在派生类的方法的前面加上override前缀,才认为是虚方法,否则就认为它是继承类单独声明的一个方法,与基类没有什么关系,同时该方法还会将基类的同名虚方法覆盖掉,因此这个时候一般用一个new的前缀来表示这个方法是继承类单独实现的方法
c#和clr允许一个类继承多个接口,当然,继承的所有接口方法都必须实现
接口允许定义事件,无参和有参属性,但是不能定义任何构造方法和实例字段
如果不将接口的方法标记为virtual,则编译器将会为它标记virtual和sealed,这将会阻止派生类重写该接口方法。如果标记为virtual,则可以在继承类中重写该接口方法
在这里的一个例子就是,实现IDisposable接口的Dispose方法
因为资源分为托管资源和非托管资源
托管资源包括在堆上分配的引用类对象等等
非托管资源包括文件,窗口句柄,流,内核对象,套接字对象等等
垃圾回收区只能自动回收托管资源,不会去回收非托管资源,此时如果不手动回收这些非托管资源,则这些资源只能在进程结束时才会被一起释放,而回收非托管资源的机制就是通过IDisposable来实现的
不过,这一切并不这么简单,一个标准的继承了IDisposable接口的类型应该像下面这样去实现。这种实现我们称之为Dispose模式:
{
//演示创建一个非托管资源
private IntPtr nativeResource = Marshal.AllocHGlobal(100);
//演示创建一个托管资源
private AnotherResource managedResource = new AnotherResource();
private bool disposed = false;
/// <summary>
/// 实现IDisposable中的Dispose方法
/// </summary>
public void Dispose()
{
//必须为true
Dispose(true);
//通知垃圾回收机制不再调用终结器(析构器)
GC.SuppressFinalize(this);
}
/// <summary>
/// 不是必要的,提供一个Close方法仅仅是为了更符合其他语言(如C++)的规范
/// </summary>
public void Close()
{
Dispose();
}
/// <summary>
/// 必须,以备程序员忘记了显式调用Dispose方法
/// </summary>
~SampleClass()
{
//必须为false
Dispose(false);
}
/// <summary>
/// 非密封类修饰用protected virtual
/// 密封类修饰用private
/// </summary>
/// <param name="disposing"></param>
protected virtual void Dispose(bool disposing)
{
if (disposed)
{
return;
}
if (disposing)
{
// 清理托管资源
if (managedResource != null)
{
managedResource.Dispose();
managedResource = null;
}
}
// 清理非托管资源
if (nativeResource != IntPtr.Zero)
{
Marshal.FreeHGlobal(nativeResource);
nativeResource = IntPtr.Zero;
}
//让类型知道自己已经被释放
disposed = true;
}
public void SamplePublicMethod()
{
if (disposed)
{
throw new ObjectDisposedException("SampleClass", "SampleClass is disposed");
}
//省略
}
}
在Dispose模式中,几乎每一行都有特殊的含义。
在标准的Dispose模式中,我们注意到一个以~开头的方法:
/// 必须,以备程序员忘记了显式调用Dispose方法
/// </summary>
~SampleClass()
{
//必须为false
Dispose(false);
}
这个方法叫做类型的终结器。提供终结器的全部意义在于:我们不能奢望类型的调用者肯定会主动调用Dispose方法,基于终结器会被垃圾回收器调用这个特点,终结器被用做资源释放的补救措施。
一个类型的Dispose方法应该允许被多次调用而不抛异常。鉴于这个原因,类型内部维护了一个私有的布尔型变量disposed:
private bool disposed = false;
在实际处理代码清理的方法中,加入了如下的判断语句:
{
return;
}
//省略清理部分的代码,并在方法的最后为disposed赋值为true
disposed = true;
这意味着类型如果被清理过一次,则清理工作将不再进行。
应该注意到:在标准的Dispose模式中,真正实现IDisposable接口的Dispose方法,并没有实际的清理工作,它实际调用的是下面这个带布尔参数的受保护的虚方法:
/// 非密封类修饰用protected virtual
/// 密封类修饰用private
/// </summary>
/// <param name="disposing"></param>
protected virtual void Dispose(bool disposing)
{
//省略代码
}
之所以提供这样一个受保护的虚方法,是为了考虑到这个类型会被其他类继承的情况。如果类型存在一个子类,子类也许会实现自己的Dispose模式。受保护 的虚方法用来提醒子类必须在实现自己的清理方法的时候注意到父类的清理工作,即子类需要在自己的释放方法中调用base.Dispose方法。
还有,我们应该已经注意到了真正撰写资源释放代码的那个虚方法是带有一个布尔参数的。之所以提供这个参数,是因为我们在资源释放时要区别对待托管资源和非托管资源。
在供调用者调用的显式释放资源的无参Dispose方法中,调用参数是true:
{
//必须为true
Dispose(true);
//其他省略
}
这表明,这个时候代码要同时处理托管资源和非托管资源。
在供垃圾回收器调用的隐式清理资源的终结器中,调用参数是false:
{
//必须为false
Dispose(false);
}
这表明,隐式清理时,只要处理非托管资源就可以了。
那么,为什么要区别对待托管资源和非托管资源。在认真阐述这个问题之前,我们需要首先弄明白:托管资源需要手动清理吗? 不妨先将C#中的类型分为两类,一类继承了IDisposable接口,一类则没有继承。前者,我们暂时称之为非普通类型,后者我们称之为普通类型。非普 通类型因为包含非托管资源,所以它需要继承IDisposable接口,但是,这个包含非托管资源的类型本身,它是一个托管资源。所以说,托管资源需要手 动清理吗?这个问题的答案是:托管资源中的普通类型,不需要手动清理,而非普通类型,是需要手动清理的(即调用Dispose方法)。
Dispose模式设计的思路基于:如果调用者显式调用了Dispose方法,那么类型就该按部就班为自己的所以资源全部释放掉。如果调用者忘记调用 Dispose方法,那么类型就假定自己的所有托管资源(哪怕是那些上段中阐述的非普通类型)全部交给垃圾回收器去回收,而不进行手工清理。理解了这一 点,我们就理解了为什么Dispose方法中,虚方法传入的参数是true,而终结器中,虚方法传入的参数是false。
在实际中我们可以通过try...finally语句来显示调用Dispose方法,也可以通过using语句来隐式调用Dispose方法,其实这两者是一样的,编译器也是把using语句翻译为try...finally的形式
using (Font font1 = new Font("Arial", 10.0f)) { byte charset = font1.GdiCharSet; }
实际上就是
{ Font font1 = new Font("Arial", 10.0f); try { byte charset = font1.GdiCharSet; } finally { if (font1 != null) ((IDisposable)font1).Dispose(); } }
在winform中,经常要用到的Form类也是继承自IDisposable接口的,所以它也要实现Dispose方法,vs会自动生成protected override void Dispose(bool disposing)的方法的实现,如下
/// <summary> /// 清理所有正在使用的资源。 /// </summary> /// <param name="disposing">如果应释放托管资源,为 true;否则为 false。</param> protected override void Dispose(bool disposing) { if (disposing && (components != null)) { components.Dispose(); } base.Dispose(disposing); }
因此在实例化Form类对象时,要在using语句里面new
选择基类还是接口的原则
1、IS-A对比CAN-DO
如果派生类和基类建立不起IS-A关系,就不用基类而用接口,接口意味着CAN-do关系,表示某些功能,如果类需要实现这些功能,就可以实现该接口
2、易用性
基类比接口要容易。因为接口必须要实现所有方法,而基类已经有了方法的默认实现,继承基类后,只需要修改不需要默认动作的方法
3、版本控制
向基类中添加新的方法,如果保持原来的方法不变,就不会影像当前的使用,也不用重新编译代码。如果向接口中添加了新的方法,就必须在当前的代码中的重新实现接口并编译才可以。
因此,尽量使用基类,如果基类不满足,在考虑添加接口