C#编程(二十三)----------实现继承

Posted Sun‘刺眼的博客

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C#编程(二十三)----------实现继承相关的知识,希望对你有一定的参考价值。

原文链接:http://blog.csdn.net/shanyongxu/article/details/46593809

 

如果要声明派生自另一个类的一个类,可以使用下面的语法:

class DerivedClass: BaseClass

{

//function and data members here

}

这个语法类似于C++Java中的语法,但是,C++程序员习惯使用公共和私有继承的概念;注意C#不支持私有继承,因此在基类名上没有public或者private限定符.支持私有继承指挥大大增加语言的复杂性,实际上私有继承在C++中也很少使用.

如果类(或结构)也派生子接口,则使用逗号分隔列表中的基类和接口.

public class DerivedClass: BaseClass,InterFace1,Interface2

{

}

对于结构:

public struct DerivedClass: Interface1,Interface2

{}

如果在类中没有定义基类,C#编译器就假定System.Object是基类.例如:

class MyClass:Object

{}

class MyClasst

{}

这两种方式是相同的结果,第二种方式比较常用,因为比较简单.C#支持object关键字,它用作System.Object类的假名,所以可以这么写:

class MyClass : object

{}

如果要引用Object,就可以使用object关键字,VS会识别它.

 

虚方法

把一个基类函数声明为virtual,就可以在任何派生类中重写该函数:

class BaseClass

{

public virtual string VirtualMethod()

{

return “the method is virtual and defined int BaseClass”;

}

}

也可以把属性声明为virtual.对于虚属性或重写属性,语法和非虚属性相同,但是要在定义中天剑virtual关键字,语法如下:

public virtual string foreName

{

get{return foreName;}

set{foreName=value;}

}

虚方法的规则同样适用于虚属性.可以在派生来中重写虚函数.在调用方法时,会调用该类对象的合适方法.C#,函数在默认情况下不是虚拟的,但是(除了构造函数以外)可以显示的声明virtual.这遵循C++的方法,即从性能的角度来看,除非显式指定,否则函数就不是虚函数.而在JAVA,所有的函数都是虚拟的.C#C++的语法不通,因为C#要求在派生类的函数重写另一个函数时,要使用override关键字现实生命.

例如:

class DerivedClass: BaseClass

{

public override string VirtualMethod()

{

return “this is an override defined in DerivedClass”;

}

}

重写方法的语法避免了C++中很容易发生的潜在运行错误:当派生类的方法签名无意中与基类版本略有差别时,该方法就不能重写基类的方法.C#,者会出现一个编译错误,因为编译器会认为函数已标记为override,单没有重写其基类的方法.

成员字段和静态函数都不能生命为virtual,因为这个概念只对类中的函数成员有意义.

例如:

 class BaseClass

    {

        public virtual string fun()

        {

            return "BaseClass method";

        }

    }

    class DerivedClass : BaseClass

    {

        public override string fun(string str)

        {

            return "DerivedClass method";

        } 

}

 

隐藏方法

如果签名相同的方法在基类和派生类中都进行了声明,但是该方法没有声明为virtualoverride,派生类方法就会隐藏基类方法.

大多数情况下,是要重写方法,而不是隐藏方法,因为隐藏方法会造成对于给定类的实例调用错误方法的危险.但是,如下例,C#语法可以确保开发人员在编译时受到这个潜在错误的警告,从而使隐藏方法更加安全.

假定有一个类HisBaseClass:

class HisBaseClass

{

//various members

}

在将来的某一刻,要编写一个派生类,用它给HisbaseClass添加某个功能,特别是要添加该基类中目前没有的方法----MyMethod():

class MyDerivedClass:HisBaseClass

{

public int MyMethod()

{

//something

return 0;

}

}

一年后,基类的编写者决定扩展基类的额功能.为了保持一致,他也添加一个名为MyMethod()的方法,该方法的名称和签名玉前面添加的方法相同,但是并不完成相同的工作.在使用基类的新方法编译代码时,程序在应该调用那个方法上就会有潜在的冲突.这在C#中完全合法,但因为MyMethod()与基类的MyMethod()不相关,运行这段代码就可能会产生以外的结果.C#可以很好地处理这种冲突.

此时,编译时系统会发出警告.C#,要隐藏一个方法应使用new关键字,语法如下:

class MyDerivedClass : HisBaseClass

{

public new int MyMethod()

{

//something

return 0;

}

}

但是新添加的MyMethod()没有生命为new,所以编译器会认为他隐藏了基类的方法,但没有显式声明,因此系统会发出一个警告(这也适用于是否把MyMethod()声明为vritual).如果愿意,就可以给新方法重命名.最好这么做,因为这会避免许多冲突,但是,如果觉得重命名方法不可能(例如,已经针对其他公司把软件发布为一个库,所以无法修改方法的名称),则所有的已有哭护短代码仍能正常运行,方法是选择新添加的MyMethod().这是因为访问这个方法的任何已有代码必须通过对MyDerivedClass(或进一步派生的类)的引用进行选择.

已有的代码不能通过对HisBaseClass类的引用方法这个方法,因为在对HisBaseClass类的早期版本进行编译时,会产生一个编译错误.这个问题只会发生在将来编写的客户端代码上.C#会发出一个警告,告诉用户在将来的代码中可能会出现问题----用户应该注意这个警告,不要试图在将来天机的代码中通过对HisBaseClass的引用调用新的MyMethod()方法,但所有已有的代码仍会正常工作.这是比较微妙的,但它很好地说明了C#如何处理类的不同版本.

 

调用函数的基类版本

C#有一种特殊的语法用语从派生类中调用方法的基类版本:base.方法名.例如,假定派生类中的一个方法要返回基类的方法90%的返回值,就可以使用下面的语法:

class CustomerAccount

{

public virtual decimal CalculatePrice()

{

return 0.0m;

}

}

class GoldAccount:CustomerAccount

{

public override decimal CalculatePrice()

{

return base.CalculatePrice()*0.9m;

}

}

注意,可以使用base.方法名语法调用基类中的任何方法,不必从同一个方法的重载中调用它.

 

抽象类和抽象函数

使用关键字abstract.C#允许把类和函数声明为abstract.抽象类不能实例化,而抽象函数不能直接调用,必须在非抽象的派生类中重写.显然,抽象函数本身也是虚函数(尽管不需要提供vritual关键字,实际上,如果提供了vritual关键字,就会产生一个语法错误).如果类包含抽象函数,则该类也是抽象的,必须声明为抽象的:

abstract class Building

{

public abstract decimal CalculateHeatingCost();//抽象函数

}

C++开发文员还要注意术语上的细微差别:C++,抽象函数常常描述为纯虚函数,而在C#,仅使用抽象这个术语.

 

密封类和密封方法

C#允许把类和方法声明为sealed.对于类,这表示不能继承该类;对于方法,表示不能重写该方法.

sealed class FinaClass

{

}

class DerivedClass:FinaClass//这是错误的

{

}

在把类或方法标记为sealed,最可能的情形是:如果要对库,类或自己编写的其他类作用域之外的类或方法进行操作,则重写某些功能导致代码混乱.一般情况下,把类或成员标记为sealed是要小心,因为这么做会严重限制他的使用方式.及时认为他不能对继承自一个类或重写类的某个成员发挥作用,仍有可能在将来的某个时刻,有人会遇到我们没有预料到的情况,此时这么做很有用..NET基类库大量使用了密封类,是希望从这些类中派生出自己的类的第三方开发人员无法访问这些类.例如:string就是一个密封类

clas MyClass:MyClassBase

{

public sealed override void FinaMethod()

{}

}

class DerivedClass : MyClass

{

public override void FinaMethod();//错误

}

要在方法或属性上使用sealed关键字,必须先从基类上把它声明为要重写的方法或者属性.如果基类不希望有重写方法或属性,就不要把它声明为vritual.

 

派生类的构造函数

先来看这样一段代码:

abstract class GenericCustomer

{

private string name;

}

class Nevermore60Customer:GenericCustomer

{

private uint hishCostMinutesUsed;

}

构造函数的调用顺序实现调用System.Object,再按照层次结构由上向下进行,指导到大编译器要实例化的类为止,还要注意在这个过程中,每个构造函数都初始化它自己的类中的字段.这是它的一般工作方式,再开始添加自己的构造函数时,也应尽可能的遵循这条规则.

注意构造函数的执行顺序.总实现调用的正是基类的额构造函数.也就是说,派生类的构造函数可以在执行过程中调用它可以访问的任何积累方法,属性和任何其他成员.因为基类已经构造出来了,其字段也初始化了,这也意味着,如果派生类不喜欢初始化基类的方式,但要访问数据,就可以改变数据的初始值.但是,好的编程方式是让基类构造函数来处理其字段.

 

首先来看最简单的情况,在层次结构中用一个无参数的构造函数来替换默认的构造函数后,看看会出现什么情况.假定要把每个人的名字初始化为字符串”<no name>”,而不是null引用.可以这样:

public abstract class GenericCustomer

{

private string name;

public GenericCustomer()

:base()

{

name=”<no name>”;

}

}

添加这段代码之后,代码运行正常.Nevermore60Customer仍有自己的默认构造函数,所以上面描述的事件的顺序保持不变,但编译器会使用自定义的GenericCustomer构造函数,而不是生成默认的构造函数,所以那么字段按照需要总是初始化为”<no name>”.

这次使用的关键字是base,而不是this,表示这是基类的构造函数,而不是要调用的当前的构造函数.base关键字后面的圆括号中没有参数,这非常重要,因为没有给基类构造函数传送任何采纳数,所以编译器必须调用无参数的构造函数.其结果是编译器会插入要调用的System.Object构造函数的代码,这正好与默认情况相同.

实际上可以省略这行代码:

public GenericCustomer()

{

name=”<no name>”;

}

basethis关键字是调用另一个构造函数时允许使用的唯一关键字,其他关键字都会产生编译错误.还要注意只能指定唯一一个其他的构造函数.

但是如果这样:

private GenericCustomer()

{

name=”<no name>”;

}

把构造函数声明为私有的,就会产生编译错误.有趣的是,该错误不是发生在GenericCustomer类中,而是发生在Nevermore60Customer派生类中.原因是:编译器试图为Nevermore60Customer生成默认的构造函数,但是又做不到,因为默认的构造函数应调用无参数的GenericCustomer构造函数.把该构造函数声明为private,他就不鞥呢访问派生类了.如果为GenericCustomer提供一个带参数的构造函数,但同时没有提供一个无参数的构造函数,也会发生类似的错误.在本例中,编译器不能为GenericCustomer生成默认构造函数,所以当编译器试图为任何派生类生成默认的构造函数时,会砸死次发现他不能做到这一点,因为没有无参数的基类构造函数可以调用.解决办法是为派生类添加自己的构造函数---实际上不需要在这些构造函数中做任何工作,这样便一起就不会为这些派生类生成任何默认的构造函数了.

 

在层次结构中添加带参数的构造函数

首先是带一个参数的GenericCustomer构造函数,仅在顾客提供其姓名的时候才实例化顾客.

abstract class GenericCustomer

{

private string name;

public GenericCustomer(string name)

{

this.name=name;

}

}

刚才说过,在编译器试图为派生类创建默认的构造函数时,会产生一个编译错误,因为编译器为Nevermore60Customer生成的默认是构造函数会试图调用一个无参数的GenericCustomer构造函数,但是GenericCustomer中没有这样的构造函数.因为,需要为派生类提供一个构造函数,来避免这个错误.

class Nevermore60Customer:GenericCustomer

{

private uint highCostMinutesUsed;

public Nevermore60Customer(string name)

:base(name)

{}

}

现在,Nevermore60Customer对象的实例化只有在提供了包含顾客姓名的字符串时才能进行,这正是我们需要的.有趣的是Nevermore60Customer构造函数对这个字符串所做的处理.他本身不能初始化name字段,因为他不能访问基类中的私有字段,但可以把顾客姓名传送给基类,以便GenericCustomer构造函数处理.具体方法是,把先执行的基类构造函数指定为顾客姓名当做参数构造函数.除此之外,不需要执行任何操作.

 

再来看如下代码:

class Nevermore60Customer:GenericCustomer

{

public Nevermore60Customer(string name,string referrerName)

:base(name)

{

this.referrerName=referrerName;

}

private string referrerName;

private uint highCostMinutesUsed;

 

}

构造函数将姓名作为参数,把他传递给GenericCustomer构造函数进行处理.referrerName是一个需要声明的变量,这样构造函数才能在其主题中处理这个参数.

但是并不是所有人都有联系人(referrerName),所以看下面的代码:

public Nevermore60Customer(stiring name)

:this(name,”<none>”)

{

}

这样就正确的建立了所有的构造函数.执行下面的代码:

GenericCustomer customer=new Nevermore60Customer(“syx”);

比哪一期认为他需要一个字符串参数的构造函数,所以他确认的构造函数是:

public Nevermore60Customer(stiring name)

:this(name,”<none>”)

{

}

在实例化customer,就会调用这个构造函数.之后立即把控制权传递给对应的Nevermore60Customer构造函数,该构造函数有两个参数,分别是”syx”和”<none>”.在这个构造函数中,把控制权依次传递给GenericCustomer构造函数,该构造函数有一个参数,即字符串”syx”.然后这个构造函数会把控制权传递给System.Object默认构造函数,现在才能执行这些构造函数,首先执行object的构造函数,接着执行genericCustomer的构造函数,它初始化name字段,然后带有两个参数的Nevermore60Customer的构造函数得到控制权,把联系人(referrerName)的姓名初始化”<none>”.最后,执行Nevermore60Customer构造函数,该构造函数带有一个参数----这个构造函数什么也不做.

该过程很合理,也很简洁.每个构造函数都负责处理变量的初始化,在这个过程中,正确的实例化了类,以备使用.如果再为类编写自己的构造函数时遵循同样的规则,就会发现,即使是最复杂的类也可以顺利的初始化.

最后总结一下:

1  当基类中没有自己编写构造函数时,派生类默认的调用基类的默认构造函数

Ex

    public class MyBaseClass
    {
    }
 

    public class MyDerivedClass : MyBaseClass
    {
        public MyDerivedClass()
        {
            Console.WriteLine("我是子类无参构造函数");
        }
 

        public MyDerivedClass(int i)
        {
            Console.WriteLine("我是子类带一个参数的构造函数");
        }
 

        public MyDerivedClass(int i, int j)
        {
            Console.WriteLine("我是子类带二个参数的构造函数");
        }
}
此时实例化派生类时,调用基类默认构造函数
2  当基类中编写构造函数时,派生类没有指定调用构造哪个构造函数时,会寻找无参的构造函数,如果没有则报错,另外无论调用派生类中的哪个构造函数都是寻找无参的那个基类构造函数,而非参数匹配。

Ex:

    public class MyBaseClass
    {
        public MyBaseClass(int i)
        {
            Console.WriteLine("我是基类带一个参数的构造函数");
        }
    }
 

    public class MyDerivedClass : MyBaseClass
    {
        public MyDerivedClass()
        {
            Console.WriteLine("我是子类无参构造函数");
        }
 

        public MyDerivedClass(int i)
        {
            Console.WriteLine("我是子类带一个参数的构造函数");
        }
 

        public MyDerivedClass(int i, int j)
        {
            Console.WriteLine("我是子类带二个参数的构造函数");
        }
    }
此时实例化派生类时则报错
3  基类中编写了构造函数,则派生类中可以指定调用基类的某个构造函数,使用base关键字。

Ex

    public class MyBaseClass
    {
        public MyBaseClass(int i)
        {
            Console.WriteLine("我是基类带一个参数的构造函数");
        }
    }
 

    public class MyDerivedClass : MyBaseClass
    {
        public MyDerivedClass() : base(i)
        {
            Console.WriteLine("我是子类无参构造函数");
        }
 

        public MyDerivedClass(int i) : base(i)
        {
            Console.WriteLine("我是子类带一个参数的构造函数");
        }
 

        public MyDerivedClass(int i, int j) : base(i)
        {
            Console.WriteLine("我是子类带二个参数的构造函数");
        }
    }
        此时实例化派生类时使用的带一个参数的构造函数时,则不会报错,因为他指定了基类的构造函数。
4  如果基类中的构造函数不含有无参构造函数,那么派生类中的构造函数必须全部指定调用的基类构造函数,否则出错

Ex

    public class MyBaseClass
    {
        public MyBaseClass(int i)
        {
            Console.WriteLine("我是基类带一个参数的构造函数");
        }
    }
 

    public class MyDerivedClass : MyBaseClass
    {
        public MyDerivedClass()
        {
            Console.WriteLine("我是子类无参构造函数");
        }
 

        public MyDerivedClass(int i) : base(i)
        {
            Console.WriteLine("我是子类带一个参数的构造函数");
        }
 

        public MyDerivedClass(int i, int j)
        {
            Console.WriteLine("我是子类带二个参数的构造函数");
        }
      此时编译将不能通过

以上是关于C#编程(二十三)----------实现继承的主要内容,如果未能解决你的问题,请参考以下文章

《C#零基础入门之百识百例》(二十三)数组排序 -- 选择排序

《C#零基础入门之百识百例》(五十三)继承介绍 -- 实现计算器

扣响C#之门笔记--第二十三章

C#编程(二十一)----------扩展方法

C#三十六道简单填空题

二十三种设计模式